mirror of
https://github.com/estruyf/vscode-front-matter.git
synced 2026-06-27 13:32:14 +02:00
Changes to the taxonomy dashboard
This commit is contained in:
@@ -259,7 +259,6 @@ export class Folders {
|
||||
});
|
||||
} catch (error) {
|
||||
// Skip the file
|
||||
console.log((error as Error).message)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { TaxonomyHelper } from './../helpers/TaxonomyHelper';
|
||||
import * as vscode from 'vscode';
|
||||
import * as fs from 'fs';
|
||||
import { TaxonomyType } from "../models";
|
||||
import { SETTING_TAXONOMY_TAGS, SETTING_TAXONOMY_CATEGORIES, EXTENSION_NAME } from '../constants';
|
||||
import { ArticleHelper, Settings as SettingsHelper, FilesHelper } from '../helpers';
|
||||
import { FrontMatterParser } from '../parsers';
|
||||
import { DumpOptions } from 'js-yaml';
|
||||
import { Notifications } from '../helpers/Notifications';
|
||||
|
||||
export class Settings {
|
||||
@@ -76,7 +75,7 @@ export class Settings {
|
||||
*/
|
||||
public static async export() {
|
||||
// Retrieve all the Markdown files
|
||||
const allMdFiles = await FilesHelper.getMdFiles();
|
||||
const allMdFiles = await FilesHelper.getAllFiles();
|
||||
if (!allMdFiles) {
|
||||
return;
|
||||
}
|
||||
@@ -157,6 +156,7 @@ export class Settings {
|
||||
canPickMany: false,
|
||||
ignoreFocusOut: true
|
||||
});
|
||||
|
||||
if (!taxType) {
|
||||
return;
|
||||
}
|
||||
@@ -196,76 +196,10 @@ export class Settings {
|
||||
}
|
||||
}
|
||||
|
||||
// Retrieve all the markdown files
|
||||
const allMdFiles = await FilesHelper.getMdFiles();
|
||||
if (!allMdFiles) {
|
||||
return;
|
||||
if (newOptionValue) {
|
||||
TaxonomyHelper.process("edit", type, selectedOption, newOptionValue);
|
||||
} else {
|
||||
TaxonomyHelper.process("delete", type, selectedOption, undefined);
|
||||
}
|
||||
|
||||
let progressText = `${EXTENSION_NAME}: Remapping "${selectedOption}" ${type === TaxonomyType.Tag ? "tag" : "category"} to "${newOptionValue}".`;
|
||||
if (!newOptionValue) {
|
||||
progressText = `${EXTENSION_NAME}: Deleting "${selectedOption}" ${type === TaxonomyType.Tag ? "tag" : "category"}.`;
|
||||
}
|
||||
vscode.window.withProgress({
|
||||
location: vscode.ProgressLocation.Notification,
|
||||
title: progressText,
|
||||
cancellable: false
|
||||
}, async (progress) => {
|
||||
// Set the initial progress
|
||||
const progressNr = allMdFiles.length/100;
|
||||
progress.report({ increment: 0});
|
||||
|
||||
const matterProp: string = type === TaxonomyType.Tag ? "tags" : "categories";
|
||||
|
||||
let i = 0;
|
||||
for (const file of allMdFiles) {
|
||||
progress.report({ increment: (++i/progressNr) });
|
||||
const mdFile = fs.readFileSync(file.path, { encoding: "utf8" });
|
||||
if (mdFile) {
|
||||
try {
|
||||
const article = FrontMatterParser.fromFile(mdFile);
|
||||
if (article && article.data) {
|
||||
const { data } = article;
|
||||
let taxonomies: string[] = data[matterProp];
|
||||
if (taxonomies && taxonomies.length > 0) {
|
||||
const idx = taxonomies.findIndex(o => o === selectedOption);
|
||||
if (idx !== -1) {
|
||||
if (newOptionValue) {
|
||||
taxonomies[idx] = newOptionValue;
|
||||
} else {
|
||||
taxonomies = taxonomies.filter(o => o !== selectedOption);
|
||||
}
|
||||
data[matterProp] = [...new Set(taxonomies)].sort();
|
||||
const spaces = vscode.window.activeTextEditor?.options?.tabSize;
|
||||
// Update the file
|
||||
fs.writeFileSync(file.path, FrontMatterParser.toFile(article.content, article.data, mdFile, {
|
||||
indent: spaces || 2
|
||||
} as DumpOptions as any), { encoding: "utf8" });
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// Continue with the next file
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update the settings
|
||||
const idx = options.findIndex(o => o === selectedOption);
|
||||
if (newOptionValue) {
|
||||
// Add or update the new option
|
||||
if (idx !== -1) {
|
||||
options[idx] = newOptionValue;
|
||||
} else {
|
||||
options.push(newOptionValue);
|
||||
}
|
||||
} else {
|
||||
// Remove the selected option
|
||||
options = options.filter(o => o !== selectedOption);
|
||||
}
|
||||
await SettingsHelper.updateTaxonomy(type, options);
|
||||
|
||||
Notifications.info(`${newOptionValue ? "Remapping" : "Deleation"} of the ${selectedOption} ${type === TaxonomyType.Tag ? "tag" : "category"} completed.`);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import * as React from 'react';
|
||||
|
||||
export interface IMergeIconProps {
|
||||
className: string;
|
||||
}
|
||||
|
||||
export const MergeIcon: React.FunctionComponent<IMergeIconProps> = ({className}: React.PropsWithChildren<IMergeIconProps>) => {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" className={className}>
|
||||
<path xmlns="http://www.w3.org/2000/svg" d="M7.586 8.00366L4 8.00366C3.44772 8.00366 3 7.55595 3 7.00366C3 6.45138 3.44772 6.00366 4 6.00366L8 6.00366C8.26509 6.00366 8.51933 6.10892 8.70685 6.2963L13.414 11H18.5845L15.2931 7.71103C14.9025 7.32065 14.9023 6.68748 15.2926 6.29681C15.683 5.90615 16.3162 5.90592 16.7068 6.2963L21.7068 11.2926C21.8945 11.4802 22 11.7346 22 11.9998C22 12.2651 21.8947 12.5195 21.7071 12.7071L16.7071 17.7071C16.3166 18.0976 15.6834 18.0976 15.2929 17.7071C14.9024 17.3166 14.9024 16.6834 15.2929 16.2929L18.5858 13H13.4142L8.70711 17.7071C8.51957 17.8947 8.26522 18 8 18H4C3.44772 18 3 17.5523 3 17C3 16.4477 3.44772 16 4 16H7.58579L11.5855 12.0003L7.586 8.00366Z" fill="currentcolor"/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
@@ -43,6 +43,12 @@ export enum DashboardMessage {
|
||||
|
||||
// Taxonomy dashboard
|
||||
getTaxonomyData = 'getTaxonomyData',
|
||||
editTaxonomy = "editTaxonomy",
|
||||
mergeTaxonomy = "mergeTaxonomy",
|
||||
deleteTaxonomy = "deleteTaxonomy",
|
||||
addToTaxonomy = "addToTaxonomy",
|
||||
createTaxonomy = "createTaxonomy",
|
||||
importTaxonomy = "importTaxonomy",
|
||||
|
||||
// Other
|
||||
getTheme = 'getTheme',
|
||||
|
||||
@@ -30,7 +30,13 @@ export const Item: React.FunctionComponent<IItemProps> = ({ fmFilePath, date, ti
|
||||
}
|
||||
|
||||
const tagField = settings.dashboardState.contents.tags;
|
||||
return pageData[tagField] || [];
|
||||
const tagsValue = pageData[tagField] || [];
|
||||
|
||||
if (Array.isArray(tagsValue)) {
|
||||
return tagsValue;
|
||||
}
|
||||
|
||||
return [tagsValue];
|
||||
}, [settings, pageData]);
|
||||
|
||||
if (view === DashboardViewType.Grid) {
|
||||
|
||||
@@ -20,6 +20,7 @@ import { ToastContainer, toast, Slide } from 'react-toastify';
|
||||
import 'react-toastify/dist/ReactToastify.css';
|
||||
import { DataType } from '../../../models/DataType';
|
||||
import { TelemetryEvent } from '../../../constants';
|
||||
import { NavigationItem } from '../Layout';
|
||||
|
||||
export interface IDataViewProps {}
|
||||
|
||||
@@ -150,14 +151,13 @@ export const DataView: React.FunctionComponent<IDataViewProps> = (props: React.P
|
||||
{
|
||||
(dataFiles && dataFiles.length > 0) && (
|
||||
dataFiles.map((dataFile, idx) => (
|
||||
<button
|
||||
<NavigationItem
|
||||
key={`${dataFile.id}-${idx}`}
|
||||
type='button'
|
||||
className={`px-4 py-2 flex items-center text-sm font-medium w-full text-left hover:bg-gray-200 dark:hover:bg-vulcan-400 hover:text-vulcan-500 dark:hover:text-whisper-500 ${selectedData?.id === dataFile.id ? 'bg-gray-300 dark:bg-vulcan-300 text-vulcan-500 dark:text-whisper-500' : 'text-gray-500 dark:text-whisper-900'}`}
|
||||
isSelected={selectedData?.id === dataFile.id}
|
||||
onClick={() => setSchema(dataFile)}>
|
||||
<ChevronRightIcon className='-ml-1 w-5 mr-2' />
|
||||
<span>{dataFile.title}</span>
|
||||
</button>
|
||||
</NavigationItem>
|
||||
)
|
||||
))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import * as React from 'react';
|
||||
|
||||
export interface INavigationBarProps {
|
||||
title?: string;
|
||||
bottom?: JSX.Element;
|
||||
}
|
||||
|
||||
export const NavigationBar: React.FunctionComponent<INavigationBarProps> = ({title, bottom, children}: React.PropsWithChildren<INavigationBarProps>) => {
|
||||
return (
|
||||
<aside className={`w-2/12 px-4 py-6 h-full flex flex-col flex-grow border-r border-gray-200 dark:border-vulcan-300`}>
|
||||
{
|
||||
title && <h2 className={`text-lg text-gray-500 dark:text-whisper-900`}>{title}</h2>
|
||||
}
|
||||
|
||||
<nav className={`flex-1 py-4 -mx-4 h-full`}>
|
||||
<div className={`divide-y divide-gray-200 dark:divide-vulcan-300 border-t border-b border-gray-200 dark:border-vulcan-300`}>
|
||||
<div>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
{
|
||||
bottom && bottom
|
||||
}
|
||||
</aside>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
import * as React from 'react';
|
||||
|
||||
export interface INavigationItemProps {
|
||||
isSelected?: boolean;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
export const NavigationItem: React.FunctionComponent<INavigationItemProps> = ({isSelected, onClick, children}: React.PropsWithChildren<INavigationItemProps>) => {
|
||||
return (
|
||||
<button
|
||||
type='button'
|
||||
className={`px-4 py-2 flex items-center text-sm font-medium w-full text-left hover:bg-gray-200 dark:hover:bg-vulcan-400 hover:text-vulcan-500 dark:hover:text-whisper-500 cursor-pointer ${isSelected ? 'bg-gray-300 dark:bg-vulcan-300 text-vulcan-500 dark:text-whisper-500' : 'text-gray-500 dark:text-whisper-900'}`}
|
||||
onClick={onClick}>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
@@ -5,11 +5,12 @@ import { Header } from '../Header';
|
||||
|
||||
export interface IPageLayoutProps {
|
||||
header?: React.ReactNode;
|
||||
folders?: string[] | undefined
|
||||
totalPages?: number | undefined
|
||||
folders?: string[] | undefined;
|
||||
totalPages?: number | undefined;
|
||||
contentClass?: string;
|
||||
}
|
||||
|
||||
export const PageLayout: React.FunctionComponent<IPageLayoutProps> = ({ header, folders, totalPages, children }: React.PropsWithChildren<IPageLayoutProps>) => {
|
||||
export const PageLayout: React.FunctionComponent<IPageLayoutProps> = ({ header, folders, totalPages, contentClass, children }: React.PropsWithChildren<IPageLayoutProps>) => {
|
||||
const settings = useRecoilValue(SettingsSelector);
|
||||
|
||||
return (
|
||||
@@ -20,7 +21,7 @@ export const PageLayout: React.FunctionComponent<IPageLayoutProps> = ({ header,
|
||||
totalPages={totalPages}
|
||||
settings={settings} />
|
||||
|
||||
<div className="w-full flex justify-between flex-col flex-grow max-w-7xl mx-auto pt-6 px-4">
|
||||
<div className={contentClass || "w-full flex justify-between flex-col flex-grow max-w-7xl mx-auto pt-6 px-4"}>
|
||||
{ children }
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from './NavigationBar';
|
||||
export * from './NavigationItem';
|
||||
export * from './PageLayout';
|
||||
@@ -83,7 +83,7 @@ export const Snippets: React.FunctionComponent<ISnippetsProps> = (props: React.P
|
||||
</FeatureFlag>
|
||||
)}>
|
||||
|
||||
<div className="flex flex-col">
|
||||
<div className="flex flex-col h-full">
|
||||
{
|
||||
viewData?.data?.filePath && (
|
||||
<div className={`text-xl text-center mb-6`}>
|
||||
|
||||
@@ -12,7 +12,7 @@ export interface ISponsorMsgProps {
|
||||
export const SponsorMsg: React.FunctionComponent<ISponsorMsgProps> = ({beta, isBacker, version}: React.PropsWithChildren<ISponsorMsgProps>) => {
|
||||
|
||||
return (
|
||||
<p className={`bg-gray-100 dark:bg-vulcan-500 w-full px-4 text-vulcan-50 dark:text-whisper-900 py-2 text-center space-x-8 flex items-center border-t border-gray-200 dark:border-vulcan-300 ${isBacker ? 'justify-center' : 'justify-between'}`}>
|
||||
<footer className={`bg-gray-100 dark:bg-vulcan-500 w-full px-4 text-vulcan-50 dark:text-whisper-900 py-2 text-center space-x-8 flex items-center border-t border-gray-200 dark:border-vulcan-300 ${isBacker ? 'justify-center' : 'justify-between'}`}>
|
||||
{
|
||||
isBacker ? (
|
||||
<span>Front Matter{version ? ` (v${version.installedVersion}${!!beta ? ` BETA` : ''})` : ''}</span>
|
||||
@@ -28,6 +28,6 @@ export const SponsorMsg: React.FunctionComponent<ISponsorMsgProps> = ({beta, isB
|
||||
</>
|
||||
)
|
||||
}
|
||||
</p>
|
||||
</footer>
|
||||
);
|
||||
};
|
||||
@@ -33,6 +33,7 @@ const Folder = ({ wsFolder, folder, folders, addFolder }: { wsFolder: string, fo
|
||||
|
||||
export const StepsToGetStarted: React.FunctionComponent<IStepsToGetStartedProps> = ({settings}: React.PropsWithChildren<IStepsToGetStartedProps>) => {
|
||||
const [framework, setFramework] = useState<string | null>(null);
|
||||
const [taxImported, setTaxImported] = useState<boolean>(false);
|
||||
|
||||
const frameworks: Framework[] = FrameworkDetectors.map((detector: any) => detector.framework);
|
||||
|
||||
@@ -55,6 +56,11 @@ export const StepsToGetStarted: React.FunctionComponent<IStepsToGetStartedProps>
|
||||
Messenger.send(DashboardMessage.reload);
|
||||
};
|
||||
|
||||
const importTaxonomy = () => {
|
||||
Messenger.send(DashboardMessage.importTaxonomy);
|
||||
setTaxImported(true);
|
||||
}
|
||||
|
||||
const steps = [
|
||||
{
|
||||
name: 'Initialize project',
|
||||
@@ -136,6 +142,12 @@ export const StepsToGetStarted: React.FunctionComponent<IStepsToGetStartedProps>
|
||||
),
|
||||
status: settings.contentFolders && settings.contentFolders.length > 0 ? Status.Completed : Status.NotStarted
|
||||
},
|
||||
{
|
||||
name: 'Import all tags and categories (optional)',
|
||||
description: <>Now that Front Matter knows all the content folders. Would you like to import all tags and categories from the available content?</>,
|
||||
status: taxImported ? Status.Completed : Status.NotStarted,
|
||||
onClick: settings.contentFolders && settings.contentFolders.length > 0 ? importTaxonomy : undefined
|
||||
},
|
||||
{
|
||||
name: 'Show the dashboard',
|
||||
description: <>Once all actions are completed, the dashboard can be loaded.</>,
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
import { Messenger } from '@estruyf/vscode/dist/client';
|
||||
import { PencilIcon, PlusIcon, TrashIcon } from '@heroicons/react/outline';
|
||||
import * as React from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import { MergeIcon } from '../../../components/icons/MergeIcon';
|
||||
import { DashboardMessage } from '../../DashboardMessage';
|
||||
|
||||
export interface ITaxonomyActionsProps {
|
||||
field: string | null;
|
||||
value: string;
|
||||
unmapped?: boolean;
|
||||
}
|
||||
|
||||
export const TaxonomyActions: React.FunctionComponent<ITaxonomyActionsProps> = ({field, value, unmapped}: React.PropsWithChildren<ITaxonomyActionsProps>) => {
|
||||
|
||||
const onEdit = useCallback(() => {
|
||||
Messenger.send(DashboardMessage.editTaxonomy, {
|
||||
type: field,
|
||||
value
|
||||
});
|
||||
}, [field, value]);
|
||||
|
||||
const onAdd = useCallback(() => {
|
||||
Messenger.send(DashboardMessage.addToTaxonomy, {
|
||||
type: field,
|
||||
value
|
||||
});
|
||||
}, [field, value]);
|
||||
|
||||
const onMerge = useCallback(() => {
|
||||
Messenger.send(DashboardMessage.mergeTaxonomy, {
|
||||
type: field,
|
||||
value
|
||||
});
|
||||
}, [field, value]);
|
||||
|
||||
const onDelete = useCallback(() => {
|
||||
Messenger.send(DashboardMessage.deleteTaxonomy, {
|
||||
type: field,
|
||||
value
|
||||
});
|
||||
}, [field, value]);
|
||||
|
||||
return (
|
||||
<div className={`space-x-2`}>
|
||||
{
|
||||
unmapped && (
|
||||
<button
|
||||
className='text-gray-500 hover:text-vulcan-600 dark:text-gray-800 dark:hover:text-whisper-600'
|
||||
type={`button`}
|
||||
title={`Add ${value} to taxonomy settings`}
|
||||
onClick={onAdd}>
|
||||
<PlusIcon className={`w-4 h-4`} aria-hidden={true} />
|
||||
<span className='sr-only'>Add to settings</span>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
<button
|
||||
className='text-gray-500 hover:text-vulcan-600 dark:text-gray-800 dark:hover:text-whisper-600'
|
||||
type={`button`}
|
||||
title={`Edit ${value}`}
|
||||
onClick={onEdit}>
|
||||
<PencilIcon className={`w-4 h-4`} aria-hidden={true} />
|
||||
<span className='sr-only'>Edit</span>
|
||||
</button>
|
||||
<button
|
||||
className='text-gray-500 hover:text-vulcan-600 dark:text-gray-800 dark:hover:text-whisper-600'
|
||||
type={`button`}
|
||||
title={`Merge ${value}`}
|
||||
onClick={onMerge}>
|
||||
<MergeIcon className={`w-4 h-4`} aria-hidden={true} />
|
||||
<span className='sr-only'>Merge</span>
|
||||
</button>
|
||||
<button
|
||||
className='text-gray-500 hover:text-vulcan-600 dark:text-gray-800 dark:hover:text-whisper-600'
|
||||
type={`button`}
|
||||
title={`Delete ${value}`}
|
||||
onClick={onDelete}>
|
||||
<TrashIcon className={`w-4 h-4`} aria-hidden={true} />
|
||||
<span className='sr-only'>Delete</span>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,40 @@
|
||||
import * as React from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import { Page } from '../../models';
|
||||
import { SettingsSelector } from '../../state';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { getTaxonomyField } from '../../utils';
|
||||
|
||||
export interface ITaxonomyLookupProps {
|
||||
taxonomy: string | null;
|
||||
value: string;
|
||||
pages: Page[];
|
||||
}
|
||||
|
||||
export const TaxonomyLookup: React.FunctionComponent<ITaxonomyLookupProps> = ({ taxonomy, value, pages }: React.PropsWithChildren<ITaxonomyLookupProps>) => {
|
||||
const settings = useRecoilValue(SettingsSelector);
|
||||
|
||||
const total = useMemo(() => {
|
||||
if (!taxonomy || !value || !pages || !settings?.contentTypes) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return pages.filter(page => {
|
||||
const contentType = settings.contentTypes.find(ct => ct.name === page.fmContentType);
|
||||
|
||||
if (!contentType) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let fieldName = getTaxonomyField(taxonomy, contentType);
|
||||
|
||||
return fieldName && page[fieldName] ? page[fieldName].includes(value) : false;
|
||||
}).length;
|
||||
}, [taxonomy, value, pages, settings?.contentTypes]);
|
||||
|
||||
return (
|
||||
<span className={``}>
|
||||
{total}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,179 @@
|
||||
import { Messenger } from '@estruyf/vscode/dist/client';
|
||||
import { ExclamationIcon, PlusSmIcon, TagIcon } from '@heroicons/react/outline';
|
||||
import * as React from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { TaxonomyData } from '../../../models';
|
||||
import { DashboardMessage } from '../../DashboardMessage';
|
||||
import { Page } from '../../models';
|
||||
import { SettingsSelector } from '../../state';
|
||||
import { getTaxonomyField } from '../../utils';
|
||||
import { TaxonomyActions } from './TaxonomyActions';
|
||||
import { TaxonomyLookup } from './TaxonomyLookup';
|
||||
|
||||
export interface ITaxonomyManagerProps {
|
||||
data: TaxonomyData | undefined;
|
||||
taxonomy: string | null;
|
||||
pages: Page[];
|
||||
}
|
||||
|
||||
export const TaxonomyManager: React.FunctionComponent<ITaxonomyManagerProps> = ({ data, taxonomy, pages }: React.PropsWithChildren<ITaxonomyManagerProps>) => {
|
||||
const settings = useRecoilValue(SettingsSelector);
|
||||
|
||||
const onCreate = () => {
|
||||
Messenger.send(DashboardMessage.createTaxonomy, {
|
||||
type: taxonomy
|
||||
});
|
||||
};
|
||||
|
||||
const items = useMemo(() => {
|
||||
if (data && taxonomy) {
|
||||
let crntItems: string[] = [];
|
||||
|
||||
if (taxonomy === "tags" || taxonomy === "categories") {
|
||||
crntItems = data[taxonomy];
|
||||
} else {
|
||||
crntItems = data.customTaxonomy.find(c => c.id === taxonomy)?.options || [];
|
||||
}
|
||||
|
||||
// Alphabetically sort the items
|
||||
crntItems = Object.assign([], crntItems).sort((a: string, b: string) => {
|
||||
if (a.toLowerCase() < b.toLowerCase()) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (a.toLowerCase() > b.toLowerCase()) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
});
|
||||
|
||||
return crntItems;
|
||||
}
|
||||
|
||||
return [];
|
||||
}, [data, taxonomy]);
|
||||
|
||||
const unmappedItems = useMemo(() => {
|
||||
let unmapped: string[] = [];
|
||||
|
||||
if (!pages || !settings?.contentTypes || !taxonomy) {
|
||||
return unmapped;
|
||||
}
|
||||
|
||||
for (const page of pages) {
|
||||
const contentType = settings.contentTypes.find(ct => ct.name === page.fmContentType);
|
||||
|
||||
if (!contentType) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let fieldName = getTaxonomyField(taxonomy, contentType);
|
||||
|
||||
if (fieldName && page[fieldName]) {
|
||||
const values = page[fieldName];
|
||||
|
||||
for (const value of values) {
|
||||
if (!items.includes(value)) {
|
||||
unmapped.push(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [...new Set(unmapped)];
|
||||
}, [items, taxonomy, pages, settings?.contentTypes]);
|
||||
|
||||
return (
|
||||
<div className={`py-6 px-4`}>
|
||||
<div className={`flex w-full justify-between`}>
|
||||
<div>
|
||||
<h2 className={`text-lg text-gray-500 dark:text-whisper-900 first-letter:uppercase`}>{taxonomy}</h2>
|
||||
<p className={`mt-2 text-sm text-gray-500 dark:text-whisper-900 first-letter:uppercase`}>Create, edit, and manage the {taxonomy} of your site</p>
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
className={`inline-flex items-center px-3 py-1 border border-transparent text-xs leading-4 font-medium text-white dark:text-vulcan-500 bg-teal-600 hover:bg-teal-700 focus:outline-none disabled:bg-gray-500`}
|
||||
title={`Create a new ${taxonomy} value`}
|
||||
onClick={onCreate}>
|
||||
<PlusSmIcon className={`mr-2 h-6 w-6`} />
|
||||
<span className={`text-sm`}>Create a new {taxonomy} value</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 flex flex-col">
|
||||
<div className="-m-1.5 overflow-x-auto">
|
||||
<div className="p-1.5 min-w-full inline-block align-middle">
|
||||
<div className="overflow-hidden">
|
||||
<table className="min-w-full divide-y divide-gray-200 dark:divide-vulcan-300">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-whisper-900 uppercase">Name</th>
|
||||
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-whisper-900 uppercase">Times used</th>
|
||||
<th scope="col" className="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-whisper-900 uppercase">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-200 dark:divide-vulcan-300">
|
||||
{
|
||||
items && items.length > 0 ?
|
||||
items.map((item, index) => (
|
||||
<tr key={index}>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-800 dark:text-gray-200">
|
||||
<TagIcon className="inline-block h-4 w-4 mr-2" />
|
||||
<span>{item}</span>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-800 dark:text-gray-200">
|
||||
<TaxonomyLookup
|
||||
taxonomy={taxonomy}
|
||||
value={item}
|
||||
pages={pages} />
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
||||
<TaxonomyActions
|
||||
field={taxonomy}
|
||||
value={item} />
|
||||
</td>
|
||||
</tr>
|
||||
)) : (
|
||||
!unmappedItems || unmappedItems.length === 0 && (
|
||||
<tr>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-800 dark:text-gray-200" colSpan={4}>No {taxonomy} found</td>
|
||||
</tr>
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
unmappedItems && unmappedItems.length > 0 &&
|
||||
unmappedItems.map((item, index) => (
|
||||
<tr key={index}>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-800 dark:text-gray-200" title='Missing in your settings'>
|
||||
<ExclamationIcon className="inline-block h-4 w-4 mr-2" />
|
||||
<span>{item}</span>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-800 dark:text-gray-200">
|
||||
<TaxonomyLookup
|
||||
taxonomy={taxonomy}
|
||||
value={item}
|
||||
pages={pages} />
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
||||
<TaxonomyActions
|
||||
field={taxonomy}
|
||||
value={item}
|
||||
unmapped />
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,44 +1,97 @@
|
||||
import { EventData } from '@estruyf/vscode';
|
||||
import { Messenger } from '@estruyf/vscode/dist/client';
|
||||
import { ChevronRightIcon, DownloadIcon } from '@heroicons/react/outline';
|
||||
import * as React from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { TelemetryEvent } from '../../../constants';
|
||||
import { DashboardCommand } from '../../DashboardCommand';
|
||||
import { TaxonomyData } from '../../../models';
|
||||
import { DashboardMessage } from '../../DashboardMessage';
|
||||
import { Page } from '../../models';
|
||||
import { SettingsSelector } from '../../state';
|
||||
import { NavigationBar, NavigationItem } from '../Layout';
|
||||
import { PageLayout } from '../Layout/PageLayout';
|
||||
import { SponsorMsg } from '../SponsorMsg';
|
||||
import { TaxonomyManager } from './TaxonomyManager';
|
||||
|
||||
export interface ITaxonomyViewProps {
|
||||
pages: Page[];
|
||||
}
|
||||
|
||||
export const TaxonomyView: React.FunctionComponent<ITaxonomyViewProps> = ({ pages }: React.PropsWithChildren<ITaxonomyViewProps>) => {
|
||||
|
||||
const messageListener = (message: MessageEvent<EventData<any>>) => {
|
||||
const { command, data } = message.data;
|
||||
const settings = useRecoilValue(SettingsSelector);
|
||||
const [ taxonomySettings, setTaxonomySettings ] = useState<TaxonomyData>();
|
||||
const [ selectedTaxonomy, setSelectedTaxonomy ] = useState<string | null>(`tags`);
|
||||
|
||||
if (command === DashboardCommand.setTaxonomyData) {
|
||||
console.log('TaxonomyView: setTaxonomyData', data);
|
||||
}
|
||||
const onImport = () => {
|
||||
Messenger.send(DashboardMessage.importTaxonomy);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setTaxonomySettings({
|
||||
tags: settings?.tags || [],
|
||||
categories: settings?.categories || [],
|
||||
customTaxonomy: settings?.customTaxonomy || [],
|
||||
});
|
||||
}, [settings?.tags, settings?.categories, settings?.customTaxonomy]);
|
||||
|
||||
useEffect(() => {
|
||||
Messenger.send(DashboardMessage.sendTelemetry, {
|
||||
event: TelemetryEvent.webviewTaxonomyDashboard
|
||||
});
|
||||
|
||||
Messenger.send(DashboardMessage.getTaxonomyData);
|
||||
|
||||
Messenger.listen(messageListener);
|
||||
|
||||
return () => {
|
||||
Messenger.unlisten(messageListener);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<PageLayout>
|
||||
{pages.length}
|
||||
<PageLayout
|
||||
contentClass={`relative w-full flex-grow flex flex-col mx-auto overflow-hidden`}>
|
||||
|
||||
<div className={`h-full w-full flex`}>
|
||||
<NavigationBar
|
||||
title='Select the taxonomy'
|
||||
bottom={(
|
||||
<button
|
||||
className={`-mb-4 text-xs opacity-80 flex items-center text-gray-500 dark:text-whisper-900 hover:text-gray-700 dark:hover:text-whisper-500`}
|
||||
title="Import taxonomy"
|
||||
onClick={onImport}>
|
||||
<DownloadIcon className={`w-5 mr-2`} />
|
||||
<span>Import taxonomy</span>
|
||||
</button>
|
||||
)}>
|
||||
<NavigationItem
|
||||
isSelected={selectedTaxonomy === "tags"}
|
||||
onClick={() => setSelectedTaxonomy(`tags`)}>
|
||||
<ChevronRightIcon className='-ml-1 w-5 mr-2' />
|
||||
<span>Tags</span>
|
||||
</NavigationItem>
|
||||
|
||||
<NavigationItem
|
||||
isSelected={selectedTaxonomy === "categories"}
|
||||
onClick={() => setSelectedTaxonomy(`categories`)}>
|
||||
<ChevronRightIcon className='-ml-1 w-5 mr-2' />
|
||||
<span>Categories</span>
|
||||
</NavigationItem>
|
||||
|
||||
{
|
||||
taxonomySettings?.customTaxonomy && taxonomySettings.customTaxonomy.map((taxonomy, index) => (
|
||||
<NavigationItem
|
||||
key={`${taxonomy.id}-${index}`}
|
||||
isSelected={selectedTaxonomy === taxonomy.id}
|
||||
onClick={() => setSelectedTaxonomy(taxonomy.id)}>
|
||||
<ChevronRightIcon className='-ml-1 w-5 mr-2' />
|
||||
<span className={`first-letter:uppercase`}>{taxonomy.id}</span>
|
||||
</NavigationItem>
|
||||
))
|
||||
}
|
||||
</NavigationBar>
|
||||
|
||||
<div className={`w-10/12`}>
|
||||
<TaxonomyManager
|
||||
data={taxonomySettings}
|
||||
taxonomy={selectedTaxonomy}
|
||||
pages={pages} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SponsorMsg beta={settings?.beta} version={settings?.versionInfo} isBacker={settings?.isBacker} />
|
||||
</PageLayout>
|
||||
);
|
||||
};
|
||||
@@ -11,6 +11,7 @@ export interface Page {
|
||||
fmPreviewImage: string;
|
||||
fmTags: string[];
|
||||
fmCategories: string[];
|
||||
fmContentType: string;
|
||||
|
||||
title: string;
|
||||
slug: string;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { DataType } from './../../models/DataType';
|
||||
import { VersionInfo } from '../../models/VersionInfo';
|
||||
import { ContentFolder } from '../../models/ContentFolder';
|
||||
import { ContentType, CustomScript, DraftField, Framework, Snippets, SortingSetting } from '../../models';
|
||||
import { ContentType, CustomScript, CustomTaxonomy, DraftField, Framework, Snippets, SortingSetting } from '../../models';
|
||||
import { SortingOption } from './SortingOption';
|
||||
import { DashboardViewType } from '.';
|
||||
import { DataFile } from '../../models/DataFile';
|
||||
@@ -13,6 +13,7 @@ export interface Settings {
|
||||
staticFolder: string;
|
||||
tags: string[];
|
||||
categories: string[];
|
||||
customTaxonomy: CustomTaxonomy[];
|
||||
openOnStart: boolean | null;
|
||||
versionInfo: VersionInfo;
|
||||
pageViewType: DashboardViewType | undefined;
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { ContentType } from '../../models';
|
||||
|
||||
|
||||
export const getTaxonomyField = (taxonomyType: string, contentType: ContentType): string | undefined => {
|
||||
let fieldName: string | undefined;
|
||||
|
||||
if (taxonomyType === "tags") {
|
||||
fieldName = contentType.fields.find(f => f.name === "tags")?.name || "tags";
|
||||
} else if (taxonomyType === "categories") {
|
||||
fieldName = contentType.fields.find(f => f.name === "categories")?.name || "categories";
|
||||
} else {
|
||||
fieldName = contentType.fields.find(f => f.type === "taxonomy" && f.taxonomyId === taxonomyType)?.name;
|
||||
}
|
||||
|
||||
return fieldName;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './getTaxonomyField';
|
||||
@@ -3,7 +3,7 @@ import { workspace } from "vscode";
|
||||
import { Folders } from "../commands/Folders";
|
||||
import { Project } from "../commands/Project";
|
||||
import { Template } from "../commands/Template";
|
||||
import { CONTEXT, ExtensionState, SETTING_CONTENT_DRAFT_FIELD, SETTING_CONTENT_SORTING, SETTING_CONTENT_SORTING_DEFAULT, SETTING_CONTENT_STATIC_FOLDER, SETTING_DASHBOARD_OPENONSTART, SETTING_DATA_FILES, SETTING_DATA_FOLDERS, SETTING_DATA_TYPES, SETTING_FRAMEWORK_ID, SETTING_MEDIA_SORTING_DEFAULT, SETTING_CUSTOM_SCRIPTS, SETTING_TAXONOMY_CONTENT_TYPES, SETTING_CONTENT_SNIPPETS, SETTING_DATE_FORMAT, SETTING_DASHBOARD_CONTENT_TAGS, SETTING_MEDIA_SUPPORTED_MIMETYPES } from "../constants";
|
||||
import { CONTEXT, ExtensionState, SETTING_CONTENT_DRAFT_FIELD, SETTING_CONTENT_SORTING, SETTING_CONTENT_SORTING_DEFAULT, SETTING_CONTENT_STATIC_FOLDER, SETTING_DASHBOARD_OPENONSTART, SETTING_DATA_FILES, SETTING_DATA_FOLDERS, SETTING_DATA_TYPES, SETTING_FRAMEWORK_ID, SETTING_MEDIA_SORTING_DEFAULT, SETTING_CUSTOM_SCRIPTS, SETTING_TAXONOMY_CONTENT_TYPES, SETTING_CONTENT_SNIPPETS, SETTING_DATE_FORMAT, SETTING_DASHBOARD_CONTENT_TAGS, SETTING_MEDIA_SUPPORTED_MIMETYPES, SETTING_TAXONOMY_CUSTOM } from "../constants";
|
||||
import { DashboardViewType, SortingOption, Settings as ISettings } from "../dashboardWebView/models";
|
||||
import { CustomScript, DraftField, Snippets, SortingSetting, TaxonomyType } from "../models";
|
||||
import { DataFile } from "../models/DataFile";
|
||||
@@ -28,6 +28,7 @@ export class DashboardSettings {
|
||||
initialized: isInitialized,
|
||||
tags: Settings.getTaxonomy(TaxonomyType.Tag),
|
||||
categories: Settings.getTaxonomy(TaxonomyType.Category),
|
||||
customTaxonomy: Settings.get(SETTING_TAXONOMY_CUSTOM, true) || [],
|
||||
openOnStart: Settings.get(SETTING_DASHBOARD_OPENONSTART),
|
||||
versionInfo: ext.getVersion(),
|
||||
pageViewType: await ext.getState<DashboardViewType | undefined>(ExtensionState.PagesView, "workspace"),
|
||||
|
||||
@@ -1,21 +1,32 @@
|
||||
import { Notifications } from './Notifications';
|
||||
import { Uri, workspace } from 'vscode';
|
||||
import { Folders } from '../commands/Folders';
|
||||
import { isValidFile } from './isValidFile';
|
||||
|
||||
export class FilesHelper {
|
||||
|
||||
/**
|
||||
* Retrieve all markdown files from the current project
|
||||
*/
|
||||
public static async getMdFiles(): Promise<Uri[] | null> {
|
||||
const mdFiles = await workspace.findFiles('**/*.md', "**/node_modules/**,**/archetypes/**");
|
||||
const markdownFiles = await workspace.findFiles('**/*.markdown', "**/node_modules/**,**/archetypes/**");
|
||||
const mdxFiles = await workspace.findFiles('**/*.mdx', "**/node_modules/**,**/archetypes/**");
|
||||
if (!mdFiles && !markdownFiles) {
|
||||
Notifications.info(`No MD files found.`);
|
||||
public static async getAllFiles(): Promise<Uri[] | null> {
|
||||
const folderInfo = await Folders.getInfo();
|
||||
const pages: Uri[] = [];
|
||||
|
||||
if (folderInfo) {
|
||||
for (const folder of folderInfo) {
|
||||
for (const file of folder.lastModified) {
|
||||
if (isValidFile(file.fileName)) {
|
||||
pages.push(Uri.file(file.filePath));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (pages.length === 0) {
|
||||
Notifications.warning(`No files found.`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const allMdFiles = [...mdFiles, ...markdownFiles, ...mdxFiles];
|
||||
return allMdFiles;
|
||||
return pages;
|
||||
}
|
||||
}
|
||||
@@ -219,10 +219,22 @@ export class Settings {
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the taxonomy settings
|
||||
*
|
||||
* @param type
|
||||
*/
|
||||
public static getCustomTaxonomy(type: string): string[] {
|
||||
const customTaxs = Settings.get<CustomTaxonomy[]>(SETTING_TAXONOMY_CUSTOM, true);
|
||||
if (customTaxs && customTaxs.length > 0) {
|
||||
return customTaxs.find(t => t.id === type)?.options || [];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the taxonomy settings
|
||||
*
|
||||
* @param config
|
||||
* @param type
|
||||
* @param options
|
||||
*/
|
||||
@@ -259,6 +271,23 @@ export class Settings {
|
||||
await Settings.update(SETTING_TAXONOMY_CUSTOM, customTaxonomies, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the taxonomy settings
|
||||
*
|
||||
* @param type
|
||||
* @param options
|
||||
*/
|
||||
public static async updateCustomTaxonomyOptions(id: string, options: string[]) {
|
||||
const customTaxonomies = Settings.get<CustomTaxonomy[]>(SETTING_TAXONOMY_CUSTOM, true) || [];
|
||||
let taxIdx = customTaxonomies?.findIndex(o => o.id === id);
|
||||
|
||||
if (taxIdx !== -1) {
|
||||
customTaxonomies[taxIdx].options = options;
|
||||
}
|
||||
|
||||
await Settings.update(SETTING_TAXONOMY_CUSTOM, customTaxonomies, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Promote settings from local to team level
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,306 @@
|
||||
import { EXTENSION_NAME } from "../constants";
|
||||
import { TaxonomyType } from "../models";
|
||||
import { FilesHelper } from "./FilesHelper";
|
||||
import { ProgressLocation, window } from "vscode";
|
||||
import { parseWinPath } from "./parseWinPath";
|
||||
import { readFileSync, writeFileSync } from "fs";
|
||||
import { FrontMatterParser } from "../parsers";
|
||||
import { DumpOptions } from "js-yaml";
|
||||
import { Settings } from "./SettingsHelper";
|
||||
import { Notifications } from "./Notifications";
|
||||
import { ArticleHelper } from './ArticleHelper';
|
||||
|
||||
|
||||
export class TaxonomyHelper {
|
||||
|
||||
/**
|
||||
* Rename an taxonomy value
|
||||
* @param data
|
||||
* @returns
|
||||
*/
|
||||
public static async rename(data: { type: string, value: string }) {
|
||||
const { type, value } = data;
|
||||
|
||||
const answer = await window.showInputBox({
|
||||
title: `Rename the "${value}"`,
|
||||
value,
|
||||
validateInput: (text) => {
|
||||
if (text === value) {
|
||||
return "The new value must be different from the old one.";
|
||||
}
|
||||
|
||||
if (!text) {
|
||||
return "A new value must be provided.";
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
ignoreFocusOut: true
|
||||
});
|
||||
|
||||
if (!answer) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.process("edit", this.getTypeFromString(type), value, answer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge a taxonomy value with another one
|
||||
* @param data
|
||||
* @returns
|
||||
*/
|
||||
public static async merge(data: { type: string, value: string }) {
|
||||
const { type, value } = data;
|
||||
const taxonomyType = this.getTypeFromString(type);
|
||||
|
||||
let options = [];
|
||||
if (taxonomyType === TaxonomyType.Tag || taxonomyType === TaxonomyType.Category) {
|
||||
options = Settings.getTaxonomy(taxonomyType);
|
||||
} else {
|
||||
options = Settings.getCustomTaxonomy(taxonomyType);
|
||||
}
|
||||
|
||||
const answer = await window.showQuickPick(options.filter(o => o !== value), {
|
||||
title: `Merge the "${value}" with another ${type} value`,
|
||||
placeHolder: `Select the ${type} value to merge with`,
|
||||
ignoreFocusOut: true
|
||||
});
|
||||
|
||||
if (!answer) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.process("merge", taxonomyType, value, answer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a taxonomy value
|
||||
* @param data
|
||||
*/
|
||||
public static async delete(data: { type: string, value: string }) {
|
||||
const { type, value } = data;
|
||||
|
||||
const answer = await window.showQuickPick(["Yes", "No"], {
|
||||
title: `Delete the "${value}" ${type} value`,
|
||||
placeHolder: `Are you sure you want to delete the "${value}" ${type} value?`,
|
||||
ignoreFocusOut: true
|
||||
});
|
||||
|
||||
if (!answer || answer === "No") {
|
||||
return;
|
||||
}
|
||||
|
||||
this.process("delete", this.getTypeFromString(type), value, undefined);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the taxonomy value to the settings
|
||||
* @param data
|
||||
*/
|
||||
public static addTaxonomy(data: { type: string, value: string }) {
|
||||
const { type, value } = data;
|
||||
this.addToSettings(this.getTypeFromString(type), value, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create new taxonomy value
|
||||
* @param data
|
||||
*/
|
||||
public static async createNew(data: { type: string }) {
|
||||
const { type } = data;
|
||||
|
||||
const taxonomyType = this.getTypeFromString(type);
|
||||
const options = this.getTaxonomyOptions(taxonomyType);
|
||||
|
||||
const newOption = await window.showInputBox({
|
||||
title: `Create a new ${taxonomyType} value`,
|
||||
placeHolder: `The value you want to add`,
|
||||
ignoreFocusOut: true,
|
||||
validateInput: (text) => {
|
||||
if (!text) {
|
||||
return "A value must be provided.";
|
||||
}
|
||||
|
||||
if (options.includes(text)) {
|
||||
return "The value already exists.";
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
if (!newOption) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.addToSettings(taxonomyType, newOption, newOption);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the taxonomy changes
|
||||
* @param type
|
||||
* @param taxonomyType
|
||||
* @param oldValue
|
||||
* @param newValue
|
||||
* @returns
|
||||
*/
|
||||
public static async process(type: "edit" | "merge" | "delete", taxonomyType: TaxonomyType | string, oldValue: string, newValue?: string) {
|
||||
// Retrieve all the markdown files
|
||||
const allFiles = await FilesHelper.getAllFiles();
|
||||
if (!allFiles) {
|
||||
return;
|
||||
}
|
||||
|
||||
let taxonomyName: string;
|
||||
if (taxonomyType === TaxonomyType.Tag) {
|
||||
taxonomyName = "tags";
|
||||
} else if (taxonomyType === TaxonomyType.Category) {
|
||||
taxonomyName = "categories";
|
||||
} else {
|
||||
taxonomyName = taxonomyType;
|
||||
}
|
||||
|
||||
let progressText = ``;
|
||||
|
||||
if (type === "edit") {
|
||||
progressText = `${EXTENSION_NAME}: Renaming "${oldValue}" from ${taxonomyName} to "${newValue}".`;
|
||||
} else if (type === "merge") {
|
||||
progressText = `${EXTENSION_NAME}: Merging "${oldValue}" from "${taxonomyName}" to "${newValue}".`;
|
||||
} else if (type === "delete") {
|
||||
progressText = `${EXTENSION_NAME}: Deleting "${oldValue}" from "${taxonomyName}".`;
|
||||
}
|
||||
|
||||
window.withProgress({
|
||||
location: ProgressLocation.Notification,
|
||||
title: progressText,
|
||||
cancellable: false
|
||||
}, async (progress) => {
|
||||
// Set the initial progress
|
||||
const progressNr = allFiles.length/100;
|
||||
progress.report({ increment: 0});
|
||||
|
||||
let i = 0;
|
||||
for (const file of allFiles) {
|
||||
progress.report({ increment: (++i/progressNr) });
|
||||
|
||||
const mdFile = readFileSync(parseWinPath(file.fsPath), { encoding: "utf8" });
|
||||
|
||||
if (mdFile) {
|
||||
try {
|
||||
const article = FrontMatterParser.fromFile(mdFile);
|
||||
const contentType = ArticleHelper.getContentType(article.data);
|
||||
let fieldName: string | undefined;
|
||||
|
||||
if (taxonomyName === "tags") {
|
||||
fieldName = contentType.fields.find(f => f.type === "tags")?.name || "tags";
|
||||
} else if (taxonomyName === "categories") {
|
||||
fieldName = contentType.fields.find(f => f.type === "categories")?.name || "categories";
|
||||
} else {
|
||||
fieldName = contentType.fields.find(f => f.type === "taxonomy" && f.taxonomyId === taxonomyName)?.name;
|
||||
}
|
||||
|
||||
if (fieldName && article && article.data) {
|
||||
const { data } = article;
|
||||
let taxonomies: string[] = data[fieldName];
|
||||
|
||||
if (taxonomies && taxonomies.length > 0) {
|
||||
const idx = taxonomies.findIndex(o => o === oldValue);
|
||||
|
||||
if (idx !== -1) {
|
||||
if (newValue) {
|
||||
taxonomies[idx] = newValue;
|
||||
} else {
|
||||
taxonomies = taxonomies.filter(o => o !== oldValue);
|
||||
}
|
||||
|
||||
data[fieldName] = [...new Set(taxonomies)].sort();
|
||||
|
||||
const spaces = window.activeTextEditor?.options?.tabSize;
|
||||
// Update the file
|
||||
writeFileSync(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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await this.addToSettings(taxonomyType, oldValue, newValue);
|
||||
|
||||
if (type === "edit") {
|
||||
Notifications.info(`Edit completed.`);
|
||||
} else if (type === "merge") {
|
||||
Notifications.info(`Merge completed.`);
|
||||
} else if (type === "delete") {
|
||||
Notifications.info(`Deletion completed.`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the taxonomy value to the settings
|
||||
* @param taxonomyType
|
||||
* @param oldValue
|
||||
* @param newValue
|
||||
*/
|
||||
private static async addToSettings(taxonomyType: TaxonomyType | string, oldValue: string, newValue?: string) {
|
||||
// Update the settings
|
||||
let options = this.getTaxonomyOptions(taxonomyType);
|
||||
|
||||
const idx = options.findIndex(o => o === oldValue);
|
||||
if (newValue) {
|
||||
// Add or update the new option
|
||||
if (idx !== -1) {
|
||||
options[idx] = newValue;
|
||||
} else {
|
||||
options.push(newValue);
|
||||
}
|
||||
} else {
|
||||
// Remove the selected option
|
||||
options = options.filter(o => o !== oldValue);
|
||||
}
|
||||
|
||||
if (taxonomyType === TaxonomyType.Tag || taxonomyType === TaxonomyType.Category) {
|
||||
await Settings.updateTaxonomy(taxonomyType, options);
|
||||
} else {
|
||||
await Settings.updateCustomTaxonomyOptions(taxonomyType, options);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the taxonomy options
|
||||
* @param taxonomyType
|
||||
* @returns
|
||||
*/
|
||||
private static getTaxonomyOptions(taxonomyType: TaxonomyType | string) {
|
||||
let options = [];
|
||||
|
||||
if (taxonomyType === TaxonomyType.Tag || taxonomyType === TaxonomyType.Category) {
|
||||
options = Settings.getTaxonomy(taxonomyType);
|
||||
} else {
|
||||
options = Settings.getCustomTaxonomy(taxonomyType);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,7 @@ export * from './SlugHelper';
|
||||
export * from './SnippetParser';
|
||||
export * from './Sorting';
|
||||
export * from './StringHelpers';
|
||||
export * from './TaxonomyHelper';
|
||||
export * from './Telemetry';
|
||||
export * from './decodeBase64Image';
|
||||
export * from './getNonce';
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { DEFAULT_CONTENT_TYPE_NAME } from './../../constants/ContentType';
|
||||
import { isValidFile } from '../../helpers/isValidFile';
|
||||
import { existsSync, unlinkSync } from "fs";
|
||||
import { basename, dirname, join } from "path";
|
||||
@@ -91,7 +92,7 @@ export class PagesListener extends BaseListener {
|
||||
// Recreate all the watchers
|
||||
for (const folder of folders) {
|
||||
const folderUri = Uri.parse(folder.path);
|
||||
let watcher = workspace.createFileSystemWatcher(new RelativePattern(folderUri, "*"), false, false, false);
|
||||
let watcher = workspace.createFileSystemWatcher(new RelativePattern(folderUri, "**/*"), false, false, false);
|
||||
watcher.onDidCreate(async (uri: Uri) => this.watcherExec(uri));
|
||||
watcher.onDidChange(async (uri: Uri) => this.watcherExec(uri));
|
||||
watcher.onDidDelete(async (uri: Uri) => this.watcherExec(uri));
|
||||
@@ -282,6 +283,7 @@ export class PagesListener extends BaseListener {
|
||||
fmPreviewImage: "",
|
||||
fmTags: [],
|
||||
fmCategories: [],
|
||||
fmContentType: DEFAULT_CONTENT_TYPE_NAME,
|
||||
fmBody: article?.content || "",
|
||||
// Make sure these are always set
|
||||
title: article?.data.title,
|
||||
@@ -292,6 +294,9 @@ export class PagesListener extends BaseListener {
|
||||
};
|
||||
|
||||
const contentType = ArticleHelper.getContentType(article.data);
|
||||
if (contentType) {
|
||||
page.fmContentType = contentType.name;
|
||||
}
|
||||
|
||||
let previewFieldParents = this.findPreviewField(contentType.fields);
|
||||
if (previewFieldParents.length === 0) {
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import { join } from "path";
|
||||
import { Uri, workspace } from "vscode";
|
||||
import { Folders } from "../../commands/Folders";
|
||||
import { DEFAULT_FILE_TYPES, SETTING_CONTENT_SUPPORTED_FILETYPES, SETTING_TAXONOMY_CATEGORIES, SETTING_TAXONOMY_CUSTOM, SETTING_TAXONOMY_TAGS } from "../../constants";
|
||||
import { commands } from "vscode";
|
||||
import { COMMAND_NAME, SETTING_TAXONOMY_CATEGORIES, SETTING_TAXONOMY_CUSTOM, SETTING_TAXONOMY_TAGS } from "../../constants";
|
||||
import { DashboardCommand } from "../../dashboardWebView/DashboardCommand";
|
||||
import { DashboardMessage } from "../../dashboardWebView/DashboardMessage";
|
||||
import { Settings } from "../../helpers";
|
||||
import { Settings, TaxonomyHelper } from "../../helpers";
|
||||
import { CustomTaxonomy } from "../../models";
|
||||
import { BaseListener } from "./BaseListener";
|
||||
|
||||
@@ -22,6 +20,24 @@ export class TaxonomyListener extends BaseListener {
|
||||
case DashboardMessage.getTaxonomyData:
|
||||
this.getData();
|
||||
break;
|
||||
case DashboardMessage.editTaxonomy:
|
||||
TaxonomyHelper.rename(msg.data);
|
||||
break;
|
||||
case DashboardMessage.mergeTaxonomy:
|
||||
TaxonomyHelper.merge(msg.data);
|
||||
break;
|
||||
case DashboardMessage.deleteTaxonomy:
|
||||
TaxonomyHelper.delete(msg.data);
|
||||
break;
|
||||
case DashboardMessage.addToTaxonomy:
|
||||
TaxonomyHelper.addTaxonomy(msg.data);
|
||||
break;
|
||||
case DashboardMessage.createTaxonomy:
|
||||
TaxonomyHelper.createNew(msg.data);
|
||||
break;
|
||||
case DashboardMessage.importTaxonomy:
|
||||
commands.executeCommand(COMMAND_NAME.exportTaxonomy);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,30 +49,6 @@ export class TaxonomyListener extends BaseListener {
|
||||
customTaxonomy: Settings.get<CustomTaxonomy[]>(SETTING_TAXONOMY_CUSTOM) || []
|
||||
};
|
||||
|
||||
const supportedFiles = Settings.get<string[]>(SETTING_CONTENT_SUPPORTED_FILETYPES) || DEFAULT_FILE_TYPES;
|
||||
const fileExtensions = supportedFiles.map(fileType => `${fileType.startsWith('.') ? '' : '.'}${fileType}`);
|
||||
|
||||
const folders = Folders.get();
|
||||
const projectName = Folders.getProjectFolderName();
|
||||
|
||||
let files: Uri[] = [];
|
||||
|
||||
for (const folder of folders) {
|
||||
let projectStart = folder.path.split(projectName).pop();
|
||||
projectStart = projectStart || "";
|
||||
projectStart = projectStart?.replace(/\\/g, '/');
|
||||
projectStart = projectStart?.startsWith('/') ? projectStart.substring(1) : projectStart;
|
||||
|
||||
for (const fileExtension of fileExtensions) {
|
||||
const crntFiles = await workspace.findFiles(join(projectStart, folder.excludeSubdir ? '/' : '**/', `*${fileExtension}`));
|
||||
if (crntFiles && crntFiles.length > 0) {
|
||||
files = [...files, ...crntFiles];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(files)
|
||||
|
||||
this.sendMsg(DashboardCommand.setTaxonomyData, taxonomyData);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { CustomTaxonomy } from ".";
|
||||
|
||||
|
||||
export interface TaxonomyData {
|
||||
tags: string[];
|
||||
categories: string[];
|
||||
customTaxonomy: CustomTaxonomy[];
|
||||
}
|
||||
@@ -15,5 +15,6 @@ export * from './Snippets';
|
||||
export * from './SortOrder';
|
||||
export * from './SortType';
|
||||
export * from './SortingSetting';
|
||||
export * from './TaxonomyData';
|
||||
export * from './TaxonomyType';
|
||||
export * from './VersionInfo';
|
||||
|
||||
Reference in New Issue
Block a user