From eb75329fb43b3a00291983590c7a860e4352dd3e Mon Sep 17 00:00:00 2001 From: Aman Harwara Date: Wed, 29 Nov 2023 22:28:04 +0530 Subject: [PATCH] feat: You can now select an existing tag to automatically add imported notes to (#2663) --- .../Domain/Syncable/UserPrefs/PrefDefaults.ts | 3 + .../src/Domain/Syncable/UserPrefs/PrefKey.ts | 6 ++ .../Dependencies/WebDependencies.ts | 3 + .../Components/ImportModal/ImportModal.tsx | 76 +++++++++++++-- .../ImportModal/ImportModalController.ts | 97 +++++++++++++++---- 5 files changed, 159 insertions(+), 26 deletions(-) diff --git a/packages/models/src/Domain/Syncable/UserPrefs/PrefDefaults.ts b/packages/models/src/Domain/Syncable/UserPrefs/PrefDefaults.ts index 3d91d2a88..8ddb2f57c 100644 --- a/packages/models/src/Domain/Syncable/UserPrefs/PrefDefaults.ts +++ b/packages/models/src/Domain/Syncable/UserPrefs/PrefDefaults.ts @@ -48,6 +48,9 @@ export const PrefDefaults = { [PrefKey.ActiveThemes]: [], [PrefKey.ActiveComponents]: [], [PrefKey.AlwaysShowSuperToolbar]: true, + [PrefKey.AddImportsToTag]: true, + [PrefKey.AlwaysCreateNewTagForImports]: true, + [PrefKey.ExistingTagForImports]: undefined, } satisfies { [key in PrefKey]: PrefValue[key] } diff --git a/packages/models/src/Domain/Syncable/UserPrefs/PrefKey.ts b/packages/models/src/Domain/Syncable/UserPrefs/PrefKey.ts index 6fc899a57..c1514f736 100644 --- a/packages/models/src/Domain/Syncable/UserPrefs/PrefKey.ts +++ b/packages/models/src/Domain/Syncable/UserPrefs/PrefKey.ts @@ -49,6 +49,9 @@ export enum PrefKey { ActiveThemes = 'activeThemes', ActiveComponents = 'activeComponents', AlwaysShowSuperToolbar = 'alwaysShowSuperToolbar', + AddImportsToTag = 'addImportsToTag', + AlwaysCreateNewTagForImports = 'alwaysCreateNewTagForImports', + ExistingTagForImports = 'existingTagForImports', } export type PrefValue = { @@ -93,4 +96,7 @@ export type PrefValue = { [PrefKey.ActiveThemes]: string[] [PrefKey.ActiveComponents]: string[] [PrefKey.AlwaysShowSuperToolbar]: boolean + [PrefKey.AddImportsToTag]: boolean + [PrefKey.AlwaysCreateNewTagForImports]: boolean + [PrefKey.ExistingTagForImports]: string | undefined } diff --git a/packages/web/src/javascripts/Application/Dependencies/WebDependencies.ts b/packages/web/src/javascripts/Application/Dependencies/WebDependencies.ts index 92fb59db5..a2468cbc0 100644 --- a/packages/web/src/javascripts/Application/Dependencies/WebDependencies.ts +++ b/packages/web/src/javascripts/Application/Dependencies/WebDependencies.ts @@ -391,6 +391,9 @@ export class WebDependencies extends DependencyContainer { this.get(Web_TYPES.NavigationController), application.items, application.mutator, + this.get(Web_TYPES.LinkingController), + application.preferences, + application.events, ) }) diff --git a/packages/web/src/javascripts/Components/ImportModal/ImportModal.tsx b/packages/web/src/javascripts/Components/ImportModal/ImportModal.tsx index 98dbc6f49..ea588faf7 100644 --- a/packages/web/src/javascripts/Components/ImportModal/ImportModal.tsx +++ b/packages/web/src/javascripts/Components/ImportModal/ImportModal.tsx @@ -7,6 +7,10 @@ import ModalOverlay from '../Modal/ModalOverlay' import { ImportModalController } from '@/Components/ImportModal/ImportModalController' import { useApplication } from '../ApplicationProvider' import Switch from '../Switch/Switch' +import LinkedItemBubble from '../LinkedItems/LinkedItemBubble' +import { createLinkFromItem } from '@/Utils/Items/Search/createLinkFromItem' +import ItemSelectionDropdown from '../ItemSelectionDropdown/ItemSelectionDropdown' +import { ContentType, SNTag } from '@standardnotes/snjs' const ImportModal = ({ importModalController }: { importModalController: ImportModalController }) => { const application = useApplication() @@ -14,8 +18,12 @@ const ImportModal = ({ importModalController }: { importModalController: ImportM const { files, setFiles, + addImportsToTag, + setAddImportsToTag, shouldCreateTag, setShouldCreateTag, + existingTagForImports, + setExistingTagForImports, updateFile, removeFile, parseAndImport, @@ -35,6 +43,7 @@ const ImportModal = ({ importModalController }: { importModalController: ImportM onClick: parseAndImport, hidden: !isReadyToImport, mobileSlot: 'right', + disabled: !isReadyToImport || (!shouldCreateTag && !existingTagForImports), }, { label: importSuccessOrError ? 'Close' : 'Cancel', @@ -43,13 +52,13 @@ const ImportModal = ({ importModalController }: { importModalController: ImportM mobileSlot: 'left', }, ], - [close, importSuccessOrError, isReadyToImport, parseAndImport], + [close, existingTagForImports, importSuccessOrError, isReadyToImport, parseAndImport, shouldCreateTag], ) return ( - -
+ +
{!files.length && } {files.length > 0 && (
@@ -66,10 +75,63 @@ const ImportModal = ({ importModalController }: { importModalController: ImportM )}
{files.length > 0 && ( - +
+ + Add all imported notes to tag + + {addImportsToTag && ( + <> + +
+
+ + {existingTagForImports && ( + { + setExistingTagForImports(undefined) + }} + isBidirectional={false} + inlineFlex={true} + /> + )} +
+ {!shouldCreateTag && ( +
+ setExistingTagForImports(tag as SNTag)} + placeholder="Select tag to add imported notes to..." + contentTypes={[ContentType.TYPES.Tag]} + /> +
+ )} +
+ + )} +
)} diff --git a/packages/web/src/javascripts/Components/ImportModal/ImportModalController.ts b/packages/web/src/javascripts/Components/ImportModal/ImportModalController.ts index 9e78791ae..ba3f5b7a2 100644 --- a/packages/web/src/javascripts/Components/ImportModal/ImportModalController.ts +++ b/packages/web/src/javascripts/Components/ImportModal/ImportModalController.ts @@ -1,14 +1,19 @@ -import { DecryptedTransferPayload, SNTag, TagContent } from '@standardnotes/models' +import { DecryptedTransferPayload, PrefDefaults, PrefKey, SNNote, SNTag, TagContent } from '@standardnotes/models' import { ContentType, + InternalEventBusInterface, ItemManagerInterface, MutatorClientInterface, pluralize, + PreferenceServiceInterface, + PreferencesServiceEvent, UuidGenerator, } from '@standardnotes/snjs' import { Importer, NoteImportType } from '@standardnotes/ui-services' -import { action, makeObservable, observable } from 'mobx' +import { action, makeObservable, observable, runInAction } from 'mobx' import { NavigationController } from '../../Controllers/Navigation/NavigationController' +import { LinkingController } from '@/Controllers/LinkingController' +import { AbstractViewController } from '@/Controllers/Abstract/AbstractViewController' type ImportModalFileCommon = { id: string @@ -27,9 +32,11 @@ export type ImportModalFile = ( ) & ImportModalFileCommon -export class ImportModalController { +export class ImportModalController extends AbstractViewController { isVisible = false - shouldCreateTag = false + addImportsToTag = false + shouldCreateTag = true + existingTagForImports: SNTag | undefined = undefined files: ImportModalFile[] = [] importTag: SNTag | undefined = undefined @@ -38,14 +45,25 @@ export class ImportModalController { private navigationController: NavigationController, private items: ItemManagerInterface, private mutator: MutatorClientInterface, + private linkingController: LinkingController, + private preferences: PreferenceServiceInterface, + eventBus: InternalEventBusInterface, ) { + super(eventBus) + makeObservable(this, { isVisible: observable, setIsVisible: action, + addImportsToTag: observable, + setAddImportsToTag: action, + shouldCreateTag: observable, setShouldCreateTag: action, + existingTagForImports: observable, + setExistingTagForImports: action, + files: observable, setFiles: action, updateFile: action, @@ -54,14 +72,38 @@ export class ImportModalController { importTag: observable, setImportTag: action, }) + + this.disposers.push( + preferences.addEventObserver((event) => { + if (event === PreferencesServiceEvent.PreferencesChanged) { + runInAction(() => { + this.addImportsToTag = preferences.getValue(PrefKey.AddImportsToTag, PrefDefaults[PrefKey.AddImportsToTag]) + this.shouldCreateTag = preferences.getValue( + PrefKey.AlwaysCreateNewTagForImports, + PrefDefaults[PrefKey.AlwaysCreateNewTagForImports], + ) + const existingTagUuid = preferences.getValue(PrefKey.ExistingTagForImports) + this.existingTagForImports = existingTagUuid ? this.items.findItem(existingTagUuid) : undefined + }) + } + }), + ) } setIsVisible = (isVisible: boolean) => { this.isVisible = isVisible } + setAddImportsToTag = (addImportsToTag: boolean) => { + this.preferences.setValue(PrefKey.AddImportsToTag, addImportsToTag).catch(console.error) + } + setShouldCreateTag = (shouldCreateTag: boolean) => { - this.shouldCreateTag = shouldCreateTag + this.preferences.setValue(PrefKey.AlwaysCreateNewTagForImports, shouldCreateTag).catch(console.error) + } + + setExistingTagForImports = (tag: SNTag | undefined) => { + this.preferences.setValue(PrefKey.ExistingTagForImports, tag?.uuid).catch(console.error) } setFiles = (files: File[], service?: NoteImportType) => { @@ -87,7 +129,6 @@ export class ImportModalController { close = () => { this.setIsVisible(false) - this.setShouldCreateTag(false) if (this.importTag) { this.navigationController .setSelectedTag(this.importTag, 'all', { @@ -175,20 +216,38 @@ export class ImportModalController { if (!importedPayloads.length) { return } - if (this.shouldCreateTag) { + if (this.addImportsToTag) { const currentDate = new Date() - const importTagItem = this.items.createTemplateItem(ContentType.TYPES.Tag, { - title: `Imported on ${currentDate.toLocaleString()}`, - expanded: false, - iconString: '', - references: importedPayloads - .filter((payload) => payload.content_type === ContentType.TYPES.Note) - .map((payload) => ({ - content_type: ContentType.TYPES.Note, - uuid: payload.uuid, - })), - }) - const importTag = await this.mutator.insertItem(importTagItem) + let importTag: SNTag | undefined + if (this.shouldCreateTag) { + const importTagItem = this.items.createTemplateItem(ContentType.TYPES.Tag, { + title: `Imported on ${currentDate.toLocaleString()}`, + expanded: false, + iconString: '', + references: importedPayloads + .filter((payload) => payload.content_type === ContentType.TYPES.Note) + .map((payload) => ({ + content_type: ContentType.TYPES.Note, + uuid: payload.uuid, + })), + }) + importTag = await this.mutator.insertItem(importTagItem) + } else if (this.existingTagForImports) { + try { + const latestExistingTag = this.items.findSureItem(this.existingTagForImports.uuid) + await Promise.all( + importedPayloads + .filter((payload) => payload.content_type === ContentType.TYPES.Note) + .map(async (payload) => { + const note = this.items.findSureItem(payload.uuid) + await this.linkingController.addTagToItem(latestExistingTag, note) + }), + ) + importTag = this.items.findSureItem(this.existingTagForImports.uuid) + } catch (error) { + console.error(error) + } + } if (importTag) { this.setImportTag(importTag as SNTag) }