1
0
mirror of https://github.com/laurent22/joplin.git synced 2024-12-21 09:38:01 +02:00

Desktop: Add Share Notebook menu item

This commit is contained in:
Laurent Cozic 2021-05-16 15:21:55 +02:00
parent 12cc64008b
commit 6f2f24171d
11 changed files with 153 additions and 22 deletions

View File

@ -1106,6 +1106,9 @@ packages/lib/services/UndoRedoService.js.map
packages/lib/services/WhenClause.d.ts packages/lib/services/WhenClause.d.ts
packages/lib/services/WhenClause.js packages/lib/services/WhenClause.js
packages/lib/services/WhenClause.js.map packages/lib/services/WhenClause.js.map
packages/lib/services/WhenClause.test.d.ts
packages/lib/services/WhenClause.test.js
packages/lib/services/WhenClause.test.js.map
packages/lib/services/commands/MenuUtils.d.ts packages/lib/services/commands/MenuUtils.d.ts
packages/lib/services/commands/MenuUtils.js packages/lib/services/commands/MenuUtils.js
packages/lib/services/commands/MenuUtils.js.map packages/lib/services/commands/MenuUtils.js.map

View File

@ -76,7 +76,7 @@ module.exports = {
// Warn only for now because fixing everything would take too much // Warn only for now because fixing everything would take too much
// refactoring, but new code should try to stick to it. // refactoring, but new code should try to stick to it.
'complexity': ['warn', { max: 10 }], // 'complexity': ['warn', { max: 10 }],
// Checks rules of Hooks // Checks rules of Hooks
'react-hooks/rules-of-hooks': 'error', 'react-hooks/rules-of-hooks': 'error',

3
.gitignore vendored
View File

@ -1092,6 +1092,9 @@ packages/lib/services/UndoRedoService.js.map
packages/lib/services/WhenClause.d.ts packages/lib/services/WhenClause.d.ts
packages/lib/services/WhenClause.js packages/lib/services/WhenClause.js
packages/lib/services/WhenClause.js.map packages/lib/services/WhenClause.js.map
packages/lib/services/WhenClause.test.d.ts
packages/lib/services/WhenClause.test.js
packages/lib/services/WhenClause.test.js.map
packages/lib/services/commands/MenuUtils.d.ts packages/lib/services/commands/MenuUtils.d.ts
packages/lib/services/commands/MenuUtils.js packages/lib/services/commands/MenuUtils.js
packages/lib/services/commands/MenuUtils.js.map packages/lib/services/commands/MenuUtils.js.map

View File

@ -18,6 +18,6 @@ export const runtime = (comp: any): CommandRuntime => {
}, },
}); });
}, },
enabledCondition: 'folderIsShareRootAndOwnedByUser || !folderIsShared', enabledCondition: 'joplinServerConnected && (folderIsShareRootAndOwnedByUser || !folderIsShared)',
}; };
}; };

View File

@ -18,5 +18,6 @@ export const runtime = (comp: any): CommandRuntime => {
}, },
}); });
}, },
enabledCondition: 'joplinServerConnected && oneNoteSelected',
}; };
}; };

View File

@ -695,11 +695,18 @@ function useMenu(props: Props) {
}, },
], ],
}, },
folder: {
label: _('Note&book'),
submenu: [
menuItemDic.showShareFolderDialog,
],
},
note: { note: {
label: _('&Note'), label: _('&Note'),
submenu: [ submenu: [
menuItemDic.toggleExternalEditing, menuItemDic.toggleExternalEditing,
menuItemDic.setTags, menuItemDic.setTags,
menuItemDic.showShareNoteDialog,
separator(), separator(),
menuItemDic.showNoteContentProperties, menuItemDic.showNoteContentProperties,
], ],
@ -818,6 +825,7 @@ function useMenu(props: Props) {
rootMenus.edit, rootMenus.edit,
rootMenus.view, rootMenus.view,
rootMenus.go, rootMenus.go,
rootMenus.folder,
rootMenus.note, rootMenus.note,
rootMenus.tools, rootMenus.tools,
rootMenus.help, rootMenus.help,

View File

@ -45,5 +45,7 @@ export default function() {
'editor.swapLineUp', 'editor.swapLineUp',
'editor.swapLineDown', 'editor.swapLineDown',
'toggleSafeMode', 'toggleSafeMode',
'showShareNoteDialog',
'showShareFolderDialog',
]; ];
} }

View File

@ -0,0 +1,39 @@
import WhenClause from './WhenClause';
describe('WhenClause', function() {
test('should work with simple condition', async function() {
const wc = new WhenClause('test1 && test2');
expect(wc.evaluate({
test1: true,
test2: true,
})).toBe(true);
expect(wc.evaluate({
test1: true,
test2: false,
})).toBe(false);
});
test('should work with parenthesis', async function() {
const wc = new WhenClause('(test1 && test2) || test3 && (test4 && !test5)');
expect(wc.evaluate({
test1: true,
test2: true,
test3: true,
test4: true,
test5: true,
})).toBe(true);
expect(wc.evaluate({
test1: false,
test2: true,
test3: false,
test4: false,
test5: true,
})).toBe(false);
});
});

View File

@ -1,17 +1,71 @@
import { ContextKeyExpr, ContextKeyExpression } from './contextkey/contextkey'; import { ContextKeyExpr, ContextKeyExpression, IContext } from './contextkey/contextkey';
// We would like to support expressions with brackets but VSCode When Clauses
// don't support this. To support this, we split the expressions with brackets
// into sub-expressions, which can then be parsed and executed separately by the
// When Clause library.
interface AdvancedExpression {
// (test1 && test2) || test3
original: string;
// __sub_1 || test3
compiledText: string;
// { __sub_1: "test1 && test2" }
subExpressions: any;
}
function parseAdvancedExpression(advancedExpression: string): AdvancedExpression {
let subExpressionIndex = -1;
let subExpressions: string = '';
let currentSubExpressionKey = '';
const subContext: any = {};
let inBrackets = false;
for (let i = 0; i < advancedExpression.length; i++) {
const c = advancedExpression[i];
if (c === '(') {
if (inBrackets) throw new Error('Nested brackets not supported');
inBrackets = true;
subExpressionIndex++;
currentSubExpressionKey = `__sub_${subExpressionIndex}`;
subContext[currentSubExpressionKey] = '';
continue;
}
if (c === ')') {
if (!inBrackets) throw new Error('Closing bracket without an opening one');
inBrackets = false;
subExpressions += currentSubExpressionKey;
currentSubExpressionKey = '';
continue;
}
if (inBrackets) {
subContext[currentSubExpressionKey] += c;
} else {
subExpressions += c;
}
}
return {
compiledText: subExpressions,
subExpressions: subContext,
original: advancedExpression,
};
}
export default class WhenClause { export default class WhenClause {
private expression_: string; private expression_: AdvancedExpression;
private validate_: boolean; private validate_: boolean;
private rules_: ContextKeyExpression = null; private ruleCache_: Record<string, ContextKeyExpression> = {};
constructor(expression: string, validate: boolean) { public constructor(expression: string, validate: boolean = true) {
this.expression_ = expression; this.expression_ = parseAdvancedExpression(expression);
this.validate_ = validate; this.validate_ = validate;
} }
private createContext(ctx: any) { private createContext(ctx: any): IContext {
return { return {
getValue: (key: string) => { getValue: (key: string) => {
return ctx[key]; return ctx[key];
@ -19,21 +73,28 @@ export default class WhenClause {
}; };
} }
private get rules(): ContextKeyExpression { private rules(exp: string): ContextKeyExpression {
if (!this.rules_) { if (this.ruleCache_[exp]) return this.ruleCache_[exp];
this.rules_ = ContextKeyExpr.deserialize(this.expression_); this.ruleCache_[exp] = ContextKeyExpr.deserialize(exp);
} return this.ruleCache_[exp];
return this.rules_;
} }
public evaluate(context: any): boolean { public evaluate(context: any): boolean {
if (this.validate_) this.validate(context); if (this.validate_) this.validate(context);
return this.rules.evaluate(this.createContext(context));
const subContext: any = {};
for (const k in this.expression_.subExpressions) {
const subExp = this.expression_.subExpressions[k];
subContext[k] = this.rules(subExp).evaluate(this.createContext(context));
}
const fullContext = { ...context, ...subContext };
return this.rules(this.expression_.compiledText).evaluate(this.createContext(fullContext));
} }
public validate(context: any) { public validate(context: any) {
const keys = this.rules.keys(); const keys = this.rules(this.expression_.original.replace(/[()]/g, ' ')).keys();
for (const key of keys) { for (const key of keys) {
if (!(key in context)) throw new Error(`No such key: ${key}`); if (!(key in context)) throw new Error(`No such key: ${key}`);
} }

View File

@ -27,6 +27,7 @@ export interface WhenClauseContext {
noteIsHtml: boolean; noteIsHtml: boolean;
folderIsShareRootAndOwnedByUser: boolean; folderIsShareRootAndOwnedByUser: boolean;
folderIsShared: boolean; folderIsShared: boolean;
joplinServerConnected: boolean;
} }
export default function stateToWhenClauseContext(state: State, options: WhenClauseContextOptions = null): WhenClauseContext { export default function stateToWhenClauseContext(state: State, options: WhenClauseContextOptions = null): WhenClauseContext {
@ -42,7 +43,7 @@ export default function stateToWhenClauseContext(state: State, options: WhenClau
// const commandNoteId = options.commandNoteId || selectedNoteId; // const commandNoteId = options.commandNoteId || selectedNoteId;
// const commandNote:NoteEntity = commandNoteId ? BaseModel.byId(state.notes, commandNoteId) : null; // const commandNote:NoteEntity = commandNoteId ? BaseModel.byId(state.notes, commandNoteId) : null;
const commandFolderId = options.commandFolderId; const commandFolderId = options.commandFolderId || state.selectedFolderId;
const commandFolder: FolderEntity = commandFolderId ? BaseModel.byId(state.folders, commandFolderId) : null; const commandFolder: FolderEntity = commandFolderId ? BaseModel.byId(state.folders, commandFolderId) : null;
return { return {
@ -75,5 +76,7 @@ export default function stateToWhenClauseContext(state: State, options: WhenClau
// Current context folder // Current context folder
folderIsShareRootAndOwnedByUser: commandFolder ? isRootSharedFolder(commandFolder) && isSharedFolderOwner(state, commandFolder.id) : false, folderIsShareRootAndOwnedByUser: commandFolder ? isRootSharedFolder(commandFolder) && isSharedFolderOwner(state, commandFolder.id) : false,
folderIsShared: commandFolder ? !!commandFolder.share_id : false, folderIsShared: commandFolder ? !!commandFolder.share_id : false,
joplinServerConnected: state.settings['sync.target'] === 9,
}; };
} }

View File

@ -27,11 +27,12 @@ export interface Command {
execute(...args: any[]): Promise<any | void>; execute(...args: any[]): Promise<any | void>;
/** /**
* Defines whether the command should be enabled or disabled, which in turns affects * Defines whether the command should be enabled or disabled, which in turns
* the enabled state of any associated button or menu item. * affects the enabled state of any associated button or menu item.
* *
* The condition should be expressed as a "when-clause" (as in Visual Studio Code). It's a simple boolean expression that evaluates to * The condition should be expressed as a "when-clause" (as in Visual Studio
* `true` or `false`. It supports the following operators: * Code). It's a simple boolean expression that evaluates to `true` or
* `false`. It supports the following operators:
* *
* Operator | Symbol | Example * Operator | Symbol | Example
* -- | -- | -- * -- | -- | --
@ -40,7 +41,17 @@ export interface Command {
* Or | \|\| | "noteIsTodo \|\| noteTodoCompleted" * Or | \|\| | "noteIsTodo \|\| noteTodoCompleted"
* And | && | "oneNoteSelected && !inConflictFolder" * And | && | "oneNoteSelected && !inConflictFolder"
* *
* Currently the supported context variables aren't documented, but you can [find the list here](https://github.com/laurent22/joplin/blob/dev/packages/lib/services/commands/stateToWhenClauseContext.ts). * Joplin, unlike VSCode, also supports parenthesis, which allows creating
* more complex expressions such as `cond1 || (cond2 && cond3)`. Only one
* level of parenthesis is possible (nested ones aren't supported).
*
* Currently the supported context variables aren't documented, but you can
* find the list below:
*
* - [Global When
* Clauses](https://github.com/laurent22/joplin/blob/dev/packages/lib/services/commands/stateToWhenClauseContext.ts).
* - [Desktop app When
* Clauses](https://github.com/laurent22/joplin/blob/dev/packages/app-desktop/services/commands/stateToWhenClauseContext.ts).
* *
* Note: Commands are enabled by default unless you use this property. * Note: Commands are enabled by default unless you use this property.
*/ */