feat: You can now select an existing tag to automatically add imported notes to (#2663)
This commit is contained in:
@@ -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]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user