#175 - Simplified snippets

This commit is contained in:
Elio Struyf
2022-03-11 13:53:33 +01:00
parent f77dce3566
commit 6ab6dda1da
16 changed files with 153 additions and 1116 deletions
+29
View File
@@ -28,6 +28,7 @@
"@types/lodash.uniqby": "4.7.6",
"@types/lodash.xor": "^4.5.6",
"@types/mocha": "^5.2.6",
"@types/mustache": "^4.1.2",
"@types/node": "10.17.48",
"@types/node-fetch": "^2.5.12",
"@types/react": "17.0.0",
@@ -56,6 +57,7 @@
"lodash.uniqby": "4.7.0",
"lodash.xor": "^4.5.0",
"mdast-util-from-markdown": "1.0.0",
"mustache": "^4.2.0",
"node-json-db": "^1.3.0",
"npm-run-all": "^4.1.5",
"path-browserify": "^1.0.1",
@@ -822,6 +824,12 @@
"integrity": "sha512-NYrtPht0wGzhwe9+/idPaBB+TqkY9AhTvOLMkThm0IoEfLaiVQZwBwyJ5puCkO3AUCWrmcoePjp2mbFocKy4SQ==",
"dev": true
},
"node_modules/@types/mustache": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/@types/mustache/-/mustache-4.1.2.tgz",
"integrity": "sha512-c4OVMMcyodKQ9dpwBwh3ofK9P6U9ZktKU9S+p33UqwMNN1vlv2P0zJZUScTshnx7OEoIIRcCFNQ904sYxZz8kg==",
"dev": true
},
"node_modules/@types/node": {
"version": "10.17.48",
"resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.48.tgz",
@@ -5986,6 +5994,15 @@
"integrity": "sha1-iZ8R2WhuXgXLkbNdXw5jt3PPyQE=",
"dev": true
},
"node_modules/mustache": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz",
"integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==",
"dev": true,
"bin": {
"mustache": "bin/mustache"
}
},
"node_modules/nanoid": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.2.0.tgz",
@@ -9932,6 +9949,12 @@
"integrity": "sha512-NYrtPht0wGzhwe9+/idPaBB+TqkY9AhTvOLMkThm0IoEfLaiVQZwBwyJ5puCkO3AUCWrmcoePjp2mbFocKy4SQ==",
"dev": true
},
"@types/mustache": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/@types/mustache/-/mustache-4.1.2.tgz",
"integrity": "sha512-c4OVMMcyodKQ9dpwBwh3ofK9P6U9ZktKU9S+p33UqwMNN1vlv2P0zJZUScTshnx7OEoIIRcCFNQ904sYxZz8kg==",
"dev": true
},
"@types/node": {
"version": "10.17.48",
"resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.48.tgz",
@@ -13835,6 +13858,12 @@
"integrity": "sha1-iZ8R2WhuXgXLkbNdXw5jt3PPyQE=",
"dev": true
},
"mustache": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz",
"integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==",
"dev": true
},
"nanoid": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.2.0.tgz",
+7 -1
View File
@@ -225,7 +225,8 @@
"additionalProperties": {
"type": "object",
"required": [
"body"
"body",
"fields"
],
"properties": {
"body": {
@@ -241,6 +242,9 @@
"description": {
"description": "The snippet description.",
"type": "string"
},
"fields": {
"$ref": "#contenttypefield"
}
},
"additionalProperties": false
@@ -1613,6 +1617,7 @@
"@types/lodash.uniqby": "4.7.6",
"@types/lodash.xor": "^4.5.6",
"@types/mocha": "^5.2.6",
"@types/mustache": "^4.1.2",
"@types/node": "10.17.48",
"@types/node-fetch": "^2.5.12",
"@types/react": "17.0.0",
@@ -1641,6 +1646,7 @@
"lodash.uniqby": "4.7.0",
"lodash.xor": "^4.5.0",
"mdast-util-from-markdown": "1.0.0",
"mustache": "^4.2.0",
"node-json-db": "^1.3.0",
"npm-run-all": "^4.1.5",
"path-browserify": "^1.0.1",
-7
View File
@@ -1,7 +0,0 @@
export const SnippetVariables = {
FM_SELECTED_TEXT: 'FM_SELECTED_TEXT',
FM_TEXT: 'FM_TEXT_',
FM_MULTILINE: 'FM_MULTILINE_',
};
-1
View File
@@ -7,7 +7,6 @@ export * from './FrameworkDetectors';
export * from './Links';
export * from './LocalStore';
export * from './Navigation';
export * from './SnippetVariables';
export * from './TelemetryEvent';
export * from './charCode';
export * from './charMap';
@@ -3,6 +3,8 @@ import { CodeIcon, DotsHorizontalIcon, PencilIcon, PlusIcon, TrashIcon } from '@
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';
@@ -12,7 +14,7 @@ import SnippetForm, { SnippetFormHandle } from './SnippetForm';
export interface IItemProps {
title: string;
snippet: any;
snippet: Snippet;
}
export const Item: React.FunctionComponent<IItemProps> = ({ title, snippet }: React.PropsWithChildren<IItemProps>) => {
@@ -53,13 +55,21 @@ export const Item: React.FunctionComponent<IItemProps> = ({ title, snippet }: Re
return;
}
const snippets = Object.assign({}, settings?.snippets || {});
const snippets: Snippets = Object.assign({}, settings?.snippets || {});
const snippetLines = snippetOriginalBody.split("\n");
const snippetContents = {
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 {
@@ -1,6 +1,4 @@
import { ChevronDownIcon, ChevronUpIcon } from '@heroicons/react/outline';
import * as React from 'react';
import { SnippetVariables } from '../../../constants';
export interface INewFormProps {
title: string;
@@ -13,7 +11,6 @@ export interface INewFormProps {
}
export const NewForm: React.FunctionComponent<INewFormProps> = ({ title, description, body, onTitleUpdate, onDescriptionUpdate, onBodyUpdate }: React.PropsWithChildren<INewFormProps>) => {
const [ showDetails, setShowDetails ] = React.useState(false);
return (
<div className='space-y-4'>
@@ -63,47 +60,6 @@ export const NewForm: React.FunctionComponent<INewFormProps> = ({ title, descrip
/>
</div>
</div>
<div>
<h3 className="text-base text-vulcan-300 dark:text-whisper-500 flex items-center">
<span>Placeholders guidelines</span>
{
showDetails ? (
<button onClick={() => setShowDetails(false)}>
<ChevronUpIcon className="w-4 h-4 text-gray-500" />
</button>
) : (
<button onClick={() => setShowDetails(true)}>
<ChevronDownIcon className="w-4 h-4 text-gray-500" />
</button>
)
}
</h3>
<dl className="divide-y divide-gray-200 dark:divide-vulcan-200" style={{ zIndex: -1 }}>
<div className="py-2 flex justify-between text-xs font-medium">
<dt className="text-vulcan-100 dark:text-whisper-900">Insert selected text (can still be updated)</dt>
<dd className="text-vulcan-300 dark:text-whisper-500 text-right">{`\${${SnippetVariables.FM_SELECTED_TEXT}}`}</dd>
</div>
<div className="py-2 flex justify-between text-xs font-medium">
<dt className="text-vulcan-100 dark:text-whisper-900">Variable without default</dt>
<dd className="text-vulcan-300 dark:text-whisper-500 text-right">{`\${variable}`}</dd>
</div>
<div className="py-2 flex justify-between text-xs font-medium">
<dt className="text-vulcan-100 dark:text-whisper-900">Variable with default</dt>
<dd className="text-vulcan-300 dark:text-whisper-500 text-right">{`\${variable:default}`}</dd>
</div>
<div className="py-2 flex justify-between text-xs font-medium">
<dt className="text-vulcan-100 dark:text-whisper-900">Variable with choices</dt>
<dd className="text-vulcan-300 dark:text-whisper-500 text-right">{`\${variable|choice 1,choice 2,choice 3|}`}</dd>
</div>
</dl>
</div>
</div>
);
};
@@ -2,16 +2,15 @@ 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 { SnippetVariables } from '../../../constants';
import { Choice, SnippetParser, Variable, VariableResolver } from '../../../helpers/SnippetParser';
import { SnippetField } from '../../../models';
import { SnippetParser } from '../../../helpers/SnippetParser';
import { Snippet, SnippetField, SnippetSpecialPlaceholders } from '../../../models';
import { DashboardMessage } from '../../DashboardMessage';
import { ViewDataSelector } from '../../state';
import { SnippetInputField } from './SnippetInputField';
export interface ISnippetFormProps {
snippet: any;
snippet: Snippet;
selection: string | undefined;
}
@@ -27,23 +26,24 @@ const SnippetForm: React.ForwardRefRenderFunction<SnippetFormHandle, ISnippetFor
setFields(prevFields => prevFields.map(f => f.name === field.name ? { ...f, value } : f));
}, [setFields]);
const insertSelectionValue = useCallback((value: string) => {
if (selection && value === SnippetVariables.FM_SELECTED_TEXT) {
return selection;
const insertSelectionValue = useCallback((value: SnippetSpecialPlaceholders) => {
if (value === "FM_SELECTED_TEXT") {
return selection || "";
}
return;
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) {
body = body.replace(field.tmString, field.value);
obj[field.name] = field.value;
}
return body;
}, [fields, selection]);
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);
@@ -53,22 +53,6 @@ const SnippetForm: React.ForwardRefRenderFunction<SnippetFormHandle, ISnippetFor
return true;
}
const fieldNameRender = (fieldName: string) => {
if (fieldName === SnippetVariables.FM_SELECTED_TEXT) {
return 'Selected Text';
}
if (fieldName.startsWith(SnippetVariables.FM_TEXT)) {
return fieldName.replace(SnippetVariables.FM_TEXT, '');
}
if (fieldName.startsWith(SnippetVariables.FM_MULTILINE)) {
return fieldName.replace(SnippetVariables.FM_MULTILINE, '');
}
return fieldName;
}
useImperativeHandle(ref, () => ({
onSave() {
if (!snippetBody) {
@@ -83,62 +67,35 @@ const SnippetForm: React.ForwardRefRenderFunction<SnippetFormHandle, ISnippetFor
}));
useEffect(() => {
// Defines the type of field that needs to be rendered
const getFieldType = (fieldName: string) => {
if (fieldName.startsWith(SnippetVariables.FM_MULTILINE)) {
return 'textarea';
}
return 'text';
}
// Get all placeholder variables from the snippet
const snippetParser = new SnippetParser();
const body = typeof snippet.body === "string" ? snippet.body : snippet.body.join(`\n`);
const parsed = snippetParser.parse(body);
const placeholders = parsed.placeholderInfo.all;
const placeholders = SnippetParser.getPlaceholders(body, snippet.openingTags, snippet.closingTags);
const allFields: any[] = [];
const allFields: SnippetField[] = [];
const snippetFields = snippet.fields || [];
for (const placeholder of placeholders) {
const tmString = placeholder.toTextmateString();
for (const fieldName of placeholders) {
const field = snippetFields.find(f => f.name === fieldName);
// If only a variable is defined, it will not contain children
if (placeholder.children.length === 0) {
if (field) {
allFields.push({
type: getFieldType(placeholder.index as string),
name: placeholder.index,
value: insertSelectionValue(placeholder.index as string) || '',
tmString
...field,
value: insertSelectionValue(field.default || "")
});
} else {
// Children are defined, so it means it is a choice field or the variable has a default value
for (const child of placeholder.children as any[]) {
if (child instanceof Choice) {
const options = child.options.map(o => o.value);
allFields.push({
type: 'select',
name: placeholder.index,
value: (child as any).value || options[0] || "",
options,
tmString
});
} else {
allFields.push({
type: getFieldType(placeholder.index as string),
name: placeholder.index,
value: insertSelectionValue((child as any).value as string) || insertSelectionValue(placeholder.index as string) || (child as any).value || "",
tmString
});
}
}
allFields.push({
name: fieldName,
title: fieldName,
type: "string",
single: true,
value: ""
});
}
}
setFields(allFields);
}, []);
}, [snippet]);
return (
<div>
@@ -152,7 +109,7 @@ const SnippetForm: React.ForwardRefRenderFunction<SnippetFormHandle, ISnippetFor
shouldShowField(field.name, index, allFields) && (
<div key={index}>
<label htmlFor={field.name} className="block text-sm font-medium capitalize">
{fieldNameRender(field.name)}
{field.title || field.name}
</label>
<div className="mt-1">
<SnippetInputField
@@ -1,7 +1,6 @@
import * as React from 'react';
import { ChevronDownIcon } from '@heroicons/react/outline';
import { SnippetField } from '../../../models';
import { SnippetVariables } from '../../../constants';
import { Choice, SnippetField } from '../../../models';
export interface ISnippetInputFieldProps {
field: SnippetField;
@@ -10,17 +9,19 @@ export interface ISnippetInputFieldProps {
export const SnippetInputField: React.FunctionComponent<ISnippetInputFieldProps> = ({ field, onValueChange }: React.PropsWithChildren<ISnippetInputFieldProps>) => {
if (field.type === 'select') {
if (field.type === 'choice') {
return (
<div className='relative'>
<select
name={field.name}
value={field.value || ""}
className="focus:outline-none block w-full sm:text-sm border-gray-300 text-vulcan-500"
className="focus:outline-none block w-full sm:text-sm border-gray-300 text-vulcan-500"
onChange={e => onValueChange(field, e.target.value)}>
{
field.options?.map((option: string, index: number) => (
<option key={index} value={option}>{option}</option>
(field.choices || [])?.map((option: string | Choice, index: number) => (
typeof option === 'string' ?
<option key={index} value={option}>{option}</option> :
<option key={index} value={option.id}>{option.title}</option>
))
}
</select>
@@ -30,7 +31,7 @@ export const SnippetInputField: React.FunctionComponent<ISnippetInputFieldProps>
)
}
if (field.type === 'textarea') {
if (field.type === 'string' && !field.single) {
return (
<textarea
name={field.name}
@@ -4,6 +4,7 @@ import * as React from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useRecoilValue } from 'recoil';
import { TelemetryEvent } from '../../../constants/TelemetryEvent';
import { SnippetParser } from '../../../helpers/SnippetParser';
import { DashboardMessage } from '../../DashboardMessage';
import { SettingsSelector, ViewDataSelector } from '../../state';
import { PageLayout } from '../Layout/PageLayout';
@@ -21,8 +22,8 @@ export const Snippets: React.FunctionComponent<ISnippetsProps> = (props: React.P
const [ snippetBody, setSnippetBody ] = useState<string>('');
const [ showCreateDialog, setShowCreateDialog ] = useState(false);
const snippetKeys = useMemo(() => Object.keys(settings?.snippets) || [], [settings?.snippets]);
const snippets = settings?.snippets || {};
const snippetKeys = useMemo(() => Object.keys(snippets) || [], [settings?.snippets]);
const onSnippetAdd = useCallback(() => {
if (!snippetTitle || !snippetBody) {
@@ -30,10 +31,13 @@ export const Snippets: React.FunctionComponent<ISnippetsProps> = (props: React.P
return;
}
const fields = SnippetParser.getFields(snippetBody, []);
Messenger.send(DashboardMessage.addSnippet, {
title: snippetTitle,
description: snippetDescription || '',
body: snippetBody
body: snippetBody,
fields
});
reset();
+2 -2
View File
@@ -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,7 +29,7 @@ export interface Settings {
dataFiles: DataFile[] | undefined;
dataTypes: DataType[] | undefined;
isBacker: boolean | undefined;
snippets: any | undefined;
snippets: Snippets | undefined;
}
export interface DashboardState {
+2 -2
View File
@@ -4,7 +4,7 @@ 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 } 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";
@@ -52,7 +52,7 @@ export class DashboardSettings {
},
dataFiles: await this.getDataFiles(),
dataTypes: Settings.get<DataType[]>(SETTING_DATA_TYPES),
snippets: Settings.get<DataType[]>(SETTING_CONTENT_SNIPPETS),
snippets: Settings.get<Snippets>(SETTING_CONTENT_SNIPPETS),
isBacker: await ext.getState<boolean | undefined>(CONTEXT.backer, 'global')
} as ISettings
}
File diff suppressed because it is too large Load Diff
+3 -2
View File
@@ -26,7 +26,7 @@ export class SnippetListener extends BaseListener {
}
private static async addSnippet(data: any) {
const { title, description, body } = data;
const { title, description, body, fields } = data;
if (!title || !body) {
Notifications.warning("Snippet missing title or body");
@@ -43,7 +43,8 @@ export class SnippetListener extends BaseListener {
snippets[title] = {
description,
body: snippetLines.length === 1 ? snippetLines[0] : snippetLines
body: snippetLines.length === 1 ? snippetLines[0] : snippetLines,
fields: fields || []
};
Settings.update(SETTING_CONTENT_SNIPPETS, snippets, true);
-7
View File
@@ -1,7 +0,0 @@
export interface SnippetField {
name: string;
value: string;
type: 'text' | 'textarea' | 'select';
tmString: string;
options?: string[];
}
+20
View File
@@ -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;
}
+1 -1
View File
@@ -10,7 +10,7 @@ export * from './DraftField';
export * from './Framework';
export * from './MediaPaths';
export * from './PanelSettings';
export * from './SnippetField';
export * from './Snippets';
export * from './SortOrder';
export * from './SortType';
export * from './SortingSetting';