Snippet dialog

This commit is contained in:
Elio Struyf
2022-03-02 22:00:45 +01:00
parent 48ac869e40
commit a6bdfc3421
5 changed files with 172 additions and 75 deletions
@@ -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,7 +13,7 @@ export interface IMetadataProps {
trigger: () => void;
}
export const Metadata: React.FunctionComponent<IMetadataProps> = ({title, description, cancelBtnText, okBtnText, dismiss, isSaveDisabled, trigger, children}: React.PropsWithChildren<IMetadataProps>) => {
export const FormDialog: React.FunctionComponent<IFormDialogProps> = ({title, description, cancelBtnText, okBtnText, dismiss, isSaveDisabled, trigger, children}: React.PropsWithChildren<IFormDialogProps>) => {
const cancelButtonRef = useRef(null);
@@ -1,17 +1,22 @@
import { Messenger } from '@estruyf/vscode/dist/client';
import { DotsHorizontalIcon, PlusIcon } from '@heroicons/react/outline';
import * as React from 'react';
import { useEffect } from 'react';
import { useEffect, useState } from 'react';
import { useRecoilValue } from 'recoil';
import { Choice, Scanner, SnippetParser, TokenType, Variable, VariableResolver } from '../../../helpers/SnippetParser';
import { DashboardMessage } from '../../DashboardMessage';
import { ViewDataSelector } from '../../state';
import { FormDialog } from '../Modals/FormDialog';
import { SnippetForm } from './SnippetForm';
export interface IItemProps {
title: string;
snippet: any;
}
export const Item: React.FunctionComponent<IItemProps> = ({ snippet }: React.PropsWithChildren<IItemProps>) => {
export const Item: React.FunctionComponent<IItemProps> = ({ title, snippet }: React.PropsWithChildren<IItemProps>) => {
const viewData = useRecoilValue(ViewDataSelector);
const [ showInsertDialog, setShowInsertDialog ] = useState(false);
// Todo: On add, show dialog to insert the placeholders and content
@@ -21,49 +26,55 @@ export const Item: React.FunctionComponent<IItemProps> = ({ snippet }: React.Pro
snippet: snippet.body.join(`\n`)
});
};
useEffect(() => {
const snippetParser = new SnippetParser();
const body = snippet.body.join(`\n`);
const parsed = snippetParser.parse(body);
const placeholders = parsed.placeholderInfo.all;
for (const placeholder of placeholders) {
const tmString = placeholder.toTextmateString();
for (const child of placeholder.children as any[]) {
if (child instanceof Choice) {
console.log(child.options)
} else {
console.log(tmString, child.value);
}
}
}
const resolver: VariableResolver = {
resolve: (variable: Variable): string | undefined => {
console.log(`variable`, variable);
return undefined;
}
};
parsed.resolveVariables(resolver);
}, []);
return (
<li className="group relative bg-gray-50 dark:bg-vulcan-200 shadow-md hover:shadow-xl dark:shadow-none dark:hover:bg-vulcan-100 border border-gray-100 dark:border-vulcan-50 p-4">
<div className="font-bold text-xl mb-2">{snippet.title}</div>
<p className="text-whisper-900 text-base">{snippet.description}</p>
<div>
{
viewData?.data?.filePath ? (
<button onClick={insertToArticle}>Add</button>
) : (
<div>Edit</div>
)
}
</div>
</li>
<>
<li className="group relative bg-gray-50 dark:bg-vulcan-200 shadow-md hover:shadow-xl dark:shadow-none dark:hover:bg-vulcan-100 border border-gray-100 dark:border-vulcan-50 p-4 space-y-2">
<div className="font-bold text-xl">{title}</div>
<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'>
<DotsHorizontalIcon className="w-4 h-4" />
</div>
<div className='hidden group-hover:flex space-x-2'>
{
viewData?.data?.filePath ? (
<>
<button onClick={() => setShowInsertDialog(true)}>
<PlusIcon className='w-4 h-4' />
<span className='sr-only'>Insert snippet</span>
</button>
</>
) : (
<div>Edit</div>
)
}
</div>
</div>
</div>
<p className="text-whisper-900 text-base">{snippet.description}</p>
</li>
{
showInsertDialog && (
<FormDialog
title={`Insert snippet: ${title}`}
description={`Insert the ${title.toLowerCase()} snippet into the current article`}
isSaveDisabled={!viewData?.data?.filePath}
trigger={insertToArticle}
dismiss={() => setShowInsertDialog(false)}
okBtnText='Insert'
cancelBtnText='Cancel'>
<SnippetForm
snippet={snippet} />
</FormDialog>
)
}
</>
);
};
@@ -0,0 +1,72 @@
import * as React from 'react';
import { useEffect, useState } from 'react';
import { Choice, SnippetParser, Variable, VariableResolver } from '../../../helpers/SnippetParser';
export interface ISnippetFormProps {
snippet: any;
}
export const SnippetForm: React.FunctionComponent<ISnippetFormProps> = ({ snippet }: React.PropsWithChildren<ISnippetFormProps>) => {
const [ fields, setFields ] = useState<any>([]);
useEffect(() => {
const snippetParser = new SnippetParser();
const body = snippet.body.join(`\n`);
const parsed = snippetParser.parse(body);
const placeholders = parsed.placeholderInfo.all;
const allFields: any[] = [];
for (const placeholder of placeholders) {
const tmString = placeholder.toTextmateString();
console.log(`tmString`, placeholder);
if (placeholder.children.length === 0) {
allFields.push({
type: 'text',
name: placeholder.index,
value: '',
tmString
});
} else {
for (const child of placeholder.children as any[]) {
if (child instanceof Choice) {
allFields.push({
type: 'select',
name: placeholder.index,
value: (child as any).value,
options: child.options,
tmString
});
} else {
allFields.push({
type: 'text',
name: placeholder.index,
value: (child as any).value,
tmString
});
}
}
}
}
setFields(allFields);
}, []);
return (
<div>
<pre className='border border-opacity-40 p-2 whitespace-normal break-words'>{snippet.body.join(`\n`)}</pre>
<ul className='mt-4'>
{
fields.map((field: any, index: number) => (
<li key={index}>
<p>{field.name} - {field.value} - {(field.options || []).join(',')}</p>
</li>
))
}
</ul>
</div>
);
};
@@ -1,7 +1,7 @@
import { CodeIcon } from '@heroicons/react/outline';
import * as React from 'react';
import { useMemo } from 'react';
import { useRecoilValue } from 'recoil';
import { AddIcon } from '../../../panelWebView/components/Icons/AddIcon';
import { SettingsSelector, ViewDataSelector } from '../../state';
import { PageLayout } from '../Layout/PageLayout';
import { Item } from './Item';
@@ -12,7 +12,8 @@ export const Snippets: React.FunctionComponent<ISnippetsProps> = (props: React.P
const settings = useRecoilValue(SettingsSelector);
const viewData = useRecoilValue(ViewDataSelector);
const snippets = settings?.snippets || [];
const snippetKeys = useMemo(() => Object.keys(settings?.snippets) || [], [settings?.snippets]);
const snippets = settings?.snippets || {};
return (
<PageLayout>
@@ -25,11 +26,14 @@ export const Snippets: React.FunctionComponent<ISnippetsProps> = (props: React.P
}
{
snippets && snippets.length > 0 ? (
snippetKeys && snippetKeys.length > 0 ? (
<ul role="list" className={`grid grid-cols-2 gap-x-4 gap-y-8 sm:grid-cols-3 sm:gap-x-6 lg:grid-cols-4 xl:gap-x-8`}>
{
snippets.map((snippet: any, index: number) => (
<Item snippet={snippet} key={index} />
snippetKeys.map((snippetKey: any, index: number) => (
<Item
key={index}
title={snippetKey}
snippet={snippets[snippetKey]} />
))
}
</ul>
+34 -24
View File
@@ -221,23 +221,8 @@ export abstract class TransformableMarker extends Marker {
}
export class Placeholder extends TransformableMarker {
static compareByIndex(a: Placeholder, b: Placeholder): number {
if (a.index === b.index) {
return 0;
} else if (a.isFinalTabstop) {
return 1;
} else if (b.isFinalTabstop) {
return -1;
} else if (a.index < b.index) {
return -1;
} else if (a.index > b.index) {
return 1;
} else {
return 0;
}
}
constructor(public index: number) {
constructor(public index: number | string) {
super();
}
@@ -629,7 +614,7 @@ export class SnippetParser {
// fill in values for placeholders. the first placeholder of an index
// that has a value defines the value for all placeholders with that index
const placeholderDefaultValues = new Map<number, Marker[] | undefined>();
const placeholderDefaultValues = new Map<number | string, Marker[] | undefined>();
const incompletePlaceholders: Placeholder[] = [];
let placeholderCount = 0;
snippet.walk(marker => {
@@ -876,7 +861,7 @@ export class SnippetParser {
return this._backTo(token);
}
const variable = new Variable(name!);
const placeholder = new Placeholder(String(name!));
if (this._accept(TokenType.Colon)) {
// ${foo:<children>}
@@ -884,24 +869,49 @@ export class SnippetParser {
// ...} -> done
if (this._accept(TokenType.CurlyClose)) {
parent.appendChild(variable);
parent.appendChild(placeholder);
return true;
}
if (this._parse(variable)) {
if (this._parse(placeholder)) {
continue;
}
// fallback
parent.appendChild(new Text('${' + name! + ':'));
variable.children.forEach(parent.appendChild, parent);
placeholder.children.forEach(parent.appendChild, parent);
return true;
}
} else if (this._accept(TokenType.Pipe)) {
const choice = new Choice();
while (true) {
if (this._parseChoiceElement(choice)) {
if (this._accept(TokenType.Comma)) {
// opt, -> more
continue;
}
if (this._accept(TokenType.Pipe)) {
placeholder.appendChild(choice);
if (this._accept(TokenType.CurlyClose)) {
// ..|} -> done
parent.appendChild(placeholder);
return true;
}
}
}
this._backTo(token);
return false;
}
} else if (this._accept(TokenType.Forwardslash)) {
// ${foo/<regex>/<format>/<options>}
if (this._parseTransform(variable)) {
parent.appendChild(variable);
if (this._parseTransform(placeholder)) {
parent.appendChild(placeholder);
return true;
}
@@ -910,7 +920,7 @@ export class SnippetParser {
} else if (this._accept(TokenType.CurlyClose)) {
// ${foo}
parent.appendChild(variable);
parent.appendChild(placeholder);
return true;
} else {