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.ActiveComponents]: [],
|
||||
[PrefKey.AlwaysShowSuperToolbar]: true,
|
||||
[PrefKey.AddImportsToTag]: true,
|
||||
[PrefKey.AlwaysCreateNewTagForImports]: true,
|
||||
[PrefKey.ExistingTagForImports]: undefined,
|
||||
} satisfies {
|
||||
[key in PrefKey]: PrefValue[key]
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -391,6 +391,9 @@ export class WebDependencies extends DependencyContainer {
|
||||
this.get<NavigationController>(Web_TYPES.NavigationController),
|
||||
application.items,
|
||||
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 { 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 (
|
||||
<ModalOverlay isOpen={isVisible} close={close}>
|
||||
<Modal title="Import" close={close} actions={modalActions}>
|
||||
<div className="px-4 py-4">
|
||||
<Modal title="Import" close={close} actions={modalActions} className="flex flex-col">
|
||||
<div className="min-h-0 flex-grow px-4 py-4">
|
||||
{!files.length && <ImportModalInitialPage setFiles={setFiles} />}
|
||||
{files.length > 0 && (
|
||||
<div className="divide-y divide-border">
|
||||
@@ -66,10 +75,63 @@ const ImportModal = ({ importModalController }: { importModalController: ImportM
|
||||
)}
|
||||
</div>
|
||||
{files.length > 0 && (
|
||||
<label className="flex items-center gap-2 border-t border-border px-4 py-2">
|
||||
<Switch checked={shouldCreateTag} onChange={setShouldCreateTag} />
|
||||
<span className="text-sm">Create tag with all imported notes</span>
|
||||
</label>
|
||||
<div className="flex flex-col gap-3 border-t border-border px-4 py-4 md:gap-2 md:py-3">
|
||||
<Switch className="flex items-center gap-2" checked={addImportsToTag} onChange={setAddImportsToTag}>
|
||||
<span className="text-sm">Add all imported notes to tag</span>
|
||||
</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>
|
||||
</ModalOverlay>
|
||||
|
||||
@@ -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<TagContent, SNTag>(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<TagContent, SNTag>(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<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) {
|
||||
this.setImportTag(importTag as SNTag)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user