Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions packages/models/src/Domain/Syncable/UserPrefs/PrefDefaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]
}
6 changes: 6 additions & 0 deletions packages/models/src/Domain/Syncable/UserPrefs/PrefKey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ export enum PrefKey {
ActiveThemes = 'activeThemes',
ActiveComponents = 'activeComponents',
AlwaysShowSuperToolbar = 'alwaysShowSuperToolbar',
AddImportsToTag = 'addImportsToTag',
AlwaysCreateNewTagForImports = 'alwaysCreateNewTagForImports',
ExistingTagForImports = 'existingTagForImports',
}

export type PrefValue = {
Expand Down Expand Up @@ -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
}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
})

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,23 @@ 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()

const {
files,
setFiles,
addImportsToTag,
setAddImportsToTag,
shouldCreateTag,
setShouldCreateTag,
existingTagForImports,
setExistingTagForImports,
updateFile,
removeFile,
parseAndImport,
Expand All @@ -35,6 +43,7 @@ const ImportModal = ({ importModalController }: { importModalController: ImportM
onClick: parseAndImport,
hidden: !isReadyToImport,
mobileSlot: 'right',
disabled: !isReadyToImport || (!shouldCreateTag && !existingTagForImports),
},
{
label: importSuccessOrError ? 'Close' : 'Cancel',
Expand All @@ -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">
Expand All @@ -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>
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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

Expand All @@ -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,
Expand All @@ -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) => {
Expand All @@ -87,7 +129,6 @@ export class ImportModalController {

close = () => {
this.setIsVisible(false)
this.setShouldCreateTag(false)
if (this.importTag) {
this.navigationController
.setSelectedTag(this.importTag, 'all', {
Expand Down Expand Up @@ -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)
}
Expand Down