feat: Added command palette for quick actions and switching between items (#2933) [skip e2e]
* wip: command palette * use code instead of key * show recent items above commands * refactor * fix command * add placeholder * Tab/Shift-Tab to switch tabs * Fix test * Add menu item to general account menu * if shortcut_id is available, use that as the id * make toggle fn more stable * small naming changes * fix name * Close open modals and popovers when opening command palette * use stable ids + make sure selectedNotesCount only changes when the count actually changes * display all commands, even ones in recents list
This commit is contained in:
Binary file not shown.
BIN
.yarn/cache/@ariakit-core-npm-0.4.15-411f831ec8-add800c855.zip
vendored
Normal file
BIN
.yarn/cache/@ariakit-core-npm-0.4.15-411f831ec8-add800c855.zip
vendored
Normal file
Binary file not shown.
Binary file not shown.
BIN
.yarn/cache/@ariakit-react-core-npm-0.4.18-d72e68fa75-53a012f087.zip
vendored
Normal file
BIN
.yarn/cache/@ariakit-react-core-npm-0.4.18-d72e68fa75-53a012f087.zip
vendored
Normal file
Binary file not shown.
Binary file not shown.
BIN
.yarn/cache/@ariakit-react-npm-0.4.18-26fc12aa5e-e8bc2df82f.zip
vendored
Normal file
BIN
.yarn/cache/@ariakit-react-npm-0.4.18-26fc12aa5e-e8bc2df82f.zip
vendored
Normal file
Binary file not shown.
@@ -128,6 +128,8 @@ export interface ItemManagerInterface extends AbstractService {
|
|||||||
getDisplayableFiles(): FileItem[]
|
getDisplayableFiles(): FileItem[]
|
||||||
setVaultDisplayOptions(options: VaultDisplayOptions): void
|
setVaultDisplayOptions(options: VaultDisplayOptions): void
|
||||||
numberOfNotesWithConflicts(): number
|
numberOfNotesWithConflicts(): number
|
||||||
|
/** Returns all notes, files, tags and views */
|
||||||
|
getInteractableItems(): DecryptedItemInterface[]
|
||||||
getDisplayableNotes(): SNNote[]
|
getDisplayableNotes(): SNNote[]
|
||||||
getDisplayableNotesAndFiles(): (SNNote | FileItem)[]
|
getDisplayableNotesAndFiles(): (SNNote | FileItem)[]
|
||||||
setPrimaryItemDisplayOptions(options: NotesAndFilesDisplayControllerOptions): void
|
setPrimaryItemDisplayOptions(options: NotesAndFilesDisplayControllerOptions): void
|
||||||
|
|||||||
@@ -220,6 +220,17 @@ export class ItemManager extends Services.AbstractService implements Services.It
|
|||||||
this.itemCounter.setVaultDisplayOptions(options)
|
this.itemCounter.setVaultDisplayOptions(options)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getInteractableItems(): Models.DecryptedItemInterface[] {
|
||||||
|
return (this.systemSmartViews as Models.DecryptedItemInterface[]).concat(
|
||||||
|
this.collection.all([
|
||||||
|
ContentType.TYPES.Note,
|
||||||
|
ContentType.TYPES.File,
|
||||||
|
ContentType.TYPES.Tag,
|
||||||
|
ContentType.TYPES.SmartView,
|
||||||
|
]) as Models.DecryptedItemInterface[],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
public getDisplayableNotes(): Models.SNNote[] {
|
public getDisplayableNotes(): Models.SNNote[] {
|
||||||
assert(this.navigationDisplayController.contentTypes.length === 2)
|
assert(this.navigationDisplayController.contentTypes.length === 2)
|
||||||
|
|
||||||
|
|||||||
@@ -41,3 +41,4 @@ export const OPEN_PREFERENCES_COMMAND = createKeyboardCommand('OPEN_PREFERENCES_
|
|||||||
export const CHANGE_EDITOR_WIDTH_COMMAND = createKeyboardCommand('CHANGE_EDITOR_WIDTH_COMMAND')
|
export const CHANGE_EDITOR_WIDTH_COMMAND = createKeyboardCommand('CHANGE_EDITOR_WIDTH_COMMAND')
|
||||||
|
|
||||||
export const TOGGLE_KEYBOARD_SHORTCUTS_MODAL = createKeyboardCommand('TOGGLE_KEYBOARD_SHORTCUTS_MODAL')
|
export const TOGGLE_KEYBOARD_SHORTCUTS_MODAL = createKeyboardCommand('TOGGLE_KEYBOARD_SHORTCUTS_MODAL')
|
||||||
|
export const TOGGLE_COMMAND_PALETTE = createKeyboardCommand('TOGGLE_COMMAND_PALETTE')
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Environment, Platform } from '@standardnotes/snjs'
|
import { Environment, Platform, UuidGenerator } from '@standardnotes/snjs'
|
||||||
import { eventMatchesKeyAndModifiers } from './eventMatchesKeyAndModifiers'
|
import { eventMatchesKeyAndModifiers } from './eventMatchesKeyAndModifiers'
|
||||||
import { KeyboardCommand } from './KeyboardCommands'
|
import { KeyboardCommand } from './KeyboardCommands'
|
||||||
import { KeyboardKeyEvent } from './KeyboardKeyEvent'
|
import { KeyboardKeyEvent } from './KeyboardKeyEvent'
|
||||||
@@ -28,6 +28,20 @@ export class KeyboardService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private isDisabled = false
|
||||||
|
/**
|
||||||
|
* When called, the service will stop triggering command handlers
|
||||||
|
* on keydown/keyup events. Useful when you need to handle events
|
||||||
|
* yourself while keeping the rest of behaviours inert.
|
||||||
|
* Make sure to call {@link enableEventHandling} once done.
|
||||||
|
*/
|
||||||
|
public disableEventHandling() {
|
||||||
|
this.isDisabled = true
|
||||||
|
}
|
||||||
|
public enableEventHandling() {
|
||||||
|
this.isDisabled = false
|
||||||
|
}
|
||||||
|
|
||||||
get isMac() {
|
get isMac() {
|
||||||
return this.platform === Platform.MacDesktop || this.platform === Platform.MacWeb
|
return this.platform === Platform.MacDesktop || this.platform === Platform.MacWeb
|
||||||
}
|
}
|
||||||
@@ -116,6 +130,9 @@ export class KeyboardService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private handleKeyboardEvent(event: KeyboardEvent, keyEvent: KeyboardKeyEvent): void {
|
private handleKeyboardEvent(event: KeyboardEvent, keyEvent: KeyboardKeyEvent): void {
|
||||||
|
if (this.isDisabled) {
|
||||||
|
return
|
||||||
|
}
|
||||||
for (const command of this.commandMap.keys()) {
|
for (const command of this.commandMap.keys()) {
|
||||||
const shortcut = this.commandMap.get(command)
|
const shortcut = this.commandMap.get(command)
|
||||||
if (!shortcut) {
|
if (!shortcut) {
|
||||||
@@ -243,6 +260,7 @@ export class KeyboardService {
|
|||||||
...shortcut,
|
...shortcut,
|
||||||
category: handler.category,
|
category: handler.category,
|
||||||
description: handler.description,
|
description: handler.description,
|
||||||
|
id: UuidGenerator.GenerateUuid(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -250,11 +268,12 @@ export class KeyboardService {
|
|||||||
* Register help item for a keyboard shortcut that is handled outside of the KeyboardService,
|
* Register help item for a keyboard shortcut that is handled outside of the KeyboardService,
|
||||||
* for example by a library like Lexical.
|
* for example by a library like Lexical.
|
||||||
*/
|
*/
|
||||||
registerExternalKeyboardShortcutHelpItem(item: KeyboardShortcutHelpItem): () => void {
|
registerExternalKeyboardShortcutHelpItem(item: Omit<KeyboardShortcutHelpItem, 'id'>): () => void {
|
||||||
this.keyboardShortcutHelpItems.add(item)
|
const itemWithId = { ...item, id: UuidGenerator.GenerateUuid() }
|
||||||
|
this.keyboardShortcutHelpItems.add(itemWithId)
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
this.keyboardShortcutHelpItems.delete(item)
|
this.keyboardShortcutHelpItems.delete(itemWithId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -262,7 +281,7 @@ export class KeyboardService {
|
|||||||
* Register help item for a keyboard shortcut that is handled outside of the KeyboardService,
|
* Register help item for a keyboard shortcut that is handled outside of the KeyboardService,
|
||||||
* for example by a library like Lexical.
|
* for example by a library like Lexical.
|
||||||
*/
|
*/
|
||||||
registerExternalKeyboardShortcutHelpItems(items: KeyboardShortcutHelpItem[]): () => void {
|
registerExternalKeyboardShortcutHelpItems(items: Omit<KeyboardShortcutHelpItem, 'id'>[]): () => void {
|
||||||
const disposers = items.map((item) => this.registerExternalKeyboardShortcutHelpItem(item))
|
const disposers = items.map((item) => this.registerExternalKeyboardShortcutHelpItem(item))
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
|||||||
@@ -21,8 +21,9 @@ export type PlatformedKeyboardShortcut = KeyboardShortcut & {
|
|||||||
|
|
||||||
export type KeyboardShortcutCategory = 'General' | 'Notes list' | 'Current note' | 'Super notes' | 'Formatting'
|
export type KeyboardShortcutCategory = 'General' | 'Notes list' | 'Current note' | 'Super notes' | 'Formatting'
|
||||||
|
|
||||||
export type KeyboardShortcutHelpItem = Omit<PlatformedKeyboardShortcut, 'command'> & {
|
export interface KeyboardShortcutHelpItem extends Omit<PlatformedKeyboardShortcut, 'command'> {
|
||||||
command?: KeyboardCommand
|
command?: KeyboardCommand
|
||||||
category: KeyboardShortcutCategory
|
category: KeyboardShortcutCategory
|
||||||
description: string
|
description: string
|
||||||
|
id: string
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import {
|
|||||||
CHANGE_EDITOR_WIDTH_COMMAND,
|
CHANGE_EDITOR_WIDTH_COMMAND,
|
||||||
SUPER_TOGGLE_TOOLBAR,
|
SUPER_TOGGLE_TOOLBAR,
|
||||||
TOGGLE_KEYBOARD_SHORTCUTS_MODAL,
|
TOGGLE_KEYBOARD_SHORTCUTS_MODAL,
|
||||||
|
TOGGLE_COMMAND_PALETTE,
|
||||||
} from './KeyboardCommands'
|
} from './KeyboardCommands'
|
||||||
import { KeyboardKey } from './KeyboardKey'
|
import { KeyboardKey } from './KeyboardKey'
|
||||||
import { KeyboardModifier, getPrimaryModifier } from './KeyboardModifier'
|
import { KeyboardModifier, getPrimaryModifier } from './KeyboardModifier'
|
||||||
@@ -108,7 +109,7 @@ export function getKeyboardShortcuts(platform: Platform, _environment: Environme
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
command: CHANGE_EDITOR_COMMAND,
|
command: CHANGE_EDITOR_COMMAND,
|
||||||
key: '/',
|
key: '?',
|
||||||
modifiers: [primaryModifier, KeyboardModifier.Shift],
|
modifiers: [primaryModifier, KeyboardModifier.Shift],
|
||||||
preventDefault: true,
|
preventDefault: true,
|
||||||
},
|
},
|
||||||
@@ -200,5 +201,10 @@ export function getKeyboardShortcuts(platform: Platform, _environment: Environme
|
|||||||
key: '/',
|
key: '/',
|
||||||
modifiers: [primaryModifier],
|
modifiers: [primaryModifier],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
command: TOGGLE_COMMAND_PALETTE,
|
||||||
|
code: 'Semicolon',
|
||||||
|
modifiers: [primaryModifier, KeyboardModifier.Shift],
|
||||||
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
export function keyboardCharacterForKeyOrCode(keyOrCode: string) {
|
export function keyboardCharacterForKeyOrCode(keyOrCode: string, shiftKey = false) {
|
||||||
if (keyOrCode.startsWith('Digit')) {
|
if (keyOrCode.startsWith('Digit')) {
|
||||||
return keyOrCode.replace('Digit', '')
|
return keyOrCode.replace('Digit', '')
|
||||||
}
|
}
|
||||||
@@ -14,6 +14,8 @@ export function keyboardCharacterForKeyOrCode(keyOrCode: string) {
|
|||||||
return '←'
|
return '←'
|
||||||
case 'ArrowRight':
|
case 'ArrowRight':
|
||||||
return '→'
|
return '→'
|
||||||
|
case 'Semicolon':
|
||||||
|
return shiftKey ? ':' : ';'
|
||||||
default:
|
default:
|
||||||
return keyOrCode
|
return keyOrCode
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -107,7 +107,7 @@
|
|||||||
"app/**/*.{js,ts,jsx,tsx,css,md}": "prettier --write"
|
"app/**/*.{js,ts,jsx,tsx,css,md}": "prettier --write"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ariakit/react": "^0.3.9",
|
"@ariakit/react": "^0.4.18",
|
||||||
"@lexical/clipboard": "0.32.1",
|
"@lexical/clipboard": "0.32.1",
|
||||||
"@lexical/headless": "0.32.1",
|
"@lexical/headless": "0.32.1",
|
||||||
"@lexical/link": "0.32.1",
|
"@lexical/link": "0.32.1",
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export const Web_TYPES = {
|
|||||||
Importer: Symbol.for('Importer'),
|
Importer: Symbol.for('Importer'),
|
||||||
ItemGroupController: Symbol.for('ItemGroupController'),
|
ItemGroupController: Symbol.for('ItemGroupController'),
|
||||||
KeyboardService: Symbol.for('KeyboardService'),
|
KeyboardService: Symbol.for('KeyboardService'),
|
||||||
|
CommandService: Symbol.for('CommandService'),
|
||||||
MobileWebReceiver: Symbol.for('MobileWebReceiver'),
|
MobileWebReceiver: Symbol.for('MobileWebReceiver'),
|
||||||
MomentsService: Symbol.for('MomentsService'),
|
MomentsService: Symbol.for('MomentsService'),
|
||||||
PersistenceService: Symbol.for('PersistenceService'),
|
PersistenceService: Symbol.for('PersistenceService'),
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import {
|
|||||||
ThemeManager,
|
ThemeManager,
|
||||||
ToastService,
|
ToastService,
|
||||||
VaultDisplayService,
|
VaultDisplayService,
|
||||||
WebApplicationInterface,
|
|
||||||
} from '@standardnotes/ui-services'
|
} from '@standardnotes/ui-services'
|
||||||
import { DependencyContainer } from '@standardnotes/utils'
|
import { DependencyContainer } from '@standardnotes/utils'
|
||||||
import { Web_TYPES } from './Types'
|
import { Web_TYPES } from './Types'
|
||||||
@@ -50,9 +49,11 @@ import { LoadPurchaseFlowUrl } from '../UseCase/LoadPurchaseFlowUrl'
|
|||||||
import { GetPurchaseFlowUrl } from '../UseCase/GetPurchaseFlowUrl'
|
import { GetPurchaseFlowUrl } from '../UseCase/GetPurchaseFlowUrl'
|
||||||
import { OpenSubscriptionDashboard } from '../UseCase/OpenSubscriptionDashboard'
|
import { OpenSubscriptionDashboard } from '../UseCase/OpenSubscriptionDashboard'
|
||||||
import { HeadlessSuperConverter } from '@/Components/SuperEditor/Tools/HeadlessSuperConverter'
|
import { HeadlessSuperConverter } from '@/Components/SuperEditor/Tools/HeadlessSuperConverter'
|
||||||
|
import { WebApplication } from '../WebApplication'
|
||||||
|
import { CommandService } from '../../Components/CommandPalette/CommandService'
|
||||||
|
|
||||||
export class WebDependencies extends DependencyContainer {
|
export class WebDependencies extends DependencyContainer {
|
||||||
constructor(private application: WebApplicationInterface) {
|
constructor(private application: WebApplication) {
|
||||||
super()
|
super()
|
||||||
|
|
||||||
this.bind(Web_TYPES.SuperConverter, () => {
|
this.bind(Web_TYPES.SuperConverter, () => {
|
||||||
@@ -124,6 +125,9 @@ export class WebDependencies extends DependencyContainer {
|
|||||||
this.bind(Web_TYPES.KeyboardService, () => {
|
this.bind(Web_TYPES.KeyboardService, () => {
|
||||||
return new KeyboardService(application.platform, application.environment)
|
return new KeyboardService(application.platform, application.environment)
|
||||||
})
|
})
|
||||||
|
this.bind(Web_TYPES.CommandService, () => {
|
||||||
|
return new CommandService(this.get<KeyboardService>(Web_TYPES.KeyboardService), application.generateUuid)
|
||||||
|
})
|
||||||
|
|
||||||
this.bind(Web_TYPES.ArchiveManager, () => {
|
this.bind(Web_TYPES.ArchiveManager, () => {
|
||||||
return new ArchiveManager(application)
|
return new ArchiveManager(application)
|
||||||
@@ -199,6 +203,7 @@ export class WebDependencies extends DependencyContainer {
|
|||||||
return new PaneController(
|
return new PaneController(
|
||||||
application.preferences,
|
application.preferences,
|
||||||
this.get<KeyboardService>(Web_TYPES.KeyboardService),
|
this.get<KeyboardService>(Web_TYPES.KeyboardService),
|
||||||
|
application.commands,
|
||||||
this.get<IsTabletOrMobileScreen>(Web_TYPES.IsTabletOrMobileScreen),
|
this.get<IsTabletOrMobileScreen>(Web_TYPES.IsTabletOrMobileScreen),
|
||||||
this.get<PanesForLayout>(Web_TYPES.PanesForLayout),
|
this.get<PanesForLayout>(Web_TYPES.PanesForLayout),
|
||||||
application.events,
|
application.events,
|
||||||
@@ -233,7 +238,7 @@ export class WebDependencies extends DependencyContainer {
|
|||||||
return new NavigationController(
|
return new NavigationController(
|
||||||
this.get<FeaturesController>(Web_TYPES.FeaturesController),
|
this.get<FeaturesController>(Web_TYPES.FeaturesController),
|
||||||
this.get<VaultDisplayService>(Web_TYPES.VaultDisplayService),
|
this.get<VaultDisplayService>(Web_TYPES.VaultDisplayService),
|
||||||
this.get<KeyboardService>(Web_TYPES.KeyboardService),
|
this.get<CommandService>(Web_TYPES.CommandService),
|
||||||
this.get<PaneController>(Web_TYPES.PaneController),
|
this.get<PaneController>(Web_TYPES.PaneController),
|
||||||
application.sync,
|
application.sync,
|
||||||
application.mutator,
|
application.mutator,
|
||||||
@@ -241,25 +246,16 @@ export class WebDependencies extends DependencyContainer {
|
|||||||
application.preferences,
|
application.preferences,
|
||||||
application.alerts,
|
application.alerts,
|
||||||
application.changeAndSaveItem,
|
application.changeAndSaveItem,
|
||||||
|
application.recents,
|
||||||
application.events,
|
application.events,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
this.bind(Web_TYPES.NotesController, () => {
|
this.bind(Web_TYPES.NotesController, () => {
|
||||||
return new NotesController(
|
return new NotesController(
|
||||||
this.get<ItemListController>(Web_TYPES.ItemListController),
|
application,
|
||||||
this.get<NavigationController>(Web_TYPES.NavigationController),
|
|
||||||
this.get<ItemGroupController>(Web_TYPES.ItemGroupController),
|
|
||||||
this.get<KeyboardService>(Web_TYPES.KeyboardService),
|
|
||||||
application.preferences,
|
|
||||||
application.items,
|
|
||||||
application.mutator,
|
|
||||||
application.sync,
|
|
||||||
application.protections,
|
|
||||||
application.alerts,
|
|
||||||
this.get<IsGlobalSpellcheckEnabled>(Web_TYPES.IsGlobalSpellcheckEnabled),
|
this.get<IsGlobalSpellcheckEnabled>(Web_TYPES.IsGlobalSpellcheckEnabled),
|
||||||
this.get<GetItemTags>(Web_TYPES.GetItemTags),
|
this.get<GetItemTags>(Web_TYPES.GetItemTags),
|
||||||
application.events,
|
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -304,6 +300,7 @@ export class WebDependencies extends DependencyContainer {
|
|||||||
application.options,
|
application.options,
|
||||||
this.get<IsNativeMobileWeb>(Web_TYPES.IsNativeMobileWeb),
|
this.get<IsNativeMobileWeb>(Web_TYPES.IsNativeMobileWeb),
|
||||||
application.changeAndSaveItem,
|
application.changeAndSaveItem,
|
||||||
|
application.recents,
|
||||||
application.events,
|
application.events,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@@ -374,6 +371,7 @@ export class WebDependencies extends DependencyContainer {
|
|||||||
application.platform,
|
application.platform,
|
||||||
application.mobileDevice,
|
application.mobileDevice,
|
||||||
this.get<IsNativeMobileWeb>(Web_TYPES.IsNativeMobileWeb),
|
this.get<IsNativeMobileWeb>(Web_TYPES.IsNativeMobileWeb),
|
||||||
|
application.recents,
|
||||||
application.events,
|
application.events,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@@ -381,7 +379,7 @@ export class WebDependencies extends DependencyContainer {
|
|||||||
this.bind(Web_TYPES.HistoryModalController, () => {
|
this.bind(Web_TYPES.HistoryModalController, () => {
|
||||||
return new HistoryModalController(
|
return new HistoryModalController(
|
||||||
this.get<NotesController>(Web_TYPES.NotesController),
|
this.get<NotesController>(Web_TYPES.NotesController),
|
||||||
this.get<KeyboardService>(Web_TYPES.KeyboardService),
|
this.get<CommandService>(Web_TYPES.CommandService),
|
||||||
application.events,
|
application.events,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|||||||
33
packages/web/src/javascripts/Application/Recents.ts
Normal file
33
packages/web/src/javascripts/Application/Recents.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
const MaxCommands = 5
|
||||||
|
const MaxItems = 10
|
||||||
|
|
||||||
|
export class RecentActionsState {
|
||||||
|
#commandUuids: string[] = []
|
||||||
|
#itemUuids: string[] = []
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recently used commands, most recent at the start
|
||||||
|
*/
|
||||||
|
get commandUuids() {
|
||||||
|
return this.#commandUuids
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Recently opened items, most recent at the start
|
||||||
|
*/
|
||||||
|
get itemUuids() {
|
||||||
|
return this.#itemUuids
|
||||||
|
}
|
||||||
|
|
||||||
|
add(id: string, action_type: 'item' | 'command' = 'item') {
|
||||||
|
const action_array = action_type === 'item' ? this.#itemUuids : this.#commandUuids
|
||||||
|
const existing = action_array.findIndex((uuid) => uuid === id)
|
||||||
|
if (existing !== -1) {
|
||||||
|
action_array.splice(existing, 1)
|
||||||
|
}
|
||||||
|
const max = action_type === 'item' ? MaxItems : MaxCommands
|
||||||
|
if (action_array.length == max) {
|
||||||
|
action_array.pop()
|
||||||
|
}
|
||||||
|
action_array.unshift(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,19 @@ import { Environment, namespacedKey, Platform, RawStorageKey, SNLog } from '@sta
|
|||||||
import { WebApplication } from '@/Application/WebApplication'
|
import { WebApplication } from '@/Application/WebApplication'
|
||||||
import { WebOrDesktopDevice } from './Device/WebOrDesktopDevice'
|
import { WebOrDesktopDevice } from './Device/WebOrDesktopDevice'
|
||||||
|
|
||||||
|
jest.mock('@standardnotes/sncrypto-web', () => {
|
||||||
|
return {
|
||||||
|
SNWebCrypto: class {
|
||||||
|
initialize() {
|
||||||
|
return Promise.resolve()
|
||||||
|
}
|
||||||
|
generateUUID() {
|
||||||
|
return 'mock-uuid'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
describe('web application', () => {
|
describe('web application', () => {
|
||||||
let application: WebApplication
|
let application: WebApplication
|
||||||
|
|
||||||
|
|||||||
@@ -80,6 +80,8 @@ import { SearchOptionsController } from '@/Controllers/SearchOptionsController'
|
|||||||
import { PersistenceService } from '@/Controllers/Abstract/PersistenceService'
|
import { PersistenceService } from '@/Controllers/Abstract/PersistenceService'
|
||||||
import { removeFromArray } from '@standardnotes/utils'
|
import { removeFromArray } from '@standardnotes/utils'
|
||||||
import { FileItemActionType } from '@/Components/AttachedFilesPopover/PopoverFileItemAction'
|
import { FileItemActionType } from '@/Components/AttachedFilesPopover/PopoverFileItemAction'
|
||||||
|
import { RecentActionsState } from './Recents'
|
||||||
|
import { CommandService } from '../Components/CommandPalette/CommandService'
|
||||||
|
|
||||||
export type WebEventObserver = (event: WebAppEvent, data?: unknown) => void
|
export type WebEventObserver = (event: WebAppEvent, data?: unknown) => void
|
||||||
|
|
||||||
@@ -95,6 +97,7 @@ export class WebApplication extends SNApplication implements WebApplicationInter
|
|||||||
public isSessionsModalVisible = false
|
public isSessionsModalVisible = false
|
||||||
|
|
||||||
public devMode?: DevMode
|
public devMode?: DevMode
|
||||||
|
public recents = new RecentActionsState()
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
deviceInterface: WebOrDesktopDevice,
|
deviceInterface: WebOrDesktopDevice,
|
||||||
@@ -597,6 +600,10 @@ export class WebApplication extends SNApplication implements WebApplicationInter
|
|||||||
return this.deps.get<KeyboardService>(Web_TYPES.KeyboardService)
|
return this.deps.get<KeyboardService>(Web_TYPES.KeyboardService)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get commands(): CommandService {
|
||||||
|
return this.deps.get<CommandService>(Web_TYPES.CommandService)
|
||||||
|
}
|
||||||
|
|
||||||
get featuresController(): FeaturesController {
|
get featuresController(): FeaturesController {
|
||||||
return this.deps.get<FeaturesController>(Web_TYPES.FeaturesController)
|
return this.deps.get<FeaturesController>(Web_TYPES.FeaturesController)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import Spinner from '@/Components/Spinner/Spinner'
|
|||||||
import { MenuItemIconSize } from '@/Constants/TailwindClassNames'
|
import { MenuItemIconSize } from '@/Constants/TailwindClassNames'
|
||||||
import { useApplication } from '../ApplicationProvider'
|
import { useApplication } from '../ApplicationProvider'
|
||||||
import MenuSection from '../Menu/MenuSection'
|
import MenuSection from '../Menu/MenuSection'
|
||||||
import { TOGGLE_KEYBOARD_SHORTCUTS_MODAL, isMobilePlatform } from '@standardnotes/ui-services'
|
import { TOGGLE_COMMAND_PALETTE, TOGGLE_KEYBOARD_SHORTCUTS_MODAL, isMobilePlatform } from '@standardnotes/ui-services'
|
||||||
import { KeyboardShortcutIndicator } from '../KeyboardShortcutIndicator/KeyboardShortcutIndicator'
|
import { KeyboardShortcutIndicator } from '../KeyboardShortcutIndicator/KeyboardShortcutIndicator'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -95,6 +95,9 @@ const GeneralAccountMenu: FunctionComponent<Props> = ({ setMenuPane, closeMenu,
|
|||||||
const keyboardShortcutsHelpShortcut = useMemo(() => {
|
const keyboardShortcutsHelpShortcut = useMemo(() => {
|
||||||
return application.keyboardService.keyboardShortcutForCommand(TOGGLE_KEYBOARD_SHORTCUTS_MODAL)
|
return application.keyboardService.keyboardShortcutForCommand(TOGGLE_KEYBOARD_SHORTCUTS_MODAL)
|
||||||
}, [application.keyboardService])
|
}, [application.keyboardService])
|
||||||
|
const commandPaletteShortcut = useMemo(() => {
|
||||||
|
return application.keyboardService.keyboardShortcutForCommand(TOGGLE_COMMAND_PALETTE)
|
||||||
|
}, [application.keyboardService])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -194,17 +197,30 @@ const GeneralAccountMenu: FunctionComponent<Props> = ({ setMenuPane, closeMenu,
|
|||||||
<span className="text-neutral">v{application.version}</span>
|
<span className="text-neutral">v{application.version}</span>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
{!isMobilePlatform(application.platform) && (
|
{!isMobilePlatform(application.platform) && (
|
||||||
<MenuItem
|
<>
|
||||||
onClick={() => {
|
<MenuItem
|
||||||
application.keyboardService.triggerCommand(TOGGLE_KEYBOARD_SHORTCUTS_MODAL)
|
onClick={() => {
|
||||||
}}
|
application.keyboardService.triggerCommand(TOGGLE_KEYBOARD_SHORTCUTS_MODAL)
|
||||||
>
|
}}
|
||||||
<Icon type="keyboard" className={iconClassName} />
|
>
|
||||||
Keyboard shortcuts
|
<Icon type="keyboard" className={iconClassName} />
|
||||||
{keyboardShortcutsHelpShortcut && (
|
Keyboard shortcuts
|
||||||
<KeyboardShortcutIndicator shortcut={keyboardShortcutsHelpShortcut} className="ml-auto" />
|
{keyboardShortcutsHelpShortcut && (
|
||||||
)}
|
<KeyboardShortcutIndicator shortcut={keyboardShortcutsHelpShortcut} className="ml-auto" />
|
||||||
</MenuItem>
|
)}
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem
|
||||||
|
onClick={() => {
|
||||||
|
application.keyboardService.triggerCommand(TOGGLE_COMMAND_PALETTE)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon type="info" className={iconClassName} />
|
||||||
|
Command palette
|
||||||
|
{commandPaletteShortcut && (
|
||||||
|
<KeyboardShortcutIndicator shortcut={commandPaletteShortcut} className="ml-auto" />
|
||||||
|
)}
|
||||||
|
</MenuItem>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</MenuSection>
|
</MenuSection>
|
||||||
{user ? (
|
{user ? (
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import ResponsivePaneProvider from '../Panes/ResponsivePaneProvider'
|
|||||||
import AndroidBackHandlerProvider from '@/NativeMobileWeb/useAndroidBackHandler'
|
import AndroidBackHandlerProvider from '@/NativeMobileWeb/useAndroidBackHandler'
|
||||||
import ConfirmDeleteAccountContainer from '@/Components/ConfirmDeleteAccountModal/ConfirmDeleteAccountModal'
|
import ConfirmDeleteAccountContainer from '@/Components/ConfirmDeleteAccountModal/ConfirmDeleteAccountModal'
|
||||||
import ApplicationProvider from '../ApplicationProvider'
|
import ApplicationProvider from '../ApplicationProvider'
|
||||||
import CommandProvider from '../CommandProvider'
|
import KeyboardServiceProvider from '../KeyboardServiceProvider'
|
||||||
import PanesSystemComponent from '../Panes/PanesSystemComponent'
|
import PanesSystemComponent from '../Panes/PanesSystemComponent'
|
||||||
import DotOrgNotice from './DotOrgNotice'
|
import DotOrgNotice from './DotOrgNotice'
|
||||||
import LinkingControllerProvider from '@/Controllers/LinkingControllerProvider'
|
import LinkingControllerProvider from '@/Controllers/LinkingControllerProvider'
|
||||||
@@ -32,6 +32,8 @@ import IosKeyboardClose from '../IosKeyboardClose/IosKeyboardClose'
|
|||||||
import EditorWidthSelectionModalWrapper from '../EditorWidthSelectionModal/EditorWidthSelectionModal'
|
import EditorWidthSelectionModalWrapper from '../EditorWidthSelectionModal/EditorWidthSelectionModal'
|
||||||
import { ProtectionEvent } from '@standardnotes/services'
|
import { ProtectionEvent } from '@standardnotes/services'
|
||||||
import KeyboardShortcutsModal from '../KeyboardShortcutsHelpModal/KeyboardShortcutsHelpModal'
|
import KeyboardShortcutsModal from '../KeyboardShortcutsHelpModal/KeyboardShortcutsHelpModal'
|
||||||
|
import CommandPalette from '../CommandPalette/CommandPalette'
|
||||||
|
import SuperExportModal from '../NotesOptions/SuperExportModal'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
application: WebApplication
|
application: WebApplication
|
||||||
@@ -212,7 +214,7 @@ const ApplicationView: FunctionComponent<Props> = ({ application, mainApplicatio
|
|||||||
if (route.type === RouteType.AppViewRoute && route.appViewRouteParam === 'extension') {
|
if (route.type === RouteType.AppViewRoute && route.appViewRouteParam === 'extension') {
|
||||||
return (
|
return (
|
||||||
<ApplicationProvider application={application}>
|
<ApplicationProvider application={application}>
|
||||||
<CommandProvider service={application.keyboardService}>
|
<KeyboardServiceProvider service={application.keyboardService}>
|
||||||
<AndroidBackHandlerProvider application={application}>
|
<AndroidBackHandlerProvider application={application}>
|
||||||
<ResponsivePaneProvider paneController={application.paneController}>
|
<ResponsivePaneProvider paneController={application.paneController}>
|
||||||
<PremiumModalProvider application={application}>
|
<PremiumModalProvider application={application}>
|
||||||
@@ -227,14 +229,14 @@ const ApplicationView: FunctionComponent<Props> = ({ application, mainApplicatio
|
|||||||
</PremiumModalProvider>
|
</PremiumModalProvider>
|
||||||
</ResponsivePaneProvider>
|
</ResponsivePaneProvider>
|
||||||
</AndroidBackHandlerProvider>
|
</AndroidBackHandlerProvider>
|
||||||
</CommandProvider>
|
</KeyboardServiceProvider>
|
||||||
</ApplicationProvider>
|
</ApplicationProvider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ApplicationProvider application={application}>
|
<ApplicationProvider application={application}>
|
||||||
<CommandProvider service={application.keyboardService}>
|
<KeyboardServiceProvider service={application.keyboardService}>
|
||||||
<AndroidBackHandlerProvider application={application}>
|
<AndroidBackHandlerProvider application={application}>
|
||||||
<ResponsivePaneProvider paneController={application.paneController}>
|
<ResponsivePaneProvider paneController={application.paneController}>
|
||||||
<PremiumModalProvider application={application}>
|
<PremiumModalProvider application={application}>
|
||||||
@@ -269,6 +271,8 @@ const ApplicationView: FunctionComponent<Props> = ({ application, mainApplicatio
|
|||||||
<ConfirmDeleteAccountContainer application={application} />
|
<ConfirmDeleteAccountContainer application={application} />
|
||||||
<ImportModal importModalController={application.importModalController} />
|
<ImportModal importModalController={application.importModalController} />
|
||||||
<KeyboardShortcutsModal keyboardService={application.keyboardService} />
|
<KeyboardShortcutsModal keyboardService={application.keyboardService} />
|
||||||
|
<SuperExportModal />
|
||||||
|
<CommandPalette />
|
||||||
</>
|
</>
|
||||||
{application.routeService.isDotOrg && <DotOrgNotice />}
|
{application.routeService.isDotOrg && <DotOrgNotice />}
|
||||||
{isIOS() && <IosKeyboardClose />}
|
{isIOS() && <IosKeyboardClose />}
|
||||||
@@ -277,7 +281,7 @@ const ApplicationView: FunctionComponent<Props> = ({ application, mainApplicatio
|
|||||||
</PremiumModalProvider>
|
</PremiumModalProvider>
|
||||||
</ResponsivePaneProvider>
|
</ResponsivePaneProvider>
|
||||||
</AndroidBackHandlerProvider>
|
</AndroidBackHandlerProvider>
|
||||||
</CommandProvider>
|
</KeyboardServiceProvider>
|
||||||
</ApplicationProvider>
|
</ApplicationProvider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,14 +55,15 @@ const ChangeEditorButton: FunctionComponent<Props> = ({ noteViewController, onCl
|
|||||||
}, [isOpen, onClickPreprocessing, onClick])
|
}, [isOpen, onClickPreprocessing, onClick])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return application.keyboardService.addCommandHandler({
|
return application.commands.addWithShortcut(
|
||||||
command: CHANGE_EDITOR_COMMAND,
|
CHANGE_EDITOR_COMMAND,
|
||||||
category: 'Current note',
|
'Current note',
|
||||||
description: 'Change note type',
|
'Change note type',
|
||||||
onKeyDown: () => {
|
() => {
|
||||||
void toggleMenu()
|
void toggleMenu()
|
||||||
},
|
},
|
||||||
})
|
'notes',
|
||||||
|
)
|
||||||
}, [application, toggleMenu])
|
}, [application, toggleMenu])
|
||||||
|
|
||||||
const shortcut = useMemo(
|
const shortcut = useMemo(
|
||||||
|
|||||||
@@ -0,0 +1,425 @@
|
|||||||
|
import { observer } from 'mobx-react-lite'
|
||||||
|
import { startTransition, useCallback, useEffect, useState } from 'react'
|
||||||
|
import { useKeyboardService } from '../KeyboardServiceProvider'
|
||||||
|
import { PlatformedKeyboardShortcut, TOGGLE_COMMAND_PALETTE } from '@standardnotes/ui-services'
|
||||||
|
import {
|
||||||
|
Combobox,
|
||||||
|
ComboboxGroup,
|
||||||
|
ComboboxGroupLabel,
|
||||||
|
ComboboxItem,
|
||||||
|
ComboboxList,
|
||||||
|
ComboboxProvider,
|
||||||
|
Dialog,
|
||||||
|
Tab,
|
||||||
|
TabList,
|
||||||
|
TabPanel,
|
||||||
|
TabProvider,
|
||||||
|
useDialogStore,
|
||||||
|
useTabContext,
|
||||||
|
} from '@ariakit/react'
|
||||||
|
import {
|
||||||
|
classNames,
|
||||||
|
DecryptedItemInterface,
|
||||||
|
FileItem,
|
||||||
|
SmartView,
|
||||||
|
SNNote,
|
||||||
|
SNTag,
|
||||||
|
UuidGenerator,
|
||||||
|
} from '@standardnotes/snjs'
|
||||||
|
import { KeyboardShortcutIndicator } from '../KeyboardShortcutIndicator/KeyboardShortcutIndicator'
|
||||||
|
import { useApplication } from '../ApplicationProvider'
|
||||||
|
import Icon from '../Icon/Icon'
|
||||||
|
import { getIconForItem } from '../../Utils/Items/Icons/getIconForItem'
|
||||||
|
import { FileItemActionType } from '../AttachedFilesPopover/PopoverFileItemAction'
|
||||||
|
import type { CommandService } from './CommandService'
|
||||||
|
import { requestCloseAllOpenModalsAndPopovers } from '../../Utils/CloseOpenModalsAndPopovers'
|
||||||
|
|
||||||
|
type CommandPaletteItem = {
|
||||||
|
id: string
|
||||||
|
description: string
|
||||||
|
icon: JSX.Element
|
||||||
|
shortcut?: PlatformedKeyboardShortcut
|
||||||
|
resultRange?: [number, number]
|
||||||
|
} & ({ section: 'notes' | 'files' | 'tags'; itemUuid: string } | { section: 'commands' })
|
||||||
|
|
||||||
|
function ListItemDescription({ item }: { item: CommandPaletteItem }) {
|
||||||
|
const range = item.resultRange
|
||||||
|
if (!range) {
|
||||||
|
return item.description
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{item.description.slice(0, range[0])}
|
||||||
|
<span className="rounded-sm bg-[color-mix(in_srgb,var(--sn-stylekit-accessory-tint-color-1),rgba(255,255,255,.1))] p-px">
|
||||||
|
{item.description.slice(range[0], range[1])}
|
||||||
|
</span>
|
||||||
|
{item.description.slice(range[1])}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const Tabs = ['all', 'commands', 'notes', 'files', 'tags'] as const
|
||||||
|
type TabId = (typeof Tabs)[number]
|
||||||
|
|
||||||
|
function CommandPaletteListItem({
|
||||||
|
id,
|
||||||
|
item,
|
||||||
|
index,
|
||||||
|
handleClick,
|
||||||
|
selectedTab,
|
||||||
|
}: {
|
||||||
|
id: string
|
||||||
|
item: CommandPaletteItem
|
||||||
|
index: number
|
||||||
|
handleClick: (item: CommandPaletteItem) => void
|
||||||
|
selectedTab: TabId
|
||||||
|
}) {
|
||||||
|
if (selectedTab !== 'all' && selectedTab !== item.section) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<ComboboxItem
|
||||||
|
id={id}
|
||||||
|
value={item.id}
|
||||||
|
hideOnClick={true}
|
||||||
|
focusOnHover={true}
|
||||||
|
blurOnHoverEnd={false}
|
||||||
|
className={classNames(
|
||||||
|
'flex scroll-m-2 items-center gap-2 whitespace-nowrap rounded-md px-2 py-2.5 text-[0.95rem] data-[active-item]:bg-info data-[active-item]:text-info-contrast [&>svg]:flex-shrink-0',
|
||||||
|
index === 0 && 'scroll-m-8',
|
||||||
|
)}
|
||||||
|
onClick={() => handleClick(item)}
|
||||||
|
>
|
||||||
|
{item.icon}
|
||||||
|
<div className="mr-auto overflow-hidden text-ellipsis whitespace-nowrap leading-none">
|
||||||
|
<ListItemDescription item={item} />
|
||||||
|
</div>
|
||||||
|
{item.shortcut && <KeyboardShortcutIndicator className="ml-auto" shortcut={item.shortcut} small={false} />}
|
||||||
|
</ComboboxItem>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ComboboxInput() {
|
||||||
|
const tab = useTabContext()
|
||||||
|
return (
|
||||||
|
<Combobox
|
||||||
|
autoSelect="always"
|
||||||
|
className="h-10 w-full appearance-none bg-transparent px-1 text-base focus:shadow-none focus:outline-none"
|
||||||
|
placeholder="Search notes, files, commands, etc..."
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
if (event.key !== 'Tab') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const activeId = tab?.getState().selectedId
|
||||||
|
const options = { activeId }
|
||||||
|
const nextId = event.shiftKey ? tab?.previous(options) : tab?.next(options)
|
||||||
|
if (nextId) {
|
||||||
|
event.preventDefault()
|
||||||
|
tab?.select(nextId)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Future TODO, nice to have: A way to expose items like the current note's options
|
||||||
|
// directly in the command palette rather than only having a way to open the menu
|
||||||
|
function CommandPalette() {
|
||||||
|
const application = useApplication()
|
||||||
|
const keyboardService = useKeyboardService()
|
||||||
|
const [isOpen, setIsOpen] = useState(false)
|
||||||
|
const [query, setQuery] = useState('')
|
||||||
|
const [recents, setRecents] = useState<CommandPaletteItem[]>([])
|
||||||
|
const [items, setItems] = useState<CommandPaletteItem[]>([])
|
||||||
|
// Storing counts as separate state to avoid iterating items multiple times
|
||||||
|
const [itemCountsPerTab, setItemCounts] = useState({
|
||||||
|
commands: 0,
|
||||||
|
notes: 0,
|
||||||
|
files: 0,
|
||||||
|
tags: 0,
|
||||||
|
})
|
||||||
|
const [selectedTab, setSelectedTab] = useState<TabId>('all')
|
||||||
|
const dialog = useDialogStore({
|
||||||
|
open: isOpen,
|
||||||
|
setOpen: setIsOpen,
|
||||||
|
})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen) {
|
||||||
|
keyboardService.disableEventHandling()
|
||||||
|
requestCloseAllOpenModalsAndPopovers()
|
||||||
|
} else {
|
||||||
|
keyboardService.enableEventHandling()
|
||||||
|
}
|
||||||
|
}, [keyboardService, isOpen])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return keyboardService.addCommandHandler({
|
||||||
|
command: TOGGLE_COMMAND_PALETTE,
|
||||||
|
category: 'General',
|
||||||
|
description: 'Toggle command palette',
|
||||||
|
onKeyDown: (e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
setIsOpen((open) => !open)
|
||||||
|
setQuery('')
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}, [keyboardService])
|
||||||
|
|
||||||
|
const handleItemClick = useCallback(
|
||||||
|
(item: CommandPaletteItem) => {
|
||||||
|
if (item.section === 'commands') {
|
||||||
|
application.commands.triggerCommand(item.id)
|
||||||
|
application.recents.add(item.id, 'command')
|
||||||
|
} else {
|
||||||
|
const decryptedItem = application.items.findItem<DecryptedItemInterface>(item.itemUuid)
|
||||||
|
if (!decryptedItem) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (decryptedItem instanceof SNNote) {
|
||||||
|
void application.itemListController.selectItemUsingInstance(decryptedItem, true)
|
||||||
|
} else if (decryptedItem instanceof FileItem) {
|
||||||
|
void application.filesController.handleFileAction({
|
||||||
|
type: FileItemActionType.PreviewFile,
|
||||||
|
payload: { file: decryptedItem },
|
||||||
|
})
|
||||||
|
} else if (decryptedItem instanceof SNTag || decryptedItem instanceof SmartView) {
|
||||||
|
void application.navigationController.setSelectedTag(decryptedItem, 'all', {
|
||||||
|
userTriggered: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[
|
||||||
|
application.commands,
|
||||||
|
application.filesController,
|
||||||
|
application.itemListController,
|
||||||
|
application.items,
|
||||||
|
application.navigationController,
|
||||||
|
application.recents,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
const createItemForInteractableItem = useCallback(
|
||||||
|
(item: DecryptedItemInterface): CommandPaletteItem => {
|
||||||
|
const icon = getIconForItem(item, application)
|
||||||
|
let section: 'notes' | 'files' | 'tags'
|
||||||
|
if (item instanceof SNNote) {
|
||||||
|
section = 'notes'
|
||||||
|
} else if (item instanceof FileItem) {
|
||||||
|
section = 'files'
|
||||||
|
} else if (item instanceof SNTag || item instanceof SmartView) {
|
||||||
|
section = 'tags'
|
||||||
|
} else {
|
||||||
|
throw new Error('Item is not a note, file or tag')
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
section,
|
||||||
|
id: UuidGenerator.GenerateUuid(),
|
||||||
|
itemUuid: item.uuid,
|
||||||
|
description: item.title || '<no title>',
|
||||||
|
icon: <Icon type={icon[0]} className={item instanceof SNNote ? icon[1] : ''} />,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[application],
|
||||||
|
)
|
||||||
|
|
||||||
|
const createItemForCommand = useCallback(
|
||||||
|
(command: ReturnType<CommandService['getCommandDescriptions']>[0]): CommandPaletteItem => {
|
||||||
|
const shortcut = command.shortcut_id
|
||||||
|
? application.keyboardService.keyboardShortcutForCommand(command.shortcut_id)
|
||||||
|
: undefined
|
||||||
|
return {
|
||||||
|
id: command.id,
|
||||||
|
description: command.description,
|
||||||
|
section: 'commands',
|
||||||
|
icon: <Icon type={command.icon} />,
|
||||||
|
shortcut,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[application.keyboardService],
|
||||||
|
)
|
||||||
|
|
||||||
|
useEffect(
|
||||||
|
function updateCommandPaletteItems() {
|
||||||
|
if (!isOpen) {
|
||||||
|
setSelectedTab('all')
|
||||||
|
setItems([])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const recents: CommandPaletteItem[] = []
|
||||||
|
const items: CommandPaletteItem[] = []
|
||||||
|
const itemCounts: typeof itemCountsPerTab = {
|
||||||
|
commands: 0,
|
||||||
|
notes: 0,
|
||||||
|
files: 0,
|
||||||
|
tags: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
const searchQuery = query.toLowerCase()
|
||||||
|
const hasQuery = searchQuery.length > 0
|
||||||
|
|
||||||
|
if (hasQuery) {
|
||||||
|
const commands = application.commands.getCommandDescriptions()
|
||||||
|
for (let i = 0; i < commands.length; i++) {
|
||||||
|
const command = commands[i]
|
||||||
|
if (!command) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (items.length >= 50) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
const index = command.description.toLowerCase().indexOf(searchQuery)
|
||||||
|
if (index === -1) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const item = createItemForCommand(command)
|
||||||
|
item.resultRange = [index, index + searchQuery.length]
|
||||||
|
items.push(item)
|
||||||
|
itemCounts[item.section]++
|
||||||
|
}
|
||||||
|
|
||||||
|
const interactableItems = application.items.getInteractableItems()
|
||||||
|
for (let i = 0; i < interactableItems.length; i++) {
|
||||||
|
if (items.length >= 50) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
const decryptedItem = interactableItems[i]
|
||||||
|
if (!decryptedItem || !decryptedItem.title) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const index = decryptedItem.title.toLowerCase().indexOf(searchQuery)
|
||||||
|
if (index === -1) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const item = createItemForInteractableItem(decryptedItem)
|
||||||
|
item.resultRange = [index, index + searchQuery.length]
|
||||||
|
items.push(item)
|
||||||
|
itemCounts[item.section]++
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const recentCommands = application.recents.commandUuids
|
||||||
|
for (let i = 0; i < recentCommands.length; i++) {
|
||||||
|
const command = application.commands.getCommandDescription(recentCommands[i])
|
||||||
|
if (!command) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const item = createItemForCommand(command)
|
||||||
|
recents.push(item)
|
||||||
|
itemCounts[item.section]++
|
||||||
|
}
|
||||||
|
const recentItems = application.recents.itemUuids
|
||||||
|
for (let i = 0; i < recentItems.length; i++) {
|
||||||
|
const decryptedItem = application.items.findItem(recentItems[i])
|
||||||
|
if (!decryptedItem) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const item = createItemForInteractableItem(decryptedItem)
|
||||||
|
recents.push(item)
|
||||||
|
itemCounts[item.section]++
|
||||||
|
}
|
||||||
|
|
||||||
|
const commands = application.commands.getCommandDescriptions()
|
||||||
|
for (let i = 0; i < commands.length; i++) {
|
||||||
|
const command = commands[i]
|
||||||
|
if (!command) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const item = createItemForCommand(command)
|
||||||
|
items.push(item)
|
||||||
|
itemCounts[item.section]++
|
||||||
|
}
|
||||||
|
items.sort((a, b) => (a.description.toLowerCase() < b.description.toLowerCase() ? -1 : 1))
|
||||||
|
}
|
||||||
|
|
||||||
|
setItems(items)
|
||||||
|
setRecents(recents)
|
||||||
|
setItemCounts(itemCounts)
|
||||||
|
},
|
||||||
|
[application, createItemForCommand, createItemForInteractableItem, isOpen, query],
|
||||||
|
)
|
||||||
|
|
||||||
|
const hasNoItemsAtAll = items.length === 0
|
||||||
|
const hasNoItemsInSelectedTab = selectedTab !== 'all' && itemCountsPerTab[selectedTab] === 0
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
store={dialog}
|
||||||
|
className="fixed inset-3 bottom-[10vh] top-[10vh] z-modal m-auto mt-0 flex h-fit max-h-[70vh] w-[min(45rem,90vw)] flex-col gap-3 overflow-auto rounded-xl border border-[--popover-border-color] bg-[--popover-background-color] px-3 py-3 shadow-main [backdrop-filter:var(--popover-backdrop-filter)]"
|
||||||
|
backdrop={<div className="bg-passive-5 opacity-50 transition-opacity duration-75 data-[enter]:opacity-85" />}
|
||||||
|
>
|
||||||
|
<ComboboxProvider
|
||||||
|
disclosure={dialog}
|
||||||
|
includesBaseElement={false}
|
||||||
|
resetValueOnHide={true}
|
||||||
|
setValue={(value) => {
|
||||||
|
startTransition(() => setQuery(value))
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TabProvider selectedId={selectedTab} setSelectedId={(id) => setSelectedTab((id as TabId) || 'all')}>
|
||||||
|
<div className="flex rounded-lg border border-[--popover-border-color] bg-[--popover-background-color] px-2">
|
||||||
|
<ComboboxInput />
|
||||||
|
</div>
|
||||||
|
<TabList className="flex items-center gap-1">
|
||||||
|
{Tabs.map((id) => (
|
||||||
|
<Tab
|
||||||
|
key={id}
|
||||||
|
id={id}
|
||||||
|
className="rounded-full px-3 py-1 capitalize disabled:opacity-65 aria-selected:bg-info aria-selected:text-info-contrast data-[active-item]:ring-1 data-[active-item]:ring-info data-[active-item]:ring-offset-1 data-[active-item]:ring-offset-transparent"
|
||||||
|
disabled={hasNoItemsAtAll || (id !== 'all' && itemCountsPerTab[id] === 0)}
|
||||||
|
accessibleWhenDisabled={false}
|
||||||
|
>
|
||||||
|
{id}
|
||||||
|
</Tab>
|
||||||
|
))}
|
||||||
|
</TabList>
|
||||||
|
<TabPanel className="flex flex-col gap-1.5 overflow-y-auto" tabId={selectedTab}>
|
||||||
|
{query.length > 0 && (hasNoItemsAtAll || hasNoItemsInSelectedTab) && (
|
||||||
|
<div className="mx-auto px-2 text-sm font-semibold opacity-75">No items found</div>
|
||||||
|
)}
|
||||||
|
<ComboboxList className="focus:shadow-none focus:outline-none">
|
||||||
|
{recents.length > 0 && (
|
||||||
|
<ComboboxGroup>
|
||||||
|
<ComboboxGroupLabel className="px-2 font-semibold opacity-75">Recent</ComboboxGroupLabel>
|
||||||
|
{recents.map((item, index) => (
|
||||||
|
<CommandPaletteListItem
|
||||||
|
key={item.id}
|
||||||
|
id={
|
||||||
|
/* ariakit doesn't like multiple items with the same id in the same combobox list */
|
||||||
|
item.id + 'recent'
|
||||||
|
}
|
||||||
|
index={index}
|
||||||
|
item={item}
|
||||||
|
handleClick={handleItemClick}
|
||||||
|
selectedTab={selectedTab}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</ComboboxGroup>
|
||||||
|
)}
|
||||||
|
{!hasNoItemsAtAll && (
|
||||||
|
<ComboboxGroup>
|
||||||
|
{recents.length > 0 && (
|
||||||
|
<ComboboxGroupLabel className="mt-2 px-2 font-semibold opacity-75">All commands</ComboboxGroupLabel>
|
||||||
|
)}
|
||||||
|
{items.map((item, index) => (
|
||||||
|
<CommandPaletteListItem
|
||||||
|
key={item.id}
|
||||||
|
id={item.id}
|
||||||
|
index={index}
|
||||||
|
item={item}
|
||||||
|
handleClick={handleItemClick}
|
||||||
|
selectedTab={selectedTab}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</ComboboxGroup>
|
||||||
|
)}
|
||||||
|
</ComboboxList>
|
||||||
|
</TabPanel>
|
||||||
|
</TabProvider>
|
||||||
|
</ComboboxProvider>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default observer(CommandPalette)
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import { GenerateUuid, IconType } from '@standardnotes/snjs'
|
||||||
|
import { KeyboardCommand, KeyboardService, KeyboardShortcutCategory } from '@standardnotes/ui-services'
|
||||||
|
import mergeRegister from '../../Hooks/mergeRegister'
|
||||||
|
|
||||||
|
type CommandInfo = {
|
||||||
|
description: string
|
||||||
|
icon: IconType
|
||||||
|
shortcut_id?: KeyboardCommand
|
||||||
|
}
|
||||||
|
|
||||||
|
type CommandDescription = { id: string } & CommandInfo
|
||||||
|
|
||||||
|
export class CommandService {
|
||||||
|
#commandInfo = new Map<string, CommandInfo>()
|
||||||
|
#commandHandlers = new Map<string, () => void>()
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private keyboardService: KeyboardService,
|
||||||
|
private generateUuid: GenerateUuid,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public add(id: string, description: string, handler: () => void, icon?: IconType, shortcut_id?: KeyboardCommand) {
|
||||||
|
this.#commandInfo.set(id, { description, icon: icon ?? 'info', shortcut_id })
|
||||||
|
this.#commandHandlers.set(id, handler)
|
||||||
|
return () => {
|
||||||
|
this.#commandInfo.delete(id)
|
||||||
|
this.#commandHandlers.delete(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public addWithShortcut(
|
||||||
|
id: KeyboardCommand,
|
||||||
|
category: KeyboardShortcutCategory,
|
||||||
|
description: string,
|
||||||
|
handler: (event?: KeyboardEvent, data?: unknown) => void,
|
||||||
|
icon?: IconType,
|
||||||
|
) {
|
||||||
|
return mergeRegister(
|
||||||
|
this.add(id.description ?? this.generateUuid.execute().getValue(), description, handler, icon, id),
|
||||||
|
this.keyboardService.addCommandHandler({ command: id, category, description, onKeyDown: handler }),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
public triggerCommand(id: string) {
|
||||||
|
const handler = this.#commandHandlers.get(id)
|
||||||
|
if (handler) {
|
||||||
|
handler()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public getCommandDescriptions() {
|
||||||
|
const descriptions: CommandDescription[] = []
|
||||||
|
for (const [id, { description, icon, shortcut_id }] of this.#commandInfo) {
|
||||||
|
descriptions.push({ id, description, icon, shortcut_id })
|
||||||
|
}
|
||||||
|
return descriptions
|
||||||
|
}
|
||||||
|
|
||||||
|
public getCommandDescription(id: string): CommandDescription | undefined {
|
||||||
|
const command = this.#commandInfo.get(id)
|
||||||
|
if (!command) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return { id, ...command }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -175,15 +175,6 @@ const ContentListView = forwardRef<HTMLDivElement, Props>(
|
|||||||
* probably better to be consistent.
|
* probably better to be consistent.
|
||||||
*/
|
*/
|
||||||
return application.keyboardService.addCommandHandlers([
|
return application.keyboardService.addCommandHandlers([
|
||||||
{
|
|
||||||
command: CREATE_NEW_NOTE_KEYBOARD_COMMAND,
|
|
||||||
category: 'General',
|
|
||||||
description: 'Create new note',
|
|
||||||
onKeyDown: (event) => {
|
|
||||||
event.preventDefault()
|
|
||||||
void addNewItem()
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
command: NEXT_LIST_ITEM_KEYBOARD_COMMAND,
|
command: NEXT_LIST_ITEM_KEYBOARD_COMMAND,
|
||||||
category: 'Notes list',
|
category: 'Notes list',
|
||||||
@@ -262,11 +253,28 @@ const ContentListView = forwardRef<HTMLDivElement, Props>(
|
|||||||
)
|
)
|
||||||
|
|
||||||
const addButtonLabel = useMemo(() => {
|
const addButtonLabel = useMemo(() => {
|
||||||
return isFilesSmartView
|
let shortcut = keyboardStringForShortcut(shortcutForCreate)
|
||||||
? 'Upload file'
|
if (shortcut) {
|
||||||
: `Create a new note in the selected tag (${shortcutForCreate && keyboardStringForShortcut(shortcutForCreate)})`
|
shortcut = '(' + shortcut + ')'
|
||||||
|
}
|
||||||
|
return isFilesSmartView ? `Upload file ${shortcut}` : `Create a new note in the selected tag ${shortcut}`
|
||||||
}, [isFilesSmartView, shortcutForCreate])
|
}, [isFilesSmartView, shortcutForCreate])
|
||||||
|
|
||||||
|
useEffect(
|
||||||
|
() =>
|
||||||
|
application.commands.addWithShortcut(
|
||||||
|
CREATE_NEW_NOTE_KEYBOARD_COMMAND,
|
||||||
|
'General',
|
||||||
|
isFilesSmartView ? 'Upload file' : 'Create new note',
|
||||||
|
(event) => {
|
||||||
|
event?.preventDefault()
|
||||||
|
void addNewItem()
|
||||||
|
},
|
||||||
|
isFilesSmartView ? 'upload' : 'add',
|
||||||
|
),
|
||||||
|
[addNewItem, application.commands, isFilesSmartView],
|
||||||
|
)
|
||||||
|
|
||||||
const dailyMode = selectedAsTag?.isDailyEntry
|
const dailyMode = selectedAsTag?.isDailyEntry
|
||||||
|
|
||||||
const handleDailyListSelection = useCallback(
|
const handleDailyListSelection = useCallback(
|
||||||
|
|||||||
@@ -103,6 +103,17 @@ const ContentListHeader = ({
|
|||||||
setShowDisplayOptionsMenu((show) => !show)
|
setShowDisplayOptionsMenu((show) => !show)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
useEffect(
|
||||||
|
() =>
|
||||||
|
application.commands.add(
|
||||||
|
'open-display-opts-menu',
|
||||||
|
'Open display options menu',
|
||||||
|
toggleDisplayOptionsMenu,
|
||||||
|
'sort-descending',
|
||||||
|
),
|
||||||
|
[application.commands, toggleDisplayOptionsMenu],
|
||||||
|
)
|
||||||
|
|
||||||
const OptionsMenu = useMemo(() => {
|
const OptionsMenu = useMemo(() => {
|
||||||
return (
|
return (
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
|
|||||||
@@ -178,11 +178,11 @@ const EditorWidthSelectionModalWrapper = () => {
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return application.keyboardService.addCommandHandler({
|
return application.commands.addWithShortcut(
|
||||||
command: CHANGE_EDITOR_WIDTH_COMMAND,
|
CHANGE_EDITOR_WIDTH_COMMAND,
|
||||||
category: 'Current note',
|
'Current note',
|
||||||
description: 'Change editor width',
|
'Change editor width',
|
||||||
onKeyDown: (_, data) => {
|
(_, data) => {
|
||||||
if (typeof data === 'boolean' && data) {
|
if (typeof data === 'boolean' && data) {
|
||||||
setIsGlobal(data)
|
setIsGlobal(data)
|
||||||
} else {
|
} else {
|
||||||
@@ -190,7 +190,8 @@ const EditorWidthSelectionModalWrapper = () => {
|
|||||||
}
|
}
|
||||||
toggle()
|
toggle()
|
||||||
},
|
},
|
||||||
})
|
'line-width',
|
||||||
|
)
|
||||||
}, [application, toggle])
|
}, [application, toggle])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import { classNames } from '@standardnotes/utils'
|
import { classNames } from '@standardnotes/utils'
|
||||||
import { useRef } from 'react'
|
import { useEffect, useRef } from 'react'
|
||||||
import AccountMenu, { AccountMenuProps } from '../AccountMenu/AccountMenu'
|
import AccountMenu, { AccountMenuProps } from '../AccountMenu/AccountMenu'
|
||||||
import Icon from '../Icon/Icon'
|
import Icon from '../Icon/Icon'
|
||||||
import Popover from '../Popover/Popover'
|
import Popover from '../Popover/Popover'
|
||||||
import StyledTooltip from '../StyledTooltip/StyledTooltip'
|
import StyledTooltip from '../StyledTooltip/StyledTooltip'
|
||||||
import { observer } from 'mobx-react-lite'
|
import { observer } from 'mobx-react-lite'
|
||||||
import { AccountMenuController } from '@/Controllers/AccountMenu/AccountMenuController'
|
import { AccountMenuController } from '@/Controllers/AccountMenu/AccountMenuController'
|
||||||
|
import { useApplication } from '../ApplicationProvider'
|
||||||
|
|
||||||
type Props = AccountMenuProps & {
|
type Props = AccountMenuProps & {
|
||||||
controller: AccountMenuController
|
controller: AccountMenuController
|
||||||
@@ -15,9 +16,15 @@ type Props = AccountMenuProps & {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const AccountMenuButton = ({ hasError, controller, mainApplicationGroup, onClickOutside, toggleMenu, user }: Props) => {
|
const AccountMenuButton = ({ hasError, controller, mainApplicationGroup, onClickOutside, toggleMenu, user }: Props) => {
|
||||||
|
const application = useApplication()
|
||||||
const buttonRef = useRef<HTMLButtonElement>(null)
|
const buttonRef = useRef<HTMLButtonElement>(null)
|
||||||
const { show: isOpen } = controller
|
const { show: isOpen } = controller
|
||||||
|
|
||||||
|
useEffect(
|
||||||
|
() => application.commands.add('open-acc-menu', 'Open account menu', toggleMenu, 'account-circle'),
|
||||||
|
[application.commands, toggleMenu],
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<StyledTooltip label="Open account menu">
|
<StyledTooltip label="Open account menu">
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { compareSemVersions, StatusServiceEvent } from '@standardnotes/snjs'
|
|||||||
import { keyboardStringForShortcut, OPEN_PREFERENCES_COMMAND } from '@standardnotes/ui-services'
|
import { keyboardStringForShortcut, OPEN_PREFERENCES_COMMAND } from '@standardnotes/ui-services'
|
||||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||||
import { useApplication } from '../ApplicationProvider'
|
import { useApplication } from '../ApplicationProvider'
|
||||||
import { useCommandService } from '../CommandProvider'
|
import { useKeyboardService } from '../KeyboardServiceProvider'
|
||||||
import Icon from '../Icon/Icon'
|
import Icon from '../Icon/Icon'
|
||||||
import StyledTooltip from '../StyledTooltip/StyledTooltip'
|
import StyledTooltip from '../StyledTooltip/StyledTooltip'
|
||||||
import { MutuallyExclusiveMediaQueryBreakpoints, useMediaQuery } from '@/Hooks/useMediaQuery'
|
import { MutuallyExclusiveMediaQueryBreakpoints, useMediaQuery } from '@/Hooks/useMediaQuery'
|
||||||
@@ -16,10 +16,10 @@ type Props = {
|
|||||||
const PreferencesButton = ({ openPreferences }: Props) => {
|
const PreferencesButton = ({ openPreferences }: Props) => {
|
||||||
const application = useApplication()
|
const application = useApplication()
|
||||||
|
|
||||||
const commandService = useCommandService()
|
const keyboardService = useKeyboardService()
|
||||||
const shortcut = useMemo(
|
const shortcut = useMemo(
|
||||||
() => keyboardStringForShortcut(commandService.keyboardShortcutForCommand(OPEN_PREFERENCES_COMMAND)),
|
() => keyboardStringForShortcut(keyboardService.keyboardShortcutForCommand(OPEN_PREFERENCES_COMMAND)),
|
||||||
[commandService],
|
[keyboardService],
|
||||||
)
|
)
|
||||||
|
|
||||||
const [changelogLastReadVersion, setChangelogLastReadVersion] = useState(() =>
|
const [changelogLastReadVersion, setChangelogLastReadVersion] = useState(() =>
|
||||||
|
|||||||
@@ -2,13 +2,14 @@ import { WebApplication } from '@/Application/WebApplication'
|
|||||||
import { UIFeature, GetDarkThemeFeature } from '@standardnotes/snjs'
|
import { UIFeature, GetDarkThemeFeature } from '@standardnotes/snjs'
|
||||||
import { TOGGLE_DARK_MODE_COMMAND } from '@standardnotes/ui-services'
|
import { TOGGLE_DARK_MODE_COMMAND } from '@standardnotes/ui-services'
|
||||||
import { classNames } from '@standardnotes/utils'
|
import { classNames } from '@standardnotes/utils'
|
||||||
import { useEffect, useRef, useState } from 'react'
|
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||||
import { useCommandService } from '../CommandProvider'
|
import { useKeyboardService } from '../KeyboardServiceProvider'
|
||||||
import Icon from '../Icon/Icon'
|
import Icon from '../Icon/Icon'
|
||||||
import Popover from '../Popover/Popover'
|
import Popover from '../Popover/Popover'
|
||||||
import QuickSettingsMenu from '../QuickSettingsMenu/QuickSettingsMenu'
|
import QuickSettingsMenu from '../QuickSettingsMenu/QuickSettingsMenu'
|
||||||
import StyledTooltip from '../StyledTooltip/StyledTooltip'
|
import StyledTooltip from '../StyledTooltip/StyledTooltip'
|
||||||
import RoundIconButton from '../Button/RoundIconButton'
|
import RoundIconButton from '../Button/RoundIconButton'
|
||||||
|
import mergeRegister from '../../Hooks/mergeRegister'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
application: WebApplication
|
application: WebApplication
|
||||||
@@ -17,30 +18,37 @@ type Props = {
|
|||||||
|
|
||||||
const QuickSettingsButton = ({ application, isMobileNavigation = false }: Props) => {
|
const QuickSettingsButton = ({ application, isMobileNavigation = false }: Props) => {
|
||||||
const buttonRef = useRef<HTMLButtonElement>(null)
|
const buttonRef = useRef<HTMLButtonElement>(null)
|
||||||
const commandService = useCommandService()
|
const keyboardService = useKeyboardService()
|
||||||
|
|
||||||
const [isOpen, setIsOpen] = useState(false)
|
const [isOpen, setIsOpen] = useState(false)
|
||||||
const toggleMenu = () => setIsOpen(!isOpen)
|
const toggleMenu = useCallback(() => setIsOpen((isOpen) => !isOpen), [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (isMobileNavigation) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const darkThemeFeature = new UIFeature(GetDarkThemeFeature())
|
const darkThemeFeature = new UIFeature(GetDarkThemeFeature())
|
||||||
|
|
||||||
return commandService.addCommandHandler({
|
return mergeRegister(
|
||||||
command: TOGGLE_DARK_MODE_COMMAND,
|
application.commands.addWithShortcut(TOGGLE_DARK_MODE_COMMAND, 'General', 'Toggle dark mode', () => {
|
||||||
category: 'General',
|
|
||||||
description: 'Toggle dark mode',
|
|
||||||
onKeyDown: () => {
|
|
||||||
void application.componentManager.toggleTheme(darkThemeFeature)
|
void application.componentManager.toggleTheme(darkThemeFeature)
|
||||||
return true
|
return true
|
||||||
},
|
}),
|
||||||
})
|
application.commands.add('open-quick-settings-menu', 'Open quick settings menu', toggleMenu, 'themes'),
|
||||||
}, [application, commandService])
|
)
|
||||||
|
}, [application, isMobileNavigation, keyboardService, toggleMenu])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<StyledTooltip label="Open quick settings menu">
|
<StyledTooltip label="Open quick settings menu">
|
||||||
{isMobileNavigation ? (
|
{isMobileNavigation ? (
|
||||||
<RoundIconButton className="ml-2.5 bg-default" onClick={toggleMenu} label="Go to vaults menu" icon="themes" />
|
<RoundIconButton
|
||||||
|
className="ml-2.5 bg-default"
|
||||||
|
onClick={toggleMenu}
|
||||||
|
label="Go to quick settings menu"
|
||||||
|
icon="themes"
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<button
|
<button
|
||||||
onClick={toggleMenu}
|
onClick={toggleMenu}
|
||||||
|
|||||||
@@ -3,13 +3,13 @@ import { ReactNode, createContext, useContext, memo } from 'react'
|
|||||||
import { observer } from 'mobx-react-lite'
|
import { observer } from 'mobx-react-lite'
|
||||||
import { KeyboardService } from '@standardnotes/ui-services'
|
import { KeyboardService } from '@standardnotes/ui-services'
|
||||||
|
|
||||||
const CommandServiceContext = createContext<KeyboardService | undefined>(undefined)
|
const KeyboardServiceContext = createContext<KeyboardService | undefined>(undefined)
|
||||||
|
|
||||||
export const useCommandService = () => {
|
export const useKeyboardService = () => {
|
||||||
const value = useContext(CommandServiceContext)
|
const value = useContext(KeyboardServiceContext)
|
||||||
|
|
||||||
if (!value) {
|
if (!value) {
|
||||||
throw new Error('Component must be a child of <CommandServiceProvider />')
|
throw new Error('Component must be a child of <KeyboardServiceProvider />')
|
||||||
}
|
}
|
||||||
|
|
||||||
return value
|
return value
|
||||||
@@ -25,12 +25,12 @@ type ProviderProps = {
|
|||||||
|
|
||||||
const MemoizedChildren = memo(({ children }: ChildrenProps) => <>{children}</>)
|
const MemoizedChildren = memo(({ children }: ChildrenProps) => <>{children}</>)
|
||||||
|
|
||||||
const CommandServiceProvider = ({ service, children }: ProviderProps) => {
|
const KeyboardServiceProvider = ({ service, children }: ProviderProps) => {
|
||||||
return (
|
return (
|
||||||
<CommandServiceContext.Provider value={service}>
|
<KeyboardServiceContext.Provider value={service}>
|
||||||
<MemoizedChildren children={children} />
|
<MemoizedChildren children={children} />
|
||||||
</CommandServiceContext.Provider>
|
</KeyboardServiceContext.Provider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default observer(CommandServiceProvider)
|
export default observer(KeyboardServiceProvider)
|
||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
keyboardCharacterForModifier,
|
keyboardCharacterForModifier,
|
||||||
isMobilePlatform,
|
isMobilePlatform,
|
||||||
keyboardCharacterForKeyOrCode,
|
keyboardCharacterForKeyOrCode,
|
||||||
|
KeyboardModifier,
|
||||||
} from '@standardnotes/ui-services'
|
} from '@standardnotes/ui-services'
|
||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
|
|
||||||
@@ -20,7 +21,7 @@ export const KeyboardShortcutIndicator = ({ shortcut, small = true, dimmed = tru
|
|||||||
const primaryKey = shortcut.key
|
const primaryKey = shortcut.key
|
||||||
? keyboardCharacterForKeyOrCode(shortcut.key)
|
? keyboardCharacterForKeyOrCode(shortcut.key)
|
||||||
: shortcut.code
|
: shortcut.code
|
||||||
? keyboardCharacterForKeyOrCode(shortcut.code)
|
? keyboardCharacterForKeyOrCode(shortcut.code, modifiers.includes(KeyboardModifier.Shift))
|
||||||
: undefined
|
: undefined
|
||||||
|
|
||||||
const results: string[] = []
|
const results: string[] = []
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { observer } from 'mobx-react-lite'
|
|||||||
import ItemLinkAutocompleteInput from './ItemLinkAutocompleteInput'
|
import ItemLinkAutocompleteInput from './ItemLinkAutocompleteInput'
|
||||||
import { LinkingController } from '@/Controllers/LinkingController'
|
import { LinkingController } from '@/Controllers/LinkingController'
|
||||||
import LinkedItemBubble from './LinkedItemBubble'
|
import LinkedItemBubble from './LinkedItemBubble'
|
||||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { useResponsiveAppPane } from '../Panes/ResponsivePaneProvider'
|
import { useResponsiveAppPane } from '../Panes/ResponsivePaneProvider'
|
||||||
import { ElementIds } from '@/Constants/ElementIDs'
|
import { ElementIds } from '@/Constants/ElementIDs'
|
||||||
import { classNames } from '@standardnotes/utils'
|
import { classNames } from '@standardnotes/utils'
|
||||||
@@ -10,12 +10,13 @@ import { ContentType, DecryptedItemInterface } from '@standardnotes/snjs'
|
|||||||
import { LinkableItem } from '@/Utils/Items/Search/LinkableItem'
|
import { LinkableItem } from '@/Utils/Items/Search/LinkableItem'
|
||||||
import { ItemLink } from '@/Utils/Items/Search/ItemLink'
|
import { ItemLink } from '@/Utils/Items/Search/ItemLink'
|
||||||
import { FOCUS_TAGS_INPUT_COMMAND, keyboardStringForShortcut } from '@standardnotes/ui-services'
|
import { FOCUS_TAGS_INPUT_COMMAND, keyboardStringForShortcut } from '@standardnotes/ui-services'
|
||||||
import { useCommandService } from '../CommandProvider'
|
|
||||||
import { useItemLinks } from '@/Hooks/useItemLinks'
|
import { useItemLinks } from '@/Hooks/useItemLinks'
|
||||||
import RoundIconButton from '../Button/RoundIconButton'
|
import RoundIconButton from '../Button/RoundIconButton'
|
||||||
import VaultNameBadge from '../Vaults/VaultNameBadge'
|
import VaultNameBadge from '../Vaults/VaultNameBadge'
|
||||||
import LastEditedByBadge from '../Vaults/LastEditedByBadge'
|
import LastEditedByBadge from '../Vaults/LastEditedByBadge'
|
||||||
import { useItemVaultInfo } from '@/Hooks/useItemVaultInfo'
|
import { useItemVaultInfo } from '@/Hooks/useItemVaultInfo'
|
||||||
|
import mergeRegister from '../../Hooks/mergeRegister'
|
||||||
|
import { useApplication } from '../ApplicationProvider'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
linkingController: LinkingController
|
linkingController: LinkingController
|
||||||
@@ -39,7 +40,8 @@ const LinkedItemBubblesContainer = ({
|
|||||||
}: Props) => {
|
}: Props) => {
|
||||||
const { toggleAppPane } = useResponsiveAppPane()
|
const { toggleAppPane } = useResponsiveAppPane()
|
||||||
|
|
||||||
const commandService = useCommandService()
|
const application = useApplication()
|
||||||
|
const keyboardService = application.keyboardService
|
||||||
|
|
||||||
const { unlinkItems, activateItem } = linkingController
|
const { unlinkItems, activateItem } = linkingController
|
||||||
const unlinkItem = useCallback(
|
const unlinkItem = useCallback(
|
||||||
@@ -57,23 +59,28 @@ const LinkedItemBubblesContainer = ({
|
|||||||
[filesLinkedToItem, notesLinkedToItem, tagsLinkedToItem],
|
[filesLinkedToItem, notesLinkedToItem, tagsLinkedToItem],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const linkInputRef = useRef<HTMLInputElement>(null)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return commandService.addCommandHandler({
|
const focusInput = () => {
|
||||||
command: FOCUS_TAGS_INPUT_COMMAND,
|
const input = linkInputRef.current
|
||||||
category: 'Current note',
|
if (input) {
|
||||||
description: 'Link tags, notes, files',
|
setTimeout(() => input.focus())
|
||||||
onKeyDown: () => {
|
}
|
||||||
const input = document.getElementById(ElementIds.ItemLinkAutocompleteInput)
|
}
|
||||||
if (input) {
|
return mergeRegister(
|
||||||
input.focus()
|
keyboardService.addCommandHandler({
|
||||||
}
|
command: FOCUS_TAGS_INPUT_COMMAND,
|
||||||
},
|
category: 'Current note',
|
||||||
})
|
description: 'Link tags, notes, files',
|
||||||
}, [commandService])
|
onKeyDown: focusInput,
|
||||||
|
}),
|
||||||
|
application.commands.add('link-items-current', 'Link items to current note', focusInput, 'link'),
|
||||||
|
)
|
||||||
|
}, [application.commands, keyboardService])
|
||||||
|
|
||||||
const shortcut = useMemo(
|
const shortcut = useMemo(
|
||||||
() => keyboardStringForShortcut(commandService.keyboardShortcutForCommand(FOCUS_TAGS_INPUT_COMMAND)),
|
() => keyboardStringForShortcut(keyboardService.keyboardShortcutForCommand(FOCUS_TAGS_INPUT_COMMAND)),
|
||||||
[commandService],
|
[keyboardService],
|
||||||
)
|
)
|
||||||
|
|
||||||
const [focusedId, setFocusedId] = useState<string>()
|
const [focusedId, setFocusedId] = useState<string>()
|
||||||
@@ -209,6 +216,7 @@ const LinkedItemBubblesContainer = ({
|
|||||||
{isCollapsed && nonVisibleItems > 0 && <span className="flex-shrink-0">and {nonVisibleItems} more...</span>}
|
{isCollapsed && nonVisibleItems > 0 && <span className="flex-shrink-0">and {nonVisibleItems} more...</span>}
|
||||||
{!readonly && (
|
{!readonly && (
|
||||||
<ItemLinkAutocompleteInput
|
<ItemLinkAutocompleteInput
|
||||||
|
ref={linkInputRef}
|
||||||
focusedId={focusedId}
|
focusedId={focusedId}
|
||||||
linkingController={linkingController}
|
linkingController={linkingController}
|
||||||
focusPreviousItem={focusPreviousItem}
|
focusPreviousItem={focusPreviousItem}
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { LinkingController } from '@/Controllers/LinkingController'
|
import { LinkingController } from '@/Controllers/LinkingController'
|
||||||
import { observer } from 'mobx-react-lite'
|
import { observer } from 'mobx-react-lite'
|
||||||
import { useRef, useCallback } from 'react'
|
import { useRef, useCallback, useEffect } from 'react'
|
||||||
import RoundIconButton from '../Button/RoundIconButton'
|
import RoundIconButton from '../Button/RoundIconButton'
|
||||||
import Popover from '../Popover/Popover'
|
import Popover from '../Popover/Popover'
|
||||||
import LinkedItemsPanel from './LinkedItemsPanel'
|
import LinkedItemsPanel from './LinkedItemsPanel'
|
||||||
|
import { useApplication } from '../ApplicationProvider'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
linkingController: LinkingController
|
linkingController: LinkingController
|
||||||
@@ -12,6 +13,7 @@ type Props = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const LinkedItemsButton = ({ linkingController, onClick, onClickPreprocessing }: Props) => {
|
const LinkedItemsButton = ({ linkingController, onClick, onClickPreprocessing }: Props) => {
|
||||||
|
const application = useApplication()
|
||||||
const { activeItem, isLinkingPanelOpen, setIsLinkingPanelOpen } = linkingController
|
const { activeItem, isLinkingPanelOpen, setIsLinkingPanelOpen } = linkingController
|
||||||
const buttonRef = useRef<HTMLButtonElement>(null)
|
const buttonRef = useRef<HTMLButtonElement>(null)
|
||||||
|
|
||||||
@@ -26,6 +28,8 @@ const LinkedItemsButton = ({ linkingController, onClick, onClickPreprocessing }:
|
|||||||
}
|
}
|
||||||
}, [isLinkingPanelOpen, onClick, onClickPreprocessing, setIsLinkingPanelOpen])
|
}, [isLinkingPanelOpen, onClick, onClickPreprocessing, setIsLinkingPanelOpen])
|
||||||
|
|
||||||
|
useEffect(() => application.commands.add('open-linked-items-panel', 'Open linked items panel', toggleMenu, 'link'))
|
||||||
|
|
||||||
if (!activeItem) {
|
if (!activeItem) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,7 +44,6 @@ const ModalOverlay = forwardRef(
|
|||||||
close()
|
close()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
animated: !isMobileScreen,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const portalId = useId()
|
const portalId = useId()
|
||||||
|
|||||||
@@ -93,11 +93,7 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
|
|||||||
|
|
||||||
onEditorComponentLoad?: () => void
|
onEditorComponentLoad?: () => void
|
||||||
|
|
||||||
private removeTrashKeyObserver?: () => void
|
#observers: (() => void)[] = []
|
||||||
private removeNoteStreamObserver?: () => void
|
|
||||||
private removeComponentManagerObserver?: () => void
|
|
||||||
private removeInnerNoteObserver?: () => void
|
|
||||||
private removeVaultUsersEventHandler?: () => void
|
|
||||||
|
|
||||||
private protectionTimeoutId: ReturnType<typeof setTimeout> | null = null
|
private protectionTimeoutId: ReturnType<typeof setTimeout> | null = null
|
||||||
private noteViewElementRef: RefObject<HTMLDivElement>
|
private noteViewElementRef: RefObject<HTMLDivElement>
|
||||||
@@ -147,20 +143,12 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
|
|||||||
super.deinit()
|
super.deinit()
|
||||||
;(this.controller as unknown) = undefined
|
;(this.controller as unknown) = undefined
|
||||||
|
|
||||||
this.removeNoteStreamObserver?.()
|
for (let i = 0; i < this.#observers.length; i++) {
|
||||||
;(this.removeNoteStreamObserver as unknown) = undefined
|
const cleanup = this.#observers[i]
|
||||||
|
cleanup()
|
||||||
this.removeInnerNoteObserver?.()
|
}
|
||||||
;(this.removeInnerNoteObserver as unknown) = undefined
|
this.#observers.length = 0
|
||||||
|
;(this.#observers as unknown) = undefined
|
||||||
this.removeComponentManagerObserver?.()
|
|
||||||
;(this.removeComponentManagerObserver as unknown) = undefined
|
|
||||||
|
|
||||||
this.removeTrashKeyObserver?.()
|
|
||||||
this.removeTrashKeyObserver = undefined
|
|
||||||
|
|
||||||
this.removeVaultUsersEventHandler?.()
|
|
||||||
this.removeVaultUsersEventHandler = undefined
|
|
||||||
|
|
||||||
this.clearNoteProtectionInactivityTimer()
|
this.clearNoteProtectionInactivityTimer()
|
||||||
;(this.ensureNoteIsInsertedBeforeUIAction as unknown) = undefined
|
;(this.ensureNoteIsInsertedBeforeUIAction as unknown) = undefined
|
||||||
@@ -213,23 +201,27 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
|
|||||||
override componentDidMount(): void {
|
override componentDidMount(): void {
|
||||||
super.componentDidMount()
|
super.componentDidMount()
|
||||||
|
|
||||||
this.removeVaultUsersEventHandler = this.application.vaultUsers.addEventObserver((event, data) => {
|
this.#observers.push(
|
||||||
if (event === VaultUserServiceEvent.InvalidatedUserCacheForVault) {
|
this.application.vaultUsers.addEventObserver((event, data) => {
|
||||||
const vault = this.application.vaults.getItemVault(this.note)
|
if (event === VaultUserServiceEvent.InvalidatedUserCacheForVault) {
|
||||||
if ((data as string) !== vault?.sharing?.sharedVaultUuid) {
|
const vault = this.application.vaults.getItemVault(this.note)
|
||||||
return
|
if ((data as string) !== vault?.sharing?.sharedVaultUuid) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.setState({
|
||||||
|
readonly: vault ? this.application.vaultUsers.isCurrentUserReadonlyVaultMember(vault) : undefined,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
this.setState({
|
}),
|
||||||
readonly: vault ? this.application.vaultUsers.isCurrentUserReadonlyVaultMember(vault) : undefined,
|
)
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
this.registerKeyboardShortcuts()
|
this.registerKeyboardShortcuts()
|
||||||
|
|
||||||
this.removeInnerNoteObserver = this.controller.addNoteInnerValueChangeObserver((note, source) => {
|
this.#observers.push(
|
||||||
this.onNoteInnerChange(note, source)
|
this.controller.addNoteInnerValueChangeObserver((note, source) => {
|
||||||
})
|
this.onNoteInnerChange(note, source)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
this.autorun(() => {
|
this.autorun(() => {
|
||||||
const syncStatus = this.controller.syncStatus
|
const syncStatus = this.controller.syncStatus
|
||||||
@@ -463,15 +455,17 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
streamItems() {
|
streamItems() {
|
||||||
this.removeNoteStreamObserver = this.application.items.streamItems<SNNote>(ContentType.TYPES.Note, async () => {
|
this.#observers.push(
|
||||||
if (!this.note) {
|
this.application.items.streamItems<SNNote>(ContentType.TYPES.Note, async () => {
|
||||||
return
|
if (!this.note) {
|
||||||
}
|
return
|
||||||
|
}
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
conflictedNotes: this.application.items.conflictsOf(this.note.uuid) as SNNote[],
|
conflictedNotes: this.application.items.conflictsOf(this.note.uuid) as SNNote[],
|
||||||
})
|
})
|
||||||
})
|
}),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private createComponentViewer(component: UIFeature<IframeComponentFeatureDescription>) {
|
private createComponentViewer(component: UIFeature<IframeComponentFeatureDescription>) {
|
||||||
@@ -766,14 +760,18 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
registerKeyboardShortcuts() {
|
registerKeyboardShortcuts() {
|
||||||
this.removeTrashKeyObserver = this.application.keyboardService.addCommandHandler({
|
const moveNoteToTrash = () => {
|
||||||
command: DELETE_NOTE_KEYBOARD_COMMAND,
|
this.deleteNote(false).catch(console.error)
|
||||||
notTags: ['INPUT', 'TEXTAREA'],
|
}
|
||||||
notElementIds: [SuperEditorContentId],
|
|
||||||
onKeyDown: () => {
|
this.#observers.push(
|
||||||
this.deleteNote(false).catch(console.error)
|
this.application.keyboardService.addCommandHandler({
|
||||||
},
|
command: DELETE_NOTE_KEYBOARD_COMMAND,
|
||||||
})
|
notTags: ['INPUT', 'TEXTAREA'],
|
||||||
|
notElementIds: [SuperEditorContentId],
|
||||||
|
onKeyDown: moveNoteToTrash,
|
||||||
|
}),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
ensureNoteIsInsertedBeforeUIAction = async () => {
|
ensureNoteIsInsertedBeforeUIAction = async () => {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import Icon from '@/Components/Icon/Icon'
|
import Icon from '@/Components/Icon/Icon'
|
||||||
import { observer } from 'mobx-react-lite'
|
import { observer } from 'mobx-react-lite'
|
||||||
import { useState, useEffect, useMemo, useCallback } from 'react'
|
import { useState, useEffect, useMemo, useCallback } from 'react'
|
||||||
import { NoteType, Platform, SNNote, pluralize } from '@standardnotes/snjs'
|
import { NoteType, Platform } from '@standardnotes/snjs'
|
||||||
import {
|
import {
|
||||||
CHANGE_EDITOR_WIDTH_COMMAND,
|
CHANGE_EDITOR_WIDTH_COMMAND,
|
||||||
OPEN_NOTE_HISTORY_COMMAND,
|
OPEN_NOTE_HISTORY_COMMAND,
|
||||||
@@ -12,7 +12,6 @@ import {
|
|||||||
import ChangeEditorOption from './ChangeEditorOption'
|
import ChangeEditorOption from './ChangeEditorOption'
|
||||||
import ListedActionsOption from './Listed/ListedActionsOption'
|
import ListedActionsOption from './Listed/ListedActionsOption'
|
||||||
import AddTagOption from './AddTagOption'
|
import AddTagOption from './AddTagOption'
|
||||||
import { addToast, dismissToast, ToastType } from '@standardnotes/toast'
|
|
||||||
import { NotesOptionsProps } from './NotesOptionsProps'
|
import { NotesOptionsProps } from './NotesOptionsProps'
|
||||||
import { useResponsiveAppPane } from '../Panes/ResponsivePaneProvider'
|
import { useResponsiveAppPane } from '../Panes/ResponsivePaneProvider'
|
||||||
import { AppPaneId } from '../Panes/AppPaneMetadata'
|
import { AppPaneId } from '../Panes/AppPaneMetadata'
|
||||||
@@ -27,13 +26,10 @@ import { iconClass } from './ClassNames'
|
|||||||
import SuperNoteOptions from './SuperNoteOptions'
|
import SuperNoteOptions from './SuperNoteOptions'
|
||||||
import MenuSwitchButtonItem from '../Menu/MenuSwitchButtonItem'
|
import MenuSwitchButtonItem from '../Menu/MenuSwitchButtonItem'
|
||||||
import MenuItem from '../Menu/MenuItem'
|
import MenuItem from '../Menu/MenuItem'
|
||||||
import ModalOverlay from '../Modal/ModalOverlay'
|
|
||||||
import SuperExportModal from './SuperExportModal'
|
|
||||||
import { useApplication } from '../ApplicationProvider'
|
import { useApplication } from '../ApplicationProvider'
|
||||||
import { MutuallyExclusiveMediaQueryBreakpoints } from '@/Hooks/useMediaQuery'
|
import { MutuallyExclusiveMediaQueryBreakpoints } from '@/Hooks/useMediaQuery'
|
||||||
import AddToVaultMenuOption from '../Vaults/AddToVaultMenuOption'
|
import AddToVaultMenuOption from '../Vaults/AddToVaultMenuOption'
|
||||||
import MenuSection from '../Menu/MenuSection'
|
import MenuSection from '../Menu/MenuSection'
|
||||||
import { downloadOrShareBlobBasedOnPlatform } from '@/Utils/DownloadOrShareBasedOnPlatform'
|
|
||||||
import { shareBlobOnMobile } from '@/NativeMobileWeb/ShareBlobOnMobile'
|
import { shareBlobOnMobile } from '@/NativeMobileWeb/ShareBlobOnMobile'
|
||||||
|
|
||||||
const iconSize = MenuItemIconSize
|
const iconSize = MenuItemIconSize
|
||||||
@@ -43,26 +39,13 @@ const iconClassSuccess = `text-success mr-2 ${iconSize}`
|
|||||||
|
|
||||||
const NotesOptions = ({ notes, closeMenu }: NotesOptionsProps) => {
|
const NotesOptions = ({ notes, closeMenu }: NotesOptionsProps) => {
|
||||||
const application = useApplication()
|
const application = useApplication()
|
||||||
|
const notesController = application.notesController
|
||||||
|
|
||||||
const [altKeyDown, setAltKeyDown] = useState(false)
|
const [altKeyDown, setAltKeyDown] = useState(false)
|
||||||
const { toggleAppPane } = useResponsiveAppPane()
|
const { toggleAppPane } = useResponsiveAppPane()
|
||||||
|
|
||||||
const toggleOn = (condition: (note: SNNote) => boolean) => {
|
const { trashed, notTrashed, pinned, unpinned, starred, archived, unarchived, locked, protect, hidePreviews } =
|
||||||
const notesMatchingAttribute = notes.filter(condition)
|
notesController.getNotesInfo(notes)
|
||||||
const notesNotMatchingAttribute = notes.filter((note) => !condition(note))
|
|
||||||
return notesMatchingAttribute.length > notesNotMatchingAttribute.length
|
|
||||||
}
|
|
||||||
|
|
||||||
const hidePreviews = toggleOn((note) => note.hidePreview)
|
|
||||||
const locked = toggleOn((note) => note.locked)
|
|
||||||
const protect = toggleOn((note) => note.protected)
|
|
||||||
const archived = notes.some((note) => note.archived)
|
|
||||||
const unarchived = notes.some((note) => !note.archived)
|
|
||||||
const trashed = notes.some((note) => note.trashed)
|
|
||||||
const notTrashed = notes.some((note) => !note.trashed)
|
|
||||||
const pinned = notes.some((note) => note.pinned)
|
|
||||||
const unpinned = notes.some((note) => !note.pinned)
|
|
||||||
const starred = notes.some((note) => note.starred)
|
|
||||||
|
|
||||||
const editorForNote = useMemo(
|
const editorForNote = useMemo(
|
||||||
() => (notes[0] ? application.componentManager.editorForNote(notes[0]) : undefined),
|
() => (notes[0] ? application.componentManager.editorForNote(notes[0]) : undefined),
|
||||||
@@ -85,55 +68,6 @@ const NotesOptions = ({ notes, closeMenu }: NotesOptionsProps) => {
|
|||||||
}
|
}
|
||||||
}, [application])
|
}, [application])
|
||||||
|
|
||||||
const [showExportSuperModal, setShowExportSuperModal] = useState(false)
|
|
||||||
const closeSuperExportModal = useCallback(() => {
|
|
||||||
setShowExportSuperModal(false)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const downloadSelectedItems = useCallback(async () => {
|
|
||||||
if (notes.length === 0) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const toast = addToast({
|
|
||||||
type: ToastType.Progress,
|
|
||||||
message: `Exporting ${notes.length} ${pluralize(notes.length, 'note', 'notes')}...`,
|
|
||||||
})
|
|
||||||
try {
|
|
||||||
const result = await createNoteExport(application, notes)
|
|
||||||
if (!result) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const { blob, fileName } = result
|
|
||||||
void downloadOrShareBlobBasedOnPlatform({
|
|
||||||
archiveService: application.archiveService,
|
|
||||||
platform: application.platform,
|
|
||||||
mobileDevice: application.mobileDevice,
|
|
||||||
blob: blob,
|
|
||||||
filename: fileName,
|
|
||||||
isNativeMobileWeb: application.isNativeMobileWeb(),
|
|
||||||
})
|
|
||||||
dismissToast(toast)
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error)
|
|
||||||
addToast({
|
|
||||||
type: ToastType.Error,
|
|
||||||
message: 'Could not export notes',
|
|
||||||
})
|
|
||||||
dismissToast(toast)
|
|
||||||
}
|
|
||||||
}, [application, notes])
|
|
||||||
|
|
||||||
const exportSelectedItems = useCallback(() => {
|
|
||||||
const hasSuperNote = notes.some((note) => note.noteType === NoteType.Super)
|
|
||||||
|
|
||||||
if (hasSuperNote) {
|
|
||||||
setShowExportSuperModal(true)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
downloadSelectedItems().catch(console.error)
|
|
||||||
}, [downloadSelectedItems, notes])
|
|
||||||
|
|
||||||
const shareSelectedItems = useCallback(() => {
|
const shareSelectedItems = useCallback(() => {
|
||||||
createNoteExport(application, notes)
|
createNoteExport(application, notes)
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
@@ -158,37 +92,14 @@ const NotesOptions = ({ notes, closeMenu }: NotesOptionsProps) => {
|
|||||||
closeMenu()
|
closeMenu()
|
||||||
}, [closeMenu, toggleAppPane])
|
}, [closeMenu, toggleAppPane])
|
||||||
|
|
||||||
const duplicateSelectedItems = useCallback(async () => {
|
const duplicateSelectedNotes = useCallback(async () => {
|
||||||
await Promise.all(
|
await notesController.duplicateSelectedNotes()
|
||||||
notes.map((note) =>
|
|
||||||
application.mutator
|
|
||||||
.duplicateItem(note)
|
|
||||||
.then((duplicated) =>
|
|
||||||
addToast({
|
|
||||||
type: ToastType.Regular,
|
|
||||||
message: `Duplicated note "${duplicated.title}"`,
|
|
||||||
actions: [
|
|
||||||
{
|
|
||||||
label: 'Open',
|
|
||||||
handler: (toastId) => {
|
|
||||||
application.itemListController.selectUuids([duplicated.uuid], true).catch(console.error)
|
|
||||||
dismissToast(toastId)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
autoClose: true,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.catch(console.error),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
void application.sync.sync()
|
|
||||||
closeMenuAndToggleNotesList()
|
closeMenuAndToggleNotesList()
|
||||||
}, [application.mutator, application.itemListController, application.sync, closeMenuAndToggleNotesList, notes])
|
}, [closeMenuAndToggleNotesList, notesController])
|
||||||
|
|
||||||
const openRevisionHistoryModal = useCallback(() => {
|
const openRevisionHistoryModal = useCallback(() => {
|
||||||
application.historyModalController.openModal(application.notesController.firstSelectedNote)
|
application.historyModalController.openModal(notesController.firstSelectedNote)
|
||||||
}, [application.historyModalController, application.notesController.firstSelectedNote])
|
}, [application.historyModalController, notesController.firstSelectedNote])
|
||||||
|
|
||||||
const historyShortcut = useMemo(
|
const historyShortcut = useMemo(
|
||||||
() => application.keyboardService.keyboardShortcutForCommand(OPEN_NOTE_HISTORY_COMMAND),
|
() => application.keyboardService.keyboardShortcutForCommand(OPEN_NOTE_HISTORY_COMMAND),
|
||||||
@@ -259,7 +170,7 @@ const NotesOptions = ({ notes, closeMenu }: NotesOptionsProps) => {
|
|||||||
<MenuSwitchButtonItem
|
<MenuSwitchButtonItem
|
||||||
checked={locked}
|
checked={locked}
|
||||||
onChange={(locked) => {
|
onChange={(locked) => {
|
||||||
application.notesController.setLockSelectedNotes(locked)
|
notesController.setLockSelectedNotes(locked)
|
||||||
}}
|
}}
|
||||||
disabled={areSomeNotesInReadonlySharedVault}
|
disabled={areSomeNotesInReadonlySharedVault}
|
||||||
>
|
>
|
||||||
@@ -269,7 +180,7 @@ const NotesOptions = ({ notes, closeMenu }: NotesOptionsProps) => {
|
|||||||
<MenuSwitchButtonItem
|
<MenuSwitchButtonItem
|
||||||
checked={!hidePreviews}
|
checked={!hidePreviews}
|
||||||
onChange={(hidePreviews) => {
|
onChange={(hidePreviews) => {
|
||||||
application.notesController.setHideSelectedNotePreviews(!hidePreviews)
|
notesController.setHideSelectedNotePreviews(!hidePreviews)
|
||||||
}}
|
}}
|
||||||
disabled={areSomeNotesInReadonlySharedVault}
|
disabled={areSomeNotesInReadonlySharedVault}
|
||||||
>
|
>
|
||||||
@@ -279,7 +190,7 @@ const NotesOptions = ({ notes, closeMenu }: NotesOptionsProps) => {
|
|||||||
<MenuSwitchButtonItem
|
<MenuSwitchButtonItem
|
||||||
checked={protect}
|
checked={protect}
|
||||||
onChange={(protect) => {
|
onChange={(protect) => {
|
||||||
application.notesController.setProtectSelectedNotes(protect).catch(console.error)
|
notesController.setProtectSelectedNotes(protect).catch(console.error)
|
||||||
}}
|
}}
|
||||||
disabled={areSomeNotesInReadonlySharedVault}
|
disabled={areSomeNotesInReadonlySharedVault}
|
||||||
>
|
>
|
||||||
@@ -318,7 +229,7 @@ const NotesOptions = ({ notes, closeMenu }: NotesOptionsProps) => {
|
|||||||
)}
|
)}
|
||||||
<MenuItem
|
<MenuItem
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
application.notesController.setStarSelectedNotes(!starred)
|
notesController.setStarSelectedNotes(!starred)
|
||||||
}}
|
}}
|
||||||
disabled={areSomeNotesInReadonlySharedVault}
|
disabled={areSomeNotesInReadonlySharedVault}
|
||||||
>
|
>
|
||||||
@@ -330,7 +241,7 @@ const NotesOptions = ({ notes, closeMenu }: NotesOptionsProps) => {
|
|||||||
{unpinned && (
|
{unpinned && (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
application.notesController.setPinSelectedNotes(true)
|
notesController.setPinSelectedNotes(true)
|
||||||
}}
|
}}
|
||||||
disabled={areSomeNotesInReadonlySharedVault}
|
disabled={areSomeNotesInReadonlySharedVault}
|
||||||
>
|
>
|
||||||
@@ -342,7 +253,7 @@ const NotesOptions = ({ notes, closeMenu }: NotesOptionsProps) => {
|
|||||||
{pinned && (
|
{pinned && (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
application.notesController.setPinSelectedNotes(false)
|
notesController.setPinSelectedNotes(false)
|
||||||
}}
|
}}
|
||||||
disabled={areSomeNotesInReadonlySharedVault}
|
disabled={areSomeNotesInReadonlySharedVault}
|
||||||
>
|
>
|
||||||
@@ -351,7 +262,7 @@ const NotesOptions = ({ notes, closeMenu }: NotesOptionsProps) => {
|
|||||||
{pinShortcut && <KeyboardShortcutIndicator className="ml-auto" shortcut={pinShortcut} />}
|
{pinShortcut && <KeyboardShortcutIndicator className="ml-auto" shortcut={pinShortcut} />}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
)}
|
)}
|
||||||
<MenuItem onClick={exportSelectedItems}>
|
<MenuItem onClick={notesController.exportSelectedNotes}>
|
||||||
<Icon type="download" className={iconClass} />
|
<Icon type="download" className={iconClass} />
|
||||||
Export
|
Export
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
@@ -361,14 +272,14 @@ const NotesOptions = ({ notes, closeMenu }: NotesOptionsProps) => {
|
|||||||
Share
|
Share
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
)}
|
)}
|
||||||
<MenuItem onClick={duplicateSelectedItems} disabled={areSomeNotesInReadonlySharedVault}>
|
<MenuItem onClick={duplicateSelectedNotes} disabled={areSomeNotesInReadonlySharedVault}>
|
||||||
<Icon type="copy" className={iconClass} />
|
<Icon type="copy" className={iconClass} />
|
||||||
Duplicate
|
Duplicate
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
{unarchived && (
|
{unarchived && (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await application.notesController.setArchiveSelectedNotes(true).catch(console.error)
|
await notesController.setArchiveSelectedNotes(true).catch(console.error)
|
||||||
closeMenuAndToggleNotesList()
|
closeMenuAndToggleNotesList()
|
||||||
}}
|
}}
|
||||||
disabled={areSomeNotesInReadonlySharedVault}
|
disabled={areSomeNotesInReadonlySharedVault}
|
||||||
@@ -380,7 +291,7 @@ const NotesOptions = ({ notes, closeMenu }: NotesOptionsProps) => {
|
|||||||
{archived && (
|
{archived && (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await application.notesController.setArchiveSelectedNotes(false).catch(console.error)
|
await notesController.setArchiveSelectedNotes(false).catch(console.error)
|
||||||
closeMenuAndToggleNotesList()
|
closeMenuAndToggleNotesList()
|
||||||
}}
|
}}
|
||||||
disabled={areSomeNotesInReadonlySharedVault}
|
disabled={areSomeNotesInReadonlySharedVault}
|
||||||
@@ -394,7 +305,7 @@ const NotesOptions = ({ notes, closeMenu }: NotesOptionsProps) => {
|
|||||||
<MenuItem
|
<MenuItem
|
||||||
disabled={areSomeNotesInReadonlySharedVault}
|
disabled={areSomeNotesInReadonlySharedVault}
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await application.notesController.deleteNotesPermanently()
|
await notesController.deleteNotesPermanently()
|
||||||
closeMenuAndToggleNotesList()
|
closeMenuAndToggleNotesList()
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -404,7 +315,7 @@ const NotesOptions = ({ notes, closeMenu }: NotesOptionsProps) => {
|
|||||||
) : (
|
) : (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await application.notesController.setTrashSelectedNotes(true)
|
await notesController.setTrashSelectedNotes(true)
|
||||||
closeMenuAndToggleNotesList()
|
closeMenuAndToggleNotesList()
|
||||||
}}
|
}}
|
||||||
disabled={areSomeNotesInReadonlySharedVault}
|
disabled={areSomeNotesInReadonlySharedVault}
|
||||||
@@ -417,7 +328,7 @@ const NotesOptions = ({ notes, closeMenu }: NotesOptionsProps) => {
|
|||||||
<>
|
<>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await application.notesController.setTrashSelectedNotes(false)
|
await notesController.setTrashSelectedNotes(false)
|
||||||
closeMenuAndToggleNotesList()
|
closeMenuAndToggleNotesList()
|
||||||
}}
|
}}
|
||||||
disabled={areSomeNotesInReadonlySharedVault}
|
disabled={areSomeNotesInReadonlySharedVault}
|
||||||
@@ -428,7 +339,7 @@ const NotesOptions = ({ notes, closeMenu }: NotesOptionsProps) => {
|
|||||||
<MenuItem
|
<MenuItem
|
||||||
disabled={areSomeNotesInReadonlySharedVault}
|
disabled={areSomeNotesInReadonlySharedVault}
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await application.notesController.deleteNotesPermanently()
|
await notesController.deleteNotesPermanently()
|
||||||
closeMenuAndToggleNotesList()
|
closeMenuAndToggleNotesList()
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -437,7 +348,7 @@ const NotesOptions = ({ notes, closeMenu }: NotesOptionsProps) => {
|
|||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await application.notesController.emptyTrash()
|
await notesController.emptyTrash()
|
||||||
closeMenuAndToggleNotesList()
|
closeMenuAndToggleNotesList()
|
||||||
}}
|
}}
|
||||||
disabled={areSomeNotesInReadonlySharedVault}
|
disabled={areSomeNotesInReadonlySharedVault}
|
||||||
@@ -446,7 +357,7 @@ const NotesOptions = ({ notes, closeMenu }: NotesOptionsProps) => {
|
|||||||
<Icon type="trash-sweep" className="mr-2 text-danger" />
|
<Icon type="trash-sweep" className="mr-2 text-danger" />
|
||||||
<div className="flex-row">
|
<div className="flex-row">
|
||||||
<div className="text-danger">Empty Trash</div>
|
<div className="text-danger">Empty Trash</div>
|
||||||
<div className="text-xs">{application.notesController.trashedNotesCount} notes in Trash</div>
|
<div className="text-xs">{notesController.trashedNotesCount} notes in Trash</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
@@ -468,7 +379,7 @@ const NotesOptions = ({ notes, closeMenu }: NotesOptionsProps) => {
|
|||||||
<MenuSection>
|
<MenuSection>
|
||||||
<SpellcheckOptions
|
<SpellcheckOptions
|
||||||
editorForNote={editorForNote}
|
editorForNote={editorForNote}
|
||||||
notesController={application.notesController}
|
notesController={notesController}
|
||||||
note={notes[0]}
|
note={notes[0]}
|
||||||
disabled={areSomeNotesInReadonlySharedVault}
|
disabled={areSomeNotesInReadonlySharedVault}
|
||||||
/>
|
/>
|
||||||
@@ -480,10 +391,6 @@ const NotesOptions = ({ notes, closeMenu }: NotesOptionsProps) => {
|
|||||||
<NoteSizeWarning note={notes[0]} />
|
<NoteSizeWarning note={notes[0]} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<ModalOverlay isOpen={showExportSuperModal} close={closeSuperExportModal} className="md:max-w-[25vw]">
|
|
||||||
<SuperExportModal notes={notes} exportNotes={downloadSelectedItems} close={closeSuperExportModal} />
|
|
||||||
</ModalOverlay>
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { PrefKey, PrefValue, SNNote } from '@standardnotes/snjs'
|
import { PrefKey, PrefValue } from '@standardnotes/snjs'
|
||||||
import { useApplication } from '../ApplicationProvider'
|
import { useApplication } from '../ApplicationProvider'
|
||||||
import Modal from '../Modal/Modal'
|
import Modal from '../Modal/Modal'
|
||||||
import usePreference from '@/Hooks/usePreference'
|
import usePreference from '@/Hooks/usePreference'
|
||||||
@@ -6,15 +6,13 @@ import { useEffect } from 'react'
|
|||||||
import Switch from '../Switch/Switch'
|
import Switch from '../Switch/Switch'
|
||||||
import { noteHasEmbeddedFiles } from '@/Utils/NoteExportUtils'
|
import { noteHasEmbeddedFiles } from '@/Utils/NoteExportUtils'
|
||||||
import Dropdown from '../Dropdown/Dropdown'
|
import Dropdown from '../Dropdown/Dropdown'
|
||||||
|
import ModalOverlay from '../Modal/ModalOverlay'
|
||||||
|
import { observer } from 'mobx-react-lite'
|
||||||
|
|
||||||
type Props = {
|
const ModalContent = observer(() => {
|
||||||
notes: SNNote[]
|
|
||||||
exportNotes: () => void
|
|
||||||
close: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
const SuperExportModal = ({ notes, exportNotes, close }: Props) => {
|
|
||||||
const application = useApplication()
|
const application = useApplication()
|
||||||
|
const notesController = application.notesController
|
||||||
|
const notes = notesController.selectedNotes
|
||||||
|
|
||||||
const superNoteExportFormat = usePreference(PrefKey.SuperNoteExportFormat)
|
const superNoteExportFormat = usePreference(PrefKey.SuperNoteExportFormat)
|
||||||
const superNoteExportEmbedBehavior = usePreference(PrefKey.SuperNoteExportEmbedBehavior)
|
const superNoteExportEmbedBehavior = usePreference(PrefKey.SuperNoteExportEmbedBehavior)
|
||||||
@@ -53,8 +51,8 @@ const SuperExportModal = ({ notes, exportNotes, close }: Props) => {
|
|||||||
label: 'Export',
|
label: 'Export',
|
||||||
type: 'primary',
|
type: 'primary',
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
close()
|
void notesController.downloadSelectedNotes()
|
||||||
exportNotes()
|
notesController.closeSuperExportModal()
|
||||||
},
|
},
|
||||||
mobileSlot: 'right',
|
mobileSlot: 'right',
|
||||||
},
|
},
|
||||||
@@ -157,6 +155,21 @@ const SuperExportModal = ({ notes, exportNotes, close }: Props) => {
|
|||||||
)}
|
)}
|
||||||
</Modal>
|
</Modal>
|
||||||
)
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
const SuperExportModal = () => {
|
||||||
|
const application = useApplication()
|
||||||
|
const notesController = application.notesController
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModalOverlay
|
||||||
|
isOpen={notesController.shouldShowSuperExportModal}
|
||||||
|
close={notesController.closeSuperExportModal}
|
||||||
|
className="md:max-w-[25vw]"
|
||||||
|
>
|
||||||
|
<ModalContent />
|
||||||
|
</ModalOverlay>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SuperExportModal
|
export default observer(SuperExportModal)
|
||||||
|
|||||||
@@ -5,30 +5,30 @@ import { iconClass } from './ClassNames'
|
|||||||
import MenuSection from '../Menu/MenuSection'
|
import MenuSection from '../Menu/MenuSection'
|
||||||
import { SUPER_SHOW_MARKDOWN_PREVIEW, SUPER_TOGGLE_SEARCH } from '@standardnotes/ui-services'
|
import { SUPER_SHOW_MARKDOWN_PREVIEW, SUPER_TOGGLE_SEARCH } from '@standardnotes/ui-services'
|
||||||
import { useMemo, useCallback } from 'react'
|
import { useMemo, useCallback } from 'react'
|
||||||
import { useCommandService } from '../CommandProvider'
|
import { useKeyboardService } from '../KeyboardServiceProvider'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
closeMenu: () => void
|
closeMenu: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const SuperNoteOptions = ({ closeMenu }: Props) => {
|
const SuperNoteOptions = ({ closeMenu }: Props) => {
|
||||||
const commandService = useCommandService()
|
const keyboardService = useKeyboardService()
|
||||||
|
|
||||||
const markdownShortcut = useMemo(
|
const markdownShortcut = useMemo(
|
||||||
() => commandService.keyboardShortcutForCommand(SUPER_SHOW_MARKDOWN_PREVIEW),
|
() => keyboardService.keyboardShortcutForCommand(SUPER_SHOW_MARKDOWN_PREVIEW),
|
||||||
[commandService],
|
[keyboardService],
|
||||||
)
|
)
|
||||||
|
|
||||||
const findShortcut = useMemo(() => commandService.keyboardShortcutForCommand(SUPER_TOGGLE_SEARCH), [commandService])
|
const findShortcut = useMemo(() => keyboardService.keyboardShortcutForCommand(SUPER_TOGGLE_SEARCH), [keyboardService])
|
||||||
|
|
||||||
const enableSuperMarkdownPreview = useCallback(() => {
|
const enableSuperMarkdownPreview = useCallback(() => {
|
||||||
commandService.triggerCommand(SUPER_SHOW_MARKDOWN_PREVIEW)
|
keyboardService.triggerCommand(SUPER_SHOW_MARKDOWN_PREVIEW)
|
||||||
}, [commandService])
|
}, [keyboardService])
|
||||||
|
|
||||||
const findInNote = useCallback(() => {
|
const findInNote = useCallback(() => {
|
||||||
commandService.triggerCommand(SUPER_TOGGLE_SEARCH)
|
keyboardService.triggerCommand(SUPER_TOGGLE_SEARCH)
|
||||||
closeMenu()
|
closeMenu()
|
||||||
}, [closeMenu, commandService])
|
}, [closeMenu, keyboardService])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MenuSection>
|
<MenuSection>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import Icon from '@/Components/Icon/Icon'
|
|||||||
import { NotesController } from '@/Controllers/NotesController/NotesController'
|
import { NotesController } from '@/Controllers/NotesController/NotesController'
|
||||||
import { classNames } from '@standardnotes/utils'
|
import { classNames } from '@standardnotes/utils'
|
||||||
import { keyboardStringForShortcut, PIN_NOTE_COMMAND } from '@standardnotes/ui-services'
|
import { keyboardStringForShortcut, PIN_NOTE_COMMAND } from '@standardnotes/ui-services'
|
||||||
import { useCommandService } from '../CommandProvider'
|
import { useKeyboardService } from '../KeyboardServiceProvider'
|
||||||
import { VisuallyHidden } from '@ariakit/react'
|
import { VisuallyHidden } from '@ariakit/react'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -24,11 +24,11 @@ const PinNoteButton: FunctionComponent<Props> = ({ className = '', notesControll
|
|||||||
notesController.togglePinSelectedNotes()
|
notesController.togglePinSelectedNotes()
|
||||||
}, [onClickPreprocessing, notesController])
|
}, [onClickPreprocessing, notesController])
|
||||||
|
|
||||||
const commandService = useCommandService()
|
const keyboardService = useKeyboardService()
|
||||||
|
|
||||||
const shortcut = useMemo(
|
const shortcut = useMemo(
|
||||||
() => keyboardStringForShortcut(commandService.keyboardShortcutForCommand(PIN_NOTE_COMMAND)),
|
() => keyboardStringForShortcut(keyboardService.keyboardShortcutForCommand(PIN_NOTE_COMMAND)),
|
||||||
[commandService],
|
[keyboardService],
|
||||||
)
|
)
|
||||||
|
|
||||||
const label = pinned ? `Unpin note (${shortcut})` : `Pin note (${shortcut})`
|
const label = pinned ? `Unpin note (${shortcut})` : `Pin note (${shortcut})`
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { FunctionComponent, useEffect } from 'react'
|
|||||||
import { observer } from 'mobx-react-lite'
|
import { observer } from 'mobx-react-lite'
|
||||||
import PreferencesView from './PreferencesView'
|
import PreferencesView from './PreferencesView'
|
||||||
import { PreferencesViewWrapperProps } from './PreferencesViewWrapperProps'
|
import { PreferencesViewWrapperProps } from './PreferencesViewWrapperProps'
|
||||||
import { useCommandService } from '../CommandProvider'
|
|
||||||
import { OPEN_PREFERENCES_COMMAND } from '@standardnotes/ui-services'
|
import { OPEN_PREFERENCES_COMMAND } from '@standardnotes/ui-services'
|
||||||
import ModalOverlay from '../Modal/ModalOverlay'
|
import ModalOverlay from '../Modal/ModalOverlay'
|
||||||
import { usePaneSwipeGesture } from '../Panes/usePaneGesture'
|
import { usePaneSwipeGesture } from '../Panes/usePaneGesture'
|
||||||
@@ -10,16 +9,15 @@ import { performSafariAnimationFix } from '../Panes/PaneAnimator'
|
|||||||
import { IosModalAnimationEasing } from '../Modal/useModalAnimation'
|
import { IosModalAnimationEasing } from '../Modal/useModalAnimation'
|
||||||
|
|
||||||
const PreferencesViewWrapper: FunctionComponent<PreferencesViewWrapperProps> = ({ application }) => {
|
const PreferencesViewWrapper: FunctionComponent<PreferencesViewWrapperProps> = ({ application }) => {
|
||||||
const commandService = useCommandService()
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return commandService.addCommandHandler({
|
return application.commands.addWithShortcut(
|
||||||
command: OPEN_PREFERENCES_COMMAND,
|
OPEN_PREFERENCES_COMMAND,
|
||||||
category: 'General',
|
'General',
|
||||||
description: 'Open preferences',
|
'Open preferences',
|
||||||
onKeyDown: () => application.preferencesController.openPreferences(),
|
() => application.preferencesController.openPreferences(),
|
||||||
})
|
'tune',
|
||||||
}, [commandService, application])
|
)
|
||||||
|
}, [application.commands, application.preferencesController])
|
||||||
|
|
||||||
const [setElement] = usePaneSwipeGesture('right', async (element) => {
|
const [setElement] = usePaneSwipeGesture('right', async (element) => {
|
||||||
const animation = element.animate(
|
const animation = element.animate(
|
||||||
|
|||||||
@@ -2,23 +2,23 @@ import { TOGGLE_LIST_PANE_KEYBOARD_COMMAND, TOGGLE_NAVIGATION_PANE_KEYBOARD_COMM
|
|||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
import { observer } from 'mobx-react-lite'
|
import { observer } from 'mobx-react-lite'
|
||||||
import { useResponsiveAppPane } from '../Panes/ResponsivePaneProvider'
|
import { useResponsiveAppPane } from '../Panes/ResponsivePaneProvider'
|
||||||
import { useCommandService } from '../CommandProvider'
|
import { useKeyboardService } from '../KeyboardServiceProvider'
|
||||||
import MenuSwitchButtonItem from '../Menu/MenuSwitchButtonItem'
|
import MenuSwitchButtonItem from '../Menu/MenuSwitchButtonItem'
|
||||||
|
|
||||||
const PanelSettingsSection = () => {
|
const PanelSettingsSection = () => {
|
||||||
const { isListPaneCollapsed, isNavigationPaneCollapsed, toggleListPane, toggleNavigationPane } =
|
const { isListPaneCollapsed, isNavigationPaneCollapsed, toggleListPane, toggleNavigationPane } =
|
||||||
useResponsiveAppPane()
|
useResponsiveAppPane()
|
||||||
|
|
||||||
const commandService = useCommandService()
|
const keyboardService = useKeyboardService()
|
||||||
|
|
||||||
const navigationShortcut = useMemo(
|
const navigationShortcut = useMemo(
|
||||||
() => commandService.keyboardShortcutForCommand(TOGGLE_NAVIGATION_PANE_KEYBOARD_COMMAND),
|
() => keyboardService.keyboardShortcutForCommand(TOGGLE_NAVIGATION_PANE_KEYBOARD_COMMAND),
|
||||||
[commandService],
|
[keyboardService],
|
||||||
)
|
)
|
||||||
|
|
||||||
const listShortcut = useMemo(
|
const listShortcut = useMemo(
|
||||||
() => commandService.keyboardShortcutForCommand(TOGGLE_LIST_PANE_KEYBOARD_COMMAND),
|
() => keyboardService.keyboardShortcutForCommand(TOGGLE_LIST_PANE_KEYBOARD_COMMAND),
|
||||||
[commandService],
|
[keyboardService],
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { isMobileScreen } from '@/Utils'
|
|||||||
import { classNames } from '@standardnotes/utils'
|
import { classNames } from '@standardnotes/utils'
|
||||||
import MenuSwitchButtonItem from '../Menu/MenuSwitchButtonItem'
|
import MenuSwitchButtonItem from '../Menu/MenuSwitchButtonItem'
|
||||||
import MenuRadioButtonItem from '../Menu/MenuRadioButtonItem'
|
import MenuRadioButtonItem from '../Menu/MenuRadioButtonItem'
|
||||||
import { useCommandService } from '../CommandProvider'
|
import { useKeyboardService } from '../KeyboardServiceProvider'
|
||||||
import { TOGGLE_DARK_MODE_COMMAND } from '@standardnotes/ui-services'
|
import { TOGGLE_DARK_MODE_COMMAND } from '@standardnotes/ui-services'
|
||||||
import { KeyboardShortcutIndicator } from '../KeyboardShortcutIndicator/KeyboardShortcutIndicator'
|
import { KeyboardShortcutIndicator } from '../KeyboardShortcutIndicator/KeyboardShortcutIndicator'
|
||||||
import { useApplication } from '../ApplicationProvider'
|
import { useApplication } from '../ApplicationProvider'
|
||||||
@@ -18,7 +18,7 @@ type Props = {
|
|||||||
|
|
||||||
const ThemesMenuButton: FunctionComponent<Props> = ({ uiFeature }) => {
|
const ThemesMenuButton: FunctionComponent<Props> = ({ uiFeature }) => {
|
||||||
const application = useApplication()
|
const application = useApplication()
|
||||||
const commandService = useCommandService()
|
const keyboardService = useKeyboardService()
|
||||||
const premiumModal = usePremiumModal()
|
const premiumModal = usePremiumModal()
|
||||||
|
|
||||||
const isThirdPartyTheme = useMemo(
|
const isThirdPartyTheme = useMemo(
|
||||||
@@ -59,9 +59,9 @@ const ThemesMenuButton: FunctionComponent<Props> = ({ uiFeature }) => {
|
|||||||
|
|
||||||
const darkThemeShortcut = useMemo(() => {
|
const darkThemeShortcut = useMemo(() => {
|
||||||
if (uiFeature.featureIdentifier === NativeFeatureIdentifier.TYPES.DarkTheme) {
|
if (uiFeature.featureIdentifier === NativeFeatureIdentifier.TYPES.DarkTheme) {
|
||||||
return commandService.keyboardShortcutForCommand(TOGGLE_DARK_MODE_COMMAND)
|
return keyboardService.keyboardShortcutForCommand(TOGGLE_DARK_MODE_COMMAND)
|
||||||
}
|
}
|
||||||
}, [commandService, uiFeature.featureIdentifier])
|
}, [keyboardService, uiFeature.featureIdentifier])
|
||||||
|
|
||||||
if (shouldHideButton) {
|
if (shouldHideButton) {
|
||||||
return null
|
return null
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ const StyledTooltip = ({
|
|||||||
side,
|
side,
|
||||||
documentElement,
|
documentElement,
|
||||||
closeOnClick = true,
|
closeOnClick = true,
|
||||||
|
portal = true,
|
||||||
...props
|
...props
|
||||||
}: {
|
}: {
|
||||||
children: ReactNode
|
children: ReactNode
|
||||||
@@ -41,7 +42,6 @@ const StyledTooltip = ({
|
|||||||
hideTimeout: 0,
|
hideTimeout: 0,
|
||||||
skipTimeout: 0,
|
skipTimeout: 0,
|
||||||
open: forceOpen,
|
open: forceOpen,
|
||||||
animated: true,
|
|
||||||
type,
|
type,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -156,9 +156,11 @@ const StyledTooltip = ({
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
Object.assign(popoverElement.style, styles)
|
for (const [key, value] of Object.entries(styles)) {
|
||||||
|
popoverElement.style.setProperty(key, value)
|
||||||
|
}
|
||||||
|
|
||||||
if (!props.portal) {
|
if (!portal) {
|
||||||
const adjustedStyles = getAdjustedStylesForNonPortalPopover(
|
const adjustedStyles = getAdjustedStylesForNonPortalPopover(
|
||||||
popoverElement,
|
popoverElement,
|
||||||
styles,
|
styles,
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import { useLifecycleAnimation } from '../../../../Hooks/useLifecycleAnimation'
|
|||||||
import { classNames, debounce } from '@standardnotes/utils'
|
import { classNames, debounce } from '@standardnotes/utils'
|
||||||
import DecoratedInput from '../../../Input/DecoratedInput'
|
import DecoratedInput from '../../../Input/DecoratedInput'
|
||||||
import { searchInElement } from './searchInElement'
|
import { searchInElement } from './searchInElement'
|
||||||
import { useCommandService } from '../../../CommandProvider'
|
import { useKeyboardService } from '../../../KeyboardServiceProvider'
|
||||||
import { ArrowDownIcon, ArrowRightIcon, ArrowUpIcon, CloseIcon } from '@standardnotes/icons'
|
import { ArrowDownIcon, ArrowRightIcon, ArrowUpIcon, CloseIcon } from '@standardnotes/icons'
|
||||||
import Button from '../../../Button/Button'
|
import Button from '../../../Button/Button'
|
||||||
import { canUseCSSHiglights, SearchHighlightRenderer, SearchHighlightRendererMethods } from './SearchHighlightRenderer'
|
import { canUseCSSHiglights, SearchHighlightRenderer, SearchHighlightRendererMethods } from './SearchHighlightRenderer'
|
||||||
@@ -272,18 +272,18 @@ export function SearchPlugin() {
|
|||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const commandService = useCommandService()
|
const keyboardService = useKeyboardService()
|
||||||
const searchToggleShortcut = useMemo(
|
const searchToggleShortcut = useMemo(
|
||||||
() => keyboardStringForShortcut(commandService.keyboardShortcutForCommand(SUPER_TOGGLE_SEARCH)),
|
() => keyboardStringForShortcut(keyboardService.keyboardShortcutForCommand(SUPER_TOGGLE_SEARCH)),
|
||||||
[commandService],
|
[keyboardService],
|
||||||
)
|
)
|
||||||
const toggleReplaceShortcut = useMemo(
|
const toggleReplaceShortcut = useMemo(
|
||||||
() => keyboardStringForShortcut(commandService.keyboardShortcutForCommand(SUPER_SEARCH_TOGGLE_REPLACE_MODE)),
|
() => keyboardStringForShortcut(keyboardService.keyboardShortcutForCommand(SUPER_SEARCH_TOGGLE_REPLACE_MODE)),
|
||||||
[commandService],
|
[keyboardService],
|
||||||
)
|
)
|
||||||
const caseSensitivityShortcut = useMemo(
|
const caseSensitivityShortcut = useMemo(
|
||||||
() => keyboardStringForShortcut(commandService.keyboardShortcutForCommand(SUPER_SEARCH_TOGGLE_CASE_SENSITIVE)),
|
() => keyboardStringForShortcut(keyboardService.keyboardShortcutForCommand(SUPER_SEARCH_TOGGLE_CASE_SENSITIVE)),
|
||||||
[commandService],
|
[keyboardService],
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!isMounted) {
|
if (!isMounted) {
|
||||||
@@ -447,7 +447,6 @@ export function SearchPlugin() {
|
|||||||
label="May lead to performance degradation, especially on large documents."
|
label="May lead to performance degradation, especially on large documents."
|
||||||
className="!z-modal"
|
className="!z-modal"
|
||||||
showOnMobile
|
showOnMobile
|
||||||
portal={false}
|
|
||||||
>
|
>
|
||||||
<button className="cursor-default">
|
<button className="cursor-default">
|
||||||
<Icon type="info" size="medium" />
|
<Icon type="info" size="medium" />
|
||||||
|
|||||||
@@ -135,7 +135,6 @@ const ToolbarButton = forwardRef(
|
|||||||
showOnHover
|
showOnHover
|
||||||
label={name}
|
label={name}
|
||||||
side="top"
|
side="top"
|
||||||
portal={false}
|
|
||||||
portalElement={isMobile ? parentElement : undefined}
|
portalElement={isMobile ? parentElement : undefined}
|
||||||
documentElement={parentElement}
|
documentElement={parentElement}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ import {
|
|||||||
ChangeContentCallbackPlugin,
|
ChangeContentCallbackPlugin,
|
||||||
ChangeEditorFunction,
|
ChangeEditorFunction,
|
||||||
} from './Plugins/ChangeContentCallback/ChangeContentCallback'
|
} from './Plugins/ChangeContentCallback/ChangeContentCallback'
|
||||||
import { useCommandService } from '@/Components/CommandProvider'
|
|
||||||
import { SUPER_SHOW_MARKDOWN_PREVIEW, getPrimaryModifier } from '@standardnotes/ui-services'
|
import { SUPER_SHOW_MARKDOWN_PREVIEW, getPrimaryModifier } from '@standardnotes/ui-services'
|
||||||
import { SuperNoteMarkdownPreview } from './SuperNoteMarkdownPreview'
|
import { SuperNoteMarkdownPreview } from './SuperNoteMarkdownPreview'
|
||||||
import GetMarkdownPlugin, { GetMarkdownPluginInterface } from './Plugins/GetMarkdownPlugin/GetMarkdownPlugin'
|
import GetMarkdownPlugin, { GetMarkdownPluginInterface } from './Plugins/GetMarkdownPlugin/GetMarkdownPlugin'
|
||||||
@@ -83,22 +82,23 @@ export const SuperEditor: FunctionComponent<Props> = ({
|
|||||||
)
|
)
|
||||||
}, [application.features])
|
}, [application.features])
|
||||||
|
|
||||||
const commandService = useCommandService()
|
const keyboardService = application.keyboardService
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return commandService.addCommandHandler({
|
return application.commands.addWithShortcut(
|
||||||
command: SUPER_SHOW_MARKDOWN_PREVIEW,
|
SUPER_SHOW_MARKDOWN_PREVIEW,
|
||||||
category: 'Super notes',
|
'Super notes',
|
||||||
description: 'Show markdown preview for current note',
|
'Show markdown preview for current note',
|
||||||
onKeyDown: () => setShowMarkdownPreview(true),
|
() => setShowMarkdownPreview((s) => !s),
|
||||||
})
|
'markdown',
|
||||||
}, [commandService])
|
)
|
||||||
|
}, [application.commands])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const platform = application.platform
|
const platform = application.platform
|
||||||
const primaryModifier = getPrimaryModifier(application.platform)
|
const primaryModifier = getPrimaryModifier(application.platform)
|
||||||
|
|
||||||
return commandService.registerExternalKeyboardShortcutHelpItems([
|
return keyboardService.registerExternalKeyboardShortcutHelpItems([
|
||||||
{
|
{
|
||||||
key: 'b',
|
key: 'b',
|
||||||
modifiers: [primaryModifier],
|
modifiers: [primaryModifier],
|
||||||
@@ -128,7 +128,7 @@ export const SuperEditor: FunctionComponent<Props> = ({
|
|||||||
platform: platform,
|
platform: platform,
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
}, [application.platform, commandService])
|
}, [application.platform, keyboardService])
|
||||||
|
|
||||||
const closeMarkdownPreview = useCallback(() => {
|
const closeMarkdownPreview = useCallback(() => {
|
||||||
setShowMarkdownPreview(false)
|
setShowMarkdownPreview(false)
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ const SmartViewsListItem: FunctionComponent<Props> = ({ view, tagsState, setEdit
|
|||||||
const inputRef = useRef<HTMLInputElement>(null)
|
const inputRef = useRef<HTMLInputElement>(null)
|
||||||
|
|
||||||
const level = 0
|
const level = 0
|
||||||
const isSelected = tagsState.selected === view
|
const isSelected = tagsState.selected?.uuid === view.uuid
|
||||||
const isEditing = tagsState.editingTag === view
|
const isEditing = tagsState.editingTag === view
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { FeaturesController } from '@/Controllers/FeaturesController'
|
|||||||
import { NavigationController } from '@/Controllers/Navigation/NavigationController'
|
import { NavigationController } from '@/Controllers/Navigation/NavigationController'
|
||||||
import { usePremiumModal } from '@/Hooks/usePremiumModal'
|
import { usePremiumModal } from '@/Hooks/usePremiumModal'
|
||||||
import { observer } from 'mobx-react-lite'
|
import { observer } from 'mobx-react-lite'
|
||||||
import { FunctionComponent, useCallback, useMemo } from 'react'
|
import { FunctionComponent, useCallback, useEffect, useMemo } from 'react'
|
||||||
import IconButton from '../Button/IconButton'
|
import IconButton from '../Button/IconButton'
|
||||||
import EditSmartViewModal from '../Preferences/Panes/General/SmartViews/EditSmartViewModal'
|
import EditSmartViewModal from '../Preferences/Panes/General/SmartViews/EditSmartViewModal'
|
||||||
import { EditSmartViewModalController } from '../Preferences/Panes/General/SmartViews/EditSmartViewModalController'
|
import { EditSmartViewModalController } from '../Preferences/Panes/General/SmartViews/EditSmartViewModalController'
|
||||||
@@ -33,6 +33,11 @@ const SmartViewsSection: FunctionComponent<Props> = ({ application, navigationCo
|
|||||||
addSmartViewModalController.setIsAddingSmartView(true)
|
addSmartViewModalController.setIsAddingSmartView(true)
|
||||||
}, [addSmartViewModalController, premiumModal, featuresController.hasSmartViews])
|
}, [addSmartViewModalController, premiumModal, featuresController.hasSmartViews])
|
||||||
|
|
||||||
|
useEffect(
|
||||||
|
() => application.commands.add('create-smart-view', 'Create a new smart view', createNewSmartView, 'add'),
|
||||||
|
[application.commands, createNewSmartView],
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section>
|
<section>
|
||||||
<div className={'section-title-bar'}>
|
<div className={'section-title-bar'}>
|
||||||
|
|||||||
@@ -27,9 +27,7 @@ const TagsSection: FunctionComponent = () => {
|
|||||||
<div className={'section-title-bar'}>
|
<div className={'section-title-bar'}>
|
||||||
<div className="section-title-bar-header">
|
<div className="section-title-bar-header">
|
||||||
<TagsSectionTitle features={application.featuresController} />
|
<TagsSectionTitle features={application.featuresController} />
|
||||||
{!application.navigationController.isSearching && (
|
{!application.navigationController.isSearching && <TagsSectionAddButton />}
|
||||||
<TagsSectionAddButton tags={application.navigationController} features={application.featuresController} />
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<TagsList type="all" />
|
<TagsList type="all" />
|
||||||
|
|||||||
@@ -1,22 +1,22 @@
|
|||||||
import IconButton from '@/Components/Button/IconButton'
|
import IconButton from '@/Components/Button/IconButton'
|
||||||
import { FeaturesController } from '@/Controllers/FeaturesController'
|
|
||||||
import { NavigationController } from '@/Controllers/Navigation/NavigationController'
|
|
||||||
import { CREATE_NEW_TAG_COMMAND, keyboardStringForShortcut } from '@standardnotes/ui-services'
|
import { CREATE_NEW_TAG_COMMAND, keyboardStringForShortcut } from '@standardnotes/ui-services'
|
||||||
import { observer } from 'mobx-react-lite'
|
import { observer } from 'mobx-react-lite'
|
||||||
import { FunctionComponent, useMemo } from 'react'
|
import { useCallback, useMemo } from 'react'
|
||||||
import { useCommandService } from '../CommandProvider'
|
import { useKeyboardService } from '../KeyboardServiceProvider'
|
||||||
|
import { useApplication } from '../ApplicationProvider'
|
||||||
|
|
||||||
type Props = {
|
function TagsSectionAddButton() {
|
||||||
tags: NavigationController
|
const application = useApplication()
|
||||||
features: FeaturesController
|
const keyboardService = useKeyboardService()
|
||||||
}
|
|
||||||
|
|
||||||
const TagsSectionAddButton: FunctionComponent<Props> = ({ tags }) => {
|
const addNewTag = useCallback(
|
||||||
const commandService = useCommandService()
|
() => application.navigationController.createNewTemplate(),
|
||||||
|
[application.navigationController],
|
||||||
|
)
|
||||||
|
|
||||||
const shortcut = useMemo(
|
const shortcut = useMemo(
|
||||||
() => keyboardStringForShortcut(commandService.keyboardShortcutForCommand(CREATE_NEW_TAG_COMMAND)),
|
() => keyboardStringForShortcut(keyboardService.keyboardShortcutForCommand(CREATE_NEW_TAG_COMMAND)),
|
||||||
[commandService],
|
[keyboardService],
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -25,7 +25,7 @@ const TagsSectionAddButton: FunctionComponent<Props> = ({ tags }) => {
|
|||||||
icon="add"
|
icon="add"
|
||||||
title={`Create a new tag (${shortcut})`}
|
title={`Create a new tag (${shortcut})`}
|
||||||
className="p-0 text-neutral"
|
className="p-0 text-neutral"
|
||||||
onClick={() => tags.createNewTemplate()}
|
onClick={addNewTag}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ import { AbstractViewController } from './Abstract/AbstractViewController'
|
|||||||
import { NotesController } from './NotesController/NotesController'
|
import { NotesController } from './NotesController/NotesController'
|
||||||
import { downloadOrShareBlobBasedOnPlatform } from '@/Utils/DownloadOrShareBasedOnPlatform'
|
import { downloadOrShareBlobBasedOnPlatform } from '@/Utils/DownloadOrShareBasedOnPlatform'
|
||||||
import { truncateString } from '@/Components/SuperEditor/Utils'
|
import { truncateString } from '@/Components/SuperEditor/Utils'
|
||||||
|
import { RecentActionsState } from '../Application/Recents'
|
||||||
|
|
||||||
const UnprotectedFileActions = [FileItemActionType.ToggleFileProtection]
|
const UnprotectedFileActions = [FileItemActionType.ToggleFileProtection]
|
||||||
const NonMutatingFileActions = [FileItemActionType.DownloadFile, FileItemActionType.PreviewFile]
|
const NonMutatingFileActions = [FileItemActionType.DownloadFile, FileItemActionType.PreviewFile]
|
||||||
@@ -105,6 +106,7 @@ export class FilesController extends AbstractViewController<FilesControllerEvent
|
|||||||
private platform: Platform,
|
private platform: Platform,
|
||||||
private mobileDevice: MobileDeviceInterface | undefined,
|
private mobileDevice: MobileDeviceInterface | undefined,
|
||||||
private _isNativeMobileWeb: IsNativeMobileWeb,
|
private _isNativeMobileWeb: IsNativeMobileWeb,
|
||||||
|
private recents: RecentActionsState,
|
||||||
eventBus: InternalEventBusInterface,
|
eventBus: InternalEventBusInterface,
|
||||||
) {
|
) {
|
||||||
super(eventBus)
|
super(eventBus)
|
||||||
@@ -278,6 +280,7 @@ export class FilesController extends AbstractViewController<FilesControllerEvent
|
|||||||
break
|
break
|
||||||
case FileItemActionType.PreviewFile:
|
case FileItemActionType.PreviewFile:
|
||||||
this.filePreviewModalController.activate(file, action.payload.otherFiles)
|
this.filePreviewModalController.activate(file, action.payload.otherFiles)
|
||||||
|
this.recents.add(file.uuid)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ describe('item list controller', () => {
|
|||||||
application.options,
|
application.options,
|
||||||
application.isNativeMobileWebUseCase,
|
application.isNativeMobileWebUseCase,
|
||||||
application.changeAndSaveItem,
|
application.changeAndSaveItem,
|
||||||
|
application.recents,
|
||||||
eventBus,
|
eventBus,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ import { Persistable } from '../Abstract/Persistable'
|
|||||||
import { PaneController } from '../PaneController/PaneController'
|
import { PaneController } from '../PaneController/PaneController'
|
||||||
import { requestCloseAllOpenModalsAndPopovers } from '@/Utils/CloseOpenModalsAndPopovers'
|
import { requestCloseAllOpenModalsAndPopovers } from '@/Utils/CloseOpenModalsAndPopovers'
|
||||||
import { PaneLayout } from '../PaneController/PaneLayout'
|
import { PaneLayout } from '../PaneController/PaneLayout'
|
||||||
|
import { RecentActionsState } from '../../Application/Recents'
|
||||||
|
|
||||||
const MinNoteCellHeight = 51.0
|
const MinNoteCellHeight = 51.0
|
||||||
const DefaultListNumNotes = 20
|
const DefaultListNumNotes = 20
|
||||||
@@ -129,6 +130,7 @@ export class ItemListController
|
|||||||
private options: FullyResolvedApplicationOptions,
|
private options: FullyResolvedApplicationOptions,
|
||||||
private _isNativeMobileWeb: IsNativeMobileWeb,
|
private _isNativeMobileWeb: IsNativeMobileWeb,
|
||||||
private _changeAndSaveItem: ChangeAndSaveItem,
|
private _changeAndSaveItem: ChangeAndSaveItem,
|
||||||
|
private recents: RecentActionsState,
|
||||||
eventBus: InternalEventBusInterface,
|
eventBus: InternalEventBusInterface,
|
||||||
) {
|
) {
|
||||||
super(eventBus)
|
super(eventBus)
|
||||||
@@ -1120,9 +1122,7 @@ export class ItemListController
|
|||||||
}
|
}
|
||||||
|
|
||||||
replaceSelection = (item: ListableContentItem): void => {
|
replaceSelection = (item: ListableContentItem): void => {
|
||||||
this.deselectAll()
|
runInAction(() => this.setSelectedUuids(new Set([item.uuid])))
|
||||||
runInAction(() => this.setSelectedUuids(this.selectedUuids.add(item.uuid)))
|
|
||||||
|
|
||||||
this.lastSelectedItem = item
|
this.lastSelectedItem = item
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1150,6 +1150,7 @@ export class ItemListController
|
|||||||
} else if (item.content_type === ContentType.TYPES.File) {
|
} else if (item.content_type === ContentType.TYPES.File) {
|
||||||
await this.openFile(item.uuid)
|
await this.openFile(item.uuid)
|
||||||
}
|
}
|
||||||
|
this.recents.add(item.uuid)
|
||||||
|
|
||||||
if (!this.paneController.isInMobileView || userTriggered) {
|
if (!this.paneController.isInMobileView || userTriggered) {
|
||||||
void this.paneController.setPaneLayout(PaneLayout.Editing)
|
void this.paneController.setPaneLayout(PaneLayout.Editing)
|
||||||
@@ -1165,21 +1166,13 @@ export class ItemListController
|
|||||||
this.isMultipleSelectionMode = true
|
this.isMultipleSelectionMode = true
|
||||||
}
|
}
|
||||||
|
|
||||||
selectItem = async (
|
selectItemUsingInstance = async (
|
||||||
uuid: UuidString,
|
item: ListableContentItem,
|
||||||
userTriggered?: boolean,
|
userTriggered?: boolean,
|
||||||
): Promise<{
|
): Promise<{ didSelect: boolean }> => {
|
||||||
didSelect: boolean
|
const uuid = item.uuid
|
||||||
}> => {
|
|
||||||
const item = this.itemManager.findItem<ListableContentItem>(uuid)
|
|
||||||
|
|
||||||
if (!item) {
|
log(LoggingDomain.Selection, 'Select item', uuid)
|
||||||
return {
|
|
||||||
didSelect: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
log(LoggingDomain.Selection, 'Select item', item.uuid)
|
|
||||||
|
|
||||||
const hasShift = this.keyboardService.activeModifiers.has(KeyboardModifier.Shift)
|
const hasShift = this.keyboardService.activeModifiers.has(KeyboardModifier.Shift)
|
||||||
const hasMoreThanOneSelected = this.selectedItemsCount > 1
|
const hasMoreThanOneSelected = this.selectedItemsCount > 1
|
||||||
@@ -1208,6 +1201,23 @@ export class ItemListController
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
selectItem = async (
|
||||||
|
uuid: UuidString,
|
||||||
|
userTriggered?: boolean,
|
||||||
|
): Promise<{
|
||||||
|
didSelect: boolean
|
||||||
|
}> => {
|
||||||
|
const item = this.itemManager.findItem<ListableContentItem>(uuid)
|
||||||
|
|
||||||
|
if (!item) {
|
||||||
|
return {
|
||||||
|
didSelect: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.selectItemUsingInstance(item, userTriggered)
|
||||||
|
}
|
||||||
|
|
||||||
selectItemWithScrollHandling = async (
|
selectItemWithScrollHandling = async (
|
||||||
item: {
|
item: {
|
||||||
uuid: ListableContentItem['uuid']
|
uuid: ListableContentItem['uuid']
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
confirmDialog,
|
confirmDialog,
|
||||||
CREATE_NEW_TAG_COMMAND,
|
CREATE_NEW_TAG_COMMAND,
|
||||||
KeyboardService,
|
|
||||||
NavigationControllerPersistableValue,
|
NavigationControllerPersistableValue,
|
||||||
VaultDisplayService,
|
VaultDisplayService,
|
||||||
VaultDisplayServiceEvent,
|
VaultDisplayServiceEvent,
|
||||||
@@ -43,6 +42,8 @@ import { TagListSectionType } from '@/Components/Tags/TagListSection'
|
|||||||
import { PaneLayout } from '../PaneController/PaneLayout'
|
import { PaneLayout } from '../PaneController/PaneLayout'
|
||||||
import { TagsCountsState } from './TagsCountsState'
|
import { TagsCountsState } from './TagsCountsState'
|
||||||
import { PaneController } from '../PaneController/PaneController'
|
import { PaneController } from '../PaneController/PaneController'
|
||||||
|
import { RecentActionsState } from '../../Application/Recents'
|
||||||
|
import { CommandService } from '../../Components/CommandPalette/CommandService'
|
||||||
|
|
||||||
export class NavigationController
|
export class NavigationController
|
||||||
extends AbstractViewController
|
extends AbstractViewController
|
||||||
@@ -73,7 +74,7 @@ export class NavigationController
|
|||||||
constructor(
|
constructor(
|
||||||
private featuresController: FeaturesController,
|
private featuresController: FeaturesController,
|
||||||
private vaultDisplayService: VaultDisplayService,
|
private vaultDisplayService: VaultDisplayService,
|
||||||
private keyboardService: KeyboardService,
|
private commands: CommandService,
|
||||||
private paneController: PaneController,
|
private paneController: PaneController,
|
||||||
private sync: SyncServiceInterface,
|
private sync: SyncServiceInterface,
|
||||||
private mutator: MutatorClientInterface,
|
private mutator: MutatorClientInterface,
|
||||||
@@ -81,6 +82,7 @@ export class NavigationController
|
|||||||
private preferences: PreferenceServiceInterface,
|
private preferences: PreferenceServiceInterface,
|
||||||
private alerts: AlertService,
|
private alerts: AlertService,
|
||||||
private _changeAndSaveItem: ChangeAndSaveItem,
|
private _changeAndSaveItem: ChangeAndSaveItem,
|
||||||
|
private recents: RecentActionsState,
|
||||||
eventBus: InternalEventBusInterface,
|
eventBus: InternalEventBusInterface,
|
||||||
) {
|
) {
|
||||||
super(eventBus)
|
super(eventBus)
|
||||||
@@ -197,14 +199,13 @@ export class NavigationController
|
|||||||
)
|
)
|
||||||
|
|
||||||
this.disposers.push(
|
this.disposers.push(
|
||||||
this.keyboardService.addCommandHandler({
|
this.commands.addWithShortcut(
|
||||||
command: CREATE_NEW_TAG_COMMAND,
|
CREATE_NEW_TAG_COMMAND,
|
||||||
category: 'General',
|
'General',
|
||||||
description: 'Create new tag',
|
'Create new tag',
|
||||||
onKeyDown: () => {
|
() => this.createNewTemplate(),
|
||||||
this.createNewTemplate()
|
'add',
|
||||||
},
|
),
|
||||||
}),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
this.setDisplayOptionsAndReloadTags = debounce(this.setDisplayOptionsAndReloadTags, 50)
|
this.setDisplayOptionsAndReloadTags = debounce(this.setDisplayOptionsAndReloadTags, 50)
|
||||||
@@ -511,6 +512,10 @@ export class NavigationController
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (tag) {
|
||||||
|
this.recents.add(tag.uuid)
|
||||||
|
}
|
||||||
|
|
||||||
await this.eventBus.publishSync(
|
await this.eventBus.publishSync(
|
||||||
{
|
{
|
||||||
type: CrossControllerEvent.TagChanged,
|
type: CrossControllerEvent.TagChanged,
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { InternalEventBusInterface, SNNote } from '@standardnotes/snjs'
|
import { InternalEventBusInterface, SNNote } from '@standardnotes/snjs'
|
||||||
import { KeyboardService, OPEN_NOTE_HISTORY_COMMAND } from '@standardnotes/ui-services'
|
import { OPEN_NOTE_HISTORY_COMMAND } from '@standardnotes/ui-services'
|
||||||
import { action, makeObservable, observable } from 'mobx'
|
import { action, makeObservable, observable } from 'mobx'
|
||||||
import { AbstractViewController } from '../Abstract/AbstractViewController'
|
import { AbstractViewController } from '../Abstract/AbstractViewController'
|
||||||
import { NotesControllerInterface } from '../NotesController/NotesControllerInterface'
|
import { NotesControllerInterface } from '../NotesController/NotesControllerInterface'
|
||||||
|
import { CommandService } from '../../Components/CommandPalette/CommandService'
|
||||||
|
|
||||||
export class HistoryModalController extends AbstractViewController {
|
export class HistoryModalController extends AbstractViewController {
|
||||||
note?: SNNote = undefined
|
note?: SNNote = undefined
|
||||||
@@ -14,7 +15,7 @@ export class HistoryModalController extends AbstractViewController {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
notesController: NotesControllerInterface,
|
notesController: NotesControllerInterface,
|
||||||
keyboardService: KeyboardService,
|
commandService: CommandService,
|
||||||
eventBus: InternalEventBusInterface,
|
eventBus: InternalEventBusInterface,
|
||||||
) {
|
) {
|
||||||
super(eventBus)
|
super(eventBus)
|
||||||
@@ -25,14 +26,9 @@ export class HistoryModalController extends AbstractViewController {
|
|||||||
})
|
})
|
||||||
|
|
||||||
this.disposers.push(
|
this.disposers.push(
|
||||||
keyboardService.addCommandHandler({
|
commandService.addWithShortcut(OPEN_NOTE_HISTORY_COMMAND, 'Current note', 'Open note history', () => {
|
||||||
command: OPEN_NOTE_HISTORY_COMMAND,
|
this.openModal(notesController.firstSelectedNote)
|
||||||
category: 'Current note',
|
return true
|
||||||
description: 'Open note history',
|
|
||||||
onKeyDown: () => {
|
|
||||||
this.openModal(notesController.firstSelectedNote)
|
|
||||||
return true
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import {
|
|||||||
confirmDialog,
|
confirmDialog,
|
||||||
GetItemTags,
|
GetItemTags,
|
||||||
IsGlobalSpellcheckEnabled,
|
IsGlobalSpellcheckEnabled,
|
||||||
KeyboardService,
|
|
||||||
PIN_NOTE_COMMAND,
|
PIN_NOTE_COMMAND,
|
||||||
STAR_NOTE_COMMAND,
|
STAR_NOTE_COMMAND,
|
||||||
} from '@standardnotes/ui-services'
|
} from '@standardnotes/ui-services'
|
||||||
@@ -16,29 +15,25 @@ import {
|
|||||||
PrefKey,
|
PrefKey,
|
||||||
ApplicationEvent,
|
ApplicationEvent,
|
||||||
EditorLineWidth,
|
EditorLineWidth,
|
||||||
InternalEventBusInterface,
|
|
||||||
MutationType,
|
MutationType,
|
||||||
PrefDefaults,
|
PrefDefaults,
|
||||||
PreferenceServiceInterface,
|
|
||||||
InternalEventHandlerInterface,
|
InternalEventHandlerInterface,
|
||||||
InternalEventInterface,
|
InternalEventInterface,
|
||||||
ItemManagerInterface,
|
|
||||||
MutatorClientInterface,
|
|
||||||
SyncServiceInterface,
|
|
||||||
AlertService,
|
|
||||||
ProtectionsClientInterface,
|
|
||||||
LocalPrefKey,
|
LocalPrefKey,
|
||||||
NoteContent,
|
NoteContent,
|
||||||
noteTypeForEditorIdentifier,
|
noteTypeForEditorIdentifier,
|
||||||
ContentReference,
|
ContentReference,
|
||||||
|
pluralize,
|
||||||
|
NoteType,
|
||||||
} from '@standardnotes/snjs'
|
} from '@standardnotes/snjs'
|
||||||
import { makeObservable, observable, action, computed, runInAction } from 'mobx'
|
import { makeObservable, observable, action, computed, runInAction, reaction } from 'mobx'
|
||||||
import { AbstractViewController } from '../Abstract/AbstractViewController'
|
import { AbstractViewController } from '../Abstract/AbstractViewController'
|
||||||
import { NavigationController } from '../Navigation/NavigationController'
|
|
||||||
import { NotesControllerInterface } from './NotesControllerInterface'
|
import { NotesControllerInterface } from './NotesControllerInterface'
|
||||||
import { ItemGroupController } from '@/Components/NoteView/Controller/ItemGroupController'
|
|
||||||
import { CrossControllerEvent } from '../CrossControllerEvent'
|
import { CrossControllerEvent } from '../CrossControllerEvent'
|
||||||
import { ItemListController } from '../ItemList/ItemListController'
|
import { addToast, dismissToast, ToastType } from '@standardnotes/toast'
|
||||||
|
import { createNoteExport } from '../../Utils/NoteExportUtils'
|
||||||
|
import { WebApplication } from '../../Application/WebApplication'
|
||||||
|
import { downloadOrShareBlobBasedOnPlatform } from '../../Utils/DownloadOrShareBasedOnPlatform'
|
||||||
|
|
||||||
export class NotesController
|
export class NotesController
|
||||||
extends AbstractViewController
|
extends AbstractViewController
|
||||||
@@ -50,27 +45,21 @@ export class NotesController
|
|||||||
contextMenuClickLocation: { x: number; y: number } = { x: 0, y: 0 }
|
contextMenuClickLocation: { x: number; y: number } = { x: 0, y: 0 }
|
||||||
contextMenuMaxHeight: number | 'auto' = 'auto'
|
contextMenuMaxHeight: number | 'auto' = 'auto'
|
||||||
showProtectedWarning = false
|
showProtectedWarning = false
|
||||||
|
shouldShowSuperExportModal = false
|
||||||
|
|
||||||
|
commandRegisterDisposers: (() => void)[] = []
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private itemListController: ItemListController,
|
private application: WebApplication,
|
||||||
private navigationController: NavigationController,
|
|
||||||
private itemControllerGroup: ItemGroupController,
|
|
||||||
private keyboardService: KeyboardService,
|
|
||||||
private preferences: PreferenceServiceInterface,
|
|
||||||
private items: ItemManagerInterface,
|
|
||||||
private mutator: MutatorClientInterface,
|
|
||||||
private sync: SyncServiceInterface,
|
|
||||||
private protections: ProtectionsClientInterface,
|
|
||||||
private alerts: AlertService,
|
|
||||||
private _isGlobalSpellcheckEnabled: IsGlobalSpellcheckEnabled,
|
private _isGlobalSpellcheckEnabled: IsGlobalSpellcheckEnabled,
|
||||||
private _getItemTags: GetItemTags,
|
private _getItemTags: GetItemTags,
|
||||||
eventBus: InternalEventBusInterface,
|
|
||||||
) {
|
) {
|
||||||
super(eventBus)
|
super(application.events)
|
||||||
|
|
||||||
makeObservable(this, {
|
makeObservable(this, {
|
||||||
contextMenuOpen: observable,
|
contextMenuOpen: observable,
|
||||||
showProtectedWarning: observable,
|
showProtectedWarning: observable,
|
||||||
|
shouldShowSuperExportModal: observable,
|
||||||
|
|
||||||
selectedNotes: computed,
|
selectedNotes: computed,
|
||||||
firstSelectedNote: computed,
|
firstSelectedNote: computed,
|
||||||
@@ -81,38 +70,121 @@ export class NotesController
|
|||||||
setContextMenuClickLocation: action,
|
setContextMenuClickLocation: action,
|
||||||
setShowProtectedWarning: action,
|
setShowProtectedWarning: action,
|
||||||
unselectNotes: action,
|
unselectNotes: action,
|
||||||
|
showSuperExportModal: action,
|
||||||
|
closeSuperExportModal: action,
|
||||||
})
|
})
|
||||||
|
|
||||||
this.shouldLinkToParentFolders = preferences.getValue(
|
this.shouldLinkToParentFolders = application.preferences.getValue(
|
||||||
PrefKey.NoteAddToParentFolders,
|
PrefKey.NoteAddToParentFolders,
|
||||||
PrefDefaults[PrefKey.NoteAddToParentFolders],
|
PrefDefaults[PrefKey.NoteAddToParentFolders],
|
||||||
)
|
)
|
||||||
|
|
||||||
eventBus.addEventHandler(this, ApplicationEvent.PreferencesChanged)
|
application.events.addEventHandler(this, ApplicationEvent.PreferencesChanged)
|
||||||
eventBus.addEventHandler(this, CrossControllerEvent.UnselectAllNotes)
|
application.events.addEventHandler(this, CrossControllerEvent.UnselectAllNotes)
|
||||||
|
|
||||||
this.disposers.push(
|
this.disposers.push(
|
||||||
this.keyboardService.addCommandHandler({
|
reaction(
|
||||||
|
() => this.selectedNotesCount,
|
||||||
|
(notes_count) => {
|
||||||
|
console.log('hello')
|
||||||
|
this.disposeCommandRegisters()
|
||||||
|
|
||||||
|
const descriptionSuffix = `${pluralize(notes_count, 'current', 'selected')} ${pluralize(
|
||||||
|
notes_count,
|
||||||
|
'note',
|
||||||
|
'note(s)',
|
||||||
|
)}`
|
||||||
|
|
||||||
|
this.commandRegisterDisposers.push(
|
||||||
|
application.commands.add(
|
||||||
|
'pin-current',
|
||||||
|
`Pin ${descriptionSuffix}`,
|
||||||
|
() => this.setPinSelectedNotes(true),
|
||||||
|
'unpin',
|
||||||
|
),
|
||||||
|
application.commands.add(
|
||||||
|
'unpin-current',
|
||||||
|
`Unpin ${descriptionSuffix}`,
|
||||||
|
() => this.setPinSelectedNotes(false),
|
||||||
|
'pin',
|
||||||
|
),
|
||||||
|
application.commands.add(
|
||||||
|
'star-current',
|
||||||
|
`Star ${descriptionSuffix}`,
|
||||||
|
() => this.setStarSelectedNotes(true),
|
||||||
|
'star',
|
||||||
|
),
|
||||||
|
application.commands.add(
|
||||||
|
'unstar-current',
|
||||||
|
`Unstar ${descriptionSuffix}`,
|
||||||
|
() => this.setStarSelectedNotes(false),
|
||||||
|
'star',
|
||||||
|
),
|
||||||
|
application.commands.add(
|
||||||
|
'archive-current',
|
||||||
|
`Archive ${descriptionSuffix}`,
|
||||||
|
() => this.setArchiveSelectedNotes(true),
|
||||||
|
'archive',
|
||||||
|
),
|
||||||
|
application.commands.add(
|
||||||
|
'unarchive-current',
|
||||||
|
`Unarchive ${descriptionSuffix}`,
|
||||||
|
() => this.setArchiveSelectedNotes(false),
|
||||||
|
'unarchive',
|
||||||
|
),
|
||||||
|
application.commands.add(
|
||||||
|
'restore-current',
|
||||||
|
`Restore ${descriptionSuffix}`,
|
||||||
|
() => this.setTrashSelectedNotes(false),
|
||||||
|
'restore',
|
||||||
|
),
|
||||||
|
application.commands.add(
|
||||||
|
'trash-current',
|
||||||
|
`Trash ${descriptionSuffix}`,
|
||||||
|
() => this.setTrashSelectedNotes(true),
|
||||||
|
'trash',
|
||||||
|
),
|
||||||
|
application.commands.add(
|
||||||
|
'delete-current',
|
||||||
|
`Delete ${descriptionSuffix} permanently`,
|
||||||
|
() => this.deleteNotesPermanently(),
|
||||||
|
'trash',
|
||||||
|
),
|
||||||
|
application.commands.add(
|
||||||
|
'export-current',
|
||||||
|
`Export ${descriptionSuffix}`,
|
||||||
|
this.exportSelectedNotes,
|
||||||
|
'download',
|
||||||
|
),
|
||||||
|
application.commands.add(
|
||||||
|
'duplicate-current',
|
||||||
|
`Duplicate ${descriptionSuffix}`,
|
||||||
|
this.duplicateSelectedNotes,
|
||||||
|
'copy',
|
||||||
|
),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
this.disposers.push(
|
||||||
|
application.keyboardService.addCommandHandler({
|
||||||
command: PIN_NOTE_COMMAND,
|
command: PIN_NOTE_COMMAND,
|
||||||
category: 'Current note',
|
category: 'Current note',
|
||||||
description: 'Pin current note',
|
description: 'Pin/unpin selected note(s)',
|
||||||
onKeyDown: () => {
|
onKeyDown: this.togglePinSelectedNotes,
|
||||||
this.togglePinSelectedNotes()
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
this.keyboardService.addCommandHandler({
|
application.keyboardService.addCommandHandler({
|
||||||
command: STAR_NOTE_COMMAND,
|
command: STAR_NOTE_COMMAND,
|
||||||
category: 'Current note',
|
category: 'Current note',
|
||||||
description: 'Star current note',
|
description: 'Star/unstar selected note(s)',
|
||||||
onKeyDown: () => {
|
onKeyDown: this.toggleStarSelectedNotes,
|
||||||
this.toggleStarSelectedNotes()
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
this.disposers.push(
|
this.disposers.push(
|
||||||
this.itemControllerGroup.addActiveControllerChangeObserver(() => {
|
application.itemControllerGroup.addActiveControllerChangeObserver(() => {
|
||||||
const controllers = this.itemControllerGroup.itemControllers
|
const controllers = application.itemControllerGroup.itemControllers
|
||||||
|
|
||||||
const activeNoteUuids = controllers.map((controller) => controller.item.uuid)
|
const activeNoteUuids = controllers.map((controller) => controller.item.uuid)
|
||||||
|
|
||||||
@@ -120,7 +192,7 @@ export class NotesController
|
|||||||
|
|
||||||
for (const selectedId of selectedUuids) {
|
for (const selectedId of selectedUuids) {
|
||||||
if (!activeNoteUuids.includes(selectedId)) {
|
if (!activeNoteUuids.includes(selectedId)) {
|
||||||
this.itemListController.deselectItem({ uuid: selectedId })
|
application.itemListController.deselectItem({ uuid: selectedId })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
@@ -129,7 +201,7 @@ export class NotesController
|
|||||||
|
|
||||||
async handleEvent(event: InternalEventInterface): Promise<void> {
|
async handleEvent(event: InternalEventInterface): Promise<void> {
|
||||||
if (event.type === ApplicationEvent.PreferencesChanged) {
|
if (event.type === ApplicationEvent.PreferencesChanged) {
|
||||||
this.shouldLinkToParentFolders = this.preferences.getValue(
|
this.shouldLinkToParentFolders = this.application.preferences.getValue(
|
||||||
PrefKey.NoteAddToParentFolders,
|
PrefKey.NoteAddToParentFolders,
|
||||||
PrefDefaults[PrefKey.NoteAddToParentFolders],
|
PrefDefaults[PrefKey.NoteAddToParentFolders],
|
||||||
)
|
)
|
||||||
@@ -138,17 +210,23 @@ export class NotesController
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private disposeCommandRegisters() {
|
||||||
|
if (this.commandRegisterDisposers.length > 0) {
|
||||||
|
for (const dispose of this.commandRegisterDisposers) {
|
||||||
|
dispose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override deinit() {
|
override deinit() {
|
||||||
super.deinit()
|
super.deinit()
|
||||||
;(this.lastSelectedNote as unknown) = undefined
|
;(this.lastSelectedNote as unknown) = undefined
|
||||||
;(this.itemListController as unknown) = undefined
|
|
||||||
;(this.navigationController as unknown) = undefined
|
|
||||||
|
|
||||||
destroyAllObjectProperties(this)
|
destroyAllObjectProperties(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
public get selectedNotes(): SNNote[] {
|
public get selectedNotes(): SNNote[] {
|
||||||
return this.itemListController.getFilteredSelectedItems<SNNote>(ContentType.TYPES.Note)
|
return this.application.itemListController.getFilteredSelectedItems<SNNote>(ContentType.TYPES.Note)
|
||||||
}
|
}
|
||||||
|
|
||||||
get firstSelectedNote(): SNNote | undefined {
|
get firstSelectedNote(): SNNote | undefined {
|
||||||
@@ -164,7 +242,7 @@ export class NotesController
|
|||||||
}
|
}
|
||||||
|
|
||||||
get trashedNotesCount(): number {
|
get trashedNotesCount(): number {
|
||||||
return this.items.trashedItems.length
|
return this.application.items.trashedItems.length
|
||||||
}
|
}
|
||||||
|
|
||||||
setContextMenuOpen = (open: boolean) => {
|
setContextMenuOpen = (open: boolean) => {
|
||||||
@@ -176,8 +254,8 @@ export class NotesController
|
|||||||
}
|
}
|
||||||
|
|
||||||
async changeSelectedNotes(mutate: (mutator: NoteMutator) => void): Promise<void> {
|
async changeSelectedNotes(mutate: (mutator: NoteMutator) => void): Promise<void> {
|
||||||
await this.mutator.changeItems(this.getSelectedNotesList(), mutate, MutationType.NoUpdateUserTimestamps)
|
await this.application.mutator.changeItems(this.getSelectedNotesList(), mutate, MutationType.NoUpdateUserTimestamps)
|
||||||
this.sync.sync().catch(console.error)
|
this.application.sync.sync().catch(console.error)
|
||||||
}
|
}
|
||||||
|
|
||||||
setHideSelectedNotePreviews(hide: boolean): void {
|
setHideSelectedNotePreviews(hide: boolean): void {
|
||||||
@@ -217,7 +295,7 @@ export class NotesController
|
|||||||
async deleteNotes(permanently: boolean): Promise<boolean> {
|
async deleteNotes(permanently: boolean): Promise<boolean> {
|
||||||
if (this.getSelectedNotesList().some((note) => note.locked)) {
|
if (this.getSelectedNotesList().some((note) => note.locked)) {
|
||||||
const text = StringUtils.deleteLockedNotesAttempt(this.selectedNotesCount)
|
const text = StringUtils.deleteLockedNotesAttempt(this.selectedNotesCount)
|
||||||
this.alerts.alert(text).catch(console.error)
|
this.application.alerts.alert(text).catch(console.error)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -236,10 +314,10 @@ export class NotesController
|
|||||||
confirmButtonStyle: 'danger',
|
confirmButtonStyle: 'danger',
|
||||||
})
|
})
|
||||||
) {
|
) {
|
||||||
this.itemListController.selectNextItem()
|
this.application.itemListController.selectNextItem()
|
||||||
if (permanently) {
|
if (permanently) {
|
||||||
await this.mutator.deleteItems(this.getSelectedNotesList())
|
await this.application.mutator.deleteItems(this.getSelectedNotesList())
|
||||||
void this.sync.sync()
|
void this.application.sync.sync()
|
||||||
} else {
|
} else {
|
||||||
await this.changeSelectedNotes((mutator) => {
|
await this.changeSelectedNotes((mutator) => {
|
||||||
mutator.trashed = true
|
mutator.trashed = true
|
||||||
@@ -251,7 +329,7 @@ export class NotesController
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
togglePinSelectedNotes(): void {
|
togglePinSelectedNotes = () => {
|
||||||
const notes = this.selectedNotes
|
const notes = this.selectedNotes
|
||||||
const pinned = notes.some((note) => note.pinned)
|
const pinned = notes.some((note) => note.pinned)
|
||||||
|
|
||||||
@@ -262,7 +340,7 @@ export class NotesController
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleStarSelectedNotes(): void {
|
toggleStarSelectedNotes = () => {
|
||||||
const notes = this.selectedNotes
|
const notes = this.selectedNotes
|
||||||
const starred = notes.some((note) => note.starred)
|
const starred = notes.some((note) => note.starred)
|
||||||
|
|
||||||
@@ -287,7 +365,9 @@ export class NotesController
|
|||||||
|
|
||||||
async setArchiveSelectedNotes(archived: boolean): Promise<void> {
|
async setArchiveSelectedNotes(archived: boolean): Promise<void> {
|
||||||
if (this.getSelectedNotesList().some((note) => note.locked)) {
|
if (this.getSelectedNotesList().some((note) => note.locked)) {
|
||||||
this.alerts.alert(StringUtils.archiveLockedNotesAttempt(archived, this.selectedNotesCount)).catch(console.error)
|
this.application.alerts
|
||||||
|
.alert(StringUtils.archiveLockedNotesAttempt(archived, this.selectedNotesCount))
|
||||||
|
.catch(console.error)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -296,7 +376,7 @@ export class NotesController
|
|||||||
})
|
})
|
||||||
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.itemListController.deselectAll()
|
this.application.itemListController.deselectAll()
|
||||||
this.contextMenuOpen = false
|
this.contextMenuOpen = false
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -315,18 +395,18 @@ export class NotesController
|
|||||||
async setProtectSelectedNotes(protect: boolean): Promise<void> {
|
async setProtectSelectedNotes(protect: boolean): Promise<void> {
|
||||||
const selectedNotes = this.getSelectedNotesList()
|
const selectedNotes = this.getSelectedNotesList()
|
||||||
if (protect) {
|
if (protect) {
|
||||||
await this.protections.protectNotes(selectedNotes)
|
await this.application.protections.protectNotes(selectedNotes)
|
||||||
this.setShowProtectedWarning(true)
|
this.setShowProtectedWarning(true)
|
||||||
} else {
|
} else {
|
||||||
await this.protections.unprotectNotes(selectedNotes)
|
await this.application.protections.unprotectNotes(selectedNotes)
|
||||||
this.setShowProtectedWarning(false)
|
this.setShowProtectedWarning(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
void this.sync.sync()
|
void this.application.sync.sync()
|
||||||
}
|
}
|
||||||
|
|
||||||
unselectNotes(): void {
|
unselectNotes(): void {
|
||||||
this.itemListController.deselectAll()
|
this.application.itemListController.deselectAll()
|
||||||
}
|
}
|
||||||
|
|
||||||
getSpellcheckStateForNote(note: SNNote) {
|
getSpellcheckStateForNote(note: SNNote) {
|
||||||
@@ -334,52 +414,55 @@ export class NotesController
|
|||||||
}
|
}
|
||||||
|
|
||||||
async toggleGlobalSpellcheckForNote(note: SNNote) {
|
async toggleGlobalSpellcheckForNote(note: SNNote) {
|
||||||
await this.mutator.changeItem<NoteMutator>(
|
await this.application.mutator.changeItem<NoteMutator>(
|
||||||
note,
|
note,
|
||||||
(mutator) => {
|
(mutator) => {
|
||||||
mutator.toggleSpellcheck()
|
mutator.toggleSpellcheck()
|
||||||
},
|
},
|
||||||
MutationType.NoUpdateUserTimestamps,
|
MutationType.NoUpdateUserTimestamps,
|
||||||
)
|
)
|
||||||
this.sync.sync().catch(console.error)
|
this.application.sync.sync().catch(console.error)
|
||||||
}
|
}
|
||||||
|
|
||||||
getEditorWidthForNote(note: SNNote) {
|
getEditorWidthForNote(note: SNNote) {
|
||||||
return (
|
return (
|
||||||
note.editorWidth ??
|
note.editorWidth ??
|
||||||
this.preferences.getLocalValue(LocalPrefKey.EditorLineWidth, PrefDefaults[LocalPrefKey.EditorLineWidth])
|
this.application.preferences.getLocalValue(
|
||||||
|
LocalPrefKey.EditorLineWidth,
|
||||||
|
PrefDefaults[LocalPrefKey.EditorLineWidth],
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
async setNoteEditorWidth(note: SNNote, editorWidth: EditorLineWidth) {
|
async setNoteEditorWidth(note: SNNote, editorWidth: EditorLineWidth) {
|
||||||
await this.mutator.changeItem<NoteMutator>(
|
await this.application.mutator.changeItem<NoteMutator>(
|
||||||
note,
|
note,
|
||||||
(mutator) => {
|
(mutator) => {
|
||||||
mutator.editorWidth = editorWidth
|
mutator.editorWidth = editorWidth
|
||||||
},
|
},
|
||||||
MutationType.NoUpdateUserTimestamps,
|
MutationType.NoUpdateUserTimestamps,
|
||||||
)
|
)
|
||||||
this.sync.sync().catch(console.error)
|
this.application.sync.sync().catch(console.error)
|
||||||
}
|
}
|
||||||
|
|
||||||
async addTagToSelectedNotes(tag: SNTag): Promise<void> {
|
async addTagToSelectedNotes(tag: SNTag): Promise<void> {
|
||||||
const selectedNotes = this.getSelectedNotesList()
|
const selectedNotes = this.getSelectedNotesList()
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
selectedNotes.map(async (note) => {
|
selectedNotes.map(async (note) => {
|
||||||
await this.mutator.addTagToNote(note, tag, this.shouldLinkToParentFolders)
|
await this.application.mutator.addTagToNote(note, tag, this.shouldLinkToParentFolders)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
this.sync.sync().catch(console.error)
|
this.application.sync.sync().catch(console.error)
|
||||||
}
|
}
|
||||||
|
|
||||||
async removeTagFromSelectedNotes(tag: SNTag): Promise<void> {
|
async removeTagFromSelectedNotes(tag: SNTag): Promise<void> {
|
||||||
const selectedNotes = this.getSelectedNotesList()
|
const selectedNotes = this.getSelectedNotesList()
|
||||||
await this.mutator.changeItem(tag, (mutator) => {
|
await this.application.mutator.changeItem(tag, (mutator) => {
|
||||||
for (const note of selectedNotes) {
|
for (const note of selectedNotes) {
|
||||||
mutator.removeItemAsRelationship(note)
|
mutator.removeItemAsRelationship(note)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
this.sync.sync().catch(console.error)
|
this.application.sync.sync().catch(console.error)
|
||||||
}
|
}
|
||||||
|
|
||||||
isTagInSelectedNotes(tag: SNTag): boolean {
|
isTagInSelectedNotes(tag: SNTag): boolean {
|
||||||
@@ -403,8 +486,8 @@ export class NotesController
|
|||||||
confirmButtonStyle: 'danger',
|
confirmButtonStyle: 'danger',
|
||||||
})
|
})
|
||||||
) {
|
) {
|
||||||
await this.mutator.emptyTrash()
|
await this.application.mutator.emptyTrash()
|
||||||
this.sync.sync().catch(console.error)
|
this.application.sync.sync().catch(console.error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -419,19 +502,174 @@ export class NotesController
|
|||||||
references: ContentReference[] = [],
|
references: ContentReference[] = [],
|
||||||
): Promise<SNNote> {
|
): Promise<SNNote> {
|
||||||
const noteType = noteTypeForEditorIdentifier(editorIdentifier)
|
const noteType = noteTypeForEditorIdentifier(editorIdentifier)
|
||||||
const selectedTag = this.navigationController.selected
|
const selectedTag = this.application.navigationController.selected
|
||||||
const templateNote = this.items.createTemplateItem<NoteContent, SNNote>(ContentType.TYPES.Note, {
|
const templateNote = this.application.items.createTemplateItem<NoteContent, SNNote>(ContentType.TYPES.Note, {
|
||||||
title,
|
title,
|
||||||
text,
|
text,
|
||||||
references,
|
references,
|
||||||
noteType,
|
noteType,
|
||||||
editorIdentifier,
|
editorIdentifier,
|
||||||
})
|
})
|
||||||
const note = await this.mutator.insertItem<SNNote>(templateNote)
|
const note = await this.application.mutator.insertItem<SNNote>(templateNote)
|
||||||
if (selectedTag instanceof SNTag) {
|
if (selectedTag instanceof SNTag) {
|
||||||
const shouldAddTagHierarchy = this.preferences.getValue(PrefKey.NoteAddToParentFolders, true)
|
const shouldAddTagHierarchy = this.application.preferences.getValue(PrefKey.NoteAddToParentFolders, true)
|
||||||
await this.mutator.addTagToNote(templateNote, selectedTag, shouldAddTagHierarchy)
|
await this.application.mutator.addTagToNote(templateNote, selectedTag, shouldAddTagHierarchy)
|
||||||
}
|
}
|
||||||
return note
|
return note
|
||||||
}
|
}
|
||||||
|
|
||||||
|
showSuperExportModal = () => {
|
||||||
|
this.shouldShowSuperExportModal = true
|
||||||
|
}
|
||||||
|
closeSuperExportModal = () => {
|
||||||
|
this.shouldShowSuperExportModal = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// gets attribute info about the given notes in a single loop
|
||||||
|
getNotesInfo = (notes: SNNote[]) => {
|
||||||
|
let pinned = false,
|
||||||
|
unpinned = false,
|
||||||
|
starred = false,
|
||||||
|
unstarred = false,
|
||||||
|
trashed = false,
|
||||||
|
notTrashed = false,
|
||||||
|
archived = false,
|
||||||
|
unarchived = false,
|
||||||
|
hiddenPreviews = 0,
|
||||||
|
unhiddenPreviews = 0,
|
||||||
|
locked = 0,
|
||||||
|
unlocked = 0,
|
||||||
|
protecteds = 0,
|
||||||
|
unprotected = 0
|
||||||
|
|
||||||
|
for (let i = 0; i < notes.length; i++) {
|
||||||
|
const note = notes[i]
|
||||||
|
if (!note) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (note.pinned) {
|
||||||
|
pinned = true
|
||||||
|
} else {
|
||||||
|
unpinned = true
|
||||||
|
}
|
||||||
|
if (note.starred) {
|
||||||
|
starred = true
|
||||||
|
} else {
|
||||||
|
unstarred = true
|
||||||
|
}
|
||||||
|
if (note.trashed) {
|
||||||
|
trashed = true
|
||||||
|
} else {
|
||||||
|
notTrashed = true
|
||||||
|
}
|
||||||
|
if (note.archived) {
|
||||||
|
archived = true
|
||||||
|
} else {
|
||||||
|
unarchived = true
|
||||||
|
}
|
||||||
|
if (note.hidePreview) {
|
||||||
|
hiddenPreviews++
|
||||||
|
} else {
|
||||||
|
unhiddenPreviews++
|
||||||
|
}
|
||||||
|
if (note.locked) {
|
||||||
|
locked++
|
||||||
|
} else {
|
||||||
|
unlocked++
|
||||||
|
}
|
||||||
|
if (note.protected) {
|
||||||
|
protecteds++
|
||||||
|
} else {
|
||||||
|
unprotected++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
pinned,
|
||||||
|
unpinned,
|
||||||
|
starred,
|
||||||
|
unstarred,
|
||||||
|
trashed,
|
||||||
|
notTrashed,
|
||||||
|
archived,
|
||||||
|
unarchived,
|
||||||
|
hidePreviews: hiddenPreviews > unhiddenPreviews,
|
||||||
|
locked: locked > unlocked,
|
||||||
|
protect: protecteds > unprotected,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadSelectedNotes = async () => {
|
||||||
|
const notes = this.selectedNotes
|
||||||
|
if (notes.length === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const toast = addToast({
|
||||||
|
type: ToastType.Progress,
|
||||||
|
message: `Exporting ${notes.length} ${pluralize(notes.length, 'note', 'notes')}...`,
|
||||||
|
})
|
||||||
|
try {
|
||||||
|
const result = await createNoteExport(this.application, notes)
|
||||||
|
if (!result) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const { blob, fileName } = result
|
||||||
|
void downloadOrShareBlobBasedOnPlatform({
|
||||||
|
archiveService: this.application.archiveService,
|
||||||
|
platform: this.application.platform,
|
||||||
|
mobileDevice: this.application.mobileDevice,
|
||||||
|
blob: blob,
|
||||||
|
filename: fileName,
|
||||||
|
isNativeMobileWeb: this.application.isNativeMobileWeb(),
|
||||||
|
})
|
||||||
|
dismissToast(toast)
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
addToast({
|
||||||
|
type: ToastType.Error,
|
||||||
|
message: 'Could not export notes',
|
||||||
|
})
|
||||||
|
dismissToast(toast)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exportSelectedNotes = () => {
|
||||||
|
const notes = this.selectedNotes
|
||||||
|
const hasSuperNote = notes.some((note) => note.noteType === NoteType.Super)
|
||||||
|
|
||||||
|
if (hasSuperNote) {
|
||||||
|
this.showSuperExportModal()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.downloadSelectedNotes().catch(console.error)
|
||||||
|
}
|
||||||
|
|
||||||
|
duplicateSelectedNotes = async () => {
|
||||||
|
const notes = this.selectedNotes
|
||||||
|
await Promise.all(
|
||||||
|
notes.map((note) =>
|
||||||
|
this.application.mutator
|
||||||
|
.duplicateItem(note)
|
||||||
|
.then((duplicated) =>
|
||||||
|
addToast({
|
||||||
|
type: ToastType.Regular,
|
||||||
|
message: `Duplicated note "${duplicated.title}"`,
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
label: 'Open',
|
||||||
|
handler: (toastId) => {
|
||||||
|
this.application.itemListController.selectUuids([duplicated.uuid], true).catch(console.error)
|
||||||
|
dismissToast(toastId)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
autoClose: true,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.catch(console.error),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
void this.application.sync.sync()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import { AbstractViewController } from '../Abstract/AbstractViewController'
|
|||||||
import { log, LoggingDomain } from '@/Logging'
|
import { log, LoggingDomain } from '@/Logging'
|
||||||
import { PaneLayout } from './PaneLayout'
|
import { PaneLayout } from './PaneLayout'
|
||||||
import { IsTabletOrMobileScreen } from '@/Application/UseCase/IsTabletOrMobileScreen'
|
import { IsTabletOrMobileScreen } from '@/Application/UseCase/IsTabletOrMobileScreen'
|
||||||
|
import { CommandService } from '../../Components/CommandPalette/CommandService'
|
||||||
|
|
||||||
const MinimumNavPanelWidth = PrefDefaults[PrefKey.TagsPanelWidth]
|
const MinimumNavPanelWidth = PrefDefaults[PrefKey.TagsPanelWidth]
|
||||||
const MinimumNotesPanelWidth = PrefDefaults[PrefKey.NotesPanelWidth]
|
const MinimumNotesPanelWidth = PrefDefaults[PrefKey.NotesPanelWidth]
|
||||||
@@ -56,7 +57,8 @@ export class PaneController extends AbstractViewController implements InternalEv
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private preferences: PreferenceServiceInterface,
|
private preferences: PreferenceServiceInterface,
|
||||||
private keyboardService: KeyboardService,
|
keyboardService: KeyboardService,
|
||||||
|
commands: CommandService,
|
||||||
private _isTabletOrMobileScreen: IsTabletOrMobileScreen,
|
private _isTabletOrMobileScreen: IsTabletOrMobileScreen,
|
||||||
private _panesForLayout: PanesForLayout,
|
private _panesForLayout: PanesForLayout,
|
||||||
eventBus: InternalEventBusInterface,
|
eventBus: InternalEventBusInterface,
|
||||||
@@ -104,33 +106,17 @@ export class PaneController extends AbstractViewController implements InternalEv
|
|||||||
eventBus.addEventHandler(this, ApplicationEvent.LocalPreferencesChanged)
|
eventBus.addEventHandler(this, ApplicationEvent.LocalPreferencesChanged)
|
||||||
|
|
||||||
this.disposers.push(
|
this.disposers.push(
|
||||||
keyboardService.addCommandHandler({
|
commands.addWithShortcut(TOGGLE_FOCUS_MODE_COMMAND, 'General', 'Toggle focus mode', (event) => {
|
||||||
command: TOGGLE_FOCUS_MODE_COMMAND,
|
event?.preventDefault()
|
||||||
category: 'General',
|
this.toggleFocusMode()
|
||||||
description: 'Toggle focus mode',
|
|
||||||
onKeyDown: (event) => {
|
|
||||||
event.preventDefault()
|
|
||||||
this.setFocusModeEnabled(!this.focusModeEnabled)
|
|
||||||
return true
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
keyboardService.addCommandHandler({
|
commands.addWithShortcut(TOGGLE_LIST_PANE_KEYBOARD_COMMAND, 'General', 'Toggle notes panel', (event) => {
|
||||||
command: TOGGLE_LIST_PANE_KEYBOARD_COMMAND,
|
event?.preventDefault()
|
||||||
category: 'General',
|
this.toggleListPane()
|
||||||
description: 'Toggle notes panel',
|
|
||||||
onKeyDown: (event) => {
|
|
||||||
event.preventDefault()
|
|
||||||
this.toggleListPane()
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
keyboardService.addCommandHandler({
|
commands.addWithShortcut(TOGGLE_NAVIGATION_PANE_KEYBOARD_COMMAND, 'General', 'Toggle tags panel', (event) => {
|
||||||
command: TOGGLE_NAVIGATION_PANE_KEYBOARD_COMMAND,
|
event?.preventDefault()
|
||||||
category: 'General',
|
this.toggleNavigationPane()
|
||||||
description: 'Toggle tags panel',
|
|
||||||
onKeyDown: (event) => {
|
|
||||||
event.preventDefault()
|
|
||||||
this.toggleNavigationPane()
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -332,4 +318,8 @@ export class PaneController extends AbstractViewController implements InternalEv
|
|||||||
}, FOCUS_MODE_ANIMATION_DURATION)
|
}, FOCUS_MODE_ANIMATION_DURATION)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toggleFocusMode = () => {
|
||||||
|
this.setFocusModeEnabled(!this.focusModeEnabled)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
44
packages/web/src/javascripts/Hooks/mergeRegister.ts
Normal file
44
packages/web/src/javascripts/Hooks/mergeRegister.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
type Func = () => void
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a function that will execute all functions passed when called. It is generally used
|
||||||
|
* to register multiple lexical listeners and then tear them down with a single function call, such
|
||||||
|
* as React's useEffect hook.
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* useEffect(() => {
|
||||||
|
* return mergeRegister(
|
||||||
|
* editor.registerCommand(...registerCommand1 logic),
|
||||||
|
* editor.registerCommand(...registerCommand2 logic),
|
||||||
|
* editor.registerCommand(...registerCommand3 logic)
|
||||||
|
* )
|
||||||
|
* }, [editor])
|
||||||
|
* ```
|
||||||
|
* In this case, useEffect is returning the function returned by mergeRegister as a cleanup
|
||||||
|
* function to be executed after either the useEffect runs again (due to one of its dependencies
|
||||||
|
* updating) or the component it resides in unmounts.
|
||||||
|
* Note the functions don't necessarily need to be in an array as all arguments
|
||||||
|
* are considered to be the func argument and spread from there.
|
||||||
|
* The order of cleanup is the reverse of the argument order. Generally it is
|
||||||
|
* expected that the first "acquire" will be "released" last (LIFO order),
|
||||||
|
* because a later step may have some dependency on an earlier one.
|
||||||
|
* @param func - An array of cleanup functions meant to be executed by the returned function.
|
||||||
|
* @returns the function which executes all the passed cleanup functions.
|
||||||
|
*/
|
||||||
|
export default function mergeRegister(...func: Array<Func>): () => void {
|
||||||
|
return () => {
|
||||||
|
for (let i = func.length - 1; i >= 0; i--) {
|
||||||
|
func[i]()
|
||||||
|
}
|
||||||
|
// Clean up the references and make future calls a no-op
|
||||||
|
func.length = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { IconType, FileItem, SNNote, SNTag, DecryptedItemInterface } from '@standardnotes/snjs'
|
import { IconType, FileItem, SNNote, SNTag, DecryptedItemInterface, SmartView } from '@standardnotes/snjs'
|
||||||
import { getIconAndTintForNoteType } from './getIconAndTintForNoteType'
|
import { getIconAndTintForNoteType } from './getIconAndTintForNoteType'
|
||||||
import { getIconForFileType } from './getIconForFileType'
|
import { getIconForFileType } from './getIconForFileType'
|
||||||
import { WebApplicationInterface } from '@standardnotes/ui-services'
|
import { WebApplicationInterface } from '@standardnotes/ui-services'
|
||||||
@@ -12,7 +12,7 @@ export function getIconForItem(item: DecryptedItemInterface, application: WebApp
|
|||||||
} else if (item instanceof FileItem) {
|
} else if (item instanceof FileItem) {
|
||||||
const icon = getIconForFileType(item.mimeType)
|
const icon = getIconForFileType(item.mimeType)
|
||||||
return [icon, 'text-info']
|
return [icon, 'text-info']
|
||||||
} else if (item instanceof SNTag) {
|
} else if (item instanceof SNTag || item instanceof SmartView) {
|
||||||
return [item.iconString as IconType, 'text-info']
|
return [item.iconString as IconType, 'text-info']
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
38
yarn.lock
38
yarn.lock
@@ -36,36 +36,36 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@ariakit/core@npm:0.3.8":
|
"@ariakit/core@npm:0.4.15":
|
||||||
version: 0.3.8
|
version: 0.4.15
|
||||||
resolution: "@ariakit/core@npm:0.3.8"
|
resolution: "@ariakit/core@npm:0.4.15"
|
||||||
checksum: e416596efe7783cba033efcf212119e228b3707006f294745d8beedb09388f4ceebe70d7e936362979d2ff98ec50a16ecc24c988b0825abd68d3a2367e96c1e0
|
checksum: add800c855c04f94a26e223ccb50f4c390182062848fb69717130ab25c33d755e4137ef5dd334194e4594c5852d64d52cd96b0d288ad9ade84bd24562cb97922
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@ariakit/react-core@npm:0.3.9":
|
"@ariakit/react-core@npm:0.4.18":
|
||||||
version: 0.3.9
|
version: 0.4.18
|
||||||
resolution: "@ariakit/react-core@npm:0.3.9"
|
resolution: "@ariakit/react-core@npm:0.4.18"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@ariakit/core": 0.3.8
|
"@ariakit/core": 0.4.15
|
||||||
"@floating-ui/dom": ^1.0.0
|
"@floating-ui/dom": ^1.0.0
|
||||||
use-sync-external-store: ^1.2.0
|
use-sync-external-store: ^1.2.0
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: ^17.0.0 || ^18.0.0
|
react: ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||||
react-dom: ^17.0.0 || ^18.0.0
|
react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||||
checksum: ef44543aecf10d69ea63cb4c68d340f88a8f17a4e0b0dc25a508665a7591126a42731522f97a984268c1d0927738ad2002d702f6bfd40f272090174d56a5ffb4
|
checksum: 53a012f087c20ccd3d36e13af2fceb8928f280cf57c1c0e6e79088e81c53475f3ec8330be8ee465d3e270c089f80d3a98950bd103645190889f565f0184d2221
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@ariakit/react@npm:^0.3.9":
|
"@ariakit/react@npm:^0.4.18":
|
||||||
version: 0.3.9
|
version: 0.4.18
|
||||||
resolution: "@ariakit/react@npm:0.3.9"
|
resolution: "@ariakit/react@npm:0.4.18"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@ariakit/react-core": 0.3.9
|
"@ariakit/react-core": 0.4.18
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: ^17.0.0 || ^18.0.0
|
react: ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||||
react-dom: ^17.0.0 || ^18.0.0
|
react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||||
checksum: 901424a4e47d36c97a35cab016249b22bfe20519cc87ffa0420c14b3547751ca282cfaa9e770937af413c96776106f1f3d2c4b08434f15abca08abd2ab94d73e
|
checksum: e8bc2df82f74dab00a0d950fb3236c78838af4f6c814ce459145c37457716133c924ccb302f29a4106ccbf04b9b0e2156e9490e641925a0465d4a6813f652491
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
@@ -8795,7 +8795,7 @@ __metadata:
|
|||||||
version: 0.0.0-use.local
|
version: 0.0.0-use.local
|
||||||
resolution: "@standardnotes/web@workspace:packages/web"
|
resolution: "@standardnotes/web@workspace:packages/web"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@ariakit/react": ^0.3.9
|
"@ariakit/react": ^0.4.18
|
||||||
"@babel/core": "*"
|
"@babel/core": "*"
|
||||||
"@babel/plugin-proposal-class-properties": ^7.18.6
|
"@babel/plugin-proposal-class-properties": ^7.18.6
|
||||||
"@babel/plugin-transform-react-jsx": ^7.19.0
|
"@babel/plugin-transform-react-jsx": ^7.19.0
|
||||||
|
|||||||
Reference in New Issue
Block a user