mirror of
https://github.com/estruyf/vscode-front-matter.git
synced 2026-06-29 22:41:17 +02:00
#335 - Merge media snippets to content snippets
This commit is contained in:
@@ -6,6 +6,7 @@
|
||||
|
||||
- [#330](https://github.com/estruyf/vscode-front-matter/issues/330): Allow custom scripts to easily update front matter
|
||||
- [#331](https://github.com/estruyf/vscode-front-matter/issues/331): Added functionality to run other type of scripts
|
||||
- [#335](https://github.com/estruyf/vscode-front-matter/issues/331): Merge media snippets with content snippets to allow you to define multiple media snippets and use these in your content
|
||||
|
||||
### ⚡️ Optimizations
|
||||
|
||||
|
||||
Generated
+24
-8552
File diff suppressed because it is too large
Load Diff
+8
-3
@@ -225,8 +225,7 @@
|
||||
"additionalProperties": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"body",
|
||||
"fields"
|
||||
"body"
|
||||
],
|
||||
"properties": {
|
||||
"body": {
|
||||
@@ -255,6 +254,11 @@
|
||||
"description": "The snippet closing tags.",
|
||||
"type": "string",
|
||||
"default": "]]"
|
||||
},
|
||||
"isMediaSnippet": {
|
||||
"description": "Specify if the snippet is to be used for media files.",
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
@@ -408,6 +412,7 @@
|
||||
"type": "array",
|
||||
"default": [],
|
||||
"markdownDescription": "Specify the a snippet for your custom media insert markup. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.dashboard.mediasnippet)",
|
||||
"deprecationMessage": "This setting is deprecated and will be removed in the next major version. Please define your media snippet in the `frontMatter.content.snippet` setting.",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"description": "Use the `{mediaUrl}`, `{caption}`, `{alt}`, `{filename}`, `{mediaHeight}`, and `{mediaWidth}` placeholders in your snippet to automatically insert the media information."
|
||||
@@ -1820,7 +1825,7 @@
|
||||
"devDependencies": {
|
||||
"@bendera/vscode-webview-elements": "0.6.2",
|
||||
"@estruyf/vscode": "0.0.3",
|
||||
"@headlessui/react": "^1.5.0",
|
||||
"@headlessui/react": "1.5.0",
|
||||
"@heroicons/react": "1.0.4",
|
||||
"@iarna/toml": "2.2.3",
|
||||
"@octokit/rest": "^18.12.0",
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
|
||||
|
||||
export enum GeneralCommands{
|
||||
setMode = "setMode"
|
||||
export const GeneralCommands = {
|
||||
toWebview: {
|
||||
setMode: "setMode",
|
||||
},
|
||||
toVSCode: {
|
||||
openLink: "openLink",
|
||||
}
|
||||
};
|
||||
@@ -61,7 +61,6 @@ export const SETTING_CONTENT_SUPPORTED_FILETYPES = "content.supportedFileTypes";
|
||||
export const SETTING_MEDIA_SUPPORTED_MIMETYPES = "media.supportedMimeTypes";
|
||||
|
||||
export const SETTING_DASHBOARD_OPENONSTART = "dashboard.openOnStart";
|
||||
export const SETTING_DASHBOARD_MEDIA_SNIPPET = "dashboard.mediaSnippet";
|
||||
export const SETTING_DASHBOARD_CONTENT_TAGS = "dashboard.content.cardTags";
|
||||
|
||||
export const SETTING_DATA_FILES = "data.files";
|
||||
@@ -89,3 +88,8 @@ export const SETTING_DATE_FIELD = "taxonomy.dateField";
|
||||
* Use the `isModifiedDate` property on the content type datetime field instead
|
||||
*/
|
||||
export const SETTING_MODIFIED_FIELD = "taxonomy.modifiedField";
|
||||
/**
|
||||
* @deprecated
|
||||
* Use the `frontMatter.content.snippets` setting instead
|
||||
*/
|
||||
export const SETTING_DASHBOARD_MEDIA_SNIPPET = "dashboard.mediaSnippet";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Messenger } from '@estruyf/vscode/dist/client';
|
||||
import { Menu } from '@headlessui/react';
|
||||
import { EyeIcon, TrashIcon } from '@heroicons/react/outline';
|
||||
import { EyeIcon, TerminalIcon, TrashIcon } from '@heroicons/react/outline';
|
||||
import * as React from 'react';
|
||||
import { CustomScript, ScriptType } from '../../../models';
|
||||
import { DashboardMessage } from '../../DashboardMessage';
|
||||
@@ -43,7 +43,7 @@ export const ContentActions: React.FunctionComponent<IContentActionsProps> = ({
|
||||
return (scripts || []).filter(script => (script.type === undefined || script.type === ScriptType.Content) && !script.bulk).map(script => (
|
||||
<MenuItem
|
||||
key={script.title}
|
||||
title={script.title}
|
||||
title={<div className='flex items-center'><TerminalIcon className="mr-2 h-5 w-5 flex-shrink-0" aria-hidden={true} /> <span>{script.title}</span></div>}
|
||||
onClick={(value, e) => runCustomScript(e, script)} />
|
||||
))
|
||||
}, [scripts]);
|
||||
@@ -70,15 +70,15 @@ export const ContentActions: React.FunctionComponent<IContentActionsProps> = ({
|
||||
|
||||
<ActionMenuButton title={`Menu`} />
|
||||
|
||||
<MenuItems widthClass='w-40' marginTopClass='mt-6'>
|
||||
<MenuItems widthClass='w-44' marginTopClass='mt-6'>
|
||||
<MenuItem
|
||||
title={`View`}
|
||||
title={<div className='flex items-center'><EyeIcon className="mr-2 h-5 w-5 flex-shrink-0" aria-hidden={true} /> <span>View</span></div>}
|
||||
onClick={(value, e) => onView(e)} />
|
||||
|
||||
{ customScriptActions }
|
||||
|
||||
<MenuItem
|
||||
title={`Delete`}
|
||||
title={<div className='flex items-center'><TrashIcon className="mr-2 h-5 w-5 flex-shrink-0" aria-hidden={true} /> <span>Delete</span></div>}
|
||||
onClick={(value, e) => onDelete(e)} />
|
||||
</MenuItems>
|
||||
</Menu>
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { Messenger } from '@estruyf/vscode/dist/client';
|
||||
import { Menu } from '@headlessui/react';
|
||||
import { ClipboardIcon, CodeIcon, DocumentIcon, EyeIcon, MusicNoteIcon, PencilIcon, PhotographIcon, PlusIcon, TrashIcon, VideoCameraIcon } from '@heroicons/react/outline';
|
||||
import { ClipboardIcon, CodeIcon, DocumentIcon, EyeIcon, MusicNoteIcon, PencilIcon, PhotographIcon, PlusIcon, TerminalIcon, TrashIcon, VideoCameraIcon } from '@heroicons/react/outline';
|
||||
import { basename, dirname } from 'path';
|
||||
import * as React from 'react';
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||
import { CustomScript } from '../../../helpers/CustomScript';
|
||||
import { parseWinPath } from '../../../helpers/parseWinPath';
|
||||
import { ScriptType } from '../../../models';
|
||||
import { SnippetParser } from '../../../helpers/SnippetParser';
|
||||
import { ScriptType, Snippet } from '../../../models';
|
||||
import { MediaInfo } from '../../../models/MediaPaths';
|
||||
import { DashboardMessage } from '../../DashboardMessage';
|
||||
import { LightboxAtom, SelectedMediaFolderSelector, SettingsSelector, ViewDataSelector } from '../../state';
|
||||
@@ -15,6 +16,7 @@ import { MenuItem, MenuItems } from '../Menu';
|
||||
import { ActionMenuButton } from '../Menu/ActionMenuButton';
|
||||
import { QuickAction } from '../Menu/QuickAction';
|
||||
import { Alert } from '../Modals/Alert';
|
||||
import { InfoDialog } from '../Modals/InfoDialog';
|
||||
import { DetailsSlideOver } from './DetailsSlideOver';
|
||||
|
||||
export interface IItemProps {
|
||||
@@ -25,6 +27,7 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
|
||||
const [ , setLightbox ] = useRecoilState(LightboxAtom);
|
||||
const [ showAlert, setShowAlert ] = React.useState(false);
|
||||
const [ showForm, setShowForm ] = React.useState(false);
|
||||
const [ showSnippetSelection, setShowSnippetSelection ] = React.useState(false);
|
||||
const [ showDetails, setShowDetails ] = React.useState(false);
|
||||
const [ caption, setCaption ] = React.useState(media.caption);
|
||||
const [ alt, setAlt ] = React.useState(media.alt);
|
||||
@@ -33,6 +36,15 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
|
||||
const selectedFolder = useRecoilValue(SelectedMediaFolderSelector);
|
||||
const viewData = useRecoilValue(ViewDataSelector);
|
||||
|
||||
const mediaSnippets = useMemo(() => {
|
||||
if (!settings?.snippets) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const keys = Object.keys(settings.snippets);
|
||||
return keys.filter(key => (settings.snippets || {})[key].isMediaSnippet).map(key => ({ title: key, ...(settings.snippets || {})[key]}));
|
||||
}, [settings]);
|
||||
|
||||
const getFolder = () => {
|
||||
if (settings?.wsFolder && media.fsPath) {
|
||||
let relPath = media.fsPath.split(settings.wsFolder).pop();
|
||||
@@ -104,25 +116,39 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
|
||||
};
|
||||
|
||||
const insertSnippet = useCallback(() => {
|
||||
const relPath = getRelPath();
|
||||
let snippet = settings?.mediaSnippet.join("\n");
|
||||
if (mediaSnippets.length === 1) {
|
||||
processSnippet(mediaSnippets[0]);
|
||||
} else {
|
||||
// Show dialog to select
|
||||
setShowSnippetSelection(true);
|
||||
}
|
||||
}, [mediaSnippets]);
|
||||
|
||||
snippet = snippet?.replace("{mediaUrl}", parseWinPath(relPath) || "");
|
||||
snippet = snippet?.replace("{alt}", alt || "");
|
||||
snippet = snippet?.replace("{caption}", caption || "");
|
||||
snippet = snippet?.replace("{title}", media.title || "");
|
||||
snippet = snippet?.replace("{filename}", basename(relPath || ""));
|
||||
snippet = snippet?.replace("{mediaWidth}", media?.dimensions?.width?.toString() || "");
|
||||
snippet = snippet?.replace("{mediaHeight}", media?.dimensions?.height?.toString() || "");
|
||||
const processSnippet = useCallback((snippet: Snippet) => {
|
||||
setShowSnippetSelection(false);
|
||||
|
||||
const relPath = getRelPath();
|
||||
|
||||
const fieldData = {
|
||||
mediaUrl: parseWinPath(relPath) || "",
|
||||
alt: alt || "",
|
||||
caption: caption || "",
|
||||
title: media.title || "",
|
||||
filename: basename(relPath || ""),
|
||||
mediaWidth: media?.dimensions?.width?.toString() || "",
|
||||
mediaHeight: media?.dimensions?.height?.toString() || "",
|
||||
};
|
||||
|
||||
const output = SnippetParser.render(snippet.body, fieldData, snippet?.openingTags, snippet?.closingTags);
|
||||
|
||||
Messenger.send(DashboardMessage.insertMedia, {
|
||||
relPath: parseWinPath(relPath) || "",
|
||||
file: viewData?.data?.filePath,
|
||||
fieldName: viewData?.data?.fieldName,
|
||||
position: viewData?.data?.position || null,
|
||||
snippet
|
||||
snippet: output
|
||||
});
|
||||
}, [alt, caption, media, settings, viewData]);
|
||||
}, [alt, caption, media, settings, viewData, mediaSnippets]);
|
||||
|
||||
const deleteMedia = () => {
|
||||
setShowAlert(true);
|
||||
@@ -198,7 +224,7 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
|
||||
return (settings?.scripts || []).filter(script => script.type === ScriptType.MediaFile).map(script => (
|
||||
<MenuItem
|
||||
key={script.title}
|
||||
title={script.title}
|
||||
title={<div className='flex items-center'><TerminalIcon className="mr-2 h-5 w-5 flex-shrink-0" aria-hidden={true} /> <span>{script.title}</span></div>}
|
||||
onClick={() => runCustomScript(script)} />
|
||||
))
|
||||
}
|
||||
@@ -323,7 +349,7 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
|
||||
</QuickAction>
|
||||
|
||||
{
|
||||
(viewData?.data?.position && settings?.mediaSnippet && settings?.mediaSnippet.length > 0) && (
|
||||
(viewData?.data?.position && mediaSnippets.length > 0) && (
|
||||
<QuickAction
|
||||
title='Insert snippet'
|
||||
onClick={insertSnippet}>
|
||||
@@ -354,7 +380,7 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
|
||||
|
||||
<MenuItems widthClass='w-40'>
|
||||
<MenuItem
|
||||
title={`Edit metadata`}
|
||||
title={<div className='flex items-center'><PencilIcon className="mr-2 h-5 w-5 flex-shrink-0" aria-hidden={true} /> <span>Edit metadata</span></div>}
|
||||
onClick={updateMetadata}
|
||||
/>
|
||||
|
||||
@@ -362,15 +388,16 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
|
||||
viewData?.data?.filePath ? (
|
||||
<>
|
||||
<MenuItem
|
||||
title={`Insert image markdown`}
|
||||
title={<div className='flex items-center'><PlusIcon className="mr-2 h-5 w-5 flex-shrink-0" aria-hidden={true} /> <span>Insert image markdown</span></div>}
|
||||
onClick={insertToArticle} />
|
||||
|
||||
{
|
||||
(viewData?.data?.position && settings?.mediaSnippet && settings?.mediaSnippet.length > 0) && (
|
||||
(viewData?.data?.position && mediaSnippets.length > 0) && mediaSnippets.map((snippet, idx) => (
|
||||
<MenuItem
|
||||
title={`Insert snippet`}
|
||||
onClick={insertSnippet} />
|
||||
)
|
||||
key={idx}
|
||||
title={<div className='flex items-center'><CodeIcon className="mr-2 h-5 w-5 flex-shrink-0" aria-hidden={true} /> <span>{snippet.title}</span></div>}
|
||||
onClick={() => processSnippet(snippet)} />
|
||||
))
|
||||
}
|
||||
|
||||
{ customScriptActions() }
|
||||
@@ -387,11 +414,11 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
|
||||
}
|
||||
|
||||
<MenuItem
|
||||
title={`Reveal media`}
|
||||
title={<div className='flex items-center'><EyeIcon className="mr-2 h-5 w-5 flex-shrink-0" aria-hidden={true} /> <span>Reveal media</span></div>}
|
||||
onClick={revealMedia} />
|
||||
|
||||
<MenuItem
|
||||
title={`Delete`}
|
||||
title={<div className='flex items-center'><TrashIcon className="mr-2 h-5 w-5 flex-shrink-0" aria-hidden={true} /> <span>Delete</span></div>}
|
||||
onClick={deleteMedia} />
|
||||
</MenuItems>
|
||||
</Menu>
|
||||
@@ -436,6 +463,32 @@ export const Item: React.FunctionComponent<IItemProps> = ({media}: React.PropsWi
|
||||
</div>
|
||||
</li>
|
||||
|
||||
{
|
||||
showSnippetSelection && (
|
||||
<InfoDialog
|
||||
icon={<CodeIcon className="h-6 w-6" aria-hidden="true" />}
|
||||
title='Insert snippet'
|
||||
description='Select the media snippet to use for the current media file.'
|
||||
dismiss={() => setShowSnippetSelection(false)}>
|
||||
|
||||
<ul className='flex justify-center'>
|
||||
{
|
||||
mediaSnippets.map((snippet, idx) => (
|
||||
<li key={idx} className="inline-flex items-center pb-2 mr-2">
|
||||
<button
|
||||
className="w-full inline-flex justify-center border border-transparent shadow-sm px-4 py-2 bg-teal-600 text-base font-medium text-white hover:bg-teal-700 dark:hover:bg-teal-900 focus:outline-none sm:w-auto sm:text-sm disabled:opacity-30"
|
||||
onClick={() => processSnippet(snippet)}>
|
||||
{snippet.title}
|
||||
</button>
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
|
||||
</InfoDialog>
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
showDetails && (
|
||||
<DetailsSlideOver
|
||||
|
||||
@@ -18,7 +18,7 @@ export const MenuItems: React.FunctionComponent<IMenuItemsProps> = ({widthClass,
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Menu.Items className={`${widthClass || ""} ${marginTopClass || "mt-2"} origin-top-right absolute right-0 z-10 rounded-md shadow-2xl bg-white dark:bg-vulcan-500 ring-1 ring-vulcan-400 dark:ring-white ring-opacity-5 focus:outline-none text-sm max-h-96 overflow-auto`}>
|
||||
<Menu.Items className={`${widthClass || ""} ${marginTopClass || "mt-2"} origin-top-right absolute right-0 z-20 rounded-md shadow-2xl bg-white dark:bg-vulcan-500 ring-1 ring-vulcan-400 dark:ring-white ring-opacity-5 focus:outline-none text-sm max-h-96 overflow-auto`}>
|
||||
<div className="py-1">
|
||||
{children}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import { Dialog, Transition } from '@headlessui/react';
|
||||
import * as React from 'react';
|
||||
import { Fragment } from 'react';
|
||||
|
||||
export interface IInfoDialogProps {
|
||||
icon?: JSX.Element;
|
||||
title: string;
|
||||
description: string;
|
||||
dismiss: () => void;
|
||||
}
|
||||
|
||||
export const InfoDialog: React.FunctionComponent<IInfoDialogProps> = ({dismiss, icon, title, description, children}: React.PropsWithChildren<IInfoDialogProps>) => {
|
||||
return (
|
||||
<Transition.Root show={true} as={Fragment}>
|
||||
<Dialog className="fixed z-10 inset-0 overflow-y-auto" onClose={dismiss}>
|
||||
<div className="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<Dialog.Overlay className="fixed inset-0 bg-vulcan-500 bg-opacity-75 transition-opacity" />
|
||||
</Transition.Child>
|
||||
|
||||
{/* This element is to trick the browser into centering the modal contents. */}
|
||||
<span className="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">
|
||||
​
|
||||
</span>
|
||||
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
enterTo="opacity-100 translate-y-0 sm:scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
>
|
||||
<div className="inline-block align-bottom bg-white dark:bg-vulcan-500 rounded-lg px-4 pt-5 pb-4 text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full sm:p-6 border-2 border-whisper-900">
|
||||
<div className="sm:flex sm:items-start">
|
||||
{
|
||||
icon && (
|
||||
<div className="mt-3 sm:mr-4 mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full sm:mx-0 sm:h-10 sm:w-10 bg-gray-50 dark:bg-vulcan-400">
|
||||
{icon}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<div className="mt-3 text-center sm:mt-0 sm:text-left">
|
||||
<Dialog.Title as="h3" className="text-lg leading-6 font-medium text-vulcan-300 dark:text-whisper-900">
|
||||
{title}
|
||||
</Dialog.Title>
|
||||
<div className="mt-2">
|
||||
<p className="text-sm text-vulcan-500 dark:text-whisper-500">
|
||||
{description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-5 sm:mt-4">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
);
|
||||
};
|
||||
@@ -1,7 +1,7 @@
|
||||
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 { useCallback, useMemo, useRef, useState } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { FeatureFlag } from '../../../components/features/FeatureFlag';
|
||||
import { FEATURE_FLAG } from '../../../constants';
|
||||
@@ -31,9 +31,12 @@ export const Item: React.FunctionComponent<IItemProps> = ({ title, snippet }: Re
|
||||
const [ snippetTitle, setSnippetTitle ] = useState<string>('');
|
||||
const [ snippetDescription, setSnippetDescription ] = useState<string>('');
|
||||
const [ snippetOriginalBody, setSnippetOriginalBody ] = useState<string>('');
|
||||
const [ mediaSnippet, setMediaSnippet ] = useState<boolean>(false);
|
||||
|
||||
const formRef = useRef<SnippetFormHandle>(null);
|
||||
|
||||
const insertToContent = useMemo(() => viewData?.data?.filePath, [ viewData ]);
|
||||
|
||||
const insertToArticle = () => {
|
||||
formRef.current?.onSave();
|
||||
setShowInsertDialog(false);
|
||||
@@ -44,6 +47,7 @@ export const Item: React.FunctionComponent<IItemProps> = ({ title, snippet }: Re
|
||||
setSnippetTitle('');
|
||||
setSnippetDescription('');
|
||||
setSnippetOriginalBody('');
|
||||
setMediaSnippet(false);
|
||||
};
|
||||
|
||||
const onOpenEdit = useCallback(() => {
|
||||
@@ -51,6 +55,7 @@ export const Item: React.FunctionComponent<IItemProps> = ({ title, snippet }: Re
|
||||
setSnippetDescription(snippet.description);
|
||||
setSnippetOriginalBody(typeof snippet.body === "string" ? snippet.body : snippet.body.join(`\n`));
|
||||
setShowEditDialog(true);
|
||||
setMediaSnippet(!!snippet.isMediaSnippet);
|
||||
}, [snippet]);
|
||||
|
||||
const onSnippetUpdate = useCallback(() => {
|
||||
@@ -68,11 +73,16 @@ export const Item: React.FunctionComponent<IItemProps> = ({ title, snippet }: Re
|
||||
|
||||
const snippetContents: Snippet = {
|
||||
...crntSnippet,
|
||||
fields,
|
||||
description: snippetDescription || '',
|
||||
body: snippetLines.length === 1 ? snippetLines[0] : snippetLines
|
||||
};
|
||||
|
||||
if (!mediaSnippet) {
|
||||
snippetContents.fields = fields;
|
||||
} else {
|
||||
snippetContents.isMediaSnippet = true;
|
||||
}
|
||||
|
||||
// Check if new or update
|
||||
if (title === snippetTitle) {
|
||||
snippets[title] = snippetContents;
|
||||
@@ -84,7 +94,7 @@ export const Item: React.FunctionComponent<IItemProps> = ({ title, snippet }: Re
|
||||
Messenger.send(DashboardMessage.updateSnippet, { snippets });
|
||||
|
||||
reset();
|
||||
}, [settings?.snippets, title, snippetTitle, snippetDescription, snippetOriginalBody]);
|
||||
}, [settings?.snippets, title, snippetTitle, snippetDescription, snippetOriginalBody, mediaSnippet]);
|
||||
|
||||
const onDelete = useCallback(() => {
|
||||
const snippets = Object.assign({}, settings?.snippets || {});
|
||||
@@ -108,7 +118,7 @@ export const Item: React.FunctionComponent<IItemProps> = ({ title, snippet }: Re
|
||||
features={mode?.features || []}
|
||||
flag={FEATURE_FLAG.dashboard.snippets.manage}
|
||||
alternative={(
|
||||
viewData?.data?.filePath ? (
|
||||
insertToContent ? (
|
||||
<div className={`absolute top-4 right-4 flex flex-col space-y-4`}>
|
||||
<div className="flex items-center border border-transparent group-hover:bg-gray-200 dark:group-hover:bg-vulcan-200 group-hover:border-gray-100 dark:group-hover:border-vulcan-50 rounded-full p-2 -mr-2 -mt-2">
|
||||
<div className='group-hover:hidden'>
|
||||
@@ -135,7 +145,7 @@ export const Item: React.FunctionComponent<IItemProps> = ({ title, snippet }: Re
|
||||
|
||||
<div className='hidden group-hover:flex'>
|
||||
{
|
||||
viewData?.data?.filePath && (
|
||||
insertToContent && !snippet.isMediaSnippet && (
|
||||
<>
|
||||
<QuickAction
|
||||
title={`Insert snippet`}
|
||||
@@ -170,7 +180,7 @@ export const Item: React.FunctionComponent<IItemProps> = ({ title, snippet }: Re
|
||||
<FormDialog
|
||||
title={`Insert snippet: ${title}`}
|
||||
description={`Insert the ${title.toLowerCase()} snippet into the current article`}
|
||||
isSaveDisabled={!viewData?.data?.filePath}
|
||||
isSaveDisabled={!insertToContent}
|
||||
trigger={insertToArticle}
|
||||
dismiss={() => setShowInsertDialog(false)}
|
||||
okBtnText='Insert'
|
||||
@@ -200,6 +210,8 @@ export const Item: React.FunctionComponent<IItemProps> = ({ title, snippet }: Re
|
||||
title={snippetTitle}
|
||||
description={snippetDescription}
|
||||
body={snippetOriginalBody}
|
||||
isMediaSnippet={mediaSnippet}
|
||||
onMediaSnippetUpdate={(value: boolean) => setMediaSnippet(value)}
|
||||
onTitleUpdate={(value: string) => setSnippetTitle(value)}
|
||||
onDescriptionUpdate={(value: string) => setSnippetDescription(value)}
|
||||
onBodyUpdate={(value: string) => setSnippetOriginalBody(value)} />
|
||||
|
||||
@@ -1,16 +1,24 @@
|
||||
import { Messenger } from '@estruyf/vscode/dist/client';
|
||||
import * as React from 'react';
|
||||
import { GeneralCommands } from '../../../constants';
|
||||
|
||||
export interface INewFormProps {
|
||||
title: string;
|
||||
description: string;
|
||||
body: string;
|
||||
isMediaSnippet: boolean;
|
||||
|
||||
onMediaSnippetUpdate: (value: boolean) => void;
|
||||
onTitleUpdate: (value: string) => void;
|
||||
onDescriptionUpdate: (value: string) => void;
|
||||
onBodyUpdate: (value: string) => void;
|
||||
}
|
||||
|
||||
export const NewForm: React.FunctionComponent<INewFormProps> = ({ title, description, body, onTitleUpdate, onDescriptionUpdate, onBodyUpdate }: React.PropsWithChildren<INewFormProps>) => {
|
||||
export const NewForm: React.FunctionComponent<INewFormProps> = ({ title, description, body, isMediaSnippet, onMediaSnippetUpdate, onTitleUpdate, onDescriptionUpdate, onBodyUpdate }: React.PropsWithChildren<INewFormProps>) => {
|
||||
|
||||
const openLink = () => {
|
||||
Messenger.send(GeneralCommands.toVSCode.openLink, "https://frontmatter.codes/docs/markdown#placeholders");
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='space-y-4'>
|
||||
@@ -60,6 +68,36 @@ export const NewForm: React.FunctionComponent<INewFormProps> = ({ title, descrip
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor={`snippet`} className="block text-sm font-medium">
|
||||
Is a media snippet?
|
||||
</label>
|
||||
<div className="mt-1 relative flex items-start">
|
||||
<div className="flex items-center h-5">
|
||||
<input
|
||||
id="isMediaSnippet"
|
||||
aria-describedby="isMediaSnippet-description"
|
||||
name="isMediaSnippet"
|
||||
type="checkbox"
|
||||
checked={isMediaSnippet}
|
||||
onChange={(e) => onMediaSnippetUpdate(e.currentTarget.checked)}
|
||||
className="focus:ring-teal-500 h-4 w-4 text-teal-600 border-gray-300 rounded"
|
||||
/>
|
||||
</div>
|
||||
<div className="ml-3 text-sm">
|
||||
<label htmlFor="isMediaSnippet" className="font-medium text-vulcan-100 dark:text-whisper-900">
|
||||
Media snippet
|
||||
</label>
|
||||
<p id="isMediaSnippet-description" className="text-vulcan-300 dark:text-whisper-500">
|
||||
Use the current snippet for inserting media files into your content.
|
||||
</p>
|
||||
<p>
|
||||
Check our <button className='text-teal-700 hover:text-teal-500' onClick={openLink} title='media snippet placeholders'>media snippet placeholders</button> documentation to know which placeholders you can use.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -25,6 +25,7 @@ export const Snippets: React.FunctionComponent<ISnippetsProps> = (props: React.P
|
||||
const [ snippetDescription, setSnippetDescription ] = useState<string>('');
|
||||
const [ snippetBody, setSnippetBody ] = useState<string>('');
|
||||
const [ showCreateDialog, setShowCreateDialog ] = useState(false);
|
||||
const [ mediaSnippet, setMediaSnippet ] = useState(false);
|
||||
|
||||
const snippets = settings?.snippets || {};
|
||||
const snippetKeys = useMemo(() => Object.keys(snippets) || [], [settings?.snippets]);
|
||||
@@ -41,11 +42,12 @@ export const Snippets: React.FunctionComponent<ISnippetsProps> = (props: React.P
|
||||
title: snippetTitle,
|
||||
description: snippetDescription || '',
|
||||
body: snippetBody,
|
||||
fields
|
||||
fields,
|
||||
isMediaSnippet: mediaSnippet
|
||||
});
|
||||
|
||||
reset();
|
||||
}, [snippetTitle, snippetDescription, snippetBody]);
|
||||
}, [snippetTitle, snippetDescription, snippetBody, mediaSnippet]);
|
||||
|
||||
const reset = () => {
|
||||
setShowCreateDialog(false);
|
||||
@@ -127,6 +129,8 @@ export const Snippets: React.FunctionComponent<ISnippetsProps> = (props: React.P
|
||||
title={snippetTitle}
|
||||
description={snippetDescription}
|
||||
body={snippetBody}
|
||||
isMediaSnippet={mediaSnippet}
|
||||
onMediaSnippetUpdate={(value: boolean) => setMediaSnippet(value)}
|
||||
onTitleUpdate={(value: string) => setSnippetTitle(value)}
|
||||
onDescriptionUpdate={(value: string) => setSnippetDescription(value)}
|
||||
onBodyUpdate={(value: string) => setSnippetBody(value)} />
|
||||
|
||||
@@ -47,7 +47,7 @@ export default function useMessages() {
|
||||
case DashboardCommand.searchReady:
|
||||
setSearchReady(true);
|
||||
break;
|
||||
case GeneralCommands.setMode:
|
||||
case GeneralCommands.toWebview.setMode:
|
||||
setMode(message.data);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@ export interface Settings {
|
||||
openOnStart: boolean | null;
|
||||
versionInfo: VersionInfo;
|
||||
pageViewType: DashboardViewType | undefined;
|
||||
mediaSnippet: string[];
|
||||
contentTypes: ContentType[];
|
||||
contentFolders: ContentFolder[];
|
||||
crntFramework: string;
|
||||
|
||||
@@ -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, 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, SETTING_DASHBOARD_CONTENT_TAGS, SETTING_MEDIA_SUPPORTED_MIMETYPES } from "../constants";
|
||||
import { CONTEXT, ExtensionState, SETTING_CONTENT_DRAFT_FIELD, SETTING_CONTENT_SORTING, SETTING_CONTENT_SORTING_DEFAULT, SETTING_CONTENT_STATIC_FOLDER, SETTING_DASHBOARD_OPENONSTART, SETTING_DATA_FILES, SETTING_DATA_FOLDERS, SETTING_DATA_TYPES, SETTING_FRAMEWORK_ID, SETTING_MEDIA_SORTING_DEFAULT, SETTING_CUSTOM_SCRIPTS, SETTING_TAXONOMY_CONTENT_TYPES, SETTING_CONTENT_SNIPPETS, SETTING_DATE_FORMAT, SETTING_DASHBOARD_CONTENT_TAGS, SETTING_MEDIA_SUPPORTED_MIMETYPES } from "../constants";
|
||||
import { DashboardViewType, SortingOption, Settings as ISettings } from "../dashboardWebView/models";
|
||||
import { CustomScript, DraftField, ScriptType, Snippets, SortingSetting, TaxonomyType } from "../models";
|
||||
import { CustomScript, DraftField, Snippets, SortingSetting, TaxonomyType } from "../models";
|
||||
import { DataFile } from "../models/DataFile";
|
||||
import { DataFolder } from "../models/DataFolder";
|
||||
import { DataType } from "../models/DataType";
|
||||
@@ -30,7 +30,6 @@ export class DashboardSettings {
|
||||
openOnStart: Settings.get(SETTING_DASHBOARD_OPENONSTART),
|
||||
versionInfo: ext.getVersion(),
|
||||
pageViewType: await ext.getState<DashboardViewType | undefined>(ExtensionState.PagesView, "workspace"),
|
||||
mediaSnippet: Settings.get<string[]>(SETTING_DASHBOARD_MEDIA_SNIPPET) || [],
|
||||
contentTypes: Settings.get(SETTING_TAXONOMY_CONTENT_TYPES) || [],
|
||||
draftField: Settings.get<DraftField>(SETTING_CONTENT_DRAFT_FIELD),
|
||||
customSorting: Settings.get<SortingSetting[]>(SETTING_CONTENT_SORTING),
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { basename } from "path";
|
||||
import { extensions, Uri, ExtensionContext, window, workspace, commands, ExtensionMode, DiagnosticCollection, languages } from "vscode";
|
||||
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 { EXTENSION_NAME, GITHUB_LINK, SETTING_DATE_FIELD, SETTING_MODIFIED_FIELD, EXTENSION_BETA_ID, EXTENSION_ID, ExtensionState, CONFIG_KEY, SETTING_CONTENT_PAGE_FOLDERS, SETTING_DASHBOARD_MEDIA_SNIPPET, SETTING_CONTENT_SNIPPETS } from "../constants";
|
||||
import { ContentFolder, Snippet } from "../models";
|
||||
import { Notifications } from "./Notifications";
|
||||
import { Settings } from "./SettingsHelper";
|
||||
|
||||
@@ -189,6 +189,30 @@ export class Extension {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (major <= 7 && minor < 3) {
|
||||
const mediaSnippet = Settings.get<string[]>(SETTING_DASHBOARD_MEDIA_SNIPPET);
|
||||
if (mediaSnippet && mediaSnippet.length > 0) {
|
||||
let snippet = mediaSnippet.join(`\n`);
|
||||
|
||||
snippet = snippet.replace(`{mediaUrl}`, `[[&mediaUrl]]`);
|
||||
snippet = snippet.replace(`{mediaHeight}`, `[[mediaHeight]]`);
|
||||
snippet = snippet.replace(`{mediaWidth}`, `[[mediaWidth]]`);
|
||||
snippet = snippet.replace(`{caption}`, `[[&caption]]`);
|
||||
snippet = snippet.replace(`{alt}`, `[[alt]]`);
|
||||
snippet = snippet.replace(`{filename}`, `[[filename]]`);
|
||||
snippet = snippet.replace(`{title}`, `[[title]]`);
|
||||
|
||||
const snippets = Settings.get<Snippet[]>(SETTING_CONTENT_SNIPPETS) || {} as any;
|
||||
snippets[`Media snippet (migrated)`] = {
|
||||
body: snippet.split(`\n`),
|
||||
isMediaSnippet: true,
|
||||
description: `Migrated media snippet from frontMatter.dashboard.mediaSnippet setting`
|
||||
}
|
||||
|
||||
await Settings.update(SETTING_CONTENT_SNIPPETS, snippets, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async setState<T>(propKey: string, propValue: T, type: "workspace" | "global" = "global"): Promise<void> {
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Dashboard } from "../../commands/Dashboard";
|
||||
import { SETTING_CONTENT_SNIPPETS } from "../../constants";
|
||||
import { DashboardMessage } from "../../dashboardWebView/DashboardMessage";
|
||||
import { Notifications, Settings } from "../../helpers";
|
||||
import { Snippet } from "../../models";
|
||||
import { BaseListener } from "./BaseListener";
|
||||
import { SettingsListener } from "./SettingsListener";
|
||||
|
||||
@@ -27,7 +28,7 @@ export class SnippetListener extends BaseListener {
|
||||
}
|
||||
|
||||
private static async addSnippet(data: any) {
|
||||
const { title, description, body, fields } = data;
|
||||
const { title, description, body, fields, isMediaSnippet } = data;
|
||||
|
||||
if (!title || !body) {
|
||||
Notifications.warning("Snippet missing title or body");
|
||||
@@ -42,11 +43,18 @@ export class SnippetListener extends BaseListener {
|
||||
|
||||
const snippetLines = body.split("\n");
|
||||
|
||||
snippets[title] = {
|
||||
const snippetContent: any = {
|
||||
description,
|
||||
body: snippetLines.length === 1 ? snippetLines[0] : snippetLines,
|
||||
fields: fields || []
|
||||
body: snippetLines.length === 1 ? snippetLines[0] : snippetLines
|
||||
};
|
||||
|
||||
if (isMediaSnippet) {
|
||||
snippetContent.isMediaSnippet = true;
|
||||
} else {
|
||||
snippetContent.fields = fields || []
|
||||
}
|
||||
|
||||
snippets[title] = snippetContent;
|
||||
|
||||
await Settings.update(SETTING_CONTENT_SNIPPETS, snippets, true);
|
||||
SettingsListener.getSettings();
|
||||
|
||||
@@ -1,21 +1,31 @@
|
||||
import { GeneralCommands } from './../../constants';
|
||||
import { GeneralCommands } from './../../constants/GeneralCommands';
|
||||
import { Dashboard } from "../../commands/Dashboard";
|
||||
import { DashboardMessage } from "../../dashboardWebView/DashboardMessage";
|
||||
import { ExplorerView } from "../../explorerView/ExplorerView";
|
||||
import { Extension } from "../../helpers";
|
||||
import { Logger } from "../../helpers/Logger";
|
||||
import { CommandToCode } from '../../panelWebView/CommandToCode';
|
||||
import { commands, Uri } from 'vscode';
|
||||
|
||||
|
||||
export abstract class BaseListener {
|
||||
|
||||
public static process(msg: { command: DashboardMessage, data: any }) {}
|
||||
public static process(msg: { command: DashboardMessage | CommandToCode | string , data: any }) {
|
||||
switch(msg.command) {
|
||||
case GeneralCommands.toVSCode.openLink:
|
||||
if (msg.data) {
|
||||
commands.executeCommand('vscode.open', Uri.parse(msg.data));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a message to the webview
|
||||
* @param command
|
||||
* @param data
|
||||
*/
|
||||
public static sendMsg(command: GeneralCommands, data: any) {
|
||||
public static sendMsg(command: string, data: any) {
|
||||
Logger.info(`Sending message to webview (panel&dashboard): ${command}`);
|
||||
|
||||
const extPath = Extension.getInstance().extensionPath;
|
||||
|
||||
@@ -35,7 +35,7 @@ export class ModeListener extends BaseListener {
|
||||
const activeMode = ModeSwitch.getMode();
|
||||
if (activeMode) {
|
||||
const mode = modes.find(m => m.id === activeMode);
|
||||
this.sendMsg(GeneralCommands.setMode as any, mode);
|
||||
this.sendMsg(GeneralCommands.toWebview.setMode as any, mode);
|
||||
|
||||
// Check the commands that need to be enabled/disabled
|
||||
const snippetsView = mode?.features.find(f => f === FEATURE_FLAG.dashboard.snippets.view);
|
||||
@@ -44,7 +44,7 @@ export class ModeListener extends BaseListener {
|
||||
await commands.executeCommand('setContext', CONTEXT.isSnippetsDashboardEnabled, !!snippetsView);
|
||||
await commands.executeCommand('setContext', CONTEXT.isDataDashboardEnabled, !!dataView);
|
||||
} else {
|
||||
this.sendMsg(GeneralCommands.setMode as any, undefined);
|
||||
this.sendMsg(GeneralCommands.toWebview.setMode as any, undefined);
|
||||
|
||||
// Enable dashboards
|
||||
await this.resetEnablement();
|
||||
|
||||
@@ -10,6 +10,7 @@ export interface Snippet {
|
||||
fields: SnippetField[];
|
||||
openingTags?: string;
|
||||
closingTags?: string;
|
||||
isMediaSnippet?: boolean;
|
||||
}
|
||||
|
||||
export type SnippetSpecialPlaceholders = "FM_SELECTED_TEXT" | string;
|
||||
|
||||
@@ -46,7 +46,7 @@ export default function useMessages() {
|
||||
case Command.mediaSelectionData:
|
||||
setMediaSelecting(message.data);
|
||||
break;
|
||||
case GeneralCommands.setMode:
|
||||
case GeneralCommands.toWebview.setMode:
|
||||
setMode(message.data);
|
||||
break;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user