From 624afe9029a428a4a5eeb87ae691782ea87eab30 Mon Sep 17 00:00:00 2001 From: Elio Struyf Date: Mon, 15 Jan 2024 14:41:10 +0100 Subject: [PATCH 1/3] Content tagging --- .../components/Modals/InfoDialog.tsx | 25 +---- .../TaxonomyView/TaxonomyActions.tsx | 104 ++++++++++-------- .../TaxonomyView/TaxonomyManager.tsx | 8 +- .../TaxonomyView/TaxonomyTagging.tsx | 88 +++++++++++++++ .../components/TaxonomyView/TaxonomyView.tsx | 36 ++++-- 5 files changed, 183 insertions(+), 78 deletions(-) create mode 100644 src/dashboardWebView/components/TaxonomyView/TaxonomyTagging.tsx diff --git a/src/dashboardWebView/components/Modals/InfoDialog.tsx b/src/dashboardWebView/components/Modals/InfoDialog.tsx index 33bd7561..0fba9d9b 100644 --- a/src/dashboardWebView/components/Modals/InfoDialog.tsx +++ b/src/dashboardWebView/components/Modals/InfoDialog.tsx @@ -1,7 +1,6 @@ import { Dialog, Transition } from '@headlessui/react'; import * as React from 'react'; import { Fragment } from 'react'; -import useThemeColors from '../../hooks/useThemeColors'; export interface IInfoDialogProps { icon?: JSX.Element; @@ -17,7 +16,6 @@ export const InfoDialog: React.FunctionComponent = ({ description, children }: React.PropsWithChildren) => { - const { getColors } = useThemeColors(); return ( @@ -32,11 +30,7 @@ export const InfoDialog: React.FunctionComponent = ({ leaveFrom="opacity-100" leaveTo="opacity-0" > - + {/* This element is to trick the browser into centering the modal contents. */} @@ -53,31 +47,22 @@ export const InfoDialog: React.FunctionComponent = ({ leaveFrom="opacity-100 translate-y-0 sm:scale-100" leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" > -
+
{icon && ( -
+
{icon}
)}
{title}
-

{description}

+

{description}

diff --git a/src/dashboardWebView/components/TaxonomyView/TaxonomyActions.tsx b/src/dashboardWebView/components/TaxonomyView/TaxonomyActions.tsx index 1a261b00..261c05f5 100644 --- a/src/dashboardWebView/components/TaxonomyView/TaxonomyActions.tsx +++ b/src/dashboardWebView/components/TaxonomyView/TaxonomyActions.tsx @@ -18,12 +18,14 @@ export interface ITaxonomyActionsProps { field: string | null; value: string; unmapped?: boolean; + onContentTagging: (value: string) => void; } export const TaxonomyActions: React.FunctionComponent = ({ field, value, - unmapped + unmapped, + onContentTagging }: React.PropsWithChildren) => { const onEdit = useCallback(() => { Messenger.send(DashboardMessage.editTaxonomy, { @@ -60,59 +62,65 @@ export const TaxonomyActions: React.FunctionComponent = ( }); }, [field, value]); + const onTagging = useCallback(() => { + onContentTagging(value); + }, [value]); + return ( -
- {unmapped && ( + <> +
+ {unmapped && ( + + + + {l10n.t(LocalizationKey.dashboardTaxonomyViewButtonAddTitle, value)} + + + )} + - + title={`Tag content`} + onClick={onTagging}> + + {l10n.t(LocalizationKey.commonEdit)} + + + + + {l10n.t(LocalizationKey.commonEdit)} + + + + - {l10n.t(LocalizationKey.dashboardTaxonomyViewButtonAddTitle, value)} + {l10n.t(LocalizationKey.dashboardTaxonomyViewButtonMergeTitle, value)} - )} - - - {l10n.t(LocalizationKey.commonEdit)} - + + + + {l10n.t(LocalizationKey.dashboardTaxonomyViewButtonMoveTitle)} + + - - - {l10n.t(LocalizationKey.commonEdit)} - - - - - - {l10n.t(LocalizationKey.dashboardTaxonomyViewButtonMergeTitle, value)} - - - - - - - {l10n.t(LocalizationKey.dashboardTaxonomyViewButtonMoveTitle)} - - - - - - - {l10n.t(LocalizationKey.dashboardTaxonomyViewButtonDeleteTitle, value)} - - -
+ + + + {l10n.t(LocalizationKey.dashboardTaxonomyViewButtonDeleteTitle, value)} + + +
+ ); }; diff --git a/src/dashboardWebView/components/TaxonomyView/TaxonomyManager.tsx b/src/dashboardWebView/components/TaxonomyView/TaxonomyManager.tsx index 705af711..704be138 100644 --- a/src/dashboardWebView/components/TaxonomyView/TaxonomyManager.tsx +++ b/src/dashboardWebView/components/TaxonomyView/TaxonomyManager.tsx @@ -21,12 +21,14 @@ export interface ITaxonomyManagerProps { data: TaxonomyData | undefined; taxonomy: string | null; pages: Page[]; + onContentTagging: (value: string) => void; } export const TaxonomyManager: React.FunctionComponent = ({ data, taxonomy, - pages + pages, + onContentTagging }: React.PropsWithChildren) => { const settings = useRecoilValue(SettingsSelector); const { getColors } = useThemeColors(); @@ -216,7 +218,7 @@ export const TaxonomyManager: React.FunctionComponent = ( - + )) @@ -247,7 +249,7 @@ export const TaxonomyManager: React.FunctionComponent = ( - + ))} diff --git a/src/dashboardWebView/components/TaxonomyView/TaxonomyTagging.tsx b/src/dashboardWebView/components/TaxonomyView/TaxonomyTagging.tsx new file mode 100644 index 00000000..eb48ccdc --- /dev/null +++ b/src/dashboardWebView/components/TaxonomyView/TaxonomyTagging.tsx @@ -0,0 +1,88 @@ +import * as React from 'react'; +import { Page } from '../../models'; +import { useRecoilValue } from 'recoil'; +import { SettingsSelector } from '../../state'; +import { getTaxonomyField } from '../../../helpers/getTaxonomyField'; +import { Sorting } from '../../../helpers/Sorting'; +import { ArrowLeftIcon } from '@heroicons/react/24/outline'; + +export interface ITaxonomyTaggingProps { + taxonomy: string | null; + value: string; + pages: Page[]; + onDismiss: () => void; +} + +export const TaxonomyTagging: React.FunctionComponent = ({ + taxonomy, + value, + pages, + onDismiss +}: React.PropsWithChildren) => { + const settings = useRecoilValue(SettingsSelector); + + const untaggedPages = React.useMemo(() => { + let untagged: Page[] = []; + + if (!pages || !settings?.contentTypes || !taxonomy) { + return untagged; + } + + for (const page of pages) { + if (taxonomy === 'tags') { + if (!page.fmTags || page.fmTags.indexOf(value) === -1) { + untagged.push(page); + } + continue; + } else if (taxonomy === 'categories') { + if (!page.fmCategories || page.fmCategories.indexOf(value) === -1) { + untagged.push(page); + } + continue; + } else { + const contentType = settings.contentTypes.find((ct) => ct.name === page.fmContentType); + + if (!contentType) { + continue; + } + + let fieldName = getTaxonomyField(taxonomy, contentType); + + if (fieldName && (!page[fieldName] || page[fieldName].indexOf(value) === -1)) { + untagged.push(page); + } + } + } + + untagged = untagged.sort(Sorting.number('fmPublished')).reverse(); + + return untagged; + }, [pages, taxonomy, value]); + + return ( +
+
+

+ + + + {taxonomy}: {value} +

+
+ +
+
    + {untaggedPages.map((page) => ( +
  • + + {page.title} +
  • + ))} +
+
+
+ ); +}; \ No newline at end of file diff --git a/src/dashboardWebView/components/TaxonomyView/TaxonomyView.tsx b/src/dashboardWebView/components/TaxonomyView/TaxonomyView.tsx index 5e64f7bf..28d56245 100644 --- a/src/dashboardWebView/components/TaxonomyView/TaxonomyView.tsx +++ b/src/dashboardWebView/components/TaxonomyView/TaxonomyView.tsx @@ -14,6 +14,7 @@ import { SponsorMsg } from '../Layout/SponsorMsg'; import { TaxonomyManager } from './TaxonomyManager'; import * as l10n from '@vscode/l10n'; import { LocalizationKey } from '../../../localization'; +import { TaxonomyTagging } from './TaxonomyTagging'; export interface ITaxonomyViewProps { pages: Page[]; @@ -25,6 +26,7 @@ export const TaxonomyView: React.FunctionComponent = ({ const settings = useRecoilValue(SettingsSelector); const [taxonomySettings, setTaxonomySettings] = useState(); const [selectedTaxonomy, setSelectedTaxonomy] = useState(`tags`); + const [contentTagging, setContentTagging] = useState(null); const onImport = () => { Messenger.send(DashboardMessage.importTaxonomy); @@ -62,7 +64,10 @@ export const TaxonomyView: React.FunctionComponent = ({ > setSelectedTaxonomy(`tags`)} + onClick={() => { + setSelectedTaxonomy(`tags`); + setContentTagging(null); + }} > {l10n.t(LocalizationKey.dashboardTaxonomyViewTaxonomyViewNavigationItemTags)} @@ -70,7 +75,10 @@ export const TaxonomyView: React.FunctionComponent = ({ setSelectedTaxonomy(`categories`)} + onClick={() => { + setSelectedTaxonomy(`categories`); + setContentTagging(null); + }} > {l10n.t(LocalizationKey.dashboardTaxonomyViewTaxonomyViewNavigationItemCategories)} @@ -81,7 +89,10 @@ export const TaxonomyView: React.FunctionComponent = ({ setSelectedTaxonomy(taxonomy.id)} + onClick={() => { + setSelectedTaxonomy(taxonomy.id); + setContentTagging(null); + }} > {taxonomy.id} @@ -90,10 +101,21 @@ export const TaxonomyView: React.FunctionComponent = ({
- + { + contentTagging ? ( + setContentTagging(null)} /> + ) : ( + setContentTagging(value)} /> + ) + }
From 033baea418d78e6131017b53c8307f6b1b604060 Mon Sep 17 00:00:00 2001 From: Elio Struyf Date: Mon, 15 Jan 2024 22:23:10 +0100 Subject: [PATCH 2/3] Insert taxonomy mappings --- l10n/bundle.l10n.json | 3 + src/dashboardWebView/DashboardMessage.ts | 1 + .../components/Common/Button.tsx | 12 +- .../components/DataView/DataFormControls.tsx | 6 +- .../TaxonomyView/TaxonomyManager.tsx | 42 +++---- .../TaxonomyView/TaxonomyTagging.tsx | 106 +++++++++++++++--- .../components/TaxonomyView/TaxonomyView.tsx | 13 ++- src/dashboardWebView/styles.css | 6 +- src/helpers/TaxonomyHelper.ts | 79 +++++++++---- src/listeners/dashboard/TaxonomyListener.ts | 28 +++++ src/localization/localization.enum.ts | 12 ++ 11 files changed, 222 insertions(+), 86 deletions(-) diff --git a/l10n/bundle.l10n.json b/l10n/bundle.l10n.json index ced9308e..9d35445c 100644 --- a/l10n/bundle.l10n.json +++ b/l10n/bundle.l10n.json @@ -294,6 +294,7 @@ "dashboard.taxonomyView.taxonomyManager.table.heading.action": "Action", "dashboard.taxonomyView.taxonomyManager.table.row.empty": "No {0} found", "dashboard.taxonomyView.taxonomyManager.table.unmapped.title": "Missing in your settings", + "dashboard.taxonomyView.taxonomyManager.filterInput.placeholder": "Filter", "dashboard.taxonomyView.taxonomyView.navigationBar.title": "Select the taxonomy", "dashboard.taxonomyView.taxonomyView.button.import": "Import taxonomy", @@ -661,9 +662,11 @@ "helpers.taxonomyHelper.createNew.input.placeholder": "Enter the value you want to add", "helpers.taxonomyHelper.createNew.input.validate.noValue": "A value must be provided.", "helpers.taxonomyHelper.createNew.input.validate.exists": "The value already exists.", + "helpers.taxonomyHelper.process.insert": "{0}: Renaming \"{1}\" from {2} to {3}.", "helpers.taxonomyHelper.process.edit": "{0}: Renaming \"{1}\" from {2} to {3}.", "helpers.taxonomyHelper.process.merge": "{0}: Merging \"{1}\" from {2} to {3}.", "helpers.taxonomyHelper.process.delete": "{0}: Deleting \"{1}\" from {2}.", + "helpers.taxonomyHelper.process.insert.success": "Insert completed.", "helpers.taxonomyHelper.process.edit.success": "Edit completed.", "helpers.taxonomyHelper.process.merge.success": "Merge completed.", "helpers.taxonomyHelper.process.delete.success": "Deletion completed.", diff --git a/src/dashboardWebView/DashboardMessage.ts b/src/dashboardWebView/DashboardMessage.ts index 9c8a2f7a..335160bc 100644 --- a/src/dashboardWebView/DashboardMessage.ts +++ b/src/dashboardWebView/DashboardMessage.ts @@ -64,6 +64,7 @@ export enum DashboardMessage { createTaxonomy = 'createTaxonomy', importTaxonomy = 'importTaxonomy', moveTaxonomy = 'moveTaxonomy', + mapTaxonomy = 'mapTaxonomy', // Other getTheme = 'getTheme', diff --git a/src/dashboardWebView/components/Common/Button.tsx b/src/dashboardWebView/components/Common/Button.tsx index 140d3f88..68384012 100644 --- a/src/dashboardWebView/components/Common/Button.tsx +++ b/src/dashboardWebView/components/Common/Button.tsx @@ -1,5 +1,4 @@ import * as React from 'react'; -import useThemeColors from '../../hooks/useThemeColors'; export interface IButtonProps { secondary?: boolean; @@ -15,19 +14,14 @@ export const Button: React.FunctionComponent = ({ secondary, children }: React.PropsWithChildren) => { - const { getColors } = useThemeColors(); return (
- +
- + {items && items.length > 0 ? items.map((item, index) => ( - - - - {untaggedPages.map((page) => ( + {untaggedPages && sortedPages && sortedPages.map((page) => (
{l10n.t(LocalizationKey.dashboardTaxonomyViewTaxonomyManagerTableHeadingName)} {l10n.t(LocalizationKey.dashboardTaxonomyViewTaxonomyManagerTableHeadingCount)} {l10n.t(LocalizationKey.dashboardTaxonomyViewTaxonomyManagerTableHeadingAction)}
+ {item} + @@ -226,7 +212,7 @@ export const TaxonomyManager: React.FunctionComponent = ( (unmappedItems.length === 0 && (
{l10n.t(LocalizationKey.dashboardTaxonomyViewTaxonomyManagerTableRowEmpty, taxonomy)} @@ -239,13 +225,13 @@ export const TaxonomyManager: React.FunctionComponent = ( unmappedItems.map((item, index) => (
{item} + diff --git a/src/dashboardWebView/components/TaxonomyView/TaxonomyTagging.tsx b/src/dashboardWebView/components/TaxonomyView/TaxonomyTagging.tsx index eb48ccdc..459eec2f 100644 --- a/src/dashboardWebView/components/TaxonomyView/TaxonomyTagging.tsx +++ b/src/dashboardWebView/components/TaxonomyView/TaxonomyTagging.tsx @@ -5,11 +5,16 @@ import { SettingsSelector } from '../../state'; import { getTaxonomyField } from '../../../helpers/getTaxonomyField'; import { Sorting } from '../../../helpers/Sorting'; import { ArrowLeftIcon } from '@heroicons/react/24/outline'; +import { Button } from '../Common/Button'; +import { VSCodeCheckbox } from '@vscode/webview-ui-toolkit/react'; +import { FilterInput } from './FilterInput'; +import { useDebounce } from '../../../hooks/useDebounce'; export interface ITaxonomyTaggingProps { taxonomy: string | null; value: string; pages: Page[]; + onContentMapping: (value: string, pages: Page[]) => void; onDismiss: () => void; } @@ -17,9 +22,28 @@ export const TaxonomyTagging: React.FunctionComponent = ( taxonomy, value, pages, - onDismiss + onContentMapping, + onDismiss, }: React.PropsWithChildren) => { const settings = useRecoilValue(SettingsSelector); + const [selectedPages, setSelectedPages] = React.useState([]); + const [filterValue, setFilterValue] = React.useState(''); + const debounceFilterValue = useDebounce(filterValue, 500); + + const onCheckboxClick = React.useCallback((page: Page) => { + const pageIdx = selectedPages.findIndex((p: Page) => p.fmFilePath === page.fmFilePath); + if (pageIdx === -1) { + setSelectedPages([...selectedPages, page]); + } else { + const newSelectedPages = [...selectedPages]; + newSelectedPages.splice(pageIdx, 1); + setSelectedPages(newSelectedPages); + } + }, [selectedPages]); + + const checkIfChecked = React.useCallback((page: Page) => { + return selectedPages.find((p: Page) => p.fmFilePath === page.fmFilePath); + }, [selectedPages]); const untaggedPages = React.useMemo(() => { let untagged: Page[] = []; @@ -56,32 +80,80 @@ export const TaxonomyTagging: React.FunctionComponent = ( untagged = untagged.sort(Sorting.number('fmPublished')).reverse(); + if (debounceFilterValue) { + return untagged.filter((p) => p.title.toLowerCase().includes(debounceFilterValue.toLowerCase())); + } + return untagged; - }, [pages, taxonomy, value]); + }, [pages, taxonomy, value, debounceFilterValue]); + + console.log(`Selected pages`, selectedPages) return (
-

- - - - {taxonomy}: {value} -

+

+ Map your content with: {value} +

+
+
+ setFilterValue(value)} + onReset={() => setFilterValue('')} /> +
-
-
    - {untaggedPages.map((page) => ( -
  • - - {page.title} -
  • - ))} -
+
+ + + + + + + + + {untaggedPages.map((page) => ( + + + + + ))} + +
+ + + Name +
+ onCheckboxClick(page)} + checked={checkIfChecked(page)}> + Tag page + + + +
+
+ +
+ +
); diff --git a/src/dashboardWebView/components/TaxonomyView/TaxonomyView.tsx b/src/dashboardWebView/components/TaxonomyView/TaxonomyView.tsx index 28d56245..2ec6bb4d 100644 --- a/src/dashboardWebView/components/TaxonomyView/TaxonomyView.tsx +++ b/src/dashboardWebView/components/TaxonomyView/TaxonomyView.tsx @@ -1,4 +1,4 @@ -import { Messenger } from '@estruyf/vscode/dist/client'; +import { Messenger, messageHandler } from '@estruyf/vscode/dist/client'; import { ChevronRightIcon, ArrowDownTrayIcon } from '@heroicons/react/24/outline'; import * as React from 'react'; import { useEffect, useState } from 'react'; @@ -32,6 +32,16 @@ export const TaxonomyView: React.FunctionComponent = ({ Messenger.send(DashboardMessage.importTaxonomy); }; + const onContentMapping = React.useCallback((value: string, pages: Page[]) => { + messageHandler.request(DashboardMessage.mapTaxonomy, { + taxonomy: selectedTaxonomy, + value, + pages + }).then(() => { + setContentTagging(null); + }); + }, [selectedTaxonomy]); + useEffect(() => { setTaxonomySettings({ tags: settings?.tags || [], @@ -107,6 +117,7 @@ export const TaxonomyView: React.FunctionComponent = ({ value={contentTagging} taxonomy={selectedTaxonomy} pages={pages} + onContentMapping={(value: string, pages: Page[]) => onContentMapping(value, pages)} onDismiss={() => setContentTagging(null)} /> ) : ( 0) { + const spaces = window.activeTextEditor?.options?.tabSize; + + if (type === 'insert' && newValue) { + if (taxonomies && taxonomies.length > 0) { + taxonomies.push(newValue); + } else { + taxonomies = [newValue]; + } + + const newTaxValue = [...new Set(taxonomies)].sort(); + ContentType.setFieldValue(data, fieldNames, newTaxValue); + + // Update the file + await writeFileAsync( + parseWinPath(filePath), + FrontMatterParser.toFile(article.content, article.data, mdFile, { + indent: spaces || 2 + } as DumpOptions as any), + { encoding: 'utf8' } + ); + } else if (type !== 'insert' && taxonomies && taxonomies.length > 0) { const idx = taxonomies.findIndex((o) => o === oldValue); if (idx !== -1) { @@ -355,7 +385,6 @@ export class TaxonomyHelper { const newTaxValue = [...new Set(taxonomies)].sort(); ContentType.setFieldValue(data, fieldNames, newTaxValue); - const spaces = window.activeTextEditor?.options?.tabSize; // Update the file await writeFileAsync( parseWinPath(file.fsPath), @@ -375,7 +404,9 @@ export class TaxonomyHelper { await this.addToSettings(taxonomyType, oldValue, newValue); - if (type === 'edit') { + if (type === 'insert') { + Notifications.info(l10n.t(LocalizationKey.helpersTaxonomyHelperProcessInsertSuccess)); + } else if (type === 'edit') { Notifications.info(l10n.t(LocalizationKey.helpersTaxonomyHelperProcessEditSuccess)); } else if (type === 'merge') { Notifications.info(l10n.t(LocalizationKey.helpersTaxonomyHelperProcessMergeSuccess)); @@ -502,6 +533,21 @@ export class TaxonomyHelper { ); } + /** + * Retrieve the taxonomy type based from the string + * @param taxonomyType + * @returns + */ + public static getTypeFromString(taxonomyType: string): TaxonomyType | string { + if (taxonomyType === 'tags') { + return TaxonomyType.Tag; + } else if (taxonomyType === 'categories') { + return TaxonomyType.Category; + } else { + return taxonomyType; + } + } + /** * Retrieve the fields for the taxonomy field * @returns @@ -587,19 +633,4 @@ export class TaxonomyHelper { return options; } - - /** - * Retrieve the taxonomy type based from the string - * @param taxonomyType - * @returns - */ - private static getTypeFromString(taxonomyType: string): TaxonomyType | string { - if (taxonomyType === 'tags') { - return TaxonomyType.Tag; - } else if (taxonomyType === 'categories') { - return TaxonomyType.Category; - } else { - return taxonomyType; - } - } } diff --git a/src/listeners/dashboard/TaxonomyListener.ts b/src/listeners/dashboard/TaxonomyListener.ts index 5df365a7..ca59e365 100644 --- a/src/listeners/dashboard/TaxonomyListener.ts +++ b/src/listeners/dashboard/TaxonomyListener.ts @@ -5,6 +5,7 @@ import { DashboardMessage } from '../../dashboardWebView/DashboardMessage'; import { TaxonomyHelper } from '../../helpers'; import { PostMessageData } from '../../models'; import { BaseListener } from './BaseListener'; +import { Page } from '../../dashboardWebView/models'; export class TaxonomyListener extends BaseListener { /** @@ -39,9 +40,36 @@ export class TaxonomyListener extends BaseListener { case DashboardMessage.importTaxonomy: commands.executeCommand(COMMAND_NAME.exportTaxonomy); break; + case DashboardMessage.mapTaxonomy: + TaxonomyListener.mapTaxonomy(msg); + break; } } + private static async mapTaxonomy({ + command, + requestId, + payload: { taxonomy, value, pages } + }: { + command: string; + requestId?: string; + payload: { taxonomy: string; value: string; pages: Page[] }; + }) { + if (!command || !requestId || !taxonomy || !value || !pages) { + return; + } + + await TaxonomyHelper.process( + 'insert', + TaxonomyHelper.getTypeFromString(taxonomy), + '', + value, + pages + ); + + this.sendRequest(command as any, requestId, {}); + } + private static getData() { // Retrieve the tags, categories and custom taxonomy const taxonomyData = TaxonomyHelper.getAll(); diff --git a/src/localization/localization.enum.ts b/src/localization/localization.enum.ts index f6d4d2a6..2d4fc0d8 100644 --- a/src/localization/localization.enum.ts +++ b/src/localization/localization.enum.ts @@ -975,6 +975,10 @@ export enum LocalizationKey { * Missing in your settings */ dashboardTaxonomyViewTaxonomyManagerTableUnmappedTitle = 'dashboard.taxonomyView.taxonomyManager.table.unmapped.title', + /** + * Filter + */ + dashboardTaxonomyViewTaxonomyManagerFilterInputPlaceholder = 'dashboard.taxonomyView.taxonomyManager.filterInput.placeholder', /** * Select the taxonomy */ @@ -2172,6 +2176,10 @@ export enum LocalizationKey { * The value already exists. */ helpersTaxonomyHelperCreateNewInputValidateExists = 'helpers.taxonomyHelper.createNew.input.validate.exists', + /** + * {0}: Renaming "{1}" from {2} to {3}. + */ + helpersTaxonomyHelperProcessInsert = 'helpers.taxonomyHelper.process.insert', /** * {0}: Renaming "{1}" from {2} to {3}. */ @@ -2184,6 +2192,10 @@ export enum LocalizationKey { * {0}: Deleting "{1}" from {2}. */ helpersTaxonomyHelperProcessDelete = 'helpers.taxonomyHelper.process.delete', + /** + * Insert completed. + */ + helpersTaxonomyHelperProcessInsertSuccess = 'helpers.taxonomyHelper.process.insert.success', /** * Edit completed. */ From 60c8dd6185cf541c15aae78c2233377d9f3e9ed7 Mon Sep 17 00:00:00 2001 From: Elio Struyf Date: Tue, 16 Jan 2024 14:22:17 +0100 Subject: [PATCH 3/3] Multiple taxonomy management #731 --- CHANGELOG.md | 2 + l10n/bundle.l10n.json | 8 +- package.json | 6 + src/commands/Dashboard.ts | 3 +- src/constants/context.ts | 3 +- src/dashboardWebView/DashboardMessage.ts | 1 + .../Configuration/Astro/AstroContentTypes.tsx | 1 - .../components/Header/Sorting.tsx | 51 ++++---- .../TaxonomyView/TaxonomyActions.tsx | 4 +- .../TaxonomyView/TaxonomyTagging.tsx | 117 +++++++++++++----- .../components/TaxonomyView/TaxonomyView.tsx | 10 +- src/dashboardWebView/hooks/usePages.tsx | 66 ++++------ src/dashboardWebView/models/PageMappings.ts | 6 + src/dashboardWebView/models/index.ts | 1 + src/extension.ts | 7 +- src/helpers/DashboardSettings.ts | 16 +++ src/helpers/TaxonomyHelper.ts | 67 +++++----- src/listeners/dashboard/ExtensionListener.ts | 16 +++ src/listeners/dashboard/TaxonomyListener.ts | 42 +++++-- src/localization/localization.enum.ts | 22 +++- src/utils/sortPages.ts | 38 ++++++ 21 files changed, 333 insertions(+), 154 deletions(-) create mode 100644 src/dashboardWebView/models/PageMappings.ts create mode 100644 src/utils/sortPages.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a2f8d9a..39f0ecbb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ ### ✨ New features +- [#731](https://github.com/estruyf/vscode-front-matter/issues/731): Added the ability to map/unmap taxonomy to multiple pages at once + ### 🎨 Enhancements - [#727](https://github.com/estruyf/vscode-front-matter/pull/727): Updated Japanese translations thanks to [mayumihara](https://github.com/mayumih387) diff --git a/l10n/bundle.l10n.json b/l10n/bundle.l10n.json index 9d35445c..d3dfda6f 100644 --- a/l10n/bundle.l10n.json +++ b/l10n/bundle.l10n.json @@ -3,6 +3,7 @@ "common.edit": "Edit", "common.delete": "Delete", "common.cancel": "Cancel", + "common.apply": "Apply", "common.clear": "Clear", "common.clear.value": "Clear value", "common.search": "Search", @@ -20,6 +21,7 @@ "common.slug": "Slug", "common.support": "Support", "common.remove.value": "Remove {0}", + "common.filter": "Back", "common.filter.value": "Filter by {0}", "common.error.message": "Sorry, something went wrong.", "common.openOnWebsite": "Open on website", @@ -32,6 +34,7 @@ "common.yes": "yes", "common.no": "no", "common.openSettings": "Open settings", + "common.back": "Back", "notifications.outputChannel.link": "output window", "notifications.outputChannel.description": "Check the {0} for more details.", @@ -296,6 +299,9 @@ "dashboard.taxonomyView.taxonomyManager.table.unmapped.title": "Missing in your settings", "dashboard.taxonomyView.taxonomyManager.filterInput.placeholder": "Filter", + "dashboard.taxonomyView.taxonomyTagging.pageTitle": "Map your content with: {0}", + "dashboard.taxonomyView.taxonomyTagging.checkbox": "Tag page with {0}", + "dashboard.taxonomyView.taxonomyView.navigationBar.title": "Select the taxonomy", "dashboard.taxonomyView.taxonomyView.button.import": "Import taxonomy", "dashboard.taxonomyView.taxonomyView.navigationItem.tags": "Tags", @@ -662,7 +668,7 @@ "helpers.taxonomyHelper.createNew.input.placeholder": "Enter the value you want to add", "helpers.taxonomyHelper.createNew.input.validate.noValue": "A value must be provided.", "helpers.taxonomyHelper.createNew.input.validate.exists": "The value already exists.", - "helpers.taxonomyHelper.process.insert": "{0}: Renaming \"{1}\" from {2} to {3}.", + "helpers.taxonomyHelper.process.insert": "{0}: Inserting \"{1}\" to your selected pages.", "helpers.taxonomyHelper.process.edit": "{0}: Renaming \"{1}\" from {2} to {3}.", "helpers.taxonomyHelper.process.merge": "{0}: Merging \"{1}\" from {2} to {3}.", "helpers.taxonomyHelper.process.delete": "{0}: Deleting \"{1}\" from {2}.", diff --git a/package.json b/package.json index b56558ba..8957161e 100644 --- a/package.json +++ b/package.json @@ -2180,6 +2180,12 @@ } ], "menus": { + "webview/context": [ + { + "command": "workbench.action.webview.openDeveloperTools", + "when": "frontMatter:isDevelopment" + } + ], "editor/title": [ { "command": "frontMatter.markup.heading", diff --git a/src/commands/Dashboard.ts b/src/commands/Dashboard.ts index a1286500..7b5001bb 100644 --- a/src/commands/Dashboard.ts +++ b/src/commands/Dashboard.ts @@ -7,7 +7,7 @@ import { } from '../constants'; import { join } from 'path'; import { commands, Uri, ViewColumn, Webview, WebviewPanel, window } from 'vscode'; -import { Logger, Settings as SettingsHelper } from '../helpers'; +import { DashboardSettings, Logger, Settings as SettingsHelper } from '../helpers'; import { DashboardCommand } from '../dashboardWebView/DashboardCommand'; import { Extension } from '../helpers/Extension'; import { WebviewHelper } from '@estruyf/vscode'; @@ -160,6 +160,7 @@ export class Dashboard { Dashboard.isDisposed = true; Dashboard._viewData = undefined; PanelMediaListener.getMediaSelection(); + DashboardSettings.updateAfterClose(); await commands.executeCommand('setContext', CONTEXT.isDashboardOpen, false); }); diff --git a/src/constants/context.ts b/src/constants/context.ts index 3fc67cda..9b68b2aa 100644 --- a/src/constants/context.ts +++ b/src/constants/context.ts @@ -6,6 +6,7 @@ export const CONTEXT = { wysiwyg: 'frontMatter:markdown:wysiwyg', backer: 'frontMatter:backers:supporter', isValidFile: 'frontMatter:file:isValid', + isDevelopment: 'frontMatter:isDevelopment', hasViewModes: 'frontMatter:has:modes', @@ -14,5 +15,5 @@ export const CONTEXT = { isGitEnabled: 'frontMatter:git:enabled', - projectSwitchEnabled: 'frontMatter:project:switch:enabled', + projectSwitchEnabled: 'frontMatter:project:switch:enabled' }; diff --git a/src/dashboardWebView/DashboardMessage.ts b/src/dashboardWebView/DashboardMessage.ts index 335160bc..ab53e96d 100644 --- a/src/dashboardWebView/DashboardMessage.ts +++ b/src/dashboardWebView/DashboardMessage.ts @@ -70,6 +70,7 @@ export enum DashboardMessage { getTheme = 'getTheme', updateSetting = 'updateSetting', setState = 'setState', + getState = 'getState', runCustomScript = 'runCustomScript', sendTelemetry = 'sendTelemetry', logError = 'logError', diff --git a/src/dashboardWebView/components/Configuration/Astro/AstroContentTypes.tsx b/src/dashboardWebView/components/Configuration/Astro/AstroContentTypes.tsx index a40c4652..75bf52a2 100644 --- a/src/dashboardWebView/components/Configuration/Astro/AstroContentTypes.tsx +++ b/src/dashboardWebView/components/Configuration/Astro/AstroContentTypes.tsx @@ -32,7 +32,6 @@ export const AstroContentTypes: React.FunctionComponent if (collections.length > 0 && settings?.contentTypes?.length > 0) { // Find created content types from the collections const astroCollection = collections.find(c => settings.contentTypes.find((ct) => ct.name === c.name)); - console.log(`astroCollection`, astroCollection) if (astroCollection) { setStatus(Status.Completed); } else { diff --git a/src/dashboardWebView/components/Header/Sorting.tsx b/src/dashboardWebView/components/Header/Sorting.tsx index b1eb0df6..9249fe13 100644 --- a/src/dashboardWebView/components/Header/Sorting.tsx +++ b/src/dashboardWebView/components/Header/Sorting.tsx @@ -1,4 +1,4 @@ -import { Messenger } from '@estruyf/vscode/dist/client'; +import { Messenger, messageHandler } from '@estruyf/vscode/dist/client'; import { Menu } from '@headlessui/react'; import * as React from 'react'; import { useRecoilState, useRecoilValue } from 'recoil'; @@ -26,6 +26,7 @@ export const Sorting: React.FunctionComponent = ({ const [crntSorting, setCrntSorting] = useRecoilState(SortingAtom); const searchValue = useRecoilValue(SearchSelector); const settings = useRecoilValue(SettingsSelector); + const [crntSort, setCrntSort] = React.useState(null); const sortOptions: SortingOption[] = [ { @@ -143,29 +144,37 @@ export const Sorting: React.FunctionComponent = ({ ]; } - let crntSortingOption = crntSorting; - if (!crntSortingOption) { - if (view === NavigationType.Contents) { - crntSortingOption = settings?.dashboardState?.contents?.sorting || null; - } else if (view === NavigationType.Media) { - crntSortingOption = settings?.dashboardState?.media?.sorting || null; - } + React.useEffect(() => { + const getSorting = async () => { + let crntSortingOption = crntSorting; + if (!crntSortingOption) { + const sortingState = await messageHandler.request<{ key: string; value: SortingOption; }>(DashboardMessage.getState, { key: view === NavigationType.Media ? ExtensionState.Dashboard.Media.Sorting : ExtensionState.Dashboard.Contents.Sorting }); + crntSortingOption = sortingState?.value || null; - if (crntSortingOption === null) { - if (view === NavigationType.Contents && settings?.dashboardState.contents.defaultSorting) { - crntSortingOption = - allOptions.find((f) => f.id === settings?.dashboardState.contents.defaultSorting) || null; - } else if ( - view === NavigationType.Media && - settings?.dashboardState.contents.defaultSorting - ) { - crntSortingOption = - allOptions.find((f) => f.id === settings?.dashboardState.contents.defaultSorting) || null; + if (crntSortingOption === null) { + if (view === NavigationType.Contents && settings?.dashboardState.contents.defaultSorting) { + crntSortingOption = + allOptions.find((f) => f.id === settings?.dashboardState.contents.defaultSorting) || null; + } else if ( + view === NavigationType.Media && + settings?.dashboardState.media.defaultSorting + ) { + crntSortingOption = + allOptions.find((f) => f.id === settings?.dashboardState.media.defaultSorting) || null; + } + } } - } - } - let crntSort = allOptions.find((x) => x.id === crntSortingOption?.id) || sortOptions[0]; + let sort = allOptions.find((x) => x.id === crntSortingOption?.id) || sortOptions[0]; + setCrntSort(sort); + }; + + getSorting(); + }, [crntSorting]); + + if (crntSort === null) { + return null; + } return (
diff --git a/src/dashboardWebView/components/TaxonomyView/TaxonomyActions.tsx b/src/dashboardWebView/components/TaxonomyView/TaxonomyActions.tsx index 261c05f5..fe8e4897 100644 --- a/src/dashboardWebView/components/TaxonomyView/TaxonomyActions.tsx +++ b/src/dashboardWebView/components/TaxonomyView/TaxonomyActions.tsx @@ -3,7 +3,7 @@ import { ArrowUpCircleIcon, PencilIcon, PlusIcon, - SquaresPlusIcon, + TagIcon, TrashIcon, } from '@heroicons/react/24/outline'; import * as React from 'react'; @@ -83,7 +83,7 @@ export const TaxonomyActions: React.FunctionComponent = ( - + {l10n.t(LocalizationKey.commonEdit)} diff --git a/src/dashboardWebView/components/TaxonomyView/TaxonomyTagging.tsx b/src/dashboardWebView/components/TaxonomyView/TaxonomyTagging.tsx index 459eec2f..7fff6ee1 100644 --- a/src/dashboardWebView/components/TaxonomyView/TaxonomyTagging.tsx +++ b/src/dashboardWebView/components/TaxonomyView/TaxonomyTagging.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; -import { Page } from '../../models'; +import { Page, PageMappings, SortingOption } from '../../models'; import { useRecoilValue } from 'recoil'; -import { SettingsSelector } from '../../state'; +import { SettingsSelector, SortingAtom } from '../../state'; import { getTaxonomyField } from '../../../helpers/getTaxonomyField'; import { Sorting } from '../../../helpers/Sorting'; import { ArrowLeftIcon } from '@heroicons/react/24/outline'; @@ -9,12 +9,18 @@ import { Button } from '../Common/Button'; import { VSCodeCheckbox } from '@vscode/webview-ui-toolkit/react'; import { FilterInput } from './FilterInput'; import { useDebounce } from '../../../hooks/useDebounce'; +import * as l10n from '@vscode/l10n'; +import { LocalizationKey } from '../../../localization'; +import { sortPages } from '../../../utils/sortPages'; +import { messageHandler } from '@estruyf/vscode/dist/client'; +import { DashboardMessage } from '../../DashboardMessage'; +import { ExtensionState } from '../../../constants'; export interface ITaxonomyTaggingProps { taxonomy: string | null; value: string; pages: Page[]; - onContentMapping: (value: string, pages: Page[]) => void; + onContentMapping: (value: string, pageMappings: PageMappings) => void; onDismiss: () => void; } @@ -26,33 +32,25 @@ export const TaxonomyTagging: React.FunctionComponent = ( onDismiss, }: React.PropsWithChildren) => { const settings = useRecoilValue(SettingsSelector); - const [selectedPages, setSelectedPages] = React.useState([]); + const [sortedPages, setSortedPages] = React.useState([]); + const [pageMappings, setPageMappings] = React.useState<{ + tagged: Page[]; + untagged: Page[]; + }>({ + tagged: [], + untagged: [] + }); const [filterValue, setFilterValue] = React.useState(''); const debounceFilterValue = useDebounce(filterValue, 500); - const onCheckboxClick = React.useCallback((page: Page) => { - const pageIdx = selectedPages.findIndex((p: Page) => p.fmFilePath === page.fmFilePath); - if (pageIdx === -1) { - setSelectedPages([...selectedPages, page]); - } else { - const newSelectedPages = [...selectedPages]; - newSelectedPages.splice(pageIdx, 1); - setSelectedPages(newSelectedPages); - } - }, [selectedPages]); - - const checkIfChecked = React.useCallback((page: Page) => { - return selectedPages.find((p: Page) => p.fmFilePath === page.fmFilePath); - }, [selectedPages]); - const untaggedPages = React.useMemo(() => { let untagged: Page[] = []; - if (!pages || !settings?.contentTypes || !taxonomy) { + if (!sortedPages || !settings?.contentTypes || !taxonomy) { return untagged; } - for (const page of pages) { + for (const page of sortedPages) { if (taxonomy === 'tags') { if (!page.fmTags || page.fmTags.indexOf(value) === -1) { untagged.push(page); @@ -85,25 +83,74 @@ export const TaxonomyTagging: React.FunctionComponent = ( } return untagged; - }, [pages, taxonomy, value, debounceFilterValue]); + }, [sortedPages, taxonomy, value, debounceFilterValue]); - console.log(`Selected pages`, selectedPages) + const onCheckboxClick = React.useCallback((page: Page) => { + const untaggedPage = untaggedPages.find((p: Page) => p.fmFilePath === page.fmFilePath); + + const clonedPageMappings = Object.assign({}, pageMappings); + const taggedIdx = pageMappings.tagged.findIndex((p: Page) => p.fmFilePath === page.fmFilePath); + const untaggedIdx = pageMappings.untagged.findIndex((p: Page) => p.fmFilePath === page.fmFilePath); + + if (untaggedPage) { + // Page was not yet tagged + if (taggedIdx === -1) { + clonedPageMappings.tagged.push(page); + + if (untaggedIdx !== -1) { + clonedPageMappings.untagged.splice(untaggedIdx, 1); + } + } else { + clonedPageMappings.tagged.splice(taggedIdx, 1); + + if (untaggedIdx === -1) { + clonedPageMappings.untagged.push(page); + } + } + } else { + // Page was already tagged, so only the untagged array needs to be updated + if (untaggedIdx === -1) { + clonedPageMappings.untagged.push(page); + } else { + clonedPageMappings.untagged.splice(taggedIdx, 1); + } + } + + setPageMappings(clonedPageMappings); + }, [pageMappings, untaggedPages]); + + const checkIfChecked = React.useCallback((page: Page) => { + return (!untaggedPages.find((p: Page) => p.fmFilePath === page.fmFilePath) && !pageMappings.untagged.find((p: Page) => p.fmFilePath === page.fmFilePath)) || pageMappings.tagged.find((p: Page) => p.fmFilePath === page.fmFilePath); + }, [untaggedPages, pageMappings.tagged]); + + React.useEffect(() => { + messageHandler.request<{ key: string; value: SortingOption; }>(DashboardMessage.getState, { + key: ExtensionState.Dashboard.Contents.Sorting + }).then(({ key, value }) => { + if (key === ExtensionState.Dashboard.Contents.Sorting && value) { + const sorted = sortPages(pages, value) + setSortedPages(sorted); + } else { + setSortedPages(pages); + } + }); + }, [pages]); return (
-

- Map your content with: {value} + {l10n.t(LocalizationKey.dashboardTaxonomyViewTaxonomyTaggingPageTitle, value)}

setFilterValue(value)} onReset={() => setFilterValue('')} /> @@ -124,23 +171,27 @@ export const TaxonomyTagging: React.FunctionComponent = ( scope="col" className={`pr-6 py-3 text-left text-xs font-medium uppercase text-[var(--frontmatter-secondary-text)]`} > - Name + {l10n.t(LocalizationKey.commonTitle)}
onCheckboxClick(page)} checked={checkIfChecked(page)}> - Tag page + + {l10n.t(LocalizationKey.dashboardTaxonomyViewTaxonomyTaggingCheckbox, value)} + @@ -152,8 +203,8 @@ export const TaxonomyTagging: React.FunctionComponent = (
- - + +
); diff --git a/src/dashboardWebView/components/TaxonomyView/TaxonomyView.tsx b/src/dashboardWebView/components/TaxonomyView/TaxonomyView.tsx index 2ec6bb4d..b92671a4 100644 --- a/src/dashboardWebView/components/TaxonomyView/TaxonomyView.tsx +++ b/src/dashboardWebView/components/TaxonomyView/TaxonomyView.tsx @@ -6,7 +6,7 @@ import { useRecoilValue } from 'recoil'; import { TelemetryEvent } from '../../../constants'; import { TaxonomyData } from '../../../models'; import { DashboardMessage } from '../../DashboardMessage'; -import { Page } from '../../models'; +import { Page, PageMappings } from '../../models'; import { SettingsSelector } from '../../state'; import { NavigationBar, NavigationItem } from '../Layout'; import { PageLayout } from '../Layout/PageLayout'; @@ -32,13 +32,15 @@ export const TaxonomyView: React.FunctionComponent = ({ Messenger.send(DashboardMessage.importTaxonomy); }; - const onContentMapping = React.useCallback((value: string, pages: Page[]) => { + const onContentMapping = React.useCallback((value: string, pageMappings: PageMappings) => { messageHandler.request(DashboardMessage.mapTaxonomy, { taxonomy: selectedTaxonomy, value, - pages + pageMappings }).then(() => { setContentTagging(null); + }).catch(() => { + setContentTagging(null); }); }, [selectedTaxonomy]); @@ -117,7 +119,7 @@ export const TaxonomyView: React.FunctionComponent = ({ value={contentTagging} taxonomy={selectedTaxonomy} pages={pages} - onContentMapping={(value: string, pages: Page[]) => onContentMapping(value, pages)} + onContentMapping={(value: string, pageMappings: PageMappings) => onContentMapping(value, pageMappings)} onDismiss={() => setContentTagging(null)} /> ) : ( { + (searchedPages: Page[]) => { const framework = settings?.crntFramework; // Filter the pages @@ -67,35 +68,7 @@ export default function usePages(pages: Page[]) { // Sort the pages let pagesSorted: Page[] = Object.assign([], pagesToShow); if (!search) { - if (sorting && sorting.id === SortOption.FileNameAsc) { - pagesSorted = pagesSorted.sort(Sorting.alphabetically('fmFileName')); - } else if (sorting && sorting.id === SortOption.FileNameDesc) { - pagesSorted = pagesSorted.sort(Sorting.alphabetically('fmFileName')).reverse(); - } else if (sorting && sorting.id === SortOption.PublishedAsc) { - pagesSorted = pagesSorted.sort(Sorting.number('fmPublished')); - } else if (sorting && sorting.id === SortOption.LastModifiedAsc) { - pagesSorted = pagesSorted.sort(Sorting.number('fmModified')); - } else if (sorting && sorting.id === SortOption.PublishedDesc) { - pagesSorted = pagesSorted.sort(Sorting.number('fmPublished')).reverse(); - } else if (sorting && sorting.id === SortOption.LastModifiedDesc) { - pagesSorted = pagesSorted.sort(Sorting.number('fmModified')).reverse(); - } else if (sorting && sorting.id && sorting.name) { - const { order, name, type } = sorting; - - if (type === SortType.string) { - pagesSorted = pagesSorted.sort(Sorting.alphabetically(name)); - } else if (type === SortType.date) { - pagesSorted = pagesSorted.sort(Sorting.date(name)); - } else if (type === SortType.number) { - pagesSorted = pagesSorted.sort(Sorting.number(name)); - } - - if (order === SortOrder.desc) { - pagesSorted = pagesSorted.reverse(); - } - } else { - pagesSorted = pagesSorted.sort(Sorting.number('fmModified')).reverse(); - } + pagesSorted = sortPages(pagesSorted, sorting); } if (folder) { @@ -208,20 +181,27 @@ export default function usePages(pages: Page[]) { useEffect(() => { let usedSorting = sorting; - if (!usedSorting) { - const lastSort = settings?.dashboardState.contents.sorting; - if (lastSort) { - setSorting(lastSort); - return; + const startPageProcessing = () => { + // Check if search needs to be performed + let searchedPages = pages; + if (search) { + Messenger.send(DashboardMessage.searchPages, { query: search }); + } else { + processPages(searchedPages); } } - // Check if search needs to be performed - let searchedPages = pages; - if (search) { - Messenger.send(DashboardMessage.searchPages, { query: search }); + if (!usedSorting) { + messageHandler.request<{ key: string; value: SortingOption; }>(DashboardMessage.getState, { + key: ExtensionState.Dashboard.Contents.Sorting + }).then(({ key, value }) => { + if (key === ExtensionState.Dashboard.Contents.Sorting && value) { + setSorting(value); + return; + } + }); } else { - processPages(searchedPages); + startPageProcessing(); } }, [settings?.draftField, pages, sorting, search, tag, category, folder]); diff --git a/src/dashboardWebView/models/PageMappings.ts b/src/dashboardWebView/models/PageMappings.ts new file mode 100644 index 00000000..8c15023a --- /dev/null +++ b/src/dashboardWebView/models/PageMappings.ts @@ -0,0 +1,6 @@ +import { Page } from './Page'; + +export interface PageMappings { + tagged: Page[]; + untagged: Page[]; +} diff --git a/src/dashboardWebView/models/index.ts b/src/dashboardWebView/models/index.ts index 06a24ac9..d64691fa 100644 --- a/src/dashboardWebView/models/index.ts +++ b/src/dashboardWebView/models/index.ts @@ -1,6 +1,7 @@ export * from './DashboardViewType'; export * from './NavigationType'; export * from './Page'; +export * from './PageMappings'; export * from './Settings'; export * from './SortingOption'; export * from './Status'; diff --git a/src/extension.ts b/src/extension.ts index f88b54d7..80c3b6a2 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,6 +1,6 @@ import { GitListener } from './listeners/general/GitListener'; import * as vscode from 'vscode'; -import { COMMAND_NAME, EXTENSION_NAME, TelemetryEvent } from './constants'; +import { COMMAND_NAME, CONTEXT, EXTENSION_NAME, TelemetryEvent } from './constants'; import { MarkdownFoldingProvider } from './providers/MarkdownFoldingProvider'; import { TagType } from './panelWebView/TagType'; import { PanelProvider } from './panelWebView/PanelProvider'; @@ -46,6 +46,11 @@ export async function activate(context: vscode.ExtensionContext) { const { subscriptions, extensionUri, extensionPath } = context; const extension = Extension.getInstance(context); + // Set development context + if (!Extension.getInstance().isProductionMode) { + vscode.commands.executeCommand('setContext', CONTEXT.isDevelopment, true); + } + Backers.init(context).then(() => {}); // Make sure the EN language file is loaded diff --git a/src/helpers/DashboardSettings.ts b/src/helpers/DashboardSettings.ts index 64e1d617..fc277e2c 100644 --- a/src/helpers/DashboardSettings.ts +++ b/src/helpers/DashboardSettings.ts @@ -59,6 +59,22 @@ export class DashboardSettings { return this.cachedSettings; } + public static async updateAfterClose() { + if (this.cachedSettings) { + this.cachedSettings = await this.getSettings(); + + const ext = Extension.getInstance(); + + // Update states + this.cachedSettings.dashboardState.contents.sorting = await ext.getState< + SortingOption | undefined + >(ExtensionState.Dashboard.Contents.Sorting, 'workspace'); + this.cachedSettings.dashboardState.media.sorting = await ext.getState< + SortingOption | undefined + >(ExtensionState.Dashboard.Media.Sorting, 'workspace'); + } + } + public static async getSettings() { const ext = Extension.getInstance(); const wsFolder = Folders.getWorkspaceFolder(); diff --git a/src/helpers/TaxonomyHelper.ts b/src/helpers/TaxonomyHelper.ts index 28a97b7f..b8dc5ad9 100644 --- a/src/helpers/TaxonomyHelper.ts +++ b/src/helpers/TaxonomyHelper.ts @@ -25,6 +25,7 @@ import { SettingsListener as DashboardSettingsListener } from '../listeners/dash import * as l10n from '@vscode/l10n'; import { LocalizationKey } from '../localization'; import { Page } from '../dashboardWebView/models'; +import { Logger } from './Logger'; export class TaxonomyHelper { private static db: JsonDB; @@ -255,22 +256,26 @@ export class TaxonomyHelper { } /** - * Process the taxonomy changes - * @param type - * @param taxonomyType - * @param oldValue - * @param newValue - * @returns + * Processes the taxonomy changes based on the specified type. + * @param type - The type of taxonomy change ('insert', 'edit', 'merge', 'delete'). + * @param taxonomyType - The type of taxonomy ('Tag', 'Category', or custom taxonomy name). + * @param oldValue - The old value of the taxonomy. + * @param newValue - The new value of the taxonomy (optional). + * @param pages - The array of Page objects (optional). + * @param needsSettingsUpdate - Indicates whether the settings need to be updated (default: true). */ public static async process( type: 'insert' | 'edit' | 'merge' | 'delete', taxonomyType: TaxonomyType | string, oldValue: string, newValue?: string, - pages?: Page[] + pages?: Page[], + needsSettingsUpdate: boolean = true ) { // Retrieve all the markdown files - const allFiles = type === 'insert' ? pages : await FilesHelper.getAllFiles(); + const allFiles = pages + ? pages?.map((p) => ({ fsPath: p.fmFilePath })) + : await FilesHelper.getAllFiles(); if (!allFiles) { return; } @@ -290,7 +295,6 @@ export class TaxonomyHelper { progressText = l10n.t( LocalizationKey.helpersTaxonomyHelperProcessInsert, EXTENSION_NAME, - taxonomyName, newValue || '' ); } else if (type === 'edit') { @@ -333,8 +337,7 @@ export class TaxonomyHelper { for (const file of allFiles) { progress.report({ increment: ++i / progressNr }); - const filePath = type === 'insert' ? (file as Page).fmFilePath : file.fsPath; - const mdFile = await readFileAsync(parseWinPath(filePath), { + const mdFile = await readFileAsync(parseWinPath(file.fsPath), { encoding: 'utf8' }); @@ -352,8 +355,7 @@ export class TaxonomyHelper { taxonomies = taxonomies.split(`,`); } - const spaces = window.activeTextEditor?.options?.tabSize; - + let needsFileUpdate = false; if (type === 'insert' && newValue) { if (taxonomies && taxonomies.length > 0) { taxonomies.push(newValue); @@ -363,15 +365,7 @@ export class TaxonomyHelper { const newTaxValue = [...new Set(taxonomies)].sort(); ContentType.setFieldValue(data, fieldNames, newTaxValue); - - // Update the file - await writeFileAsync( - parseWinPath(filePath), - FrontMatterParser.toFile(article.content, article.data, mdFile, { - indent: spaces || 2 - } as DumpOptions as any), - { encoding: 'utf8' } - ); + needsFileUpdate = true; } else if (type !== 'insert' && taxonomies && taxonomies.length > 0) { const idx = taxonomies.findIndex((o) => o === oldValue); @@ -384,25 +378,32 @@ export class TaxonomyHelper { const newTaxValue = [...new Set(taxonomies)].sort(); ContentType.setFieldValue(data, fieldNames, newTaxValue); - - // Update the file - await writeFileAsync( - parseWinPath(file.fsPath), - FrontMatterParser.toFile(article.content, article.data, mdFile, { - indent: spaces || 2 - } as DumpOptions as any), - { encoding: 'utf8' } - ); + needsFileUpdate = true; } } + + // Update the file when needed + if (needsFileUpdate) { + const spaces = window.activeTextEditor?.options?.tabSize; + + await writeFileAsync( + parseWinPath(file.fsPath), + FrontMatterParser.toFile(article.content, article.data, mdFile, { + indent: spaces || 2 + } as DumpOptions as any), + { encoding: 'utf8' } + ); + } } } catch (e) { - // Continue with the next file + Logger.error(`Failed to ${type} taxonomy value in ${file.fsPath}`); } } } - await this.addToSettings(taxonomyType, oldValue, newValue); + if (needsSettingsUpdate) { + await this.addToSettings(taxonomyType, oldValue, newValue); + } if (type === 'insert') { Notifications.info(l10n.t(LocalizationKey.helpersTaxonomyHelperProcessInsertSuccess)); diff --git a/src/listeners/dashboard/ExtensionListener.ts b/src/listeners/dashboard/ExtensionListener.ts index af7b5eb5..9a594d1a 100644 --- a/src/listeners/dashboard/ExtensionListener.ts +++ b/src/listeners/dashboard/ExtensionListener.ts @@ -27,6 +27,9 @@ export class ExtensionListener extends BaseListener { case DashboardMessage.setState: this.setState(msg?.payload); break; + case DashboardMessage.getState: + this.getState(msg.command, msg?.payload, msg.requestId); + break; } } @@ -36,4 +39,17 @@ export class ExtensionListener extends BaseListener { Extension.getInstance().setState(key, value, 'workspace'); } } + + private static async getState(command: string, data: any, requestId?: string) { + if (!command || !requestId || !data) { + return; + } + + const { key } = data; + if (key) { + const value = await Extension.getInstance().getState(key, 'workspace'); + + this.sendRequest(command as any, requestId, { key, value }); + } + } } diff --git a/src/listeners/dashboard/TaxonomyListener.ts b/src/listeners/dashboard/TaxonomyListener.ts index ca59e365..e566293e 100644 --- a/src/listeners/dashboard/TaxonomyListener.ts +++ b/src/listeners/dashboard/TaxonomyListener.ts @@ -5,7 +5,7 @@ import { DashboardMessage } from '../../dashboardWebView/DashboardMessage'; import { TaxonomyHelper } from '../../helpers'; import { PostMessageData } from '../../models'; import { BaseListener } from './BaseListener'; -import { Page } from '../../dashboardWebView/models'; +import { Page, PageMappings } from '../../dashboardWebView/models'; export class TaxonomyListener extends BaseListener { /** @@ -49,25 +49,43 @@ export class TaxonomyListener extends BaseListener { private static async mapTaxonomy({ command, requestId, - payload: { taxonomy, value, pages } + payload: { taxonomy, value, pageMappings } }: { command: string; requestId?: string; - payload: { taxonomy: string; value: string; pages: Page[] }; + payload: { taxonomy: string; value: string; pageMappings: PageMappings }; }) { - if (!command || !requestId || !taxonomy || !value || !pages) { + if (!command || !requestId || !taxonomy || !value || !pageMappings) { return; } - await TaxonomyHelper.process( - 'insert', - TaxonomyHelper.getTypeFromString(taxonomy), - '', - value, - pages - ); + try { + if (pageMappings.tagged.length > 0) { + await TaxonomyHelper.process( + 'insert', + TaxonomyHelper.getTypeFromString(taxonomy), + '', + value, + pageMappings.tagged, + false + ); + } - this.sendRequest(command as any, requestId, {}); + if (pageMappings.untagged.length > 0) { + await TaxonomyHelper.process( + 'delete', + TaxonomyHelper.getTypeFromString(taxonomy), + value, + '', + pageMappings.untagged, + false + ); + } + + this.sendRequest(command as any, requestId, {}); + } catch (e) { + this.sendError(command as any, requestId, e); + } } private static getData() { diff --git a/src/localization/localization.enum.ts b/src/localization/localization.enum.ts index 2d4fc0d8..c8062bd1 100644 --- a/src/localization/localization.enum.ts +++ b/src/localization/localization.enum.ts @@ -15,6 +15,10 @@ export enum LocalizationKey { * Cancel */ commonCancel = 'common.cancel', + /** + * Apply + */ + commonApply = 'common.apply', /** * Clear */ @@ -83,6 +87,10 @@ export enum LocalizationKey { * Remove {0} */ commonRemoveValue = 'common.remove.value', + /** + * Back + */ + commonFilter = 'common.filter', /** * Filter by {0} */ @@ -131,6 +139,10 @@ export enum LocalizationKey { * Open settings */ commonOpenSettings = 'common.openSettings', + /** + * Back + */ + commonBack = 'common.back', /** * output window */ @@ -979,6 +991,14 @@ export enum LocalizationKey { * Filter */ dashboardTaxonomyViewTaxonomyManagerFilterInputPlaceholder = 'dashboard.taxonomyView.taxonomyManager.filterInput.placeholder', + /** + * Map your content with: {0} + */ + dashboardTaxonomyViewTaxonomyTaggingPageTitle = 'dashboard.taxonomyView.taxonomyTagging.pageTitle', + /** + * Tag page with {0} + */ + dashboardTaxonomyViewTaxonomyTaggingCheckbox = 'dashboard.taxonomyView.taxonomyTagging.checkbox', /** * Select the taxonomy */ @@ -2177,7 +2197,7 @@ export enum LocalizationKey { */ helpersTaxonomyHelperCreateNewInputValidateExists = 'helpers.taxonomyHelper.createNew.input.validate.exists', /** - * {0}: Renaming "{1}" from {2} to {3}. + * {0}: Inserting "{1}" to your selected pages. */ helpersTaxonomyHelperProcessInsert = 'helpers.taxonomyHelper.process.insert', /** diff --git a/src/utils/sortPages.ts b/src/utils/sortPages.ts new file mode 100644 index 00000000..81d1a2b3 --- /dev/null +++ b/src/utils/sortPages.ts @@ -0,0 +1,38 @@ +import { SortOption } from '../dashboardWebView/constants/SortOption'; +import { Page, SortingOption } from '../dashboardWebView/models'; +import { Sorting } from '../helpers/Sorting'; +import { SortOrder, SortType } from '../models'; + +export const sortPages = (pages: Page[], sorting: SortingOption | null) => { + if (sorting && sorting.id === SortOption.FileNameAsc) { + pages = pages.sort(Sorting.alphabetically('fmFileName')); + } else if (sorting && sorting.id === SortOption.FileNameDesc) { + pages = pages.sort(Sorting.alphabetically('fmFileName')).reverse(); + } else if (sorting && sorting.id === SortOption.PublishedAsc) { + pages = pages.sort(Sorting.number('fmPublished')); + } else if (sorting && sorting.id === SortOption.LastModifiedAsc) { + pages = pages.sort(Sorting.number('fmModified')); + } else if (sorting && sorting.id === SortOption.PublishedDesc) { + pages = pages.sort(Sorting.number('fmPublished')).reverse(); + } else if (sorting && sorting.id === SortOption.LastModifiedDesc) { + pages = pages.sort(Sorting.number('fmModified')).reverse(); + } else if (sorting && sorting.id && sorting.name) { + const { order, name, type } = sorting; + + if (type === SortType.string) { + pages = pages.sort(Sorting.alphabetically(name)); + } else if (type === SortType.date) { + pages = pages.sort(Sorting.date(name)); + } else if (type === SortType.number) { + pages = pages.sort(Sorting.number(name)); + } + + if (order === SortOrder.desc) { + pages = pages.reverse(); + } + } else { + pages = pages.sort(Sorting.number('fmModified')).reverse(); + } + + return pages; +};