mirror of
https://github.com/laurent22/joplin.git
synced 2025-02-19 20:00:20 +02:00
Chore: Desktop: Update for share permissions (#8528)
This commit is contained in:
parent
880304c2fb
commit
1c1d20f82c
@ -802,8 +802,8 @@ packages/lib/themes/solarizedLight.js
|
||||
packages/lib/themes/type.js
|
||||
packages/lib/time.js
|
||||
packages/lib/utils/credentialFiles.js
|
||||
packages/lib/utils/inboxFetcher.js
|
||||
packages/lib/utils/joplinCloud.js
|
||||
packages/lib/utils/userFetcher.js
|
||||
packages/lib/utils/webDAVUtils.js
|
||||
packages/lib/utils/webDAVUtils.test.js
|
||||
packages/lib/uuid.js
|
||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -787,8 +787,8 @@ packages/lib/themes/solarizedLight.js
|
||||
packages/lib/themes/type.js
|
||||
packages/lib/time.js
|
||||
packages/lib/utils/credentialFiles.js
|
||||
packages/lib/utils/inboxFetcher.js
|
||||
packages/lib/utils/joplinCloud.js
|
||||
packages/lib/utils/userFetcher.js
|
||||
packages/lib/utils/webDAVUtils.js
|
||||
packages/lib/utils/webDAVUtils.test.js
|
||||
packages/lib/uuid.js
|
||||
|
@ -66,8 +66,7 @@ import syncDebugLog from '@joplin/lib/services/synchronizer/syncDebugLog';
|
||||
import eventManager from '@joplin/lib/eventManager';
|
||||
import path = require('path');
|
||||
import { checkPreInstalledDefaultPlugins, installDefaultPlugins, setSettingsForDefaultPlugins } from '@joplin/lib/services/plugins/defaultPlugins/defaultPluginsUtils';
|
||||
// import { runIntegrationTests } from '@joplin/lib/services/e2ee/ppkTestUtils';
|
||||
import { initializeInboxFetcher, inboxFetcher } from '@joplin/lib/utils/inboxFetcher';
|
||||
import userFetcher, { initializeUserFetcher } from '@joplin/lib/utils/userFetcher';
|
||||
|
||||
const pluginClasses = [
|
||||
require('./plugins/GotoAnything').default,
|
||||
@ -488,8 +487,8 @@ class Application extends BaseApplication {
|
||||
shim.setInterval(() => { runAutoUpdateCheck(); }, 12 * 60 * 60 * 1000);
|
||||
}
|
||||
|
||||
initializeInboxFetcher();
|
||||
shim.setInterval(() => { void inboxFetcher(); }, 1000 * 60 * 60);
|
||||
initializeUserFetcher();
|
||||
shim.setInterval(() => { void userFetcher(); }, 1000 * 60 * 60);
|
||||
|
||||
this.updateTray();
|
||||
|
||||
|
@ -25,7 +25,7 @@ const JoplinCloudConfigScreen = (props: JoplinCloudConfigScreenProps) => {
|
||||
|
||||
const mapStateToProps = (state: AppState) => {
|
||||
return {
|
||||
inboxEmail: state.settings['emailToNote.inboxEmail'],
|
||||
inboxEmail: state.settings['sync.10.inboxEmail'],
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -18,7 +18,7 @@ export const runtime = (): CommandRuntime => {
|
||||
if (!folder) throw new Error(`No such folder: ${folderId}`);
|
||||
|
||||
let deleteMessage = _('Delete notebook "%s"?\n\nAll notes and sub-notebooks within this notebook will also be deleted.', substrWithEllipsis(folder.title, 0, 32));
|
||||
if (folderId === context.state.settings['emailToNote.inboxJopId']) {
|
||||
if (folderId === context.state.settings['sync.10.inboxId']) {
|
||||
deleteMessage = _('Delete the Inbox notebook?\n\nIf you delete the inbox notebook, any email that\'s recently been sent to it may be lost.');
|
||||
}
|
||||
|
||||
|
@ -96,7 +96,7 @@ interface Props {
|
||||
onClose(): void;
|
||||
shares: StateShare[];
|
||||
shareUsers: Record<string, StateShareUser[]>;
|
||||
isJoplinCloud: boolean;
|
||||
canUseSharePermissions: boolean;
|
||||
}
|
||||
|
||||
interface RecipientDeleteEvent {
|
||||
@ -261,7 +261,7 @@ function ShareFolderDialog(props: Props) {
|
||||
function renderAddRecipient() {
|
||||
const disabled = shareState !== ShareState.Idle;
|
||||
|
||||
const dropdown = !props.isJoplinCloud ? null : <Dropdown className="permission-dropdown" options={permissionOptions} value={recipientPermissions} onChange={recipientPermissions_change}/>;
|
||||
const dropdown = !props.canUseSharePermissions ? null : <Dropdown className="permission-dropdown" options={permissionOptions} value={recipientPermissions} onChange={recipientPermissions_change}/>;
|
||||
|
||||
return (
|
||||
<StyledAddRecipient>
|
||||
@ -306,7 +306,7 @@ function ShareFolderDialog(props: Props) {
|
||||
|
||||
const permission = shareUser.can_write ? 'can_read_and_write' : 'can_read';
|
||||
const enabled = !recipientsBeingUpdated[shareUser.id];
|
||||
const dropdown = !props.isJoplinCloud ? null : <Dropdown disabled={!enabled} className="permission-dropdown" value={permission} options={permissionOptions} variant={DropdownVariant.NoBorder} onChange={event => recipient_permissionChange(shareUser.id, event.value)}/>;
|
||||
const dropdown = !props.canUseSharePermissions ? null : <Dropdown disabled={!enabled} className="permission-dropdown" value={permission} options={permissionOptions} variant={DropdownVariant.NoBorder} onChange={event => recipient_permissionChange(shareUser.id, event.value)}/>;
|
||||
|
||||
return (
|
||||
<StyledRecipient key={shareUser.user.email} index={index}>
|
||||
@ -407,7 +407,7 @@ const mapStateToProps = (state: State) => {
|
||||
return {
|
||||
shares: state.shareService.shares,
|
||||
shareUsers: state.shareService.shareUsers,
|
||||
isJoplinCloud: state.settings['sync.target'] === 10,
|
||||
canUseSharePermissions: state.settings['sync.target'] === 10 && state.settings['sync.10.canUseSharePermissions'],
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -31,6 +31,12 @@
|
||||
# ./runForTesting.sh 1 createUsers,createData,reset,sync && ./runForTesting.sh 1a reset,sync && ./runForTesting.sh 1
|
||||
# ./runForTesting.sh 1a
|
||||
|
||||
# ----------------------------------------------------------------------------------
|
||||
# Team accounts:
|
||||
# ----------------------------------------------------------------------------------
|
||||
|
||||
# ./runForTesting.sh 1 createTeams,createData,resetTeam,sync && ./runForTesting.sh 2 resetTeam,sync && ./runForTesting.sh 1
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
|
||||
@ -54,6 +60,16 @@ if [ "$USER_NUM" = "1b" ]; then
|
||||
USER_PROFILE_NUM=1b
|
||||
fi
|
||||
|
||||
if [ "$USER_NUM" = "2a" ]; then
|
||||
USER_NUM=2
|
||||
USER_PROFILE_NUM=2a
|
||||
fi
|
||||
|
||||
if [ "$USER_NUM" = "2b" ]; then
|
||||
USER_NUM=2
|
||||
USER_PROFILE_NUM=2b
|
||||
fi
|
||||
|
||||
COMMANDS=($(echo $2 | tr "," "\n"))
|
||||
PROFILE_DIR=~/.config/joplindev-desktop-$USER_PROFILE_NUM
|
||||
SYNC_TARGET=10
|
||||
@ -74,6 +90,10 @@ do
|
||||
|
||||
curl --data '{"action": "createUserDeletions"}' -H 'Content-Type: application/json' http://api.joplincloud.local:22300/api/debug
|
||||
|
||||
elif [[ $CMD == "createTeams" ]]; then
|
||||
|
||||
curl --data '{"action": "createTeams"}' -H 'Content-Type: application/json' http://api.joplincloud.local:22300/api/debug
|
||||
|
||||
elif [[ $CMD == "createData" ]]; then
|
||||
|
||||
echo 'mkbook "shared"' >> "$CMD_FILE"
|
||||
@ -96,6 +116,16 @@ do
|
||||
echo "config sync.$SYNC_TARGET.path http://api.joplincloud.local:22300" >> "$CMD_FILE"
|
||||
echo "config sync.$SYNC_TARGET.userContentPath http://joplinusercontent.local:22300" >> "$CMD_FILE"
|
||||
fi
|
||||
|
||||
elif [[ $CMD == "resetTeam" ]]; then
|
||||
|
||||
USER_EMAIL="teamuser1-$USER_NUM@example.com"
|
||||
rm -rf "$PROFILE_DIR"
|
||||
|
||||
echo "config keychain.supported 0" >> "$CMD_FILE"
|
||||
echo "config sync.target $SYNC_TARGET" >> "$CMD_FILE"
|
||||
echo "config sync.$SYNC_TARGET.username $USER_EMAIL" >> "$CMD_FILE"
|
||||
echo "config sync.$SYNC_TARGET.password 111111" >> "$CMD_FILE"
|
||||
|
||||
elif [[ $CMD == "e2ee" ]]; then
|
||||
|
||||
|
@ -362,13 +362,13 @@ class ConfigScreenComponent extends BaseScreenComponent {
|
||||
<View key="joplinCloud">
|
||||
<View style={this.styles().settingContainerNoBottomBorder}>
|
||||
<Text style={this.styles().settingText}>{_('Email to note')}</Text>
|
||||
<Text style={{ fontWeight: 'bold' }}>{this.props.settings['emailToNote.inboxEmail']}</Text>
|
||||
<Text style={{ fontWeight: 'bold' }}>{this.props.settings['sync.10.inboxEmail']}</Text>
|
||||
</View>
|
||||
{
|
||||
this.renderButton(
|
||||
'emailToNote.inboxEmail',
|
||||
'sync.10.inboxEmail',
|
||||
_('Copy to clipboard'),
|
||||
() => Clipboard.setString(this.props.settings['emailToNote.inboxEmail']),
|
||||
() => Clipboard.setString(this.props.settings['sync.10.inboxEmail']),
|
||||
{ description }
|
||||
)
|
||||
}
|
||||
|
@ -528,6 +528,6 @@ export default connect((state: AppState) => {
|
||||
isOnMobileData: state.isOnMobileData,
|
||||
syncOnlyOverWifi: state.settings['sync.mobileWifiOnly'],
|
||||
profileConfig: state.profileConfig,
|
||||
inboxJopId: state.settings['emailToNote.inboxJopId'],
|
||||
inboxJopId: state.settings['sync.10.inboxId'],
|
||||
};
|
||||
})(SideMenuContentComponent);
|
||||
|
@ -117,7 +117,7 @@ import sensorInfo, { SensorInfo } from './components/biometrics/sensorInfo';
|
||||
import { getCurrentProfile } from '@joplin/lib/services/profileConfig';
|
||||
import { getDatabaseName, getProfilesRootDir, getResourceDir, setDispatch } from './services/profiles';
|
||||
import { ReactNode } from 'react';
|
||||
import { initializeInboxFetcher, inboxFetcher } from '@joplin/lib/utils/inboxFetcher';
|
||||
import userFetcher, { initializeUserFetcher } from '@joplin/lib/utils/userFetcher';
|
||||
import { parseShareCache } from '@joplin/lib/services/share/reducer';
|
||||
import autodetectTheme, { onSystemColorSchemeChange } from './utils/autodetectTheme';
|
||||
|
||||
@ -665,8 +665,8 @@ async function initialize(dispatch: Function) {
|
||||
|
||||
reg.setupRecurrentSync();
|
||||
|
||||
initializeInboxFetcher();
|
||||
PoorManIntervals.setInterval(() => { void inboxFetcher(); }, 1000 * 60 * 60);
|
||||
initializeUserFetcher();
|
||||
PoorManIntervals.setInterval(() => { void userFetcher(); }, 1000 * 60 * 60);
|
||||
|
||||
PoorManIntervals.setTimeout(() => {
|
||||
void AlarmService.garbageCollect();
|
||||
|
@ -38,6 +38,8 @@ export interface DeleteOptions {
|
||||
// sync, we don't need to track the deletion, because the operation doesn't
|
||||
// need to applied again on next sync.
|
||||
trackDeleted?: boolean;
|
||||
|
||||
disableReadOnlyCheck?: boolean;
|
||||
}
|
||||
|
||||
class BaseModel {
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { ModelType, DeleteOptions } from '../BaseModel';
|
||||
import { BaseItemEntity, NoteEntity } from '../services/database/types';
|
||||
import { BaseItemEntity, DeletedItemEntity, NoteEntity } from '../services/database/types';
|
||||
import Setting from './Setting';
|
||||
import BaseModel from '../BaseModel';
|
||||
import time from '../time';
|
||||
@ -285,7 +285,7 @@ export default class BaseItem extends BaseModel {
|
||||
});
|
||||
}
|
||||
|
||||
if (needsReadOnlyChecks(this.modelType(), options.changeSource, this.syncShareCache)) {
|
||||
if (needsReadOnlyChecks(this.modelType(), options.changeSource, this.syncShareCache, options.disableReadOnlyCheck)) {
|
||||
const previousItems = await this.loadItemsByTypeAndIds(this.modelType(), ids, { fields: ['share_id', 'id'] });
|
||||
checkIfItemsCanBeChanged(this.modelType(), options.changeSource, previousItems, this.syncShareCache);
|
||||
}
|
||||
@ -321,7 +321,7 @@ export default class BaseItem extends BaseModel {
|
||||
// - Client 1 syncs with target 2 only => the note is *not* deleted from target 2 because no information
|
||||
// that it was previously deleted exist (deleted_items entry has been deleted).
|
||||
// The solution would be to permanently store the list of deleted items on each client.
|
||||
public static deletedItems(syncTarget: number) {
|
||||
public static deletedItems(syncTarget: number): Promise<DeletedItemEntity[]> {
|
||||
return this.db().selectAll('SELECT * FROM deleted_items WHERE sync_target = ?', [syncTarget]);
|
||||
}
|
||||
|
||||
|
@ -80,6 +80,21 @@ export default class Folder extends BaseItem {
|
||||
return this.db().exec(query);
|
||||
}
|
||||
|
||||
public static async deleteAllByShareId(shareId: string, deleteOptions: DeleteOptions = null) {
|
||||
const tableNameToClasses: Record<string, any> = {
|
||||
'folders': Folder,
|
||||
'notes': Note,
|
||||
'resources': Resource,
|
||||
};
|
||||
|
||||
for (const tableName of ['folders', 'notes', 'resources']) {
|
||||
const ItemClass = tableNameToClasses[tableName];
|
||||
const rows = await this.db().selectAll(`SELECT id FROM ${tableName} WHERE share_id = ?`, [shareId]);
|
||||
const ids: string[] = rows.map(r => r.id);
|
||||
await ItemClass.batchDelete(ids, deleteOptions);
|
||||
}
|
||||
}
|
||||
|
||||
public static async delete(folderId: string, options: DeleteOptions = null) {
|
||||
options = {
|
||||
deleteChildren: true,
|
||||
@ -90,12 +105,16 @@ export default class Folder extends BaseItem {
|
||||
if (!folder) return; // noop
|
||||
|
||||
if (options.deleteChildren) {
|
||||
const childrenDeleteOptions: DeleteOptions = {
|
||||
disableReadOnlyCheck: options.disableReadOnlyCheck,
|
||||
};
|
||||
|
||||
const noteIds = await Folder.noteIds(folderId);
|
||||
await Note.batchDelete(noteIds);
|
||||
await Note.batchDelete(noteIds, childrenDeleteOptions);
|
||||
|
||||
const subFolderIds = await Folder.subFolderIds(folderId);
|
||||
for (let i = 0; i < subFolderIds.length; i++) {
|
||||
await Folder.delete(subFolderIds[i]);
|
||||
await Folder.delete(subFolderIds[i], childrenDeleteOptions);
|
||||
}
|
||||
}
|
||||
|
||||
@ -762,7 +781,14 @@ export default class Folder extends BaseItem {
|
||||
|
||||
syncDebugLog.info('Folder Save:', o);
|
||||
|
||||
const savedFolder: FolderEntity = await super.save(o, options);
|
||||
let savedFolder: FolderEntity = await super.save(o, options);
|
||||
|
||||
// Ensures that any folder added to the state has all the required
|
||||
// properties, in particular "share_id" and "parent_id', which are
|
||||
// required in various parts of the code.
|
||||
if (!('share_id' in savedFolder) || !('parent_id' in savedFolder)) {
|
||||
savedFolder = await this.load(savedFolder.id);
|
||||
}
|
||||
|
||||
this.dispatch({
|
||||
type: 'FOLDER_UPDATE_ONE',
|
||||
|
@ -21,6 +21,7 @@ export default class ItemChange extends BaseModel {
|
||||
public static SOURCE_UNSPECIFIED = 1;
|
||||
public static SOURCE_SYNC = 2;
|
||||
public static SOURCE_DECRYPTION = 2; // CAREFUL - SAME ID AS SOURCE_SYNC!
|
||||
public static SOURCE_SHARE_SERVICE = 4;
|
||||
|
||||
public static tableName() {
|
||||
return 'item_changes';
|
||||
|
@ -1,4 +1,4 @@
|
||||
import BaseModel, { ModelType } from '../BaseModel';
|
||||
import BaseModel, { DeleteOptions, ModelType } from '../BaseModel';
|
||||
import BaseItem from './BaseItem';
|
||||
import ItemChange from './ItemChange';
|
||||
import Setting from './Setting';
|
||||
@ -744,7 +744,7 @@ export default class Note extends BaseItem {
|
||||
return note;
|
||||
}
|
||||
|
||||
public static async batchDelete(ids: string[], options: any = null) {
|
||||
public static async batchDelete(ids: string[], options: DeleteOptions = null) {
|
||||
ids = ids.slice();
|
||||
|
||||
while (ids.length) {
|
||||
|
@ -717,6 +717,12 @@ class Setting extends BaseModel {
|
||||
secure: true,
|
||||
},
|
||||
|
||||
'sync.10.inboxEmail': { value: '', type: SettingItemType.String, public: false },
|
||||
|
||||
'sync.10.inboxId': { value: '', type: SettingItemType.String, public: false },
|
||||
|
||||
'sync.10.canUseSharePermissions': { value: false, type: SettingItemType.Bool, public: false },
|
||||
|
||||
'sync.5.syncTargets': { value: {}, type: SettingItemType.Object, public: false },
|
||||
|
||||
'sync.resourceDownloadMode': {
|
||||
@ -1714,10 +1720,6 @@ class Setting extends BaseModel {
|
||||
label: () => _('Voice typing language files (URL)'),
|
||||
section: 'note',
|
||||
},
|
||||
|
||||
'emailToNote.inboxEmail': { value: '', type: SettingItemType.String, public: false },
|
||||
|
||||
'emailToNote.inboxJopId': { value: '', type: SettingItemType.String, public: false },
|
||||
};
|
||||
|
||||
this.metadata_ = { ...this.buildInMetadata_ };
|
||||
|
@ -13,7 +13,8 @@ export interface ItemSlice {
|
||||
// This function can be called to wrap any read-only-related code. It should be
|
||||
// fast and allows an early exit for cases that don't apply, for example if not
|
||||
// synchronising with Joplin Cloud or if not sharing any notebook.
|
||||
export const needsReadOnlyChecks = (itemType: ModelType, changeSource: number, shareState: ShareState) => {
|
||||
export const needsReadOnlyChecks = (itemType: ModelType, changeSource: number, shareState: ShareState, disableReadOnlyCheck = false) => {
|
||||
if (disableReadOnlyCheck) return false;
|
||||
if (Setting.value('sync.target') !== 10) return false;
|
||||
if (changeSource === ItemChange.SOURCE_SYNC) return false;
|
||||
if (!Setting.value('sync.userId')) return false;
|
||||
|
@ -33,4 +33,5 @@ export interface SaveOptions {
|
||||
ignoreProvisionalFlag?: boolean;
|
||||
dispatchUpdateAction?: boolean;
|
||||
changeSource?: number;
|
||||
disableReadOnlyCheck?: boolean;
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import Note from '../../models/Note';
|
||||
import { encryptionService, loadEncryptionMasterKey, msleep, resourceService, setupDatabaseAndSynchronizer, supportDir, switchClient } from '../../testing/test-utils';
|
||||
import { createFolderTree, encryptionService, loadEncryptionMasterKey, msleep, resourceService, setupDatabaseAndSynchronizer, simulateReadOnlyShareEnv, supportDir, switchClient } from '../../testing/test-utils';
|
||||
import ShareService from './ShareService';
|
||||
import reducer, { defaultState } from '../../reducer';
|
||||
import { createStore } from 'redux';
|
||||
@ -15,6 +15,9 @@ import shim from '../../shim';
|
||||
import Resource from '../../models/Resource';
|
||||
import { readFile } from 'fs-extra';
|
||||
import BaseItem from '../../models/BaseItem';
|
||||
import ResourceService from '../ResourceService';
|
||||
import Setting from '../../models/Setting';
|
||||
import { ModelType } from '../../BaseModel';
|
||||
|
||||
interface TestShareFolderServiceOptions {
|
||||
master_key_id?: string;
|
||||
@ -239,5 +242,40 @@ describe('ShareService', () => {
|
||||
Logger.globalLogger.setLevel(previousLogLevel);
|
||||
});
|
||||
|
||||
it('should leave a shared folder', async () => {
|
||||
const folder1 = await createFolderTree('', [
|
||||
{
|
||||
title: 'folder 1',
|
||||
children: [
|
||||
{
|
||||
title: 'note 1',
|
||||
},
|
||||
{
|
||||
title: 'note 2',
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
const resourceService = new ResourceService();
|
||||
await Folder.save({ id: folder1.id, share_id: '123456789' });
|
||||
await Folder.updateAllShareIds(resourceService);
|
||||
|
||||
const cleanup = simulateReadOnlyShareEnv('123456789');
|
||||
|
||||
const shareService = testShareFolderService();
|
||||
await shareService.leaveSharedFolder(folder1.id, 'somethingrandom');
|
||||
|
||||
expect(await Folder.count()).toBe(0);
|
||||
expect(await Note.count()).toBe(0);
|
||||
|
||||
const deletedItems = await BaseItem.deletedItems(Setting.value('sync.target'));
|
||||
|
||||
expect(deletedItems.length).toBe(1);
|
||||
expect(deletedItems[0].item_type).toBe(ModelType.Folder);
|
||||
expect(deletedItems[0].item_id).toBe(folder1.id);
|
||||
|
||||
cleanup();
|
||||
});
|
||||
|
||||
});
|
||||
|
@ -173,14 +173,21 @@ export default class ShareService {
|
||||
|
||||
// This is when a share recipient decides to leave the shared folder.
|
||||
//
|
||||
// In that case, we should only delete the folder but none of its children.
|
||||
// Deleting the folder tells the server that we want to leave the share. The
|
||||
// server will then proceed to delete all associated user_items. So
|
||||
// eventually all the notebook content will also be deleted for the current
|
||||
// user.
|
||||
// In that case we delete the root folder. Deleting the folder tells the
|
||||
// server that we want to leave the share.
|
||||
//
|
||||
// We don't delete the children here because that would delete them for the
|
||||
// other share participants too.
|
||||
// We also immediately delete the children, but we do not sync the changes
|
||||
// otherwise it would delete the items for other users too.
|
||||
//
|
||||
// If we do not delete them now it would also cause all kind of issues with
|
||||
// read-only shares, because the read-only status will be lost after the
|
||||
// deletion of the root folder, which means various services may modify the
|
||||
// data. The changes will then be rejected by the sync target and cause
|
||||
// conflicts.
|
||||
//
|
||||
// We do not need to sync the children deletion, because the server will
|
||||
// take care of deleting all associated user_items. So eventually all the
|
||||
// notebook content will also be deleted for the current user.
|
||||
//
|
||||
// If `folderShareUserId` is provided, the function will check that the user
|
||||
// does not own the share. It would be an error to leave such a folder
|
||||
@ -191,7 +198,14 @@ export default class ShareService {
|
||||
if (folderShareUserId === userId) throw new Error('Cannot leave own notebook');
|
||||
}
|
||||
|
||||
await Folder.delete(folderId, { deleteChildren: false });
|
||||
const folder = await Folder.load(folderId);
|
||||
|
||||
// We call this to make sure all items are correctly linked before we
|
||||
// call deleteAllByShareId()
|
||||
await Folder.updateAllShareIds(ResourceService.instance());
|
||||
|
||||
await Folder.delete(folderId, { deleteChildren: false, disableReadOnlyCheck: true });
|
||||
await Folder.deleteAllByShareId(folder.share_id, { disableReadOnlyCheck: true, trackDeleted: false });
|
||||
}
|
||||
|
||||
// Finds any folder that is associated with a share, but the user no longer
|
||||
|
@ -94,6 +94,11 @@ export function isSharedFolderOwner(state: RootState, folderId: string): boolean
|
||||
}
|
||||
|
||||
export function isRootSharedFolder(folder: FolderEntity): boolean {
|
||||
if (!('share_id' in folder) || !('parent_id' in folder)) {
|
||||
logger.warn('Calling isRootSharedFolder without specifying share_id and parent_id:', folder);
|
||||
return false;
|
||||
}
|
||||
|
||||
return !!folder.share_id && !folder.parent_id;
|
||||
}
|
||||
|
||||
|
@ -1,30 +0,0 @@
|
||||
import SyncTargetRegistry from '../SyncTargetRegistry';
|
||||
import eventManager from '../eventManager';
|
||||
import Setting from '../models/Setting';
|
||||
import { reg } from '../registry';
|
||||
|
||||
export const inboxFetcher = async () => {
|
||||
|
||||
if (Setting.value('sync.target') !== SyncTargetRegistry.nameToId('joplinCloud')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const syncTarget = reg.syncTarget();
|
||||
const fileApi = await syncTarget.fileApi();
|
||||
const api = fileApi.driver().api();
|
||||
|
||||
const owner = await api.exec('GET', `api/users/${api.userId}`);
|
||||
|
||||
if (owner.inbox) {
|
||||
Setting.setValue('emailToNote.inboxJopId', owner.inbox.jop_id);
|
||||
}
|
||||
|
||||
if (owner.inbox_email) {
|
||||
Setting.setValue('emailToNote.inboxEmail', owner.inbox_email);
|
||||
}
|
||||
};
|
||||
|
||||
// Listen to the event only once
|
||||
export const initializeInboxFetcher = () => {
|
||||
eventManager.once('sessionEstablished', inboxFetcher);
|
||||
};
|
41
packages/lib/utils/userFetcher.ts
Normal file
41
packages/lib/utils/userFetcher.ts
Normal file
@ -0,0 +1,41 @@
|
||||
import SyncTargetRegistry from '../SyncTargetRegistry';
|
||||
import eventManager from '../eventManager';
|
||||
import Setting from '../models/Setting';
|
||||
import { reg } from '../registry';
|
||||
import Logger from '../Logger';
|
||||
|
||||
const logger = Logger.create('userFetcher');
|
||||
|
||||
interface UserApiResponse {
|
||||
inbox?: {
|
||||
jop_id: string;
|
||||
};
|
||||
inbox_email?: string;
|
||||
can_use_share_permissions?: number;
|
||||
}
|
||||
|
||||
const userFetcher = async () => {
|
||||
|
||||
if (Setting.value('sync.target') !== SyncTargetRegistry.nameToId('joplinCloud')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const syncTarget = reg.syncTarget();
|
||||
const fileApi = await syncTarget.fileApi();
|
||||
const api = fileApi.driver().api();
|
||||
|
||||
const owner: UserApiResponse = await api.exec('GET', `api/users/${api.userId}`);
|
||||
|
||||
logger.info('Got user:', owner);
|
||||
|
||||
Setting.setValue('sync.10.inboxId', owner.inbox ? owner.inbox.jop_id : '');
|
||||
Setting.setValue('sync.10.inboxEmail', owner.inbox_email ? owner.inbox_email : '');
|
||||
Setting.setValue('sync.10.canUseSharePermissions', !!owner.can_use_share_permissions);
|
||||
};
|
||||
|
||||
// Listen to the event only once
|
||||
export const initializeUserFetcher = () => {
|
||||
eventManager.once('sessionEstablished', userFetcher);
|
||||
};
|
||||
|
||||
export default userFetcher;
|
Loading…
x
Reference in New Issue
Block a user