You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2025-11-26 22:41:17 +02:00
Cli: Support managing shared notebooks (#12637)
This commit is contained in:
@@ -122,6 +122,8 @@ packages/app-cli/app/command-rmnote.test.js
|
|||||||
packages/app-cli/app/command-rmnote.js
|
packages/app-cli/app/command-rmnote.js
|
||||||
packages/app-cli/app/command-set.js
|
packages/app-cli/app/command-set.js
|
||||||
packages/app-cli/app/command-settingschema.js
|
packages/app-cli/app/command-settingschema.js
|
||||||
|
packages/app-cli/app/command-share.test.js
|
||||||
|
packages/app-cli/app/command-share.js
|
||||||
packages/app-cli/app/command-sync.js
|
packages/app-cli/app/command-sync.js
|
||||||
packages/app-cli/app/command-testing.js
|
packages/app-cli/app/command-testing.js
|
||||||
packages/app-cli/app/command-use.js
|
packages/app-cli/app/command-use.js
|
||||||
@@ -130,6 +132,8 @@ packages/app-cli/app/gui/FolderListWidget.js
|
|||||||
packages/app-cli/app/gui/StatusBarWidget.js
|
packages/app-cli/app/gui/StatusBarWidget.js
|
||||||
packages/app-cli/app/services/plugins/PluginRunner.js
|
packages/app-cli/app/services/plugins/PluginRunner.js
|
||||||
packages/app-cli/app/setupCommand.js
|
packages/app-cli/app/setupCommand.js
|
||||||
|
packages/app-cli/app/utils/initializeCommandService.js
|
||||||
|
packages/app-cli/app/utils/shimInitCli.js
|
||||||
packages/app-cli/app/utils/testUtils.js
|
packages/app-cli/app/utils/testUtils.js
|
||||||
packages/app-cli/tests/HtmlToMd.js
|
packages/app-cli/tests/HtmlToMd.js
|
||||||
packages/app-cli/tests/MarkupToHtml.js
|
packages/app-cli/tests/MarkupToHtml.js
|
||||||
@@ -449,7 +453,6 @@ packages/app-desktop/gui/WindowCommandsAndDialogs/commands/exportPdf.js
|
|||||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/gotoAnything.js
|
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/gotoAnything.js
|
||||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/hideModalMessage.js
|
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/hideModalMessage.js
|
||||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/index.js
|
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/index.js
|
||||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/leaveSharedFolder.js
|
|
||||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/linkToNote.js
|
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/linkToNote.js
|
||||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/moveToFolder.js
|
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/moveToFolder.js
|
||||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/newFolder.js
|
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/newFolder.js
|
||||||
@@ -1081,6 +1084,7 @@ packages/lib/commands/deleteNote.js
|
|||||||
packages/lib/commands/historyBackward.js
|
packages/lib/commands/historyBackward.js
|
||||||
packages/lib/commands/historyForward.js
|
packages/lib/commands/historyForward.js
|
||||||
packages/lib/commands/index.js
|
packages/lib/commands/index.js
|
||||||
|
packages/lib/commands/leaveSharedFolder.js
|
||||||
packages/lib/commands/openMasterPasswordDialog.js
|
packages/lib/commands/openMasterPasswordDialog.js
|
||||||
packages/lib/commands/permanentlyDeleteNote.js
|
packages/lib/commands/permanentlyDeleteNote.js
|
||||||
packages/lib/commands/renderMarkup.test.js
|
packages/lib/commands/renderMarkup.test.js
|
||||||
|
|||||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -97,6 +97,8 @@ packages/app-cli/app/command-rmnote.test.js
|
|||||||
packages/app-cli/app/command-rmnote.js
|
packages/app-cli/app/command-rmnote.js
|
||||||
packages/app-cli/app/command-set.js
|
packages/app-cli/app/command-set.js
|
||||||
packages/app-cli/app/command-settingschema.js
|
packages/app-cli/app/command-settingschema.js
|
||||||
|
packages/app-cli/app/command-share.test.js
|
||||||
|
packages/app-cli/app/command-share.js
|
||||||
packages/app-cli/app/command-sync.js
|
packages/app-cli/app/command-sync.js
|
||||||
packages/app-cli/app/command-testing.js
|
packages/app-cli/app/command-testing.js
|
||||||
packages/app-cli/app/command-use.js
|
packages/app-cli/app/command-use.js
|
||||||
@@ -105,6 +107,8 @@ packages/app-cli/app/gui/FolderListWidget.js
|
|||||||
packages/app-cli/app/gui/StatusBarWidget.js
|
packages/app-cli/app/gui/StatusBarWidget.js
|
||||||
packages/app-cli/app/services/plugins/PluginRunner.js
|
packages/app-cli/app/services/plugins/PluginRunner.js
|
||||||
packages/app-cli/app/setupCommand.js
|
packages/app-cli/app/setupCommand.js
|
||||||
|
packages/app-cli/app/utils/initializeCommandService.js
|
||||||
|
packages/app-cli/app/utils/shimInitCli.js
|
||||||
packages/app-cli/app/utils/testUtils.js
|
packages/app-cli/app/utils/testUtils.js
|
||||||
packages/app-cli/tests/HtmlToMd.js
|
packages/app-cli/tests/HtmlToMd.js
|
||||||
packages/app-cli/tests/MarkupToHtml.js
|
packages/app-cli/tests/MarkupToHtml.js
|
||||||
@@ -424,7 +428,6 @@ packages/app-desktop/gui/WindowCommandsAndDialogs/commands/exportPdf.js
|
|||||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/gotoAnything.js
|
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/gotoAnything.js
|
||||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/hideModalMessage.js
|
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/hideModalMessage.js
|
||||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/index.js
|
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/index.js
|
||||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/leaveSharedFolder.js
|
|
||||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/linkToNote.js
|
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/linkToNote.js
|
||||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/moveToFolder.js
|
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/moveToFolder.js
|
||||||
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/newFolder.js
|
packages/app-desktop/gui/WindowCommandsAndDialogs/commands/newFolder.js
|
||||||
@@ -1056,6 +1059,7 @@ packages/lib/commands/deleteNote.js
|
|||||||
packages/lib/commands/historyBackward.js
|
packages/lib/commands/historyBackward.js
|
||||||
packages/lib/commands/historyForward.js
|
packages/lib/commands/historyForward.js
|
||||||
packages/lib/commands/index.js
|
packages/lib/commands/index.js
|
||||||
|
packages/lib/commands/leaveSharedFolder.js
|
||||||
packages/lib/commands/openMasterPasswordDialog.js
|
packages/lib/commands/openMasterPasswordDialog.js
|
||||||
packages/lib/commands/permanentlyDeleteNote.js
|
packages/lib/commands/permanentlyDeleteNote.js
|
||||||
packages/lib/commands/renderMarkup.test.js
|
packages/lib/commands/renderMarkup.test.js
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import Folder from '@joplin/lib/models/Folder';
|
|||||||
import BaseItem from '@joplin/lib/models/BaseItem';
|
import BaseItem from '@joplin/lib/models/BaseItem';
|
||||||
import Note from '@joplin/lib/models/Note';
|
import Note from '@joplin/lib/models/Note';
|
||||||
import Tag from '@joplin/lib/models/Tag';
|
import Tag from '@joplin/lib/models/Tag';
|
||||||
import Setting from '@joplin/lib/models/Setting';
|
import Setting, { Env } from '@joplin/lib/models/Setting';
|
||||||
import { reg } from '@joplin/lib/registry.js';
|
import { reg } from '@joplin/lib/registry.js';
|
||||||
import { dirname, fileExtension } from '@joplin/lib/path-utils';
|
import { dirname, fileExtension } from '@joplin/lib/path-utils';
|
||||||
import { splitCommandString } from '@joplin/utils';
|
import { splitCommandString } from '@joplin/utils';
|
||||||
@@ -16,6 +16,7 @@ import RevisionService from '@joplin/lib/services/RevisionService';
|
|||||||
import shim from '@joplin/lib/shim';
|
import shim from '@joplin/lib/shim';
|
||||||
import setupCommand from './setupCommand';
|
import setupCommand from './setupCommand';
|
||||||
import { FolderEntity, NoteEntity } from '@joplin/lib/services/database/types';
|
import { FolderEntity, NoteEntity } from '@joplin/lib/services/database/types';
|
||||||
|
import initializeCommandService from './utils/initializeCommandService';
|
||||||
const { cliUtils } = require('./cli-utils.js');
|
const { cliUtils } = require('./cli-utils.js');
|
||||||
const Cache = require('@joplin/lib/Cache');
|
const Cache = require('@joplin/lib/Cache');
|
||||||
const { splitCommandBatch } = require('@joplin/lib/string-utils');
|
const { splitCommandBatch } = require('@joplin/lib/string-utils');
|
||||||
@@ -76,6 +77,12 @@ class Application extends BaseApplication {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async loadItemOrFail(type: ModelType | 'folderOrNote', pattern: string) {
|
||||||
|
const output = await this.loadItem(type, pattern);
|
||||||
|
if (!output) throw new Error(_('Cannot find "%s".', pattern));
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||||
public async loadItems(type: ModelType | 'folderOrNote', pattern: string, options: any = null): Promise<(FolderEntity | NoteEntity)[]> {
|
public async loadItems(type: ModelType | 'folderOrNote', pattern: string, options: any = null): Promise<(FolderEntity | NoteEntity)[]> {
|
||||||
if (type === 'folderOrNote') {
|
if (type === 'folderOrNote') {
|
||||||
@@ -414,6 +421,8 @@ class Application extends BaseApplication {
|
|||||||
|
|
||||||
if (!shim.sharpEnabled()) this.logger().warn('Sharp is disabled - certain image-related features will not be available');
|
if (!shim.sharpEnabled()) this.logger().warn('Sharp is disabled - certain image-related features will not be available');
|
||||||
|
|
||||||
|
initializeCommandService(this.store(), Setting.value('env') === Env.Dev);
|
||||||
|
|
||||||
// If we have some arguments left at this point, it's a command
|
// If we have some arguments left at this point, it's a command
|
||||||
// so execute it.
|
// so execute it.
|
||||||
if (argv.length) {
|
if (argv.length) {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ describe('command-done', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should make a note as "done"', async () => {
|
it('should make a note as "done"', async () => {
|
||||||
const note = await Note.save({ title: 'hello', is_todo: 1, todo_completed: 0 });
|
const note = await Note.save({ title: 'hello', is_todo: 1, todo_completed: 0, parent_id: '' });
|
||||||
|
|
||||||
const command = setupCommandForTesting(Command);
|
const command = setupCommandForTesting(Command);
|
||||||
|
|
||||||
|
|||||||
@@ -26,8 +26,7 @@ class Command extends BaseCommand {
|
|||||||
const pattern = args['notebook'];
|
const pattern = args['notebook'];
|
||||||
const force = args.options && args.options.force === true;
|
const force = args.options && args.options.force === true;
|
||||||
|
|
||||||
const folder = await app().loadItem(BaseModel.TYPE_FOLDER, pattern);
|
const folder = await app().loadItemOrFail(BaseModel.TYPE_FOLDER, pattern);
|
||||||
if (!folder) throw new Error(_('Cannot find "%s".', pattern));
|
|
||||||
|
|
||||||
const permanent = args.options?.permanent === true || !!folder.deleted_time;
|
const permanent = args.options?.permanent === true || !!folder.deleted_time;
|
||||||
const ellipsizedFolderTitle = substrWithEllipsis(folder.title, 0, 32);
|
const ellipsizedFolderTitle = substrWithEllipsis(folder.title, 0, 32);
|
||||||
|
|||||||
179
packages/app-cli/app/command-share.test.ts
Normal file
179
packages/app-cli/app/command-share.test.ts
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
import { setupDatabaseAndSynchronizer, switchClient } from '@joplin/lib/testing/test-utils';
|
||||||
|
import mockShareService, { ApiMock } from '@joplin/lib/testing/share/mockShareService';
|
||||||
|
import { setupCommandForTesting, setupApplication } from './utils/testUtils';
|
||||||
|
import Folder from '@joplin/lib/models/Folder';
|
||||||
|
import ShareService from '@joplin/lib/services/share/ShareService';
|
||||||
|
import BaseItem from '@joplin/lib/models/BaseItem';
|
||||||
|
import { ModelType } from '@joplin/lib/BaseModel';
|
||||||
|
import { ShareInvitation, ShareUserStatus, StateShare } from '@joplin/lib/services/share/reducer';
|
||||||
|
import app from './app';
|
||||||
|
const Command = require('./command-share');
|
||||||
|
|
||||||
|
const setUpCommand = () => {
|
||||||
|
const output: string[] = [];
|
||||||
|
const stdout = (content: string) => {
|
||||||
|
output.push(...content.split('\n'));
|
||||||
|
};
|
||||||
|
|
||||||
|
const command = setupCommandForTesting(Command, stdout);
|
||||||
|
return { command, output };
|
||||||
|
};
|
||||||
|
|
||||||
|
const shareId = 'test-id';
|
||||||
|
const defaultFolderShare: StateShare = {
|
||||||
|
id: shareId,
|
||||||
|
type: ModelType.Folder,
|
||||||
|
folder_id: 'some-folder-id-here',
|
||||||
|
note_id: undefined,
|
||||||
|
master_key_id: undefined,
|
||||||
|
user: {
|
||||||
|
full_name: 'Test user',
|
||||||
|
email: 'test@localhost',
|
||||||
|
id: 'some-user-id',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockShareServiceForFolderSharing = (eventHandlerOverrides: Partial<ApiMock>&{ onExec?: undefined }) => {
|
||||||
|
const invitations: ShareInvitation[] = [];
|
||||||
|
|
||||||
|
mockShareService({
|
||||||
|
getShareInvitations: async () => ({
|
||||||
|
items: invitations,
|
||||||
|
}),
|
||||||
|
getShares: async () => ({ items: [defaultFolderShare] }),
|
||||||
|
getShareUsers: async (_id: string) => ({ items: [] }),
|
||||||
|
postShareUsers: async (_id, _body) => { },
|
||||||
|
postShares: async () => ({ id: shareId }),
|
||||||
|
...eventHandlerOverrides,
|
||||||
|
}, ShareService.instance(), app().store());
|
||||||
|
|
||||||
|
return {
|
||||||
|
addInvitation: (invitation: Partial<ShareInvitation>) => {
|
||||||
|
const defaultInvitation: ShareInvitation = {
|
||||||
|
share: defaultFolderShare,
|
||||||
|
id: 'some-invitation-id',
|
||||||
|
master_key: undefined,
|
||||||
|
status: ShareUserStatus.Waiting,
|
||||||
|
can_read: 1,
|
||||||
|
can_write: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
invitations.push({ ...defaultInvitation, ...invitation });
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
describe('command-share', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await setupDatabaseAndSynchronizer(1);
|
||||||
|
await switchClient(1);
|
||||||
|
await setupApplication();
|
||||||
|
BaseItem.shareService_ = ShareService.instance();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should allow adding a user to a share', async () => {
|
||||||
|
const folder = await Folder.save({ title: 'folder1' });
|
||||||
|
|
||||||
|
let lastShareUserUpdate: unknown|null = null;
|
||||||
|
mockShareServiceForFolderSharing({
|
||||||
|
getShares: async () => {
|
||||||
|
const isShared = !!lastShareUserUpdate;
|
||||||
|
if (isShared) {
|
||||||
|
return {
|
||||||
|
items: [{ ...defaultFolderShare, folder_id: folder.id }],
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return { items: [] };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// Called when a new user is added to a share
|
||||||
|
postShareUsers: async (_id, body) => {
|
||||||
|
lastShareUserUpdate = body;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { command } = setUpCommand();
|
||||||
|
|
||||||
|
// Should share read-write by default
|
||||||
|
await command.action({
|
||||||
|
'command': 'add',
|
||||||
|
'notebook': 'folder1',
|
||||||
|
'user': 'test@localhost',
|
||||||
|
options: {},
|
||||||
|
});
|
||||||
|
expect(lastShareUserUpdate).toMatchObject({
|
||||||
|
email: 'test@localhost',
|
||||||
|
can_write: 1,
|
||||||
|
can_read: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Should also support sharing as read only
|
||||||
|
await command.action({
|
||||||
|
'command': 'add',
|
||||||
|
'notebook': 'folder1',
|
||||||
|
'user': 'test2@localhost',
|
||||||
|
options: {
|
||||||
|
'read-only': true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(lastShareUserUpdate).toMatchObject({
|
||||||
|
email: 'test2@localhost',
|
||||||
|
can_write: 0,
|
||||||
|
can_read: 1,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.each([
|
||||||
|
{
|
||||||
|
label: 'should list a single pending invitation',
|
||||||
|
invitations: [{ id: 'test', status: ShareUserStatus.Waiting }],
|
||||||
|
expectedOutput: [
|
||||||
|
'Incoming shares:',
|
||||||
|
'\tWaiting: Notebook some-folder-id-here from test@localhost',
|
||||||
|
'All shared folders:',
|
||||||
|
'\tNone',
|
||||||
|
].join('\n'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'should list accepted invitations for non-existent folders with [None] as the folder title',
|
||||||
|
invitations: [
|
||||||
|
{ id: 'test2', status: ShareUserStatus.Accepted },
|
||||||
|
],
|
||||||
|
expectedOutput: [
|
||||||
|
'Incoming shares:',
|
||||||
|
'\tAccepted: Notebook [None] from test@localhost',
|
||||||
|
'All shared folders:',
|
||||||
|
'\tNone',
|
||||||
|
].join('\n'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'should not list rejected shares',
|
||||||
|
invitations: [
|
||||||
|
{ id: 'test3', status: ShareUserStatus.Rejected },
|
||||||
|
],
|
||||||
|
expectedOutput: [
|
||||||
|
'Incoming shares:',
|
||||||
|
'\tNone',
|
||||||
|
'All shared folders:',
|
||||||
|
'\tNone',
|
||||||
|
].join('\n'),
|
||||||
|
},
|
||||||
|
])('share invitations: $label', async ({ invitations, expectedOutput }) => {
|
||||||
|
const mock = mockShareServiceForFolderSharing({});
|
||||||
|
for (const invitation of invitations) {
|
||||||
|
mock.addInvitation(invitation);
|
||||||
|
}
|
||||||
|
|
||||||
|
await ShareService.instance().refreshShareInvitations();
|
||||||
|
|
||||||
|
const { command, output } = setUpCommand();
|
||||||
|
await command.action({
|
||||||
|
'command': 'list',
|
||||||
|
options: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(output.join('\n')).toBe(expectedOutput);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
297
packages/app-cli/app/command-share.ts
Normal file
297
packages/app-cli/app/command-share.ts
Normal file
@@ -0,0 +1,297 @@
|
|||||||
|
import { _ } from '@joplin/lib/locale';
|
||||||
|
import BaseCommand from './base-command';
|
||||||
|
import app from './app';
|
||||||
|
import { reg } from '@joplin/lib/registry';
|
||||||
|
import Logger from '@joplin/utils/Logger';
|
||||||
|
import ShareService from '@joplin/lib/services/share/ShareService';
|
||||||
|
import { ModelType } from '@joplin/lib/BaseModel';
|
||||||
|
import { FolderEntity } from '@joplin/lib/services/database/types';
|
||||||
|
import { ShareUserStatus } from '@joplin/lib/services/share/reducer';
|
||||||
|
import Folder from '@joplin/lib/models/Folder';
|
||||||
|
import invitationRespond from '@joplin/lib/services/share/invitationRespond';
|
||||||
|
import CommandService from '@joplin/lib/services/CommandService';
|
||||||
|
import { substrWithEllipsis } from '@joplin/lib/string-utils';
|
||||||
|
|
||||||
|
const logger = Logger.create('command-share');
|
||||||
|
|
||||||
|
type Args = {
|
||||||
|
command: string;
|
||||||
|
// eslint-disable-next-line id-denylist -- The "notebook" identifier comes from the UI.
|
||||||
|
notebook?: string;
|
||||||
|
user?: string;
|
||||||
|
options: {
|
||||||
|
'read-only'?: boolean;
|
||||||
|
json?: boolean;
|
||||||
|
force?: boolean;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const folderTitle = (folder: FolderEntity|null) => {
|
||||||
|
return folder ? substrWithEllipsis(folder.title, 0, 32) : _('[None]');
|
||||||
|
};
|
||||||
|
|
||||||
|
const getShareState = () => app().store().getState().shareService;
|
||||||
|
const getShareFromFolderId = (folderId: string) => {
|
||||||
|
const shareState = getShareState();
|
||||||
|
const allShares = shareState.shares;
|
||||||
|
const share = allShares.find(share => share.folder_id === folderId);
|
||||||
|
return share;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getShareUsers = (folderId: string) => {
|
||||||
|
const share = getShareFromFolderId(folderId);
|
||||||
|
if (!share) {
|
||||||
|
throw new Error(`No share found for folder ${folderId}`);
|
||||||
|
}
|
||||||
|
return getShareState().shareUsers[share.id];
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
class Command extends BaseCommand {
|
||||||
|
public usage() {
|
||||||
|
return 'share <command> [notebook] [user]';
|
||||||
|
}
|
||||||
|
|
||||||
|
public description() {
|
||||||
|
return [
|
||||||
|
_('Shares or unshares the specified [notebook] with [user]. Requires Joplin Cloud or Joplin Server.'),
|
||||||
|
_('Commands: `add`, `remove`, `list`, `delete`, `accept`, `leave`, and `reject`.'),
|
||||||
|
].join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
public options() {
|
||||||
|
return [
|
||||||
|
['--read-only', _('Don\'t allow the share recipient to write to the shared notebook. Valid only for the `add` subcommand.')],
|
||||||
|
['-f, --force', _('Do not ask for user confirmation.')],
|
||||||
|
['--json', _('Prefer JSON output.')],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public async action(args: Args) {
|
||||||
|
const commandShareAdd = async (folder: FolderEntity, email: string) => {
|
||||||
|
await reg.waitForSyncFinishedThenSync();
|
||||||
|
|
||||||
|
const share = await ShareService.instance().shareFolder(folder.id);
|
||||||
|
|
||||||
|
const permissions = {
|
||||||
|
can_read: 1,
|
||||||
|
can_write: args.options['read-only'] ? 0 : 1,
|
||||||
|
};
|
||||||
|
logger.debug('Sharing folder', folder.id, 'with', email, 'permissions=', permissions);
|
||||||
|
|
||||||
|
await ShareService.instance().addShareRecipient(share.id, share.master_key_id, email, permissions);
|
||||||
|
|
||||||
|
await ShareService.instance().refreshShares();
|
||||||
|
await ShareService.instance().refreshShareUsers(share.id);
|
||||||
|
|
||||||
|
await reg.waitForSyncFinishedThenSync();
|
||||||
|
};
|
||||||
|
|
||||||
|
const commandShareRemove = async (folder: FolderEntity, email: string) => {
|
||||||
|
await ShareService.instance().refreshShares();
|
||||||
|
|
||||||
|
const share = getShareFromFolderId(folder.id);
|
||||||
|
if (!share) {
|
||||||
|
throw new Error(`No share found for folder ${folder.id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
await ShareService.instance().refreshShareUsers(share.id);
|
||||||
|
|
||||||
|
const shareUsers = getShareUsers(folder.id);
|
||||||
|
if (!shareUsers) {
|
||||||
|
throw new Error(`No share found for folder ${folder.id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetUser = shareUsers.find(user => user.user?.email === email);
|
||||||
|
if (!targetUser) {
|
||||||
|
throw new Error(`No recipient found with email ${email}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
await ShareService.instance().deleteShareRecipient(targetUser.id);
|
||||||
|
this.stdout(_('Removed %s from share.', targetUser.user.email));
|
||||||
|
};
|
||||||
|
|
||||||
|
const commandShareList = async () => {
|
||||||
|
let folder = null;
|
||||||
|
if (args.notebook) {
|
||||||
|
folder = await app().loadItemOrFail(ModelType.Folder, args.notebook);
|
||||||
|
}
|
||||||
|
|
||||||
|
await ShareService.instance().maintenance();
|
||||||
|
|
||||||
|
if (folder) {
|
||||||
|
const share = getShareFromFolderId(folder.id);
|
||||||
|
await ShareService.instance().refreshShareUsers(share.id);
|
||||||
|
|
||||||
|
const shareUsers = getShareUsers(folder.id);
|
||||||
|
const output = {
|
||||||
|
folderTitle: folderTitle(folder),
|
||||||
|
sharedWith: (shareUsers ?? []).map(user => ({
|
||||||
|
email: user.user.email,
|
||||||
|
readOnly: user.can_read && !user.can_write,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (args.options.json) {
|
||||||
|
this.stdout(JSON.stringify(output));
|
||||||
|
} else {
|
||||||
|
this.stdout(_('Folder "%s" is shared with:', output.folderTitle));
|
||||||
|
for (const user of output.sharedWith) {
|
||||||
|
this.stdout(`\t${user.email}\t${user.readOnly ? _('(Read-only)') : ''}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const shareState = getShareState();
|
||||||
|
const output = {
|
||||||
|
invitations: shareState.shareInvitations.map(invitation => ({
|
||||||
|
accepted: invitation.status === ShareUserStatus.Accepted,
|
||||||
|
waiting: invitation.status === ShareUserStatus.Waiting,
|
||||||
|
rejected: invitation.status === ShareUserStatus.Rejected,
|
||||||
|
folderId: invitation.share.folder_id,
|
||||||
|
fromUser: {
|
||||||
|
email: invitation.share.user?.email,
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
shares: shareState.shares.map(share => ({
|
||||||
|
isFolder: !!share.folder_id,
|
||||||
|
isNote: !!share.note_id,
|
||||||
|
itemId: share.folder_id ?? share.note_id,
|
||||||
|
fromUser: {
|
||||||
|
email: share.user?.email,
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (args.options.json) {
|
||||||
|
this.stdout(JSON.stringify(output));
|
||||||
|
} else {
|
||||||
|
this.stdout(_('Incoming shares:'));
|
||||||
|
let loggedInvitation = false;
|
||||||
|
for (const invitation of output.invitations) {
|
||||||
|
let message;
|
||||||
|
if (invitation.waiting) {
|
||||||
|
message = _('Waiting: Notebook %s from %s', invitation.folderId, invitation.fromUser.email);
|
||||||
|
}
|
||||||
|
if (invitation.accepted) {
|
||||||
|
const folder = await Folder.load(invitation.folderId);
|
||||||
|
message = _('Accepted: Notebook %s from %s', folderTitle(folder), invitation.fromUser.email);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message) {
|
||||||
|
this.stdout(`\t${message}`);
|
||||||
|
loggedInvitation = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!loggedInvitation) {
|
||||||
|
this.stdout(`\t${_('None')}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.stdout(_('All shared folders:'));
|
||||||
|
if (output.shares.length) {
|
||||||
|
for (const share of output.shares) {
|
||||||
|
let title;
|
||||||
|
if (share.isFolder) {
|
||||||
|
title = folderTitle(await Folder.load(share.itemId));
|
||||||
|
} else {
|
||||||
|
title = share.itemId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (share.fromUser?.email) {
|
||||||
|
this.stdout(`\t${_('%s from %s', title, share.fromUser?.email)}`);
|
||||||
|
} else {
|
||||||
|
this.stdout(`\t${title} - ${share.itemId}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.stdout(`\t${_('None')}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const commandShareAcceptOrReject = async (folderId: string, accept: boolean) => {
|
||||||
|
await ShareService.instance().maintenance();
|
||||||
|
|
||||||
|
const shareState = getShareState();
|
||||||
|
const invitations = shareState.shareInvitations.filter(invitation => {
|
||||||
|
return invitation.share.folder_id === folderId && invitation.status === ShareUserStatus.Waiting;
|
||||||
|
});
|
||||||
|
if (invitations.length === 0) throw new Error('No such invitation found');
|
||||||
|
|
||||||
|
// If there are multiple invitations for the same folder, stop early to avoid
|
||||||
|
// accepting the wrong invitation.
|
||||||
|
if (invitations.length > 1) throw new Error('Multiple invitations found with the same ID');
|
||||||
|
|
||||||
|
const invitation = invitations[0];
|
||||||
|
|
||||||
|
this.stdout(accept ? _('Accepting share...') : _('Rejecting share...'));
|
||||||
|
await invitationRespond(invitation.id, invitation.share.folder_id, invitation.master_key, accept);
|
||||||
|
};
|
||||||
|
|
||||||
|
const commandShareAccept = (folderId: string) => (
|
||||||
|
commandShareAcceptOrReject(folderId, true)
|
||||||
|
);
|
||||||
|
|
||||||
|
const commandShareReject = (folderId: string) => (
|
||||||
|
commandShareAcceptOrReject(folderId, false)
|
||||||
|
);
|
||||||
|
|
||||||
|
const commandShareDelete = async (folder: FolderEntity) => {
|
||||||
|
const force = args.options.force;
|
||||||
|
const ok = force ? true : await this.prompt(
|
||||||
|
_('Unshare notebook "%s"? This may cause other users to lose access to the notebook.', folderTitle(folder)),
|
||||||
|
{ booleanAnswerDefault: 'n' },
|
||||||
|
);
|
||||||
|
if (!ok) return;
|
||||||
|
|
||||||
|
logger.info('Unsharing folder', folder.id);
|
||||||
|
await ShareService.instance().unshareFolder(folder.id);
|
||||||
|
await reg.scheduleSync();
|
||||||
|
};
|
||||||
|
|
||||||
|
if (args.command === 'add' || args.command === 'remove' || args.command === 'delete') {
|
||||||
|
if (!args.notebook) throw new Error('[notebook] is required');
|
||||||
|
const folder = await app().loadItemOrFail(ModelType.Folder, args.notebook);
|
||||||
|
|
||||||
|
if (args.command === 'delete') {
|
||||||
|
return commandShareDelete(folder);
|
||||||
|
} else {
|
||||||
|
if (!args.user) throw new Error('[user] is required');
|
||||||
|
|
||||||
|
const email = args.user;
|
||||||
|
if (args.command === 'add') {
|
||||||
|
return commandShareAdd(folder, email);
|
||||||
|
} else if (args.command === 'remove') {
|
||||||
|
return commandShareRemove(folder, email);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.command === 'leave') {
|
||||||
|
const folder = args.notebook ? await app().loadItemOrFail(ModelType.Folder, args.notebook) : null;
|
||||||
|
|
||||||
|
await ShareService.instance().maintenance();
|
||||||
|
|
||||||
|
return CommandService.instance().execute(
|
||||||
|
'leaveSharedFolder', folder?.id, { force: args.options.force },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.command === 'list') {
|
||||||
|
return commandShareList();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.command === 'accept') {
|
||||||
|
return commandShareAccept(args.notebook);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.command === 'reject') {
|
||||||
|
return commandShareReject(args.notebook);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Unknown subcommand: ${args.command}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Command;
|
||||||
@@ -22,7 +22,7 @@ const Setting = require('@joplin/lib/models/Setting').default;
|
|||||||
const Revision = require('@joplin/lib/models/Revision').default;
|
const Revision = require('@joplin/lib/models/Revision').default;
|
||||||
const Logger = require('@joplin/utils/Logger').default;
|
const Logger = require('@joplin/utils/Logger').default;
|
||||||
const FsDriverNode = require('@joplin/lib/fs-driver-node').default;
|
const FsDriverNode = require('@joplin/lib/fs-driver-node').default;
|
||||||
const { shimInit } = require('@joplin/lib/shim-init-node.js');
|
const shimInitCli = require('./utils/shimInitCli').default;
|
||||||
const shim = require('@joplin/lib/shim').default;
|
const shim = require('@joplin/lib/shim').default;
|
||||||
const { _ } = require('@joplin/lib/locale');
|
const { _ } = require('@joplin/lib/locale');
|
||||||
const FileApiDriverLocal = require('@joplin/lib/file-api-driver-local').default;
|
const FileApiDriverLocal = require('@joplin/lib/file-api-driver-local').default;
|
||||||
@@ -73,7 +73,7 @@ function appVersion() {
|
|||||||
return p.version;
|
return p.version;
|
||||||
}
|
}
|
||||||
|
|
||||||
shimInit({ sharp, keytar, appVersion, nodeSqlite });
|
shimInitCli({ sharp, keytar, appVersion, nodeSqlite });
|
||||||
|
|
||||||
const logger = new Logger();
|
const logger = new Logger();
|
||||||
Logger.initializeGlobalLogger(logger);
|
Logger.initializeGlobalLogger(logger);
|
||||||
|
|||||||
14
packages/app-cli/app/utils/initializeCommandService.ts
Normal file
14
packages/app-cli/app/utils/initializeCommandService.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import CommandService from '@joplin/lib/services/CommandService';
|
||||||
|
import stateToWhenClauseContext from '@joplin/lib/services/commands/stateToWhenClauseContext';
|
||||||
|
import libCommands from '@joplin/lib/commands/index';
|
||||||
|
import { State } from '@joplin/lib/reducer';
|
||||||
|
import { Store } from 'redux';
|
||||||
|
|
||||||
|
export default function initializeCommandService(store: Store<State>, devMode: boolean) {
|
||||||
|
CommandService.instance().initialize(store, devMode, stateToWhenClauseContext);
|
||||||
|
|
||||||
|
for (const command of libCommands) {
|
||||||
|
CommandService.instance().registerDeclaration(command.declaration);
|
||||||
|
CommandService.instance().registerRuntime(command.declaration.name, command.runtime());
|
||||||
|
}
|
||||||
|
}
|
||||||
32
packages/app-cli/app/utils/shimInitCli.ts
Normal file
32
packages/app-cli/app/utils/shimInitCli.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import shim, { ShowMessageBoxOptions } from '@joplin/lib/shim';
|
||||||
|
import type { ShimInitOptions } from '@joplin/lib/shim-init-node';
|
||||||
|
import app from '../app';
|
||||||
|
import { _ } from '@joplin/lib/locale';
|
||||||
|
const { shimInit } = require('@joplin/lib/shim-init-node.js');
|
||||||
|
|
||||||
|
const shimInitCli = (options: ShimInitOptions) => {
|
||||||
|
shimInit(options);
|
||||||
|
|
||||||
|
shim.showMessageBox = async (message: string, options: ShowMessageBoxOptions) => {
|
||||||
|
const gui = app()?.gui();
|
||||||
|
let answers = options.buttons ?? [_('Ok'), _('Cancel')];
|
||||||
|
|
||||||
|
if (options.type === 'error' || options.type === 'info') {
|
||||||
|
answers = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
message += answers.length ? `(${answers.join(', ')})` : '';
|
||||||
|
|
||||||
|
const answer = await gui.prompt(options.title ?? '', `${message} `, { answers });
|
||||||
|
|
||||||
|
if (answers.includes(answer)) {
|
||||||
|
return answers.indexOf(answer);
|
||||||
|
} else if (answer) {
|
||||||
|
return answers.findIndex(a => a.startsWith(answer));
|
||||||
|
}
|
||||||
|
|
||||||
|
return -1;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default shimInitCli;
|
||||||
@@ -15,4 +15,7 @@ export const setupApplication = async () => {
|
|||||||
// such notebook.
|
// such notebook.
|
||||||
await Folder.save({ title: 'default' });
|
await Folder.save({ title: 'default' });
|
||||||
await app().refreshCurrentFolder();
|
await app().refreshCurrentFolder();
|
||||||
|
|
||||||
|
// Some tests also need access to the Redux store
|
||||||
|
app().initRedux();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -55,6 +55,7 @@
|
|||||||
"node-rsa": "1.1.1",
|
"node-rsa": "1.1.1",
|
||||||
"open": "8.4.2",
|
"open": "8.4.2",
|
||||||
"proper-lockfile": "4.1.2",
|
"proper-lockfile": "4.1.2",
|
||||||
|
"redux": "4.2.1",
|
||||||
"server-destroy": "1.0.1",
|
"server-destroy": "1.0.1",
|
||||||
"sharp": "0.33.5",
|
"sharp": "0.33.5",
|
||||||
"sprintf-js": "1.1.3",
|
"sprintf-js": "1.1.3",
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import * as editAlarm from './editAlarm';
|
|||||||
import * as exportPdf from './exportPdf';
|
import * as exportPdf from './exportPdf';
|
||||||
import * as gotoAnything from './gotoAnything';
|
import * as gotoAnything from './gotoAnything';
|
||||||
import * as hideModalMessage from './hideModalMessage';
|
import * as hideModalMessage from './hideModalMessage';
|
||||||
import * as leaveSharedFolder from './leaveSharedFolder';
|
|
||||||
import * as linkToNote from './linkToNote';
|
import * as linkToNote from './linkToNote';
|
||||||
import * as moveToFolder from './moveToFolder';
|
import * as moveToFolder from './moveToFolder';
|
||||||
import * as newFolder from './newFolder';
|
import * as newFolder from './newFolder';
|
||||||
@@ -56,7 +55,6 @@ const index: any[] = [
|
|||||||
exportPdf,
|
exportPdf,
|
||||||
gotoAnything,
|
gotoAnything,
|
||||||
hideModalMessage,
|
hideModalMessage,
|
||||||
leaveSharedFolder,
|
|
||||||
linkToNote,
|
linkToNote,
|
||||||
moveToFolder,
|
moveToFolder,
|
||||||
newFolder,
|
newFolder,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import * as deleteNote from './deleteNote';
|
import * as deleteNote from './deleteNote';
|
||||||
import * as historyBackward from './historyBackward';
|
import * as historyBackward from './historyBackward';
|
||||||
import * as historyForward from './historyForward';
|
import * as historyForward from './historyForward';
|
||||||
|
import * as leaveSharedFolder from './leaveSharedFolder';
|
||||||
import * as openMasterPasswordDialog from './openMasterPasswordDialog';
|
import * as openMasterPasswordDialog from './openMasterPasswordDialog';
|
||||||
import * as permanentlyDeleteNote from './permanentlyDeleteNote';
|
import * as permanentlyDeleteNote from './permanentlyDeleteNote';
|
||||||
import * as renderMarkup from './renderMarkup';
|
import * as renderMarkup from './renderMarkup';
|
||||||
@@ -14,6 +15,7 @@ const index: any[] = [
|
|||||||
deleteNote,
|
deleteNote,
|
||||||
historyBackward,
|
historyBackward,
|
||||||
historyForward,
|
historyForward,
|
||||||
|
leaveSharedFolder,
|
||||||
openMasterPasswordDialog,
|
openMasterPasswordDialog,
|
||||||
permanentlyDeleteNote,
|
permanentlyDeleteNote,
|
||||||
renderMarkup,
|
renderMarkup,
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { CommandRuntime, CommandDeclaration, CommandContext } from '@joplin/lib/services/CommandService';
|
import { CommandRuntime, CommandDeclaration, CommandContext } from '../services/CommandService';
|
||||||
import { _ } from '@joplin/lib/locale';
|
import { _ } from '../locale';
|
||||||
import ShareService from '@joplin/lib/services/share/ShareService';
|
import ShareService from '../services/share/ShareService';
|
||||||
import Logger from '@joplin/utils/Logger';
|
import Logger from '@joplin/utils/Logger';
|
||||||
import shim from '@joplin/lib/shim';
|
import shim from '../shim';
|
||||||
|
|
||||||
const logger = Logger.create('leaveSharedFolder');
|
const logger = Logger.create('leaveSharedFolder');
|
||||||
|
|
||||||
@@ -11,10 +11,16 @@ export const declaration: CommandDeclaration = {
|
|||||||
label: () => _('Leave notebook...'),
|
label: () => _('Leave notebook...'),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
interface Options {
|
||||||
|
force?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export const runtime = (): CommandRuntime => {
|
export const runtime = (): CommandRuntime => {
|
||||||
return {
|
return {
|
||||||
execute: async (_context: CommandContext, folderId: string = null) => {
|
execute: async (_context: CommandContext, folderId: string = null, { force = false }: Options = {}) => {
|
||||||
const answer = await shim.showConfirmationDialog(_('This will remove the notebook from your collection and you will no longer have access to its content. Do you wish to continue?'));
|
const answer = force ? true : await shim.showConfirmationDialog(
|
||||||
|
_('This will remove the notebook from your collection and you will no longer have access to its content. Do you wish to continue?'),
|
||||||
|
);
|
||||||
if (!answer) return;
|
if (!answer) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -98,7 +98,7 @@ function setupProxySettings(options: any) {
|
|||||||
proxySettings.proxyUrl = options.proxyUrl;
|
proxySettings.proxyUrl = options.proxyUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ShimInitOptions {
|
export interface ShimInitOptions {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||||
sharp: any;
|
sharp: any;
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied
|
||||||
|
|||||||
@@ -3,20 +3,33 @@ import reducer, { State, defaultState } from '../../reducer';
|
|||||||
import ShareService from '../../services/share/ShareService';
|
import ShareService from '../../services/share/ShareService';
|
||||||
import { encryptionService } from '../test-utils';
|
import { encryptionService } from '../test-utils';
|
||||||
import JoplinServerApi, { ExecOptions } from '../../JoplinServerApi';
|
import JoplinServerApi, { ExecOptions } from '../../JoplinServerApi';
|
||||||
import { ShareInvitation, StateShare } from '../../services/share/reducer';
|
import { ShareInvitation, StateShare, StateShareUser } from '../../services/share/reducer';
|
||||||
|
|
||||||
const testReducer = (state = defaultState, action: unknown) => {
|
const testReducer = (state = defaultState, action: unknown) => {
|
||||||
return reducer(state, action);
|
return reducer(state, action);
|
||||||
};
|
};
|
||||||
|
|
||||||
type Query = Record<string, unknown>;
|
interface ShareStateResponse {
|
||||||
type OnShareGetListener = (query: Query)=> Promise<{ items: Partial<StateShare>[] }>;
|
items: Partial<StateShare>[];
|
||||||
type OnSharePostListener = (query: Query)=> Promise<{ id: string }>;
|
}
|
||||||
type OnInvitationGetListener = (query: Query)=> Promise<{ items: Partial<ShareInvitation>[] }>;
|
interface ShareInvitationResponse {
|
||||||
|
items: Partial<ShareInvitation>[];
|
||||||
|
}
|
||||||
|
interface ShareUsersResponse {
|
||||||
|
items: Partial<StateShareUser>[];
|
||||||
|
}
|
||||||
|
|
||||||
|
type Json = Record<string, unknown>;
|
||||||
|
type OnShareGetListener = (query: Json)=> Promise<ShareStateResponse>;
|
||||||
|
type OnSharePostListener = (query: Json)=> Promise<{ id: string }>;
|
||||||
|
type OnInvitationGetListener = (query: Json)=> Promise<ShareInvitationResponse>;
|
||||||
|
type OnShareUsersGetListener = (shareId: string)=> Promise<ShareUsersResponse>;
|
||||||
|
type OnShareUsersPostListener = (shareId: string, body: Json)=> Promise<void>;
|
||||||
|
|
||||||
type OnApiExecListener = (
|
type OnApiExecListener = (
|
||||||
method: string,
|
method: string,
|
||||||
path: string,
|
path: string,
|
||||||
query: Query,
|
query: Json,
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Needs to interface with old code from before the rule was applied
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Needs to interface with old code from before the rule was applied
|
||||||
body: any,
|
body: any,
|
||||||
headers: Record<string, unknown>,
|
headers: Record<string, unknown>,
|
||||||
@@ -27,6 +40,8 @@ export type ApiMock = {
|
|||||||
getShares: OnShareGetListener;
|
getShares: OnShareGetListener;
|
||||||
postShares: OnSharePostListener;
|
postShares: OnSharePostListener;
|
||||||
getShareInvitations: OnInvitationGetListener;
|
getShareInvitations: OnInvitationGetListener;
|
||||||
|
getShareUsers?: OnShareUsersGetListener;
|
||||||
|
postShareUsers?: OnShareUsersPostListener;
|
||||||
onUnhandled?: OnApiExecListener;
|
onUnhandled?: OnApiExecListener;
|
||||||
|
|
||||||
onExec?: undefined;
|
onExec?: undefined;
|
||||||
@@ -37,6 +52,8 @@ export type ApiMock = {
|
|||||||
getShareInvitations?: undefined;
|
getShareInvitations?: undefined;
|
||||||
getShares?: undefined;
|
getShares?: undefined;
|
||||||
postShares?: undefined;
|
postShares?: undefined;
|
||||||
|
getShareUsers?: undefined;
|
||||||
|
postShareUsers?: undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Initializes a share service with mocks
|
// Initializes a share service with mocks
|
||||||
@@ -57,6 +74,16 @@ const mockShareService = (apiCallHandler: ApiMock, service?: ShareService, store
|
|||||||
return apiCallHandler.getShareInvitations(query);
|
return apiCallHandler.getShareInvitations(query);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const shareUsersMatch = path.match(/^api\/shares\/([^/]+)\/users$/);
|
||||||
|
const shareId = shareUsersMatch?.[1];
|
||||||
|
if (shareId) {
|
||||||
|
if (method === 'GET' && apiCallHandler.getShareUsers) {
|
||||||
|
return apiCallHandler.getShareUsers(shareId);
|
||||||
|
}
|
||||||
|
if (method === 'POST' && apiCallHandler.postShareUsers) {
|
||||||
|
return apiCallHandler.postShareUsers(shareId, body);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (apiCallHandler.onUnhandled) {
|
if (apiCallHandler.onUnhandled) {
|
||||||
return apiCallHandler.onUnhandled(method, path, query, body, headers, options);
|
return apiCallHandler.onUnhandled(method, path, query, body, headers, options);
|
||||||
|
|||||||
@@ -32561,6 +32561,7 @@ __metadata:
|
|||||||
node-rsa: "npm:1.1.1"
|
node-rsa: "npm:1.1.1"
|
||||||
open: "npm:8.4.2"
|
open: "npm:8.4.2"
|
||||||
proper-lockfile: "npm:4.1.2"
|
proper-lockfile: "npm:4.1.2"
|
||||||
|
redux: "npm:4.2.1"
|
||||||
server-destroy: "npm:1.0.1"
|
server-destroy: "npm:1.0.1"
|
||||||
sharp: "npm:0.33.5"
|
sharp: "npm:0.33.5"
|
||||||
sprintf-js: "npm:1.1.3"
|
sprintf-js: "npm:1.1.3"
|
||||||
|
|||||||
Reference in New Issue
Block a user