setSelectedFolder(folder)}>
+
+
+ {
+ isContentFolder && (
+ C
+ )
+ }
diff --git a/src/dashboardWebView/components/Media/Item.tsx b/src/dashboardWebView/components/Media/Item.tsx
index 9e7a651a..ba79099c 100644
--- a/src/dashboardWebView/components/Media/Item.tsx
+++ b/src/dashboardWebView/components/Media/Item.tsx
@@ -1,6 +1,6 @@
import { Messenger } from '@estruyf/vscode/dist/client';
import { Menu } from '@headlessui/react';
-import { ClipboardIcon, CodeIcon, PencilIcon, PhotographIcon, PlusIcon, TrashIcon } from '@heroicons/react/outline';
+import { ClipboardIcon, CodeIcon, EyeIcon, PencilIcon, PhotographIcon, PlusIcon, TrashIcon } from '@heroicons/react/outline';
import { basename, dirname } from 'path';
import * as React from 'react';
import { useEffect } from 'react';
@@ -10,10 +10,10 @@ import { parseWinPath } from '../../../helpers/parseWinPath';
import { ScriptType } from '../../../models';
import { MediaInfo } from '../../../models/MediaPaths';
import { DashboardMessage } from '../../DashboardMessage';
-import { LightboxAtom, PageSelector, SelectedMediaFolderSelector, SettingsSelector, ViewDataSelector } from '../../state';
+import { LightboxAtom, SelectedMediaFolderSelector, SettingsSelector, ViewDataSelector } from '../../state';
import { MenuItem, MenuItems } from '../Menu';
import { Alert } from '../Modals/Alert';
-import { Metadata } from '../Modals/Metadata';
+import { DetailsSlideOver } from './DetailsSlideOver';
import { MenuButton } from './MenuButton'
import { QuickAction } from './QuickAction';
@@ -25,13 +25,13 @@ export const Item: React.FunctionComponent = ({media}: React.PropsWi
const [ , setLightbox ] = useRecoilState(LightboxAtom);
const [ showAlert, setShowAlert ] = React.useState(false);
const [ showForm, setShowForm ] = React.useState(false);
+ const [ showDetails, setShowDetails ] = React.useState(false);
const [ caption, setCaption ] = React.useState(media.caption);
const [ alt, setAlt ] = React.useState(media.alt);
const [ filename, setFilename ] = React.useState(null);
const settings = useRecoilValue(SettingsSelector);
const selectedFolder = useRecoilValue(SelectedMediaFolderSelector);
const viewData = useRecoilValue(ViewDataSelector);
- const page = useRecoilValue(PageSelector);
const getFolder = () => {
if (settings?.wsFolder && media.fsPath) {
@@ -125,25 +125,45 @@ export const Item: React.FunctionComponent = ({media}: React.PropsWi
});
};
- const calculateSize = () => {
- let sizeDetails = [];
-
- if (media?.dimensions) {
- if (media.dimensions.width && media.dimensions.height) {
- sizeDetails.push(`${media.dimensions.width}x${media.dimensions.height}`);
- }
+ const getDimensions = () => {
+ if (media.dimensions) {
+ return `${media.dimensions.width} x ${media.dimensions.height}`;
}
+ return "";
+ };
+
+ const getSize = () => {
if (media?.size) {
const size = media.size / (1024*1024);
if (size > 1) {
- sizeDetails.push(`${size.toFixed(2)} MB`);
+ return `${size.toFixed(2)} MB`;
} else {
- sizeDetails.push(`${(size * 1024).toFixed(2)} KB`);
+ return `${(size * 1024).toFixed(2)} KB`;
}
}
- return sizeDetails.join(" — ");
+ return '';
+ };
+
+ const getMediaDetails = () => {
+ let sizeDetails = [];
+
+ const dimensions = getDimensions();
+ if (dimensions) {
+ sizeDetails.push(dimensions);
+ }
+
+ const size = getSize();
+ if (size) {
+ sizeDetails.push(size);
+ }
+
+ return sizeDetails.join(" - ");
+ };
+
+ const viewMediaDetails = () => {
+ setShowDetails(true);
};
const openLightbox = () => {
@@ -152,24 +172,7 @@ export const Item: React.FunctionComponent = ({media}: React.PropsWi
const updateMetadata = () => {
setShowForm(true);
- };
-
- const submitMetadata = () => {
- Messenger.send(DashboardMessage.updateMediaMetadata, {
- file: media.fsPath,
- filename,
- caption,
- alt,
- folder: selectedFolder,
- page
- });
-
- setShowForm(false);
-
- // Reset the values
- setAlt(media.alt);
- setCaption(media.caption);
- setFilename(getFileName());
+ setShowDetails(true);
};
const customScriptActions = () => {
@@ -200,13 +203,9 @@ export const Item: React.FunctionComponent = ({media}: React.PropsWi
}
}, [media.fsPath]);
- const fileInfo = filename ? basename(filename).split('.') : null;
- const extension = fileInfo?.pop();
- const name = fileInfo?.join('.');
-
return (
<>
-
+
@@ -218,9 +217,15 @@ export const Item: React.FunctionComponent
= ({media}: React.PropsWi
-
+
+
+
+
+
@@ -325,22 +330,18 @@ export const Item: React.FunctionComponent = ({media}: React.PropsWi
)
}
{
- media.alt && (
+ (!media.caption && media.alt) && (
Alt:
{media.alt}
)
}
-
- Folder:
- {getFolder()}
-
{
(media?.size || media?.dimensions) && (
Size:
- {calculateSize()}
+ {getMediaDetails()}
)
}
@@ -348,60 +349,17 @@ export const Item: React.FunctionComponent = ({media}: React.PropsWi
{
- showForm && (
- setShowForm(false)}
- trigger={submitMetadata}
- isSaveDisabled={!filename}>
-
-
-
- Filename
-
-
-
setFilename(`${e.target.value}.${extension}`)} />
-
-
-
- .{extension}
-
-
-
-
-
-
-
- Alt tag value
-
-
- setAlt(e.target.value)}
- />
-
-
-
-
+ showDetails && (
+ setShowForm(true)}
+ onEditClose={() => setShowForm(false)}
+ onDismiss={() => { setShowDetails(false); setShowForm(false); }} />
)
}
diff --git a/src/dashboardWebView/components/Media/Media.tsx b/src/dashboardWebView/components/Media/Media.tsx
index 7f33f42e..15319156 100644
--- a/src/dashboardWebView/components/Media/Media.tsx
+++ b/src/dashboardWebView/components/Media/Media.tsx
@@ -3,7 +3,6 @@ import {UploadIcon} from '@heroicons/react/outline';
import * as React from 'react';
import { useRecoilValue } from 'recoil';
import { LoadingAtom, MediaFoldersAtom, SelectedMediaFolderAtom, SettingsSelector, ViewDataSelector } from '../../state';
-import { Header } from '../Header';
import { Spinner } from '../Spinner';
import { SponsorMsg } from '../SponsorMsg';
import { Item } from './Item';
@@ -16,6 +15,9 @@ import { FrontMatterIcon } from '../../../panelWebView/components/Icons/FrontMat
import { FolderItem } from './FolderItem';
import useMedia from '../../hooks/useMedia';
import { TelemetryEvent } from '../../../constants';
+import { PageLayout } from '../Layout/PageLayout';
+import { parseWinPath } from '../../../helpers/parseWinPath';
+import { join } from 'path';
export interface IMediaProps {}
@@ -27,6 +29,25 @@ export const Media: React.FunctionComponent = (props: React.PropsWi
const folders = useRecoilValue(MediaFoldersAtom);
const loading = useRecoilValue(LoadingAtom);
+
+ const allFolders = React.useMemo(() => {
+ // Check if content allows page bundle
+ if (viewData && viewData.data && typeof viewData.data.pageBundle !== "undefined" && !viewData.data.pageBundle) {
+ return folders.filter(f => parseWinPath(f).includes(join('/', settings?.staticFolder || '', '/')));
+ }
+
+ return folders;
+ }, [folders, viewData, settings?.staticFolder]);
+
+ const allMedia = React.useMemo(() => {
+ // Check if content allows page bundle
+ if (viewData && viewData.data && typeof viewData.data.pageBundle !== "undefined" && !viewData.data.pageBundle) {
+ return media.filter(m => parseWinPath(m.fsPath).includes(join('/', settings?.staticFolder || '', '/')));
+ }
+
+ return media;
+ }, [media, viewData, settings?.staticFolder]);
+
const onDrop = useCallback((acceptedFiles: File[]) => {
acceptedFiles.forEach((file) => {
const reader = new FileReader();
@@ -56,16 +77,13 @@ export const Media: React.FunctionComponent = (props: React.PropsWi
});
return (
-
-
-
-
-
+
+
{
viewData?.data?.filePath && (
-
Select the image you want to use for your article.
-
You can also drag and drop images from your desktop and select that once uploaded.
+
Select the media file to add to your content.
+
You can also drag and drop images from your desktop and select them once uploaded.
)
}
@@ -82,7 +100,7 @@ export const Media: React.FunctionComponent
= (props: React.PropsWi
}
{
- (media.length === 0 && folders.length === 0 && !loading) && (
+ (allMedia.length === 0 && folders.length === 0 && !loading) && (
@@ -94,11 +112,11 @@ export const Media: React.FunctionComponent
= (props: React.PropsWi
}
{
- folders && folders.length > 0 && (
+ allFolders && allFolders.length > 0 && (
{
- folders && folders.map((folder) => (
+ allFolders.map((folder) => (
))
}
@@ -109,7 +127,7 @@ export const Media: React.FunctionComponent = (props: React.PropsWi
{
- media.map((file) => (
+ allMedia.map((file) => (
))
}
@@ -123,6 +141,6 @@ export const Media: React.FunctionComponent = (props: React.PropsWi
-
+
);
};
\ No newline at end of file
diff --git a/src/dashboardWebView/components/Modals/Metadata.tsx b/src/dashboardWebView/components/Modals/FormDialog.tsx
similarity index 82%
rename from src/dashboardWebView/components/Modals/Metadata.tsx
rename to src/dashboardWebView/components/Modals/FormDialog.tsx
index df9d5109..701dc06d 100644
--- a/src/dashboardWebView/components/Modals/Metadata.tsx
+++ b/src/dashboardWebView/components/Modals/FormDialog.tsx
@@ -2,7 +2,7 @@ import { Dialog, Transition } from '@headlessui/react';
import * as React from 'react';
import { Fragment, useRef } from 'react';
-export interface IMetadataProps {
+export interface IFormDialogProps {
title: string;
description: string;
okBtnText: string;
@@ -13,11 +13,10 @@ export interface IMetadataProps {
trigger: () => void;
}
-export const Metadata: React.FunctionComponent = ({title, description, cancelBtnText, okBtnText, dismiss, isSaveDisabled, trigger, children}: React.PropsWithChildren) => {
+export const FormDialog: React.FunctionComponent = ({title, description, cancelBtnText, okBtnText, dismiss, isSaveDisabled, trigger, children}: React.PropsWithChildren) => {
const cancelButtonRef = useRef(null);
-
-
+
return (
dismiss()}>
@@ -67,7 +66,7 @@ export const Metadata: React.FunctionComponent = ({title, descri
trigger()}
disabled={isSaveDisabled}
>
@@ -75,7 +74,7 @@ export const Metadata: React.FunctionComponent = ({title, descri
dismiss()}
ref={cancelButtonRef}
>
diff --git a/src/dashboardWebView/components/SnippetsView/Item.tsx b/src/dashboardWebView/components/SnippetsView/Item.tsx
new file mode 100644
index 00000000..c9a27478
--- /dev/null
+++ b/src/dashboardWebView/components/SnippetsView/Item.tsx
@@ -0,0 +1,193 @@
+import { Messenger } from '@estruyf/vscode/dist/client';
+import { CodeIcon, DotsHorizontalIcon, PencilIcon, PlusIcon, TrashIcon } from '@heroicons/react/outline';
+import * as React from 'react';
+import { useCallback, useRef, useState } from 'react';
+import { useRecoilValue } from 'recoil';
+import { SnippetParser } from '../../../helpers/SnippetParser';
+import { Snippet, SnippetField, Snippets } from '../../../models';
+import { DashboardMessage } from '../../DashboardMessage';
+import { SettingsSelector, ViewDataSelector } from '../../state';
+import { Alert } from '../Modals/Alert';
+import { FormDialog } from '../Modals/FormDialog';
+import { NewForm } from './NewForm';
+import SnippetForm, { SnippetFormHandle } from './SnippetForm';
+
+export interface IItemProps {
+ title: string;
+ snippet: Snippet;
+}
+
+export const Item: React.FunctionComponent = ({ title, snippet }: React.PropsWithChildren) => {
+ const viewData = useRecoilValue(ViewDataSelector);
+ const settings = useRecoilValue(SettingsSelector);
+ const [ showInsertDialog, setShowInsertDialog ] = useState(false);
+ const [ showEditDialog, setShowEditDialog ] = useState(false);
+ const [ showAlert, setShowAlert ] = React.useState(false);
+
+ const [ snippetTitle, setSnippetTitle ] = useState('');
+ const [ snippetDescription, setSnippetDescription ] = useState('');
+ const [ snippetOriginalBody, setSnippetOriginalBody ] = useState('');
+
+ const formRef = useRef(null);
+
+ const insertToArticle = () => {
+ formRef.current?.onSave();
+ setShowInsertDialog(false);
+ };
+
+ const reset = () => {
+ setShowEditDialog(false);
+ setSnippetTitle('');
+ setSnippetDescription('');
+ setSnippetOriginalBody('');
+ };
+
+ const onOpenEdit = useCallback(() => {
+ setSnippetTitle(title);
+ setSnippetDescription(snippet.description);
+ setSnippetOriginalBody(typeof snippet.body === "string" ? snippet.body : snippet.body.join(`\n`));
+ setShowEditDialog(true);
+ }, [snippet]);
+
+ const onSnippetUpdate = useCallback(() => {
+ if (!snippetTitle || !snippetOriginalBody) {
+ reset();
+ return;
+ }
+
+ const snippets: Snippets = Object.assign({}, settings?.snippets || {});
+ const snippetLines = snippetOriginalBody.split("\n");
+
+ const crntSnippet = Object.assign({}, snippets[title]);
+
+ const fields = SnippetParser.getFields(snippetLines, crntSnippet.fields || [], crntSnippet?.openingTags, crntSnippet?.closingTags);
+
+ const snippetContents: Snippet = {
+ ...crntSnippet,
+ fields,
+ description: snippetDescription || '',
+ body: snippetLines.length === 1 ? snippetLines[0] : snippetLines
+ };
+
+ // Check if new or update
+ if (title === snippetTitle) {
+ snippets[title] = snippetContents;
+ } else {
+ delete snippets[title];
+ snippets[snippetTitle] = snippetContents;
+ }
+
+ Messenger.send(DashboardMessage.updateSnippet, { snippets });
+
+ reset();
+ }, [settings?.snippets, title, snippetTitle, snippetDescription, snippetOriginalBody]);
+
+ const onDelete = useCallback(() => {
+ const snippets = Object.assign({}, settings?.snippets || {});
+ delete snippets[title];
+
+ Messenger.send(DashboardMessage.updateSnippet, { snippets });
+
+ setShowAlert(false);
+ }, [settings?.snippets, title]);
+
+ return (
+ <>
+
+
+
+
+
+ {title}
+
+
+
+
+
+
+
+
+ {
+ viewData?.data?.filePath && (
+ <>
+
setShowInsertDialog(true)}>
+
+ Insert snippet
+
+ >
+ )
+ }
+
+
+
+ Edit snippet
+
+
+
setShowAlert(true)}>
+
+ Delete snippet
+
+
+
+
+
+ {snippet.description}
+
+
+ {
+ showInsertDialog && (
+ setShowInsertDialog(false)}
+ okBtnText='Insert'
+ cancelBtnText='Cancel'>
+
+
+
+
+ )
+ }
+
+ {
+ showEditDialog && (
+
+
+ setSnippetTitle(value)}
+ onDescriptionUpdate={(value: string) => setSnippetDescription(value)}
+ onBodyUpdate={(value: string) => setSnippetOriginalBody(value)} />
+
+
+ )
+ }
+
+ {
+ showAlert && (
+ setShowAlert(false)}
+ trigger={onDelete} />
+ )
+ }
+ >
+ );
+};
\ No newline at end of file
diff --git a/src/dashboardWebView/components/SnippetsView/NewForm.tsx b/src/dashboardWebView/components/SnippetsView/NewForm.tsx
new file mode 100644
index 00000000..bd2185d1
--- /dev/null
+++ b/src/dashboardWebView/components/SnippetsView/NewForm.tsx
@@ -0,0 +1,65 @@
+import * as React from 'react';
+
+export interface INewFormProps {
+ title: string;
+ description: string;
+ body: string;
+
+ onTitleUpdate: (value: string) => void;
+ onDescriptionUpdate: (value: string) => void;
+ onBodyUpdate: (value: string) => void;
+}
+
+export const NewForm: React.FunctionComponent = ({ title, description, body, onTitleUpdate, onDescriptionUpdate, onBodyUpdate }: React.PropsWithChildren) => {
+
+ return (
+
+
+
+ Title *
+
+
+ onTitleUpdate(e.currentTarget.value)}
+ />
+
+
+
+
+
+ Description
+
+
+ onDescriptionUpdate(e.currentTarget.value)}
+ />
+
+
+
+
+
+ );
+};
\ No newline at end of file
diff --git a/src/dashboardWebView/components/SnippetsView/SnippetForm.tsx b/src/dashboardWebView/components/SnippetsView/SnippetForm.tsx
new file mode 100644
index 00000000..8e9c7412
--- /dev/null
+++ b/src/dashboardWebView/components/SnippetsView/SnippetForm.tsx
@@ -0,0 +1,132 @@
+import { Messenger } from '@estruyf/vscode/dist/client';
+import * as React from 'react';
+import { useCallback, useEffect, useImperativeHandle, useMemo, useState } from 'react';
+import { useRecoilValue } from 'recoil';
+import { processKnownPlaceholders } from '../../../helpers/PlaceholderHelper';
+import { SnippetParser } from '../../../helpers/SnippetParser';
+import { Snippet, SnippetField, SnippetSpecialPlaceholders } from '../../../models';
+import { DashboardMessage } from '../../DashboardMessage';
+import { SettingsAtom, ViewDataSelector } from '../../state';
+import { SnippetInputField } from './SnippetInputField';
+
+
+export interface ISnippetFormProps {
+ snippet: Snippet;
+ selection: string | undefined;
+}
+
+export interface SnippetFormHandle {
+ onSave: () => void;
+}
+
+const SnippetForm: React.ForwardRefRenderFunction = ({ snippet, selection }, ref) => {
+ const viewData = useRecoilValue(ViewDataSelector);
+ const [ fields, setFields ] = useState([]);
+ const settings = useRecoilValue(SettingsAtom);
+
+ const onTextChange = useCallback((field: SnippetField, value: string) => {
+ setFields(prevFields => prevFields.map(f => f.name === field.name ? { ...f, value } : f));
+ }, [setFields]);
+
+ const insertPlaceholderValues = useCallback((value: SnippetSpecialPlaceholders) => {
+ if (value === "FM_SELECTED_TEXT") {
+ return selection || "";
+ }
+
+ value = processKnownPlaceholders(value, viewData?.data?.fileTitle || "", settings?.date.format || "");
+
+ return value;
+ }, [selection]);
+
+ const snippetBody = useMemo(() => {
+ let body = typeof snippet.body === "string" ? snippet.body : snippet.body.join(`\n`);
+
+ const obj: any = {};
+ for (const field of fields) {
+ obj[field.name] = field.value;
+ }
+
+ return SnippetParser.render(body, obj, snippet.openingTags, snippet.closingTags);
+ }, [fields, snippet]);
+
+ const shouldShowField = (fieldName: string, idx: number, allFields: SnippetField[]) => {
+ const crntField = allFields.findIndex(f => f.name === fieldName);
+ if (crntField < idx) {
+ return false;
+ }
+ return true;
+ }
+
+ useImperativeHandle(ref, () => ({
+ onSave() {
+ if (!snippetBody) {
+ return;
+ }
+
+ Messenger.send(DashboardMessage.insertSnippet, {
+ file: viewData?.data?.filePath,
+ snippet: snippetBody
+ });
+ }
+ }));
+
+ useEffect(() => {
+ // Get all placeholder variables from the snippet
+ const body = typeof snippet.body === "string" ? snippet.body : snippet.body.join(`\n`);
+
+ const placeholders = SnippetParser.getPlaceholders(body, snippet.openingTags, snippet.closingTags);
+
+ const allFields: SnippetField[] = [];
+ const snippetFields = snippet.fields || [];
+
+ for (const fieldName of placeholders) {
+ const field = snippetFields.find(f => f.name === fieldName);
+
+ if (field) {
+ allFields.push({
+ ...field,
+ value: insertPlaceholderValues(field.default || "")
+ });
+ } else {
+ allFields.push({
+ name: fieldName,
+ title: fieldName,
+ type: "string",
+ single: true,
+ value: ""
+ });
+ }
+ }
+
+ setFields(allFields);
+ }, [snippet]);
+
+ return (
+
+
+ {snippetBody}
+
+
+
+ {
+ fields.map((field: SnippetField, index: number, allFields: SnippetField[]) => (
+ shouldShowField(field.name, index, allFields) && (
+
+
+ {field.title || field.name}
+
+
+
+
+
+ )
+ ))
+ }
+
+
+ );
+};
+
+export default React.forwardRef(SnippetForm);
\ No newline at end of file
diff --git a/src/dashboardWebView/components/SnippetsView/SnippetInputField.tsx b/src/dashboardWebView/components/SnippetsView/SnippetInputField.tsx
new file mode 100644
index 00000000..3d6c8215
--- /dev/null
+++ b/src/dashboardWebView/components/SnippetsView/SnippetInputField.tsx
@@ -0,0 +1,54 @@
+import * as React from 'react';
+import { ChevronDownIcon } from '@heroicons/react/outline';
+import { Choice, SnippetField } from '../../../models';
+
+export interface ISnippetInputFieldProps {
+ field: SnippetField;
+ onValueChange: (field: SnippetField, value: string) => void
+}
+
+export const SnippetInputField: React.FunctionComponent = ({ field, onValueChange }: React.PropsWithChildren) => {
+
+ if (field.type === 'choice') {
+ return (
+
+ onValueChange(field, e.target.value)}>
+ {
+ (field.choices || [])?.map((option: string | Choice, index: number) => (
+ typeof option === 'string' ?
+ {option} :
+ {option.title}
+ ))
+ }
+
+
+
+
+ )
+ }
+
+ if (field.type === 'string' && !field.single) {
+ return (
+
+ )}>
+
+ {
+ viewData?.data?.filePath && (
+
+
Select the snippet to add to your content.
+
+ )
+ }
+
+ {
+ snippetKeys && snippetKeys.length > 0 ? (
+
+ {
+ snippetKeys.map((snippetKey: any, index: number) => (
+
+ ))
+ }
+
+ ) : (
+
+ )
+ }
+
+ {
+ showCreateDialog && (
+
+
+ setSnippetTitle(value)}
+ onDescriptionUpdate={(value: string) => setSnippetDescription(value)}
+ onBodyUpdate={(value: string) => setSnippetBody(value)} />
+
+
+ )
+ }
+
+
+
+ );
+};
\ No newline at end of file
diff --git a/src/dashboardWebView/components/Startup.tsx b/src/dashboardWebView/components/Startup.tsx
index 07156d94..8cb95fbe 100644
--- a/src/dashboardWebView/components/Startup.tsx
+++ b/src/dashboardWebView/components/Startup.tsx
@@ -1,5 +1,5 @@
import * as React from 'react';
-import { SETTINGS_DASHBOARD_OPENONSTART } from '../../constants';
+import { SETTING_DASHBOARD_OPENONSTART } from '../../constants';
import { Messenger } from '@estruyf/vscode/dist/client';
import { DashboardMessage } from '../DashboardMessage';
import { Settings } from '../models/Settings';
@@ -13,7 +13,7 @@ export const Startup: React.FunctionComponent = ({settings}: Reac
const onChange = (e: React.ChangeEvent) => {
setIsChecked(e.target.checked);
- Messenger.send(DashboardMessage.updateSetting, { name: SETTINGS_DASHBOARD_OPENONSTART, value: e.target.checked });
+ Messenger.send(DashboardMessage.updateSetting, { name: SETTING_DASHBOARD_OPENONSTART, value: e.target.checked });
};
React.useEffect(() => {
diff --git a/src/dashboardWebView/constants/SortOption.ts b/src/dashboardWebView/constants/SortOption.ts
index 71c676d4..2e5ef496 100644
--- a/src/dashboardWebView/constants/SortOption.ts
+++ b/src/dashboardWebView/constants/SortOption.ts
@@ -1,4 +1,6 @@
export enum SortOption {
+ PublishedAsc = "PublishedAsc",
+ PublishedDesc = "PublishedDesc",
LastModifiedAsc = "LastModifiedAsc",
LastModifiedDesc = "LastModifiedDesc",
FileNameAsc = "FileNameAsc",
diff --git a/src/dashboardWebView/hooks/useMessages.tsx b/src/dashboardWebView/hooks/useMessages.tsx
index fec94a32..a5213550 100644
--- a/src/dashboardWebView/hooks/useMessages.tsx
+++ b/src/dashboardWebView/hooks/useMessages.tsx
@@ -3,13 +3,13 @@ import { useRecoilState } from 'recoil';
import { DashboardCommand } from '../DashboardCommand';
import { DashboardMessage } from '../DashboardMessage';
import { Page } from '../models/Page';
-import { DashboardViewAtom, SettingsAtom, ViewDataAtom } from '../state';
+import { DashboardViewAtom, LoadingAtom, SettingsAtom, ViewDataAtom } from '../state';
import { Messenger } from '@estruyf/vscode/dist/client';
import { EventData } from '@estruyf/vscode/dist/models';
import { NavigationType } from '../models';
export default function useMessages() {
- const [loading, setLoading] = useState(false);
+ const [loading, setLoading] = useRecoilState(LoadingAtom);
const [pages, setPages] = useState([]);
const [settings, setSettings] = useRecoilState(SettingsAtom);
const [viewData, setViewData] = useRecoilState(ViewDataAtom);
@@ -28,6 +28,8 @@ export default function useMessages() {
setView(NavigationType.Contents);
} else if (message.data.data?.type === NavigationType.Data) {
setView(NavigationType.Data);
+ } else if (message.data.data?.type === NavigationType.Snippets) {
+ setView(NavigationType.Snippets);
}
break;
case DashboardCommand.settings:
diff --git a/src/dashboardWebView/hooks/usePages.tsx b/src/dashboardWebView/hooks/usePages.tsx
index 1fd405c2..ad73bff8 100644
--- a/src/dashboardWebView/hooks/usePages.tsx
+++ b/src/dashboardWebView/hooks/usePages.tsx
@@ -13,7 +13,9 @@ const fuseOptions: Fuse.IFuseOptions = {
{ name: 'title', weight: 0.8 },
{ name: 'slug', weight: 0.8 },
{ name: 'description', weight: 0.5 }
- ]
+ ],
+ includeScore: true,
+ threshold: 0.1
};
export default function usePages(pages: Page[]) {
@@ -73,8 +75,12 @@ export default function usePages(pages: Page[]) {
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) {
diff --git a/src/dashboardWebView/models/NavigationType.ts b/src/dashboardWebView/models/NavigationType.ts
index eba82bf9..2fd1da24 100644
--- a/src/dashboardWebView/models/NavigationType.ts
+++ b/src/dashboardWebView/models/NavigationType.ts
@@ -2,4 +2,5 @@ export enum NavigationType {
Contents = "contents",
Media = "media",
Data = "data",
+ Snippets = "snippets",
}
\ No newline at end of file
diff --git a/src/dashboardWebView/models/Page.ts b/src/dashboardWebView/models/Page.ts
index 4c4a4fd9..a6b88940 100644
--- a/src/dashboardWebView/models/Page.ts
+++ b/src/dashboardWebView/models/Page.ts
@@ -5,6 +5,7 @@ export interface Page {
fmFilePath: string;
fmFileName: string;
fmModified: number;
+ fmPublished: number | null | undefined;
fmDraft: "Draft" | "Published",
fmYear: number | null | undefined;
fmPreviewImage: string;
diff --git a/src/dashboardWebView/models/Settings.ts b/src/dashboardWebView/models/Settings.ts
index 0dca39c7..af1e52e1 100644
--- a/src/dashboardWebView/models/Settings.ts
+++ b/src/dashboardWebView/models/Settings.ts
@@ -1,7 +1,7 @@
import { DataType } from './../../models/DataType';
import { VersionInfo } from '../../models/VersionInfo';
import { ContentFolder } from '../../models/ContentFolder';
-import { ContentType, CustomScript, DraftField, Framework, SortingSetting } from '../../models';
+import { ContentType, CustomScript, DraftField, Framework, Snippets, SortingSetting } from '../../models';
import { SortingOption } from './SortingOption';
import { DashboardViewType } from '.';
import { DataFile } from '../../models/DataFile';
@@ -29,6 +29,8 @@ export interface Settings {
dataFiles: DataFile[] | undefined;
dataTypes: DataType[] | undefined;
isBacker: boolean | undefined;
+ snippets: Snippets | undefined;
+ date: { format: string };
}
export interface DashboardState {
diff --git a/src/extension.ts b/src/extension.ts
index 76aac326..e04ea0bf 100644
--- a/src/extension.ts
+++ b/src/extension.ts
@@ -1,7 +1,7 @@
+import * as vscode from 'vscode';
import { Telemetry } from './helpers/Telemetry';
import { ContentType } from './helpers/ContentType';
import { Dashboard } from './commands/Dashboard';
-import * as vscode from 'vscode';
import { Article, Settings, StatusListener } from './commands';
import { Folders } from './commands/Folders';
import { Preview } from './commands/Preview';
@@ -14,7 +14,7 @@ import { TagType } from './panelWebView/TagType';
import { ExplorerView } from './explorerView/ExplorerView';
import { Extension } from './helpers/Extension';
import { DashboardData } from './models/DashboardData';
-import { Settings as SettingsHelper } from './helpers';
+import { Logger, Settings as SettingsHelper } from './helpers';
import { Content } from './commands/Content';
import ContentProvider from './providers/ContentProvider';
import { Wysiwyg } from './commands/Wysiwyg';
@@ -22,6 +22,7 @@ import { Diagnostics } from './commands/Diagnostics';
import { PagesListener } from './listeners/dashboard';
import { Backers } from './commands/Backers';
import { DataListener, SettingsListener } from './listeners/panel';
+import { NavigationType } from './dashboardWebView/models';
let frontMatterStatusBar: vscode.StatusBarItem;
let statusDebouncer: { (fnc: any, time: number): void; };
@@ -57,7 +58,7 @@ export async function activate(context: vscode.ExtensionContext) {
subscriptions.push(vscode.commands.registerCommand(COMMAND_NAME.dashboard, (data?: DashboardData) => {
Telemetry.send(TelemetryEvent.openContentDashboard);
if (!data) {
- Dashboard.open({ type: "contents" });
+ Dashboard.open({ type: NavigationType.Contents });
} else {
Dashboard.open(data);
}
@@ -65,12 +66,17 @@ export async function activate(context: vscode.ExtensionContext) {
subscriptions.push(vscode.commands.registerCommand(COMMAND_NAME.dashboardMedia, (data?: DashboardData) => {
Telemetry.send(TelemetryEvent.openMediaDashboard);
- Dashboard.open({ type: "media" });
+ Dashboard.open({ type: NavigationType.Media });
+ }));
+
+ subscriptions.push(vscode.commands.registerCommand(COMMAND_NAME.dashboardSnippets, (data?: DashboardData) => {
+ Telemetry.send(TelemetryEvent.openSnippetsDashboard);
+ Dashboard.open({ type: NavigationType.Snippets });
}));
subscriptions.push(vscode.commands.registerCommand(COMMAND_NAME.dashboardData, (data?: DashboardData) => {
Telemetry.send(TelemetryEvent.openDataDashboard);
- Dashboard.open({ type: "data" });
+ Dashboard.open({ type: NavigationType.Data });
}));
subscriptions.push(vscode.commands.registerCommand(COMMAND_NAME.dashboardClose, (data?: DashboardData) => {
@@ -133,7 +139,7 @@ export async function activate(context: vscode.ExtensionContext) {
const toggleDraftCommand = COMMAND_NAME.toggleDraft;
const toggleDraft = vscode.commands.registerCommand(toggleDraftCommand, async () => {
await Article.toggleDraft();
- triggerShowDraftStatus();
+ triggerShowDraftStatus(`toggleDraft`);
});
// Register project folders
@@ -182,22 +188,21 @@ export async function activate(context: vscode.ExtensionContext) {
statusDebouncer = debounceCallback();
// Register listeners that make sure the status bar updates
- subscriptions.push(vscode.window.onDidChangeActiveTextEditor(triggerShowDraftStatus));
- subscriptions.push(vscode.window.onDidChangeTextEditorSelection(triggerShowDraftStatus));
+ subscriptions.push(vscode.window.onDidChangeActiveTextEditor(() => triggerShowDraftStatus(`onDidChangeActiveTextEditor`)));
+ subscriptions.push(vscode.window.onDidChangeTextEditorSelection((e) => {
+ if (e.kind === vscode.TextEditorSelectionChangeKind.Mouse) {
+ triggerShowDraftStatus(`onDidChangeTextEditorSelection`);
+ }
+ }));
// Automatically run the command
- triggerShowDraftStatus();
+ triggerShowDraftStatus(`triggerShowDraftStatus`);
// Listener for file edit changes
subscriptions.push(vscode.workspace.onWillSaveTextDocument(handleAutoDateUpdate));
// Listener for file saves
- subscriptions.push(vscode.workspace.onDidSaveTextDocument((doc: vscode.TextDocument) => {
- if (doc.languageId === 'markdown') {
- // Optimize the list of recently changed files
- DataListener.getFoldersAndFiles();
- }
- }));
+ subscriptions.push(PagesListener.saveFileWatcher());
// Webview for preview
Preview.init();
@@ -206,6 +211,9 @@ export async function activate(context: vscode.ExtensionContext) {
// Inserting an image in Markdown
subscriptions.push(vscode.commands.registerCommand(COMMAND_NAME.insertImage, Article.insertImage));
+ // Inserting a snippet in Markdown
+ subscriptions.push(vscode.commands.registerCommand(COMMAND_NAME.insertSnippet, Article.insertSnippet));
+
// Create the editor experience for bulk scripts
subscriptions.push(vscode.workspace.registerTextDocumentContentProvider(ContentProvider.scheme, new ContentProvider()));
@@ -248,7 +256,8 @@ const handleAutoDateUpdate = (e: vscode.TextDocumentWillSaveEvent) => {
Article.autoUpdate(e);
};
-const triggerShowDraftStatus = () => {
+const triggerShowDraftStatus = (location: string) => {
+ Logger.info(`Triggering draft status update: ${location}`);
statusDebouncer(() => { StatusListener.verify(frontMatterStatusBar, collection); }, 1000);
};
diff --git a/src/helpers/ArticleHelper.ts b/src/helpers/ArticleHelper.ts
index 71ab9e66..7e6ddf5d 100644
--- a/src/helpers/ArticleHelper.ts
+++ b/src/helpers/ArticleHelper.ts
@@ -2,7 +2,7 @@ import { MarkdownFoldingProvider } from './../providers/MarkdownFoldingProvider'
import { DEFAULT_CONTENT_TYPE, DEFAULT_CONTENT_TYPE_NAME } from './../constants/ContentType';
import * as vscode from 'vscode';
import * as fs from "fs";
-import { DefaultFields, SETTINGS_CONTENT_DEFAULT_FILETYPE, SETTINGS_CONTENT_PLACEHOLDERS, SETTINGS_CONTENT_SUPPORTED_FILETYPES, SETTINGS_FILE_PRESERVE_CASING, SETTING_COMMA_SEPARATED_FIELDS, SETTING_DATE_FIELD, SETTING_DATE_FORMAT, SETTING_INDENT_ARRAY, SETTING_REMOVE_QUOTES, SETTING_SITE_BASEURL, SETTING_TAXONOMY_CONTENT_TYPES, SETTING_TEMPLATES_PREFIX } from '../constants';
+import { DefaultFields, SETTING_CONTENT_DEFAULT_FILETYPE, SETTING_CONTENT_PLACEHOLDERS, SETTING_CONTENT_SUPPORTED_FILETYPES, SETTING_FILE_PRESERVE_CASING, SETTING_COMMA_SEPARATED_FIELDS, SETTING_DATE_FIELD, SETTING_DATE_FORMAT, SETTING_INDENT_ARRAY, SETTING_REMOVE_QUOTES, SETTING_SITE_BASEURL, SETTING_TAXONOMY_CONTENT_TYPES, SETTING_TEMPLATES_PREFIX, SETTING_MODIFIED_FIELD } from '../constants';
import { DumpOptions } from 'js-yaml';
import { FrontMatterParser, ParsedFrontMatter } from '../parsers';
import { Extension, Logger, Settings, SlugHelper } from '.';
@@ -20,6 +20,7 @@ import { DEFAULT_FILE_TYPES } from '../constants/DefaultFileTypes';
import { fromMarkdown } from 'mdast-util-from-markdown';
import { Link, Parent } from 'mdast-util-from-markdown/lib';
import { Content } from 'mdast';
+import { processKnownPlaceholders } from './PlaceholderHelper';
export class ArticleHelper {
private static notifiedFiles: string[] = [];
@@ -141,9 +142,9 @@ export class ArticleHelper {
/**
* Checks if the current file is a markdown file
*/
- public static isMarkdownFile(document: vscode.TextDocument | undefined | null = null) {
+ public static isSupportedFile(document: vscode.TextDocument | undefined | null = null) {
const supportedLanguages = ["markdown", "mdx"];
- const fileTypes = Settings.get(SETTINGS_CONTENT_SUPPORTED_FILETYPES);
+ const fileTypes = Settings.get(SETTING_CONTENT_SUPPORTED_FILETYPES);
const supportedFileExtensions = fileTypes ? fileTypes.map(f => f.startsWith(`.`) ? f : `.${f}`) : DEFAULT_FILE_TYPES;
const languageId = document?.languageId?.toLowerCase();
const isSupportedLanguage = languageId && supportedLanguages.includes(languageId);
@@ -167,12 +168,12 @@ export class ArticleHelper {
* Get date from front matter
*/
public static getDate(article: ParsedFrontMatter | null) {
- if (!article) {
+ if (!article || !article.data) {
return;
}
const dateFormat = Settings.get(SETTING_DATE_FORMAT) as string;
- const dateField = Settings.get(SETTING_DATE_FIELD) as string || DefaultFields.PublishingDate;
+ const dateField = ArticleHelper.getPublishDateField(article) || DefaultFields.PublishingDate;
if (typeof article.data[dateField] !== "undefined") {
if (dateFormat && typeof dateFormat === "string") {
@@ -186,12 +187,52 @@ export class ArticleHelper {
return;
}
+ /**
+ * Retrieve the publishing date field name
+ * @param article
+ * @returns
+ */
+ public static getPublishDateField(article: ParsedFrontMatter | null) {
+ if (!article || !article.data) {
+ return;
+ }
+
+ const articleCt = ArticleHelper.getContentType(article.data);
+ const pubDateField = articleCt.fields.find(f => f.isPublishDate);
+
+ return pubDateField?.name || Settings.get(SETTING_DATE_FIELD) as string || DefaultFields.PublishingDate;
+ }
+
+ /**
+ * Retrieve the publishing date field name
+ * @param article
+ * @returns
+ */
+ public static getModifiedDateField(article: ParsedFrontMatter | null) {
+ if (!article || !article.data) {
+ return;
+ }
+
+ const articleCt = ArticleHelper.getContentType(article.data);
+ const modDateField = articleCt.fields.find(f => f.isModifiedDate);
+
+ return modDateField?.name || Settings.get(SETTING_MODIFIED_FIELD) as string || DefaultFields.LastModified;
+ }
+
+ /**
+ * Retrieve all the content types
+ * @returns
+ */
+ public static getContentTypes() {
+ return Settings.get(SETTING_TAXONOMY_CONTENT_TYPES) || [DEFAULT_CONTENT_TYPE];
+ }
+
/**
* Retrieve the content type of the current file
* @param updatedMetadata
*/
public static getContentType(metadata: { [field: string]: string; }): ContentType {
- const contentTypes = Settings.get(SETTING_TAXONOMY_CONTENT_TYPES);
+ const contentTypes = ArticleHelper.getContentTypes();
if (!contentTypes || !metadata) {
return DEFAULT_CONTENT_TYPE;
@@ -201,7 +242,16 @@ export class ArticleHelper {
if (!contentType) {
contentType = contentTypes.find(ct => ct.name === DEFAULT_CONTENT_TYPE_NAME);
}
- return contentType || DEFAULT_CONTENT_TYPE;
+
+ if (contentType) {
+ if (!contentType.fields) {
+ contentType.fields = DEFAULT_CONTENT_TYPE.fields;
+ }
+
+ return contentType;
+ }
+
+ return DEFAULT_CONTENT_TYPE;
}
/**
@@ -227,7 +277,7 @@ export class ArticleHelper {
* @returns
*/
public static sanitize(value: string): string {
- const preserveCasing = Settings.get(SETTINGS_FILE_PRESERVE_CASING) as boolean;
+ const preserveCasing = Settings.get(SETTING_FILE_PRESERVE_CASING) as boolean;
return sanitize((preserveCasing ? value : value.toLowerCase()).replace(/ /g, "-"));
}
@@ -240,7 +290,7 @@ export class ArticleHelper {
*/
public static createContent(contentType: ContentType | undefined, folderPath: string, titleValue: string, fileExtension?: string): string | undefined {
const prefix = Settings.get(SETTING_TEMPLATES_PREFIX);
- const fileType = Settings.get(SETTINGS_CONTENT_DEFAULT_FILETYPE);
+ const fileType = Settings.get(SETTING_CONTENT_DEFAULT_FILETYPE);
// Name of the file or folder to create
const sanitizedName = ArticleHelper.sanitize(titleValue);
@@ -281,6 +331,7 @@ export class ArticleHelper {
* @returns
*/
public static updatePlaceholders(data: any, title: string) {
+ const dateFormat = Settings.get(SETTING_DATE_FORMAT) as string;
const fmData = Object.assign({}, data);
for (const fieldName of Object.keys(fmData)) {
@@ -294,40 +345,13 @@ export class ArticleHelper {
fmData[fieldName] = SlugHelper.createSlug(title);
}
- fmData[fieldName] = this.processKnownPlaceholders(fmData[fieldName], title);
+ fmData[fieldName] = processKnownPlaceholders(fmData[fieldName], title, dateFormat);
fmData[fieldName] = this.processCustomPlaceholders(fmData[fieldName], title);
}
return fmData;
}
- /**
- * Replace the known placeholders
- * @param value
- * @param title
- * @returns
- */
- public static processKnownPlaceholders(value: string, title: string) {
- if (value && typeof value === "string") {
- if (value.includes("{{title}}")) {
- const regex = new RegExp("{{title}}", "g");
- value = value.replace(regex, title);
- }
-
- if (value.includes("{{slug}}")) {
- const regex = new RegExp("{{slug}}", "g");
- value = value.replace(regex, SlugHelper.createSlug(title) || "");
- }
-
- if (value.includes("{{now}}")) {
- const regex = new RegExp("{{now}}", "g");
- value = value.replace(regex, Article.formatDate(new Date()));
- }
- }
-
- return value;
- }
-
/**
* Replace the custom placeholders
* @param value
@@ -336,12 +360,13 @@ export class ArticleHelper {
*/
public static processCustomPlaceholders(value: string, title: string) {
if (value && typeof value === "string") {
- const placeholders = Settings.get<{id: string, value: string}[]>(SETTINGS_CONTENT_PLACEHOLDERS);
+ const dateFormat = Settings.get(SETTING_DATE_FORMAT) as string;
+ const placeholders = Settings.get<{id: string, value: string}[]>(SETTING_CONTENT_PLACEHOLDERS);
if (placeholders && placeholders.length > 0) {
for (const placeholder of placeholders) {
if (value.includes(`{{${placeholder.id}}}`)) {
const regex = new RegExp(`{{${placeholder.id}}}`, "g");
- const updatedValue = this.processKnownPlaceholders(placeholder.value, title);
+ const updatedValue = processKnownPlaceholders(placeholder.value, title, dateFormat);
value = value.replace(regex, updatedValue);
}
}
@@ -362,7 +387,7 @@ export class ArticleHelper {
return null;
}
- if (!ArticleHelper.isMarkdownFile()) {
+ if (!ArticleHelper.isSupportedFile()) {
return null;
}
@@ -481,7 +506,7 @@ export class ArticleHelper {
}
}];
- Logger.error(error.message);
+ Logger.error(`ArticleHelper::parseFile: ${fileName} - ${error.message}`);
const editor = window.activeTextEditor;
if (editor?.document.uri) {
diff --git a/src/helpers/ContentType.ts b/src/helpers/ContentType.ts
index 89693f95..c3fafed3 100644
--- a/src/helpers/ContentType.ts
+++ b/src/helpers/ContentType.ts
@@ -1,6 +1,6 @@
import { PagesListener } from './../listeners/dashboard';
import { ArticleHelper, Settings } from ".";
-import { SETTINGS_CONTENT_DRAFT_FIELD, SETTING_TAXONOMY_CONTENT_TYPES, TelemetryEvent } from "../constants";
+import { SETTING_CONTENT_DRAFT_FIELD, SETTING_DATE_FORMAT, SETTING_TAXONOMY_CONTENT_TYPES, TelemetryEvent } from "../constants";
import { ContentType as IContentType, DraftField, Field } from '../models';
import { Uri, commands } from 'vscode';
import { Folders } from "../commands/Folders";
@@ -9,6 +9,7 @@ import { writeFileSync } from "fs";
import { Notifications } from "./Notifications";
import { DEFAULT_CONTENT_TYPE_NAME } from "../constants/ContentType";
import { Telemetry } from './Telemetry';
+import { processKnownPlaceholders } from './PlaceholderHelper';
export class ContentType {
@@ -18,7 +19,7 @@ export class ContentType {
* @returns
*/
public static getDraftField() {
- const draftField = Settings.get(SETTINGS_CONTENT_DRAFT_FIELD);
+ const draftField = Settings.get(SETTING_CONTENT_DRAFT_FIELD);
if (draftField) {
return draftField;
}
@@ -140,10 +141,11 @@ export class ContentType {
private static processFields(obj: IContentType | Field, titleValue: string, data: any) {
if (obj.fields) {
+ const dateFormat = Settings.get(SETTING_DATE_FORMAT) as string;
for (const field of obj.fields) {
if (field.name === "title") {
if (field.default) {
- data[field.name] = ArticleHelper.processKnownPlaceholders(field.default, titleValue);
+ data[field.name] = processKnownPlaceholders(field.default, titleValue, dateFormat);
data[field.name] = ArticleHelper.processCustomPlaceholders(data[field.name], titleValue);
} else {
data[field.name] = titleValue;
@@ -152,7 +154,7 @@ export class ContentType {
if (field.type === "fields") {
data[field.name] = this.processFields(field, titleValue, {});
} else {
- data[field.name] = field.default ? ArticleHelper.processKnownPlaceholders(field.default, titleValue) : "";
+ data[field.name] = field.default ? processKnownPlaceholders(field.default, titleValue, dateFormat) : "";
data[field.name] = field.default ? ArticleHelper.processCustomPlaceholders(data[field.name], titleValue) : "";
}
}
diff --git a/src/helpers/CustomScript.ts b/src/helpers/CustomScript.ts
index 5ac4b9ea..719b3915 100644
--- a/src/helpers/CustomScript.ts
+++ b/src/helpers/CustomScript.ts
@@ -1,6 +1,6 @@
import { CustomScript as ICustomScript, ScriptType } from '../models/PanelSettings';
import { window, env as vscodeEnv, ProgressLocation } from 'vscode';
-import { ArticleHelper } from '.';
+import { ArticleHelper, Telemetry } from '.';
import { Folders } from '../commands/Folders';
import { exec } from 'child_process';
import * as os from 'os';
@@ -10,6 +10,7 @@ import ContentProvider from '../providers/ContentProvider';
import { Dashboard } from '../commands/Dashboard';
import { DashboardCommand } from '../dashboardWebView/DashboardCommand';
import { ParsedFrontMatter } from '../parsers';
+import { TelemetryEvent } from '../constants/TelemetryEvent';
export class CustomScript {
@@ -20,8 +21,12 @@ export class CustomScript {
const wsPath = wsFolder.fsPath;
if (script.type === ScriptType.MediaFile || script.type === ScriptType.MediaFolder) {
+ Telemetry.send(TelemetryEvent.runMediaScript);
+
CustomScript.runMediaScript(wsPath, path, script);
} else {
+ Telemetry.send(TelemetryEvent.runCustomScript);
+
if (script.bulk) {
// Run script on all files
CustomScript.bulkRun(wsPath, script);
diff --git a/src/helpers/DashboardSettings.ts b/src/helpers/DashboardSettings.ts
index de5faaaa..bc966717 100644
--- a/src/helpers/DashboardSettings.ts
+++ b/src/helpers/DashboardSettings.ts
@@ -2,9 +2,9 @@ import { basename, join } from "path";
import { workspace } from "vscode";
import { Folders } from "../commands/Folders";
import { Template } from "../commands/Template";
-import { CONTEXT, ExtensionState, SETTINGS_CONTENT_DRAFT_FIELD, SETTINGS_CONTENT_SORTING, SETTINGS_CONTENT_SORTING_DEFAULT, SETTINGS_CONTENT_STATIC_FOLDER, SETTINGS_DASHBOARD_MEDIA_SNIPPET, SETTINGS_DASHBOARD_OPENONSTART, SETTINGS_DATA_FILES, SETTINGS_DATA_FOLDERS, SETTINGS_DATA_TYPES, SETTINGS_FRAMEWORK_ID, SETTINGS_MEDIA_SORTING_DEFAULT, SETTING_CUSTOM_SCRIPTS, SETTING_TAXONOMY_CONTENT_TYPES } from "../constants";
+import { CONTEXT, ExtensionState, SETTING_CONTENT_DRAFT_FIELD, SETTING_CONTENT_SORTING, SETTING_CONTENT_SORTING_DEFAULT, SETTING_CONTENT_STATIC_FOLDER, SETTING_DASHBOARD_MEDIA_SNIPPET, 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 } from "../constants";
import { DashboardViewType, SortingOption, Settings as ISettings } from "../dashboardWebView/models";
-import { CustomScript, DraftField, ScriptType, SortingSetting, TaxonomyType } from "../models";
+import { CustomScript, DraftField, ScriptType, Snippets, SortingSetting, TaxonomyType } from "../models";
import { DataFile } from "../models/DataFile";
import { DataFolder } from "../models/DataFolder";
import { DataType } from "../models/DataType";
@@ -23,35 +23,39 @@ export class DashboardSettings {
return {
beta: ext.isBetaVersion(),
wsFolder: wsFolder ? wsFolder.fsPath : '',
- staticFolder: Settings.get(SETTINGS_CONTENT_STATIC_FOLDER),
+ staticFolder: Settings.get(SETTING_CONTENT_STATIC_FOLDER),
folders: Folders.get(),
initialized: isInitialized,
tags: Settings.getTaxonomy(TaxonomyType.Tag),
categories: Settings.getTaxonomy(TaxonomyType.Category),
- openOnStart: Settings.get(SETTINGS_DASHBOARD_OPENONSTART),
+ openOnStart: Settings.get(SETTING_DASHBOARD_OPENONSTART),
versionInfo: ext.getVersion(),
pageViewType: await ext.getState(ExtensionState.PagesView, "workspace"),
- mediaSnippet: Settings.get(SETTINGS_DASHBOARD_MEDIA_SNIPPET) || [],
+ mediaSnippet: Settings.get(SETTING_DASHBOARD_MEDIA_SNIPPET) || [],
contentTypes: Settings.get(SETTING_TAXONOMY_CONTENT_TYPES) || [],
- draftField: Settings.get(SETTINGS_CONTENT_DRAFT_FIELD),
- customSorting: Settings.get(SETTINGS_CONTENT_SORTING),
+ draftField: Settings.get(SETTING_CONTENT_DRAFT_FIELD),
+ customSorting: Settings.get(SETTING_CONTENT_SORTING),
contentFolders: Folders.get(),
- crntFramework: Settings.get(SETTINGS_FRAMEWORK_ID),
+ crntFramework: Settings.get(SETTING_FRAMEWORK_ID),
framework: (!isInitialized && wsFolder) ? FrameworkDetector.get(wsFolder.fsPath) : null,
scripts: (Settings.get(SETTING_CUSTOM_SCRIPTS) || []),
+ date: {
+ format: Settings.get(SETTING_DATE_FORMAT) || ""
+ },
dashboardState: {
contents: {
sorting: await ext.getState(ExtensionState.Dashboard.Contents.Sorting, "workspace"),
- defaultSorting: Settings.get(SETTINGS_CONTENT_SORTING_DEFAULT)
+ defaultSorting: Settings.get(SETTING_CONTENT_SORTING_DEFAULT)
},
media: {
sorting: await ext.getState(ExtensionState.Dashboard.Media.Sorting, "workspace"),
- defaultSorting: Settings.get(SETTINGS_MEDIA_SORTING_DEFAULT),
+ defaultSorting: Settings.get(SETTING_MEDIA_SORTING_DEFAULT),
selectedFolder: await ext.getState(ExtensionState.SelectedFolder, "workspace")
}
},
dataFiles: await this.getDataFiles(),
- dataTypes: Settings.get(SETTINGS_DATA_TYPES),
+ dataTypes: Settings.get(SETTING_DATA_TYPES),
+ snippets: Settings.get(SETTING_CONTENT_SNIPPETS),
isBacker: await ext.getState(CONTEXT.backer, 'global')
} as ISettings
}
@@ -62,8 +66,8 @@ export class DashboardSettings {
*/
private static async getDataFiles(): Promise {
const wsPath = Folders.getWorkspaceFolder()?.fsPath;
- const files = Settings.get(SETTINGS_DATA_FILES);
- const folders = Settings.get(SETTINGS_DATA_FOLDERS);
+ const files = Settings.get(SETTING_DATA_FILES);
+ const folders = Settings.get(SETTING_DATA_FOLDERS);
let clonedFiles = Object.assign([], files);
if (folders) {
diff --git a/src/helpers/Extension.ts b/src/helpers/Extension.ts
index eba367b0..a53b040f 100644
--- a/src/helpers/Extension.ts
+++ b/src/helpers/Extension.ts
@@ -1,11 +1,9 @@
-import { existsSync, renameSync } from "fs";
-import { basename, join } from "path";
+import { basename } from "path";
import { extensions, Uri, ExtensionContext, window, workspace, commands, ExtensionMode, DiagnosticCollection, languages } from "vscode";
-import { Folders, WORKSPACE_PLACEHOLDER } from "../commands/Folders";
-import { EXTENSION_NAME, GITHUB_LINK, SETTINGS_CONTENT_FOLDERS, SETTINGS_CONTENT_PAGE_FOLDERS, SETTING_DATE_FIELD, SETTING_MODIFIED_FIELD, SETTING_SEO_DESCRIPTION_FIELD, SETTING_TAXONOMY_CONTENT_TYPES, DEFAULT_CONTENT_TYPE_NAME, EXTENSION_BETA_ID, EXTENSION_ID, ExtensionState, DefaultFields, LocalStore, SETTING_TEMPLATES_FOLDER } from "../constants";
-import { ContentType } from "../models";
+import { Folders } from "../commands/Folders";
+import { EXTENSION_NAME, GITHUB_LINK, SETTING_DATE_FIELD, SETTING_MODIFIED_FIELD, EXTENSION_BETA_ID, EXTENSION_ID, ExtensionState, CONFIG_KEY, SETTING_CONTENT_PAGE_FOLDERS } from "../constants";
+import { ContentFolder } from "../models";
import { Notifications } from "./Notifications";
-import { parseWinPath } from "./parseWinPath";
import { Settings } from "./SettingsHelper";
@@ -135,114 +133,53 @@ export class Extension {
const minor = parseInt(version[1]);
const patch = parseInt(version[2]);
- // Migration to version 3.1.0
- if (major < 3 || (major === 3 && minor < 1)) {
- const folders = Settings.get(SETTINGS_CONTENT_FOLDERS);
- if (folders && folders.length > 0) {
- const workspace = Folders.getWorkspaceFolder();
- const projectFolder = basename(workspace?.fsPath || "");
-
- const paths = folders.map((folder: any) => ({
- ...folder,
- path: `${WORKSPACE_PLACEHOLDER}${folder.fsPath.split(projectFolder).slice(1).join('')}`.split('\\').join('/')
- }));
-
- await Settings.update(SETTINGS_CONTENT_PAGE_FOLDERS, paths);
- }
- }
// Create team settings
if (Settings.hasSettings()) {
Settings.createTeamSettings();
}
- // Migration to version 4.0.0
- if (major < 4) {
- const dateField = Settings.get(SETTING_DATE_FIELD);
- const lastModField = Settings.get(SETTING_MODIFIED_FIELD);
- const description = Settings.get(SETTING_SEO_DESCRIPTION_FIELD);
- const contentTypes = Settings.get(SETTING_TAXONOMY_CONTENT_TYPES);
+ const hideDateDeprecation = await Extension.getInstance().getState(ExtensionState.Updates.v7_0_0.dateFields, "workspace");
+ if (!hideDateDeprecation) {
+ // Migration scripts can be written here
+ const publishField = Settings.inspect(SETTING_DATE_FIELD);
+ const modifiedField = Settings.inspect(SETTING_MODIFIED_FIELD);
- if (contentTypes) {
- let needsUpdate = false;
- let defaultContentType = contentTypes.find(ct => ct.name === DEFAULT_CONTENT_TYPE_NAME);
-
- // Check if fields need to be changed for the default content type
- if (defaultContentType) {
- if (dateField && dateField !== DefaultFields.PublishingDate) {
- const newDateField = defaultContentType.fields.find(f => f.name === dateField);
-
- if (!newDateField) {
- defaultContentType.fields = defaultContentType.fields.filter(f => f.name !== DefaultFields.PublishingDate);
- defaultContentType.fields.push({
- title: dateField,
- name: dateField,
- type: "datetime"
- });
- needsUpdate = true;
- }
+ // Check for extension deprecations
+ if (publishField?.workspaceValue ||
+ publishField?.globalValue ||
+ publishField?.teamValue ||
+ modifiedField?.workspaceValue ||
+ modifiedField?.globalValue ||
+ modifiedField?.teamValue) {
+ Notifications.warning(`The "${CONFIG_KEY}.${SETTING_DATE_FIELD}" and "${CONFIG_KEY}.${SETTING_MODIFIED_FIELD}" settings have been deprecated. Please use the "isPublishDate" and "isModifiedDate" datetime field properties instead.`, "Hide", "See migration guide").then(async (value) => {
+ if (value === "See migration guide") {
+ const isProd = this.isProductionMode;
+ commands.executeCommand("vscode.open", Uri.parse(`https://${isProd ? '' : 'beta.'}frontmatter.codes/docs/troubleshooting#publish-and-modified-date-migration`));
+ await Extension.getInstance().setState(ExtensionState.Updates.v7_0_0.dateFields, true, "workspace");
+ } else if (value === "Hide") {
+ await Extension.getInstance().setState(ExtensionState.Updates.v7_0_0.dateFields, true, "workspace");
}
-
- if (lastModField && lastModField !== DefaultFields.LastModified) {
- const newModField = defaultContentType.fields.find(f => f.name === lastModField);
-
- if (!newModField) {
- defaultContentType.fields = defaultContentType.fields.filter(f => f.name !== DefaultFields.LastModified);
- defaultContentType.fields.push({
- title: lastModField,
- name: lastModField,
- type: "datetime"
- });
- needsUpdate = true;
- }
- }
-
- if (description && description !== DefaultFields.Description) {
- const newDescField = defaultContentType.fields.find(f => f.name === description);
-
- if (!newDescField) {
- defaultContentType.fields = defaultContentType.fields.filter(f => f.name !== DefaultFields.Description);
- defaultContentType.fields.push({
- title: description,
- name: description,
- type: "string"
- });
- needsUpdate = true;
- }
- }
-
- if (needsUpdate) {
- await Settings.update(SETTING_TAXONOMY_CONTENT_TYPES, contentTypes, true);
- }
- }
+ });
}
}
- // Migration to version 5
- if (major <= 5) {
- const isMoved = await Extension.getInstance().getState(ExtensionState.MoveTemplatesFolder);
- if (!isMoved) {
- const wsFolder= Folders.getWorkspaceFolder();
- if (wsFolder) {
- const templateFolder = join(parseWinPath(wsFolder.fsPath), `.templates`);
- if (existsSync(templateFolder)) {
- window.showInformationMessage(`Would you like to move your ".templates" folder to the new ".frontmatter" folder?`, 'Yes', 'No').then(async (result) => {
- if (result === "Yes") {
- const newFolderPath = join(parseWinPath(wsFolder.fsPath), LocalStore.rootFolder, LocalStore.templatesFolder);
- renameSync(templateFolder, newFolderPath);
- commands.executeCommand(`workbench.action.reloadWindow`);
- Settings.update(SETTING_TEMPLATES_FOLDER, undefined, true);
- Settings.update(SETTING_TEMPLATES_FOLDER, undefined);
- } else if (result === "No") {
- Settings.update(SETTING_TEMPLATES_FOLDER, `.templates`, true);
- }
-
- if (result === "No" || result === "Yes") {
- Extension.getInstance().setState(ExtensionState.MoveTemplatesFolder, true);
- }
- });
+ if (major < 7) {
+ const contentFolders: ContentFolder[] = Settings.get(SETTING_CONTENT_PAGE_FOLDERS) as ContentFolder[];
+ const wsFolder = Folders.getWorkspaceFolder();
+ if (wsFolder) {
+ let update = false;
+
+ for (const cFolder of contentFolders) {
+ if (cFolder.path.indexOf(wsFolder.fsPath) !== -1) {
+ update = true;
+ cFolder.path = Folders.relWsFolder(cFolder, wsFolder);
}
}
+
+ if (update) {
+ Folders.update(contentFolders);
+ }
}
}
}
diff --git a/src/helpers/ImageHelper.ts b/src/helpers/ImageHelper.ts
index d5567e51..73c8be0a 100644
--- a/src/helpers/ImageHelper.ts
+++ b/src/helpers/ImageHelper.ts
@@ -5,7 +5,7 @@ import { Field } from '../models';
import { existsSync } from 'fs';
import { Folders } from '../commands/Folders';
import { Settings } from './SettingsHelper';
-import { SETTINGS_CONTENT_STATIC_FOLDER } from '../constants';
+import { SETTING_CONTENT_STATIC_FOLDER } from '../constants';
import { parseWinPath } from './parseWinPath';
export class ImageHelper {
@@ -51,7 +51,7 @@ export class ImageHelper {
*/
public static relToAbs(filePath: string, value: string) {
const wsFolder = Folders.getWorkspaceFolder();
- const staticFolder = Settings.get(SETTINGS_CONTENT_STATIC_FOLDER);
+ const staticFolder = Settings.get(SETTING_CONTENT_STATIC_FOLDER);
const staticPath = join(parseWinPath(wsFolder?.fsPath || ""), staticFolder || "", value);
const contentFolderPath = filePath ? join(dirname(filePath), value) : null;
@@ -73,7 +73,7 @@ export class ImageHelper {
*/
public static absToRel(imgValue: string) {
const wsFolder = Folders.getWorkspaceFolder();
- const staticFolder = Settings.get(SETTINGS_CONTENT_STATIC_FOLDER);
+ const staticFolder = Settings.get(SETTING_CONTENT_STATIC_FOLDER);
let relPath = imgValue || "";
if (imgValue) {
diff --git a/src/helpers/MediaHelpers.ts b/src/helpers/MediaHelpers.ts
index 3383977b..995142c4 100644
--- a/src/helpers/MediaHelpers.ts
+++ b/src/helpers/MediaHelpers.ts
@@ -1,17 +1,17 @@
import { decodeBase64Image, Extension, MediaLibrary, Notifications, parseWinPath, Settings, Sorting } from ".";
import { Dashboard } from "../commands/Dashboard";
import { Folders } from "../commands/Folders";
-import { ExtensionState, HOME_PAGE_NAVIGATION_ID, SETTINGS_CONTENT_STATIC_FOLDER } from "../constants";
+import { DEFAULT_CONTENT_TYPE, ExtensionState, HOME_PAGE_NAVIGATION_ID, SETTING_CONTENT_STATIC_FOLDER } from "../constants";
import { SortingOption } from "../dashboardWebView/models";
import { MediaInfo, MediaPaths, SortOrder, SortType } from "../models";
-import { basename, extname, join, parse, dirname } from "path";
+import { basename, extname, join, parse, dirname, relative } from "path";
import { existsSync, readdirSync, statSync, unlinkSync, writeFileSync } from "fs";
import { commands, Uri, workspace, window, Position } from "vscode";
import imageSize from "image-size";
import { EditorHelper } from "@estruyf/vscode";
-import { ExplorerView } from "../explorerView/ExplorerView";
import { SortOption } from "../dashboardWebView/constants/SortOption";
import { DataListener, MediaListener } from "../listeners/panel";
+import { ArticleHelper } from "./ArticleHelper";
export class MediaHelpers {
@@ -26,11 +26,15 @@ export class MediaHelpers {
*/
public static async getMedia(page: number = 0, requestedFolder: string = '', sort: SortingOption | null = null) {
const wsFolder = Folders.getWorkspaceFolder();
- const staticFolder = Settings.get(SETTINGS_CONTENT_STATIC_FOLDER);
+ const staticFolder = Settings.get(SETTING_CONTENT_STATIC_FOLDER);
const contentFolders = Folders.get();
const viewData = Dashboard.viewData;
let selectedFolder = requestedFolder;
+ // Check if there are any content types that are set to use page bundles
+ const contentTypes = ArticleHelper.getContentTypes();
+ const pageBundleContentTypes = contentTypes.filter(ct => ct.pageBundle);
+
const ext = Extension.getInstance();
const crntSort = sort === null ? await ext.getState(ExtensionState.Dashboard.Media.Sorting, "workspace") : sort;
@@ -80,15 +84,17 @@ export class MediaHelpers {
allMedia = [...media];
}
- if (contentFolders && wsFolder) {
- for (let i = 0; i < contentFolders.length; i++) {
- const contentFolder = contentFolders[i];
- const relFolderPath = contentFolder.path.substring(wsFolder.fsPath.length + 1);
- const folderSearch = relSelectedFolderPath ? join(relSelectedFolderPath, '/*') : join(relFolderPath, '/*');
- const files = await workspace.findFiles(folderSearch);
- const media = await MediaHelpers.updateMediaData(MediaHelpers.filterMedia(files));
-
- allMedia = [...allMedia, ...media];
+ if (pageBundleContentTypes.length > 0) {
+ if (contentFolders && wsFolder) {
+ for (let i = 0; i < contentFolders.length; i++) {
+ const contentFolder = contentFolders[i];
+ const relFolderPath = contentFolder.path.substring(wsFolder.fsPath.length + 1);
+ const folderSearch = relSelectedFolderPath ? join(relSelectedFolderPath, '/*') : join(relFolderPath, '/*');
+ const files = await workspace.findFiles(folderSearch);
+ const media = await MediaHelpers.updateMediaData(MediaHelpers.filterMedia(files));
+
+ allMedia = [...allMedia, ...media];
+ }
}
}
}
@@ -145,11 +151,13 @@ export class MediaHelpers {
allFolders = readdirSync(selectedFolder, { withFileTypes: true }).filter(dir => dir.isDirectory()).map(dir => parseWinPath(join(selectedFolder, dir.name)));
}
} else {
- for (const contentFolder of contentFolders) {
- const contentPath = contentFolder.path;
- if (contentPath && existsSync(contentPath)) {
- const subFolders = readdirSync(contentPath, { withFileTypes: true }).filter(dir => dir.isDirectory()).map(dir => parseWinPath(join(contentPath, dir.name)));
- allContentFolders = [...allContentFolders, ...subFolders];
+ if (pageBundleContentTypes.length > 0) {
+ for (const contentFolder of contentFolders) {
+ const contentPath = contentFolder.path;
+ if (contentPath && existsSync(contentPath)) {
+ const subFolders = readdirSync(contentPath, { withFileTypes: true }).filter(dir => dir.isDirectory()).map(dir => parseWinPath(join(contentPath, dir.name)));
+ allContentFolders = [...allContentFolders, ...subFolders];
+ }
}
}
@@ -199,7 +207,7 @@ export class MediaHelpers {
public static async saveFile({fileName, contents, folder}: { fileName: string; contents: string; folder: string | null }) {
if (fileName && contents) {
const wsFolder = Folders.getWorkspaceFolder();
- const staticFolder = Settings.get(SETTINGS_CONTENT_STATIC_FOLDER);
+ const staticFolder = Settings.get(SETTING_CONTENT_STATIC_FOLDER);
const wsPath = wsFolder ? wsFolder.fsPath : "";
let absFolderPath = join(wsPath, staticFolder || "");
@@ -266,32 +274,46 @@ export class MediaHelpers {
await EditorHelper.showFile(data.file);
Dashboard.resetViewData();
-
- const extensionUri = Extension.getInstance().extensionPath;
- const panel = ExplorerView.getInstance(extensionUri);
+ const editor = window.activeTextEditor;
+ const wsFolder = Folders.getWorkspaceFolder();
+ const filePath = data.file;
+ let imgPath = data.image;
+
+ const article = editor ? ArticleHelper.getFrontMatter(editor) : null;
+ const articleCt = article && article.data ? ArticleHelper.getContentType(article.data) : DEFAULT_CONTENT_TYPE;
+
+ const absImgPath = join(parseWinPath(wsFolder?.fsPath || ""), imgPath);
+ const fileDir = parseWinPath(dirname(filePath));
+ const imgDir = parseWinPath(dirname(absImgPath));
+ const contentFolders = Folders.get();
+
+ // Check if relative paths need to be created for the media files
+ if (articleCt.pageBundle) {
+ // Check if image exists in one of the content folders
+ const existsInContent = contentFolders.some(contentFolder => {
+ const contentPath = contentFolder.path;
+ return imgDir.toLowerCase().indexOf(contentPath.toLowerCase()) !== -1
+ });
+
+ // If the image exists in a content folder, the relative path needs to be used
+ if (existsInContent) {
+ const relImgPath = relative(fileDir, imgDir);
+
+ imgPath = join(relImgPath, basename(imgPath));
+
+ // Snippets are already parsed, so update the URL of the image
+ if (data.snippet) {
+ data.snippet = data.snippet.replace(data.image, imgPath);
+ }
+ }
+ }
+
+ // Check if the image needs to be inserted in the content or front matter of the article
if (data?.position) {
- const wsFolder = Folders.getWorkspaceFolder();
- const editor = window.activeTextEditor;
const line = data.position.line;
const character = data.position.character;
if (line) {
- let imgPath = data.image;
- const filePath = data.file;
- const absImgPath = join(parseWinPath(wsFolder?.fsPath || ""), imgPath);
-
- const imgDir = dirname(absImgPath);
- const fileDir = dirname(filePath);
-
- if (imgDir === fileDir) {
- imgPath = join('/', basename(imgPath));
-
- // Snippets are already parsed, so update the URL of the image
- if (data.snippet) {
- data.snippet = data.snippet.replace(data.image, imgPath);
- }
- }
-
const selection = editor?.selection;
await editor?.edit(builder => {
const snippet = data.snippet || ``;
@@ -305,9 +327,10 @@ export class MediaHelpers {
MediaListener.getMediaSelection();
} else {
MediaListener.getMediaSelection();
+
DataListener.updateMetadata({
field: data.fieldName,
- value: data.image,
+ value: imgPath,
parents: data.parents,
blockData: data.blockData
});
diff --git a/src/helpers/Notifications.ts b/src/helpers/Notifications.ts
index b3adff91..516ebdad 100644
--- a/src/helpers/Notifications.ts
+++ b/src/helpers/Notifications.ts
@@ -6,31 +6,31 @@ import { Settings } from "./SettingsHelper";
export class Notifications {
- public static info(message: string, items?: any): Thenable {
+ public static info(message: string, ...items: any): Thenable {
Logger.info(`${EXTENSION_NAME}: ${message}`, "INFO");
if (this.shouldShow("INFO")) {
- return window.showInformationMessage(`${EXTENSION_NAME}: ${message}`, items);
+ return window.showInformationMessage(`${EXTENSION_NAME}: ${message}`, ...items);
}
return Promise.resolve(undefined);
}
- public static warning(message: string, items?: any): Thenable {
+ public static warning(message: string, ...items: any): Thenable {
Logger.info(`${EXTENSION_NAME}: ${message}`, "WARNING");
if (this.shouldShow("WARNING")) {
- return window.showWarningMessage(`${EXTENSION_NAME}: ${message}`, items);
+ return window.showWarningMessage(`${EXTENSION_NAME}: ${message}`, ...items);
}
return Promise.resolve(undefined);
}
- public static error(message: string, items?: any): Thenable {
+ public static error(message: string, ...items: any): Thenable {
Logger.info(`${EXTENSION_NAME}: ${message}`, "ERROR");
if (this.shouldShow("ERROR")) {
- return window.showErrorMessage(`${EXTENSION_NAME}: ${message}`, items);
+ return window.showErrorMessage(`${EXTENSION_NAME}: ${message}`, ...items);
}
return Promise.resolve(undefined);
diff --git a/src/helpers/PanelSettings.ts b/src/helpers/PanelSettings.ts
index 3d860625..6664baf6 100644
--- a/src/helpers/PanelSettings.ts
+++ b/src/helpers/PanelSettings.ts
@@ -3,7 +3,7 @@ import { Extension, Settings } from "."
import { Dashboard } from "../commands/Dashboard"
import { Preview } from "../commands/Preview"
import { Template } from "../commands/Template"
-import { CONTEXT, DefaultFields, SETTINGS_CONTENT_DRAFT_FIELD, SETTINGS_CONTENT_FRONTMATTER_HIGHLIGHT, SETTINGS_DATA_TYPES, SETTINGS_FRAMEWORK_ID, SETTINGS_FRAMEWORK_START, SETTING_AUTO_UPDATE_DATE, SETTING_COMMA_SEPARATED_FIELDS, SETTING_CUSTOM_SCRIPTS, SETTING_DATE_FORMAT, SETTING_PANEL_FREEFORM, SETTING_SEO_CONTENT_MIN_LENGTH, SETTING_SEO_DESCRIPTION_FIELD, SETTING_SEO_DESCRIPTION_LENGTH, SETTING_SEO_SLUG_LENGTH, SETTING_SEO_TITLE_LENGTH, SETTING_SLUG_PREFIX, SETTING_SLUG_SUFFIX, SETTING_SLUG_UPDATE_FILE_NAME, SETTING_TAXONOMY_CATEGORIES, SETTING_TAXONOMY_CONTENT_TYPES, SETTING_TAXONOMY_CUSTOM, SETTING_TAXONOMY_FIELD_GROUPS, SETTING_TAXONOMY_TAGS } from "../constants"
+import { CONTEXT, DefaultFields, SETTING_CONTENT_DRAFT_FIELD, SETTING_CONTENT_FRONTMATTER_HIGHLIGHT, SETTING_DATA_TYPES, SETTING_FRAMEWORK_ID, SETTING_FRAMEWORK_START, SETTING_AUTO_UPDATE_DATE, SETTING_COMMA_SEPARATED_FIELDS, SETTING_CUSTOM_SCRIPTS, SETTING_DATE_FORMAT, SETTING_PANEL_FREEFORM, SETTING_SEO_CONTENT_MIN_LENGTH, SETTING_SEO_DESCRIPTION_FIELD, SETTING_SEO_DESCRIPTION_LENGTH, SETTING_SEO_SLUG_LENGTH, SETTING_SEO_TITLE_LENGTH, SETTING_SLUG_PREFIX, SETTING_SLUG_SUFFIX, SETTING_SLUG_UPDATE_FILE_NAME, SETTING_TAXONOMY_CATEGORIES, SETTING_TAXONOMY_CONTENT_TYPES, SETTING_TAXONOMY_CUSTOM, SETTING_TAXONOMY_FIELD_GROUPS, SETTING_TAXONOMY_TAGS } from "../constants"
import { CustomScript, DataType, DraftField, FieldGroup, PanelSettings as IPanelSettings, ScriptType } from "../models"
export class PanelSettings {
@@ -33,18 +33,18 @@ export class PanelSettings {
isInitialized: await Template.isInitialized(),
modifiedDateUpdate: Settings.get(SETTING_AUTO_UPDATE_DATE) || false,
writingSettingsEnabled: this.isWritingSettingsEnabled() || false,
- fmHighlighting: Settings.get(SETTINGS_CONTENT_FRONTMATTER_HIGHLIGHT),
+ fmHighlighting: Settings.get(SETTING_CONTENT_FRONTMATTER_HIGHLIGHT),
preview: Preview.getSettings(),
commaSeparatedFields: Settings.get(SETTING_COMMA_SEPARATED_FIELDS) || [],
contentTypes: Settings.get(SETTING_TAXONOMY_CONTENT_TYPES) || [],
dashboardViewData: Dashboard.viewData,
- draftField: Settings.get(SETTINGS_CONTENT_DRAFT_FIELD),
+ draftField: Settings.get(SETTING_CONTENT_DRAFT_FIELD),
isBacker: await Extension.getInstance().getState(CONTEXT.backer, 'global'),
- framework: Settings.get(SETTINGS_FRAMEWORK_ID),
+ framework: Settings.get(SETTING_FRAMEWORK_ID),
commands: {
- start: Settings.get(SETTINGS_FRAMEWORK_START)
+ start: Settings.get(SETTING_FRAMEWORK_START)
},
- dataTypes: Settings.get(SETTINGS_DATA_TYPES),
+ dataTypes: Settings.get(SETTING_DATA_TYPES),
fieldGroups: Settings.get(SETTING_TAXONOMY_FIELD_GROUPS),
}
}
diff --git a/src/helpers/PlaceholderHelper.ts b/src/helpers/PlaceholderHelper.ts
new file mode 100644
index 00000000..f1fa404c
--- /dev/null
+++ b/src/helpers/PlaceholderHelper.ts
@@ -0,0 +1,50 @@
+import { format } from "date-fns";
+import { DateHelper } from "./DateHelper";
+import { SlugHelper } from "./SlugHelper";
+
+/**
+ * Replace the known placeholders
+ * @param value
+ * @param title
+ * @returns
+ */
+export const processKnownPlaceholders = (value: string, title: string, dateFormat: string) => {
+ if (value && typeof value === "string") {
+ if (value.includes("{{title}}")) {
+ const regex = new RegExp("{{title}}", "g");
+ value = value.replace(regex, title);
+ }
+
+ if (value.includes("{{slug}}")) {
+ const regex = new RegExp("{{slug}}", "g");
+ value = value.replace(regex, SlugHelper.createSlug(title) || "");
+ }
+
+ if (value.includes("{{now}}")) {
+ const regex = new RegExp("{{now}}", "g");
+
+ if (dateFormat && typeof dateFormat === "string") {
+ value = value.replace(regex, format(new Date(), DateHelper.formatUpdate(dateFormat) as string));
+ } else {
+ return (new Date()).toISOString();
+ }
+ }
+
+ if (value.includes("{{year}}")) {
+ const regex = new RegExp("{{year}}", "g");
+ value = value.replace(regex, format(new Date(), "yyyy"));
+ }
+
+ if (value.includes("{{month}}")) {
+ const regex = new RegExp("{{month}}", "g");
+ value = value.replace(regex, format(new Date(), "MM"));
+ }
+
+ if (value.includes("{{day}}")) {
+ const regex = new RegExp("{{day}}", "g");
+ value = value.replace(regex, format(new Date(), "dd"));
+ }
+ }
+
+ return value;
+}
\ No newline at end of file
diff --git a/src/helpers/SettingsHelper.ts b/src/helpers/SettingsHelper.ts
index f3902211..7382aed3 100644
--- a/src/helpers/SettingsHelper.ts
+++ b/src/helpers/SettingsHelper.ts
@@ -94,6 +94,22 @@ export class Settings {
});
}
+ /**
+ * Inspect a setting
+ * @param name
+ * @returns
+ */
+ public static inspect(name: string): any {
+ const configInpection = Settings.config.inspect(name);
+ const settingKey = `${CONFIG_KEY}.${name}`;
+ const teamValue = Settings.globalConfig && typeof Settings.globalConfig[settingKey] !== "undefined" ? Settings.globalConfig[settingKey] : undefined;
+
+ return {
+ ...configInpection,
+ teamValue
+ };
+ }
+
/**
* Retrieve a setting from global and local config
*/
@@ -162,6 +178,10 @@ export class Settings {
this.createGlobalFile(wsFolder);
}
+ /**
+ * Create the frontmatter.json file
+ * @param wsFolder
+ */
public static createGlobalFile(wsFolder: Uri | undefined | null) {
const initialConfig = {
"$schema": `https://${Extension.getInstance().isBetaVersion() ? `beta.` : ``}frontmatter.codes/frontmatter.schema.json`
diff --git a/src/helpers/SnippetParser.ts b/src/helpers/SnippetParser.ts
new file mode 100644
index 00000000..7686a9fc
--- /dev/null
+++ b/src/helpers/SnippetParser.ts
@@ -0,0 +1,44 @@
+import * as Mustache from 'mustache';
+import { SnippetField } from '../models';
+
+export class SnippetParser {
+
+ public static getPlaceholders(value: string[] | string, openingTags: string = '[[', closingTags: string = ']]'): string[] {
+ const template = SnippetParser.template(value);
+ return Mustache.parse(template, [openingTags, closingTags])
+ .filter((v) => v[0] === 'name' || v[0] === '&')
+ .map((v) => { return v[1]; });
+ }
+
+ public static render(value: string[] | string, data: any, openingTags: string = '[[', closingTags: string = ']]'): string {
+ const template = SnippetParser.template(value);
+ return Mustache.render(template, data, undefined, [openingTags, closingTags]);
+ }
+
+ public static getFields(value: string[] | string, fields: SnippetField[], openingTags: string = '[[', closingTags: string = ']]') {
+ const placeholders = SnippetParser.getPlaceholders(value, openingTags, closingTags);
+
+ const allFields: SnippetField[] = [];
+
+ for (const placeholder of placeholders) {
+ const field = fields.find(f => f.name === placeholder)
+ if (field) {
+ allFields.push(field)
+ } else {
+ allFields.push({
+ name: placeholder,
+ title: placeholder,
+ type: "string",
+ single: true,
+ default: ""
+ });
+ }
+ }
+
+ return allFields;
+ }
+
+ public static template(value: string[] | string) {
+ return typeof value === 'string' ? value : value.join('\n');
+ }
+}
\ No newline at end of file
diff --git a/src/helpers/isValidFile.ts b/src/helpers/isValidFile.ts
index 80ebdf4c..a7ccf599 100644
--- a/src/helpers/isValidFile.ts
+++ b/src/helpers/isValidFile.ts
@@ -1,11 +1,11 @@
import { DEFAULT_FILE_TYPES } from './../constants/DefaultFileTypes';
import { Settings } from ".";
-import { SETTINGS_CONTENT_SUPPORTED_FILETYPES } from "../constants";
+import { SETTING_CONTENT_SUPPORTED_FILETYPES } from "../constants";
import { extname } from 'path';
export const isValidFile = (fileName: string) => {
- let supportedFiles = Settings.get(SETTINGS_CONTENT_SUPPORTED_FILETYPES) || DEFAULT_FILE_TYPES;
+ let supportedFiles = Settings.get(SETTING_CONTENT_SUPPORTED_FILETYPES) || DEFAULT_FILE_TYPES;
supportedFiles = supportedFiles.map(f => f.startsWith(`.`) ? f : `.${f}`);
// Get the extension of the file path
diff --git a/src/hooks/useContentType.tsx b/src/hooks/useContentType.tsx
index 5ca5951f..02111a63 100644
--- a/src/hooks/useContentType.tsx
+++ b/src/hooks/useContentType.tsx
@@ -1,10 +1,10 @@
import { useState, useEffect, useCallback, useMemo } from 'react';
import { DEFAULT_CONTENT_TYPE, DEFAULT_CONTENT_TYPE_NAME } from '../constants/ContentType';
import { Settings } from '../dashboardWebView/models';
-import { ContentType, Field, PanelSettings } from '../models';
+import { ContentType, PanelSettings } from '../models';
export default function useContentType(settings: PanelSettings | Settings | undefined | null, metadata: any) {
- const [contentType, setContentType] = useState(DEFAULT_CONTENT_TYPE);
+ const [contentType, setContentType] = useState(null);
useEffect(() => {
if (settings) {
diff --git a/src/listeners/dashboard/DataListener.ts b/src/listeners/dashboard/DataListener.ts
index 27e6c382..bf382d1f 100644
--- a/src/listeners/dashboard/DataListener.ts
+++ b/src/listeners/dashboard/DataListener.ts
@@ -69,7 +69,7 @@ export class DataListener extends BaseListener {
this.sendMsg(DashboardCommand.dataFileEntries, jsonData);
}
} catch (ex) {
- Logger.error((ex as Error).message);
+ Logger.error(`DataListener::processDataFile: ${(ex as Error).message}`);
const btnClick = await Notifications.error(`Something went wrong while processing the data file. Check your file and output log for more information.`, 'Open output');
if (btnClick && btnClick === 'Open output') {
diff --git a/src/listeners/dashboard/PagesListener.ts b/src/listeners/dashboard/PagesListener.ts
index eeea4fe8..9629890a 100644
--- a/src/listeners/dashboard/PagesListener.ts
+++ b/src/listeners/dashboard/PagesListener.ts
@@ -1,10 +1,10 @@
import { isValidFile } from '../../helpers/isValidFile';
import { existsSync } from "fs";
import { basename, dirname, join } from "path";
-import { commands, FileSystemWatcher, RelativePattern, Uri, workspace } from "vscode";
+import { commands, FileSystemWatcher, RelativePattern, TextDocument, Uri, workspace } from "vscode";
import { Dashboard } from "../../commands/Dashboard";
import { Folders } from "../../commands/Folders";
-import { COMMAND_NAME, DefaultFields, SETTINGS_CONTENT_STATIC_FOLDER, SETTING_DATE_FIELD, SETTING_SEO_DESCRIPTION_FIELD } from "../../constants";
+import { COMMAND_NAME, DefaultFields, SETTING_CONTENT_STATIC_FOLDER, SETTING_SEO_DESCRIPTION_FIELD } from "../../constants";
import { DashboardCommand } from "../../dashboardWebView/DashboardCommand";
import { DashboardMessage } from "../../dashboardWebView/DashboardMessage";
import { Page } from "../../dashboardWebView/models";
@@ -14,12 +14,25 @@ import { DateHelper } from "../../helpers/DateHelper";
import { Notifications } from "../../helpers/Notifications";
import { BaseListener } from "./BaseListener";
import { Field, FieldType } from '../../models';
+import { DataListener } from '../panel';
export class PagesListener extends BaseListener {
private static watchers: { [path: string]: FileSystemWatcher } = {};
private static lastPages: Page[] = [];
+ public static saveFileWatcher() {
+ return workspace.onDidSaveTextDocument((doc: TextDocument) => {
+ if (ArticleHelper.isSupportedFile(doc)) {
+ Logger.info(`File saved ${doc.uri.fsPath}`);
+ // Optimize the list of recently changed files
+ DataListener.getFoldersAndFiles();
+ // Trigger the metadata update
+ this.watcherExec(doc.uri);
+ }
+ })
+ }
+
/**
* Start watching the folders in the current workspace for content changes
*/
@@ -69,6 +82,9 @@ export class PagesListener extends BaseListener {
case DashboardMessage.createByTemplate:
await commands.executeCommand(COMMAND_NAME.createByTemplate);
break;
+ case DashboardMessage.refreshPages:
+ this.getPagesData();
+ break;
}
}
@@ -110,6 +126,7 @@ export class PagesListener extends BaseListener {
}
} catch (error: any) {
+ Logger.error(`PagesListener::getPagesData: ${file.filePath} - ${error.message}`);
Notifications.error(`File error: ${file.filePath} - ${error?.message || error}`);
}
}
@@ -139,18 +156,25 @@ export class PagesListener extends BaseListener {
if (article?.data.title) {
const wsFolder = Folders.getWorkspaceFolder();
const descriptionField = Settings.get(SETTING_SEO_DESCRIPTION_FIELD) as string || DefaultFields.Description;
- const dateField = Settings.get(SETTING_DATE_FIELD) as string || DefaultFields.PublishingDate;
- const staticFolder = Settings.get(SETTINGS_CONTENT_STATIC_FOLDER);
+
+ const dateField = ArticleHelper.getPublishDateField(article) || DefaultFields.PublishingDate;
+ const dateFieldValue = article?.data[dateField] ? DateHelper.tryParse(article?.data[dateField]) : undefined;
+
+ const modifiedField = ArticleHelper.getModifiedDateField(article) || null;
+ const modifiedFieldValue = modifiedField && article?.data[modifiedField] ? DateHelper.tryParse(article?.data[modifiedField])?.getTime() : undefined;
+
+ const staticFolder = Settings.get(SETTING_CONTENT_STATIC_FOLDER);
const page: Page = {
...article.data,
// FrontMatter properties
fmFolder: folderTitle,
- fmModified: fileMtime,
fmFilePath: filePath,
fmFileName: fileName,
fmDraft: ContentType.getDraftStatus(article?.data),
- fmYear: article?.data[dateField] ? DateHelper.tryParse(article?.data[dateField])?.getFullYear() : null,
+ fmModified: modifiedFieldValue ? modifiedFieldValue : fileMtime,
+ fmPublished: dateFieldValue ? dateFieldValue.getTime() : null,
+ fmYear: dateFieldValue ? dateFieldValue.getFullYear() : null,
fmPreviewImage: "",
fmTags: [],
fmCategories: [],
diff --git a/src/listeners/dashboard/SettingsListener.ts b/src/listeners/dashboard/SettingsListener.ts
index 18be3859..806e4585 100644
--- a/src/listeners/dashboard/SettingsListener.ts
+++ b/src/listeners/dashboard/SettingsListener.ts
@@ -1,4 +1,4 @@
-import { SETTINGS_CONTENT_STATIC_FOLDER, SETTINGS_FRAMEWORK_ID } from "../../constants";
+import { SETTING_CONTENT_STATIC_FOLDER, SETTING_FRAMEWORK_ID } from "../../constants";
import { DashboardCommand } from "../../dashboardWebView/DashboardCommand";
import { DashboardMessage } from "../../dashboardWebView/DashboardMessage";
import { DashboardSettings, Settings } from "../../helpers";
@@ -54,15 +54,15 @@ export class SettingsListener extends BaseListener {
* @param frameworkId
*/
private static setFramework(frameworkId: string | null) {
- Settings.update(SETTINGS_FRAMEWORK_ID, frameworkId, true);
+ Settings.update(SETTING_FRAMEWORK_ID, frameworkId, true);
if (frameworkId) {
const allFrameworks = FrameworkDetector.getAll();
const framework = allFrameworks.find((f: Framework) => f.name === frameworkId);
if (framework) {
- Settings.update(SETTINGS_CONTENT_STATIC_FOLDER, framework.static, true);
+ Settings.update(SETTING_CONTENT_STATIC_FOLDER, framework.static, true);
} else {
- Settings.update(SETTINGS_CONTENT_STATIC_FOLDER, "", true);
+ Settings.update(SETTING_CONTENT_STATIC_FOLDER, "", true);
}
}
}
diff --git a/src/listeners/dashboard/SnippetListener.ts b/src/listeners/dashboard/SnippetListener.ts
new file mode 100644
index 00000000..ab9e9fea
--- /dev/null
+++ b/src/listeners/dashboard/SnippetListener.ts
@@ -0,0 +1,89 @@
+import { EditorHelper } from "@estruyf/vscode";
+import { Position, window } from "vscode";
+import { Dashboard } from "../../commands/Dashboard";
+import { SETTING_CONTENT_SNIPPETS } from "../../constants";
+import { DashboardMessage } from "../../dashboardWebView/DashboardMessage";
+import { Notifications, Settings } from "../../helpers";
+import { BaseListener } from "./BaseListener";
+
+
+export class SnippetListener extends BaseListener {
+
+ public static process(msg: { command: DashboardMessage, data: any }) {
+ super.process(msg);
+
+ switch(msg.command) {
+ case DashboardMessage.addSnippet:
+ this.addSnippet(msg.data);
+ break;
+ case DashboardMessage.updateSnippet:
+ this.updateSnippet(msg.data);
+ break;
+ case DashboardMessage.insertSnippet:
+ this.insertSnippet(msg.data);
+ break;
+ }
+ }
+
+ private static async addSnippet(data: any) {
+ const { title, description, body, fields } = data;
+
+ if (!title || !body) {
+ Notifications.warning("Snippet missing title or body");
+ return;
+ }
+
+ const snippets = Settings.get(SETTING_CONTENT_SNIPPETS);
+ if (snippets && snippets[title]) {
+ Notifications.warning("Snippet with the same title already exists");
+ return;
+ }
+
+ const snippetLines = body.split("\n");
+
+ snippets[title] = {
+ description,
+ body: snippetLines.length === 1 ? snippetLines[0] : snippetLines,
+ fields: fields || []
+ };
+
+ Settings.update(SETTING_CONTENT_SNIPPETS, snippets, true);
+ }
+
+ private static async updateSnippet(data: any) {
+ const { snippets } = data;
+
+ if (!snippets) {
+ Notifications.warning("No snippets to update");
+ return;
+ }
+
+ Settings.update(SETTING_CONTENT_SNIPPETS, snippets, true);
+ }
+
+ private static async insertSnippet(data: any) {
+ const { file, snippet } = data;
+
+ if (!file || !snippet) {
+ return;
+ }
+
+ await EditorHelper.showFile(data.file);
+ Dashboard.resetViewData();
+
+ const editor = window.activeTextEditor;
+ const position = editor?.selection?.active;
+ if (!position) {
+ return;
+ }
+
+ const selection = editor?.selection;
+ await editor?.edit(builder => {
+ if (selection !== undefined) {
+ builder.replace(selection, snippet);
+ } else {
+ builder.insert(position, snippet);
+ }
+ });
+ }
+}
\ No newline at end of file
diff --git a/src/listeners/dashboard/index.ts b/src/listeners/dashboard/index.ts
index ab3dbdfa..dcef562a 100644
--- a/src/listeners/dashboard/index.ts
+++ b/src/listeners/dashboard/index.ts
@@ -1,7 +1,9 @@
+export * from './BaseListener';
export * from './DashboardListener';
export * from './DataListener';
export * from './ExtensionListener';
export * from './MediaListener';
export * from './PagesListener';
export * from './SettingsListener';
+export * from './SnippetListener';
export * from './TelemetryListener';
diff --git a/src/listeners/panel/DataListener.ts b/src/listeners/panel/DataListener.ts
index 4fd9a5c9..638d51fd 100644
--- a/src/listeners/panel/DataListener.ts
+++ b/src/listeners/panel/DataListener.ts
@@ -6,13 +6,15 @@ import { CommandToCode } from "../../panelWebView/CommandToCode";
import { BaseListener } from "./BaseListener";
import { commands, ThemeIcon, window } from 'vscode';
import { ArticleHelper, Logger, Settings } from "../../helpers";
-import { COMMAND_NAME, DefaultFields, SETTING_COMMA_SEPARATED_FIELDS, SETTING_TAXONOMY_CONTENT_TYPES } from "../../constants";
+import { COMMAND_NAME, DefaultFields, SETTING_COMMA_SEPARATED_FIELDS, SETTING_DATE_FORMAT, SETTING_TAXONOMY_CONTENT_TYPES } from "../../constants";
import { Article } from '../../commands';
import { ParsedFrontMatter } from '../../parsers';
+import { processKnownPlaceholders } from '../../helpers/PlaceholderHelper';
const FILE_LIMIT = 10;
export class DataListener extends BaseListener {
+ private static lastMetadataUpdate: any = {};
/**
* Process the messages for the dashboard views
@@ -106,7 +108,11 @@ export class DataListener extends BaseListener {
}
}
- this.sendMsg(Command.metadata, updatedMetadata);
+ if (JSON.stringify(DataListener.lastMetadataUpdate) !== JSON.stringify(updatedMetadata)) {
+ this.sendMsg(Command.metadata, updatedMetadata);
+ }
+
+ DataListener.lastMetadataUpdate = updatedMetadata;
}
/**
@@ -145,30 +151,34 @@ export class DataListener extends BaseListener {
// Support multi-level fields
const parentObj = DataListener.getParentObject(article.data, article, parents, blockData);
- for (const dateField of dateFields) {
- if ((field === dateField.name) && value) {
- parentObj[field] = Article.formatDate(new Date(value));
- } else if (!imageFields.find(f => f.name === field)) {
- // Only override the field data if it is not an multiselect image field
- parentObj[field] = value;
- }
- }
+ const isDateField = dateFields.some(f => f.name === field);
+ const isMultiImageField = imageFields.some(f => f.name === field);
- for (const imageField of imageFields) {
- if (field === imageField.name) {
- // If value is an array, it means it comes from the explorer view itself (deletion)
- if (Array.isArray(value)) {
- parentObj[field] = value || [];
- } else { // Otherwise it is coming from the media dashboard (addition)
- let fieldValue = parentObj[field];
- if (fieldValue && !Array.isArray(fieldValue)) {
- fieldValue = [fieldValue];
- }
- const crntData = Object.assign([], fieldValue);
- const allRelPaths = [...(crntData || []), value];
- parentObj[field] = [...new Set(allRelPaths)].filter(f => f);
+ if (isDateField) {
+ for (const dateField of dateFields) {
+ if ((field === dateField.name) && value) {
+ parentObj[field] = Article.formatDate(new Date(value));
}
}
+ } else if (isMultiImageField) {
+ for (const imageField of imageFields) {
+ if (field === imageField.name) {
+ // If value is an array, it means it comes from the explorer view itself (deletion)
+ if (Array.isArray(value)) {
+ parentObj[field] = value || [];
+ } else { // Otherwise it is coming from the media dashboard (addition)
+ let fieldValue = parentObj[field];
+ if (fieldValue && !Array.isArray(fieldValue)) {
+ fieldValue = [fieldValue];
+ }
+ const crntData = Object.assign([], fieldValue);
+ const allRelPaths = [...(crntData || []), value];
+ parentObj[field] = [...new Set(allRelPaths)].filter(f => f);
+ }
+ }
+ }
+ } else {
+ parentObj[field] = value;
}
ArticleHelper.update(editor, article);
@@ -283,7 +293,8 @@ export class DataListener extends BaseListener {
private static updatePlaceholder(field: string, value: string, title: string) {
if (field && value) {
- value = ArticleHelper.processKnownPlaceholders(value, title || "");
+ const dateFormat = Settings.get(SETTING_DATE_FORMAT) as string;
+ value = processKnownPlaceholders(value, title || "", dateFormat);
value = ArticleHelper.processCustomPlaceholders(value, title || "");
}
diff --git a/src/listeners/panel/SettingsListener.ts b/src/listeners/panel/SettingsListener.ts
index 9bb9f414..416f0043 100644
--- a/src/listeners/panel/SettingsListener.ts
+++ b/src/listeners/panel/SettingsListener.ts
@@ -1,5 +1,5 @@
import { commands, workspace } from "vscode";
-import { EXTENSION_BETA_ID, EXTENSION_ID, SETTINGS_CONTENT_FRONTMATTER_HIGHLIGHT, SETTINGS_FRAMEWORK_START, SETTING_AUTO_UPDATE_DATE, SETTING_PREVIEW_HOST } from "../../constants";
+import { EXTENSION_BETA_ID, EXTENSION_ID, SETTING_CONTENT_FRONTMATTER_HIGHLIGHT, SETTING_FRAMEWORK_START, SETTING_AUTO_UPDATE_DATE, SETTING_PREVIEW_HOST } from "../../constants";
import { Extension, Settings } from "../../helpers";
import { PanelSettings } from "../../helpers/PanelSettings";
import { Command } from "../../panelWebView/Command";
@@ -30,13 +30,13 @@ export class SettingsListener extends BaseListener {
this.updateSetting(SETTING_AUTO_UPDATE_DATE, msg.data || false);
break;
case CommandToCode.updateFmHighlight:
- this.updateSetting(SETTINGS_CONTENT_FRONTMATTER_HIGHLIGHT, (msg.data !== null && msg.data !== undefined) ? msg.data : false);
+ this.updateSetting(SETTING_CONTENT_FRONTMATTER_HIGHLIGHT, (msg.data !== null && msg.data !== undefined) ? msg.data : false);
break;
case CommandToCode.updatePreviewUrl:
this.updateSetting(SETTING_PREVIEW_HOST, msg.data || "");
break;
case CommandToCode.updateStartCommand:
- this.updateSetting(SETTINGS_FRAMEWORK_START, msg.data || "");
+ this.updateSetting(SETTING_FRAMEWORK_START, msg.data || "");
break;
}
}
diff --git a/src/models/DashboardData.ts b/src/models/DashboardData.ts
index df0f8f93..b33b2385 100644
--- a/src/models/DashboardData.ts
+++ b/src/models/DashboardData.ts
@@ -1,4 +1,22 @@
+import { Position } from 'vscode';
+import { NavigationType } from '../dashboardWebView/models';
+import { BlockFieldData } from './BlockFieldData';
+
export interface DashboardData {
- type: "contents" | "media" | "data";
- data?: any;
+ type: NavigationType;
+ data?: ViewData;
+}
+
+export interface ViewData {
+ filePath?: string;
+ fieldName?: string;
+ position?: Position;
+ fileTitle?: string;
+ selection?: string;
+ pageBundle?: boolean;
+ metadataInsert?: boolean;
+ blockData?: BlockFieldData;
+ parents?: string[];
+ multiple?: string[];
+ value?: string;
}
\ No newline at end of file
diff --git a/src/models/PanelSettings.ts b/src/models/PanelSettings.ts
index d547571d..565c93e4 100644
--- a/src/models/PanelSettings.ts
+++ b/src/models/PanelSettings.ts
@@ -65,6 +65,10 @@ export interface Field {
fieldGroup?: string | string[];
dataType?: string | string[];
taxonomyLimit?: number;
+
+ // Date fields
+ isPublishDate?: boolean;
+ isModifiedDate?: boolean;
}
export interface DateInfo {
@@ -94,6 +98,7 @@ export interface FolderInfo {
export interface FileInfo extends FileStat {
filePath: string;
fileName: string;
+ folderName: string | undefined;
};
export interface CustomScript {
diff --git a/src/models/Snippets.ts b/src/models/Snippets.ts
new file mode 100644
index 00000000..037b76ab
--- /dev/null
+++ b/src/models/Snippets.ts
@@ -0,0 +1,20 @@
+import { Field } from "./PanelSettings";
+
+export interface Snippets {
+ [snippetName: string]: Snippet;
+}
+
+export interface Snippet {
+ description: string;
+ body: string[] | string;
+ fields: SnippetField[];
+ openingTags?: string;
+ closingTags?: string;
+}
+
+export type SnippetSpecialPlaceholders = "FM_SELECTED_TEXT" | string;
+
+export interface SnippetField extends Field {
+ default?: SnippetSpecialPlaceholders;
+ value?: any;
+}
\ No newline at end of file
diff --git a/src/models/index.ts b/src/models/index.ts
index 680f6091..49ac2af0 100644
--- a/src/models/index.ts
+++ b/src/models/index.ts
@@ -10,6 +10,7 @@ export * from './DraftField';
export * from './Framework';
export * from './MediaPaths';
export * from './PanelSettings';
+export * from './Snippets';
export * from './SortOrder';
export * from './SortType';
export * from './SortingSetting';
diff --git a/src/panelWebView/components/Fields/DateTimeField.tsx b/src/panelWebView/components/Fields/DateTimeField.tsx
index e885b8c0..ffa732e2 100644
--- a/src/panelWebView/components/Fields/DateTimeField.tsx
+++ b/src/panelWebView/components/Fields/DateTimeField.tsx
@@ -24,6 +24,11 @@ const CustomInput = forwardRef(({ value, onClick }
export const DateTimeField: React.FunctionComponent = ({label, date, format, onChange}: React.PropsWithChildren) => {
const [ dateValue, setDateValue ] = React.useState(null);
+
+ const onDateChange = (date: Date) => {
+ setDateValue(date);
+ onChange(date);
+ };
React.useEffect(() => {
const crntValue = DateHelper.tryParse(date, format);
@@ -32,12 +37,7 @@ export const DateTimeField: React.FunctionComponent = ({lab
if (crntValue?.toISOString() !== stateValue?.toISOString()) {
setDateValue(date);
}
- }, [ date ]);
-
- const onDateChange = (date: Date) => {
- setDateValue(date);
- onChange(date);
- };
+ }, [ date, dateValue ]);
return (
@@ -49,7 +49,7 @@ export const DateTimeField: React.FunctionComponent
= ({lab
= ({
value = field.default;
if (field.type === 'datetime') {
+ if (value === "{{now}}") {
+ value = new Date();
+ }
+
value = getDate(value) || null;
}
- onSendUpdate(field.name, value, parentFields);
+ //onSendUpdate(field.name, value, parentFields);
}
// Check if the field value contains a placeholder
diff --git a/src/panelWebView/components/FileItem.tsx b/src/panelWebView/components/FileItem.tsx
index 899e84a4..bfd7fb35 100644
--- a/src/panelWebView/components/FileItem.tsx
+++ b/src/panelWebView/components/FileItem.tsx
@@ -1,4 +1,5 @@
import * as React from 'react';
+import { useMemo } from 'react';
import { DEFAULT_FILE_TYPES } from '../../constants/DefaultFileTypes';
import { MessageHelper } from '../../helpers/MessageHelper';
import { CommandToCode } from '../CommandToCode';
@@ -8,16 +9,25 @@ import { MarkdownIcon } from './Icons/MarkdownIcon';
export interface IFileItemProps {
name: string;
path: string;
+ folderName: string | undefined;
}
-const FileItem: React.FunctionComponent = ({ name, path }: React.PropsWithChildren) => {
-
+const FileItem: React.FunctionComponent = ({ name, folderName, path }: React.PropsWithChildren) => {
+
const openFile = () => {
MessageHelper.sendMessage(CommandToCode.openInEditor, path);
};
+ const itemName = useMemo(() => {
+ if (folderName && name === 'index.md') {
+ return folderName;
+ }
+
+ return name;
+ }, [name, folderName]);
+
// File extension
- const fileExtension = `.${name.split('.').pop()}`;
+ const fileExtension = useMemo(() => `.${name.split('.').pop()}`, [name]);
return (
= ({ name, path }: React
)
}
- {name}
+ {itemName}
);
};
diff --git a/src/panelWebView/components/FileList.tsx b/src/panelWebView/components/FileList.tsx
index 85b8d9e3..badc8134 100644
--- a/src/panelWebView/components/FileList.tsx
+++ b/src/panelWebView/components/FileList.tsx
@@ -22,7 +22,7 @@ const FileList: React.FunctionComponent = ({files, folderName, t
{
(files && files.length > 0) && files.map(file => (
-
+
))
}
diff --git a/src/panelWebView/components/Metadata.tsx b/src/panelWebView/components/Metadata.tsx
index f055be8c..bae2b7a1 100644
--- a/src/panelWebView/components/Metadata.tsx
+++ b/src/panelWebView/components/Metadata.tsx
@@ -82,7 +82,7 @@ const Metadata: React.FunctionComponent = ({settings, metadata,
{
- renderFields(contentType?.fields, metadata)
+ renderFields(contentType?.fields || [], metadata)
}
{
diff --git a/src/panelWebView/components/SeoKeywordInfo.tsx b/src/panelWebView/components/SeoKeywordInfo.tsx
index 148df429..8ccb8998 100644
--- a/src/panelWebView/components/SeoKeywordInfo.tsx
+++ b/src/panelWebView/components/SeoKeywordInfo.tsx
@@ -52,7 +52,7 @@ const SeoKeywordInfo: React.FunctionComponent = ({keyword,
return 0} />;
};
- if (!keyword) {
+ if (!keyword || typeof keyword !== "string") {
return null;
}
diff --git a/src/panelWebView/components/SeoKeywords.tsx b/src/panelWebView/components/SeoKeywords.tsx
index 282e9a83..6171a01b 100644
--- a/src/panelWebView/components/SeoKeywords.tsx
+++ b/src/panelWebView/components/SeoKeywords.tsx
@@ -1,6 +1,7 @@
import * as React from 'react';
import { SeoKeywordInfo } from './SeoKeywordInfo';
import { VsTable, VsTableBody, VsTableHeader, VsTableHeaderCell } from './VscodeComponents';
+import { ErrorBoundary } from '@sentry/react';
export interface ISeoKeywordsProps {
keywords: string[] | null;
@@ -48,7 +49,9 @@ const SeoKeywords: React.FunctionComponent = ({keywords, ...d
{
validateKeywords().map((keyword, index) => {
return (
-
+ }>
+
+
);
})
}
diff --git a/src/providers/MarkdownFoldingProvider.ts b/src/providers/MarkdownFoldingProvider.ts
index fbf4235a..34bc482c 100644
--- a/src/providers/MarkdownFoldingProvider.ts
+++ b/src/providers/MarkdownFoldingProvider.ts
@@ -1,7 +1,7 @@
import { ArticleHelper } from '../helpers';
import { languages, TextEditorDecorationType } from 'vscode';
import { CancellationToken, FoldingContext, FoldingRange, FoldingRangeKind, FoldingRangeProvider, Range, TextDocument, window, Position } from 'vscode';
-import { SETTINGS_CONTENT_FRONTMATTER_HIGHLIGHT, SETTINGS_CONTENT_SUPPORTED_FILETYPES, SETTING_FRONTMATTER_TYPE } from '../constants';
+import { SETTING_CONTENT_FRONTMATTER_HIGHLIGHT, SETTING_CONTENT_SUPPORTED_FILETYPES, SETTING_FRONTMATTER_TYPE } from '../constants';
import { Settings } from '../helpers';
import { FrontMatterDecorationProvider } from './FrontMatterDecorationProvider';
@@ -12,7 +12,7 @@ export class MarkdownFoldingProvider implements FoldingRangeProvider {
private static decType: TextEditorDecorationType | null = null;
public static register() {
- const supportedFiles = Settings.get(SETTINGS_CONTENT_SUPPORTED_FILETYPES);
+ const supportedFiles = Settings.get(SETTING_CONTENT_SUPPORTED_FILETYPES);
languages.registerFoldingRangeProvider({ language: 'markdown', scheme: 'file' }, new MarkdownFoldingProvider());
@@ -39,9 +39,9 @@ export class MarkdownFoldingProvider implements FoldingRangeProvider {
public static triggerHighlighting() {
const activeDoc = window.activeTextEditor?.document;
- const isSupported = ArticleHelper.isMarkdownFile(activeDoc);
+ const isSupported = ArticleHelper.isSupportedFile(activeDoc);
if (isSupported) {
- const fmHighlight = Settings.get(SETTINGS_CONTENT_FRONTMATTER_HIGHLIGHT);
+ const fmHighlight = Settings.get(SETTING_CONTENT_FRONTMATTER_HIGHLIGHT);
const range = this.getFrontMatterRange();
diff --git a/webpack/dashboard.config.js b/webpack/dashboard.config.js
index 688ade97..25d50555 100644
--- a/webpack/dashboard.config.js
+++ b/webpack/dashboard.config.js
@@ -35,8 +35,7 @@ const config = [
]
},
performance: {
- maxEntrypointSize: 400000,
- maxAssetSize: 400000
+ hints: false
},
plugins: [],
devServer: {
@@ -56,6 +55,8 @@ module.exports = (env, argv) => {
configItem.mode = argv.mode;
if (argv.mode === 'production') {
+ configItem.devtool = "hidden-source-map";
+
configItem.plugins.push(new BundleAnalyzerPlugin({
analyzerMode: 'static',
reportFilename: "dashboard.html",
diff --git a/webpack/extension.config.js b/webpack/extension.config.js
index 6fa06a1b..ea2617af 100644
--- a/webpack/extension.config.js
+++ b/webpack/extension.config.js
@@ -40,8 +40,7 @@ const config = [
}]
},
performance: {
- maxEntrypointSize: 400000,
- maxAssetSize: 400000
+ hints: false
},
optimization: {
splitChunks: {
@@ -66,6 +65,8 @@ module.exports = (env, argv) => {
configItem.mode = argv.mode;
if (argv.mode === 'production') {
+ configItem.devtool = "hidden-source-map";
+
configItem.plugins.push(new BundleAnalyzerPlugin({
analyzerMode: 'static',
reportFilename: "extension.html",
diff --git a/webpack/panel.config.js b/webpack/panel.config.js
index 10a63193..73842121 100644
--- a/webpack/panel.config.js
+++ b/webpack/panel.config.js
@@ -42,8 +42,7 @@ const config = [{
]
},
performance: {
- maxEntrypointSize: 400000,
- maxAssetSize: 400000
+ hints: false
},
plugins: [],
devServer: {
@@ -62,6 +61,8 @@ module.exports = (env, argv) => {
configItem.mode = argv.mode;
if (argv.mode === 'production') {
+ configItem.devtool = "hidden-source-map";
+
configItem.plugins.push(new BundleAnalyzerPlugin({
analyzerMode: 'static',
reportFilename: "viewpanel.html",