feat: You can now select an existing tag to automatically add imported notes to (#2663)

This commit is contained in:
Aman Harwara
2023-11-29 22:28:04 +05:30
committed by GitHub
parent c9c3c394e5
commit eb75329fb4
5 changed files with 159 additions and 26 deletions

View File

@@ -48,6 +48,9 @@ export const PrefDefaults = {
[PrefKey.ActiveThemes]: [], [PrefKey.ActiveThemes]: [],
[PrefKey.ActiveComponents]: [], [PrefKey.ActiveComponents]: [],
[PrefKey.AlwaysShowSuperToolbar]: true, [PrefKey.AlwaysShowSuperToolbar]: true,
[PrefKey.AddImportsToTag]: true,
[PrefKey.AlwaysCreateNewTagForImports]: true,
[PrefKey.ExistingTagForImports]: undefined,
} satisfies { } satisfies {
[key in PrefKey]: PrefValue[key] [key in PrefKey]: PrefValue[key]
} }

View File

@@ -49,6 +49,9 @@ export enum PrefKey {
ActiveThemes = 'activeThemes', ActiveThemes = 'activeThemes',
ActiveComponents = 'activeComponents', ActiveComponents = 'activeComponents',
AlwaysShowSuperToolbar = 'alwaysShowSuperToolbar', AlwaysShowSuperToolbar = 'alwaysShowSuperToolbar',
AddImportsToTag = 'addImportsToTag',
AlwaysCreateNewTagForImports = 'alwaysCreateNewTagForImports',
ExistingTagForImports = 'existingTagForImports',
} }
export type PrefValue = { export type PrefValue = {
@@ -93,4 +96,7 @@ export type PrefValue = {
[PrefKey.ActiveThemes]: string[] [PrefKey.ActiveThemes]: string[]
[PrefKey.ActiveComponents]: string[] [PrefKey.ActiveComponents]: string[]
[PrefKey.AlwaysShowSuperToolbar]: boolean [PrefKey.AlwaysShowSuperToolbar]: boolean
[PrefKey.AddImportsToTag]: boolean
[PrefKey.AlwaysCreateNewTagForImports]: boolean
[PrefKey.ExistingTagForImports]: string | undefined
} }

View File

@@ -391,6 +391,9 @@ export class WebDependencies extends DependencyContainer {
this.get<NavigationController>(Web_TYPES.NavigationController), this.get<NavigationController>(Web_TYPES.NavigationController),
application.items, application.items,
application.mutator, application.mutator,
this.get<LinkingController>(Web_TYPES.LinkingController),
application.preferences,
application.events,
) )
}) })

View File

@@ -7,6 +7,10 @@ import ModalOverlay from '../Modal/ModalOverlay'
import { ImportModalController } from '@/Components/ImportModal/ImportModalController' import { ImportModalController } from '@/Components/ImportModal/ImportModalController'
import { useApplication } from '../ApplicationProvider' import { useApplication } from '../ApplicationProvider'
import Switch from '../Switch/Switch' 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 ImportModal = ({ importModalController }: { importModalController: ImportModalController }) => {
const application = useApplication() const application = useApplication()
@@ -14,8 +18,12 @@ const ImportModal = ({ importModalController }: { importModalController: ImportM
const { const {
files, files,
setFiles, setFiles,
addImportsToTag,
setAddImportsToTag,
shouldCreateTag, shouldCreateTag,
setShouldCreateTag, setShouldCreateTag,
existingTagForImports,
setExistingTagForImports,
updateFile, updateFile,
removeFile, removeFile,
parseAndImport, parseAndImport,
@@ -35,6 +43,7 @@ const ImportModal = ({ importModalController }: { importModalController: ImportM
onClick: parseAndImport, onClick: parseAndImport,
hidden: !isReadyToImport, hidden: !isReadyToImport,
mobileSlot: 'right', mobileSlot: 'right',
disabled: !isReadyToImport || (!shouldCreateTag && !existingTagForImports),
}, },
{ {
label: importSuccessOrError ? 'Close' : 'Cancel', label: importSuccessOrError ? 'Close' : 'Cancel',
@@ -43,13 +52,13 @@ const ImportModal = ({ importModalController }: { importModalController: ImportM
mobileSlot: 'left', mobileSlot: 'left',
}, },
], ],
[close, importSuccessOrError, isReadyToImport, parseAndImport], [close, existingTagForImports, importSuccessOrError, isReadyToImport, parseAndImport, shouldCreateTag],
) )
return ( return (
<ModalOverlay isOpen={isVisible} close={close}> <ModalOverlay isOpen={isVisible} close={close}>
<Modal title="Import" close={close} actions={modalActions}> <Modal title="Import" close={close} actions={modalActions} className="flex flex-col">
<div className="px-4 py-4"> <div className="min-h-0 flex-grow px-4 py-4">
{!files.length && <ImportModalInitialPage setFiles={setFiles} />} {!files.length && <ImportModalInitialPage setFiles={setFiles} />}
{files.length > 0 && ( {files.length > 0 && (
<div className="divide-y divide-border"> <div className="divide-y divide-border">
@@ -66,10 +75,63 @@ const ImportModal = ({ importModalController }: { importModalController: ImportM
)} )}
</div> </div>
{files.length > 0 && ( {files.length > 0 && (
<label className="flex items-center gap-2 border-t border-border px-4 py-2"> <div className="flex flex-col gap-3 border-t border-border px-4 py-4 md:gap-2 md:py-3">
<Switch checked={shouldCreateTag} onChange={setShouldCreateTag} /> <Switch className="flex items-center gap-2" checked={addImportsToTag} onChange={setAddImportsToTag}>
<span className="text-sm">Create tag with all imported notes</span> <span className="text-sm">Add all imported notes to tag</span>
</label> </Switch>
{addImportsToTag && (
<>
<label className="mt-1.5 flex items-center gap-2 text-sm">
<input
type="radio"
name="import-tag"
className="h-6 w-6 md:h-4 md:w-4"
checked={shouldCreateTag}
onChange={() => {
setShouldCreateTag(true)
}}
/>
Create new tag
</label>
<div className="flex flex-col gap-2">
<div className="flex items-center gap-2">
<label className="flex items-center gap-2 text-sm">
<input
type="radio"
name="import-tag"
className="h-6 w-6 md:h-4 md:w-4"
checked={!shouldCreateTag}
onChange={() => {
setShouldCreateTag(false)
}}
/>
Add to existing tag
</label>
{existingTagForImports && (
<LinkedItemBubble
className="m-1 mr-2"
link={createLinkFromItem(existingTagForImports, 'linked')}
unlinkItem={async () => {
setExistingTagForImports(undefined)
}}
isBidirectional={false}
inlineFlex={true}
/>
)}
</div>
{!shouldCreateTag && (
<div className="ml-8 md:ml-6">
<ItemSelectionDropdown
onSelection={(tag) => setExistingTagForImports(tag as SNTag)}
placeholder="Select tag to add imported notes to..."
contentTypes={[ContentType.TYPES.Tag]}
/>
</div>
)}
</div>
</>
)}
</div>
)} )}
</Modal> </Modal>
</ModalOverlay> </ModalOverlay>

View File

@@ -1,14 +1,19 @@
import { DecryptedTransferPayload, SNTag, TagContent } from '@standardnotes/models' import { DecryptedTransferPayload, PrefDefaults, PrefKey, SNNote, SNTag, TagContent } from '@standardnotes/models'
import { import {
ContentType, ContentType,
InternalEventBusInterface,
ItemManagerInterface, ItemManagerInterface,
MutatorClientInterface, MutatorClientInterface,
pluralize, pluralize,
PreferenceServiceInterface,
PreferencesServiceEvent,
UuidGenerator, UuidGenerator,
} from '@standardnotes/snjs' } from '@standardnotes/snjs'
import { Importer, NoteImportType } from '@standardnotes/ui-services' 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 { NavigationController } from '../../Controllers/Navigation/NavigationController'
import { LinkingController } from '@/Controllers/LinkingController'
import { AbstractViewController } from '@/Controllers/Abstract/AbstractViewController'
type ImportModalFileCommon = { type ImportModalFileCommon = {
id: string id: string
@@ -27,9 +32,11 @@ export type ImportModalFile = (
) & ) &
ImportModalFileCommon ImportModalFileCommon
export class ImportModalController { export class ImportModalController extends AbstractViewController {
isVisible = false isVisible = false
shouldCreateTag = false addImportsToTag = false
shouldCreateTag = true
existingTagForImports: SNTag | undefined = undefined
files: ImportModalFile[] = [] files: ImportModalFile[] = []
importTag: SNTag | undefined = undefined importTag: SNTag | undefined = undefined
@@ -38,14 +45,25 @@ export class ImportModalController {
private navigationController: NavigationController, private navigationController: NavigationController,
private items: ItemManagerInterface, private items: ItemManagerInterface,
private mutator: MutatorClientInterface, private mutator: MutatorClientInterface,
private linkingController: LinkingController,
private preferences: PreferenceServiceInterface,
eventBus: InternalEventBusInterface,
) { ) {
super(eventBus)
makeObservable(this, { makeObservable(this, {
isVisible: observable, isVisible: observable,
setIsVisible: action, setIsVisible: action,
addImportsToTag: observable,
setAddImportsToTag: action,
shouldCreateTag: observable, shouldCreateTag: observable,
setShouldCreateTag: action, setShouldCreateTag: action,
existingTagForImports: observable,
setExistingTagForImports: action,
files: observable, files: observable,
setFiles: action, setFiles: action,
updateFile: action, updateFile: action,
@@ -54,14 +72,38 @@ export class ImportModalController {
importTag: observable, importTag: observable,
setImportTag: action, 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) => { setIsVisible = (isVisible: boolean) => {
this.isVisible = isVisible this.isVisible = isVisible
} }
setAddImportsToTag = (addImportsToTag: boolean) => {
this.preferences.setValue(PrefKey.AddImportsToTag, addImportsToTag).catch(console.error)
}
setShouldCreateTag = (shouldCreateTag: boolean) => { 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) => { setFiles = (files: File[], service?: NoteImportType) => {
@@ -87,7 +129,6 @@ export class ImportModalController {
close = () => { close = () => {
this.setIsVisible(false) this.setIsVisible(false)
this.setShouldCreateTag(false)
if (this.importTag) { if (this.importTag) {
this.navigationController this.navigationController
.setSelectedTag(this.importTag, 'all', { .setSelectedTag(this.importTag, 'all', {
@@ -175,20 +216,38 @@ export class ImportModalController {
if (!importedPayloads.length) { if (!importedPayloads.length) {
return return
} }
if (this.shouldCreateTag) { if (this.addImportsToTag) {
const currentDate = new Date() const currentDate = new Date()
const importTagItem = this.items.createTemplateItem<TagContent, SNTag>(ContentType.TYPES.Tag, { let importTag: SNTag | undefined
title: `Imported on ${currentDate.toLocaleString()}`, if (this.shouldCreateTag) {
expanded: false, const importTagItem = this.items.createTemplateItem<TagContent, SNTag>(ContentType.TYPES.Tag, {
iconString: '', title: `Imported on ${currentDate.toLocaleString()}`,
references: importedPayloads expanded: false,
.filter((payload) => payload.content_type === ContentType.TYPES.Note) iconString: '',
.map((payload) => ({ references: importedPayloads
content_type: ContentType.TYPES.Note, .filter((payload) => payload.content_type === ContentType.TYPES.Note)
uuid: payload.uuid, .map((payload) => ({
})), content_type: ContentType.TYPES.Note,
}) uuid: payload.uuid,
const importTag = await this.mutator.insertItem(importTagItem) })),
})
importTag = await this.mutator.insertItem<SNTag>(importTagItem)
} else if (this.existingTagForImports) {
try {
const latestExistingTag = this.items.findSureItem<SNTag>(this.existingTagForImports.uuid)
await Promise.all(
importedPayloads
.filter((payload) => payload.content_type === ContentType.TYPES.Note)
.map(async (payload) => {
const note = this.items.findSureItem<SNNote>(payload.uuid)
await this.linkingController.addTagToItem(latestExistingTag, note)
}),
)
importTag = this.items.findSureItem<SNTag>(this.existingTagForImports.uuid)
} catch (error) {
console.error(error)
}
}
if (importTag) { if (importTag) {
this.setImportTag(importTag as SNTag) this.setImportTag(importTag as SNTag)
} }