#824 - Field actions

This commit is contained in:
Elio Struyf
2024-06-28 09:23:26 +02:00
parent 0e6e776f70
commit b03d972d31
15 changed files with 347 additions and 238 deletions
+1
View File
@@ -5,6 +5,7 @@
### ✨ New features
- [#823](https://github.com/estruyf/vscode-front-matter/issues/823): Integrated GitHub Copilot support for titles, descriptions, and tags
- [#824](https://github.com/estruyf/vscode-front-matter/issues/824): Added the ability to link custom actions to fields
### 🎨 Enhancements
+3
View File
@@ -438,6 +438,9 @@
"panel.fields.wrapperField.unknown": "Unkown field type: {0}",
"panel.fields.fieldCustomAction.button.title": "Custom action",
"panel.fields.fieldCustomAction.executing": "Executing field action...",
"panel.actions.title": "Actions",
"panel.articleDetails.title": "More details",
+150 -184
View File
@@ -10,8 +10,7 @@
"color": "#0e131f",
"theme": "dark"
},
"badges": [
{
"badges": [{
"description": "version",
"url": "https://img.shields.io/github/package-json/v/estruyf/vscode-front-matter?color=green&label=vscode-front-matter&style=flat-square",
"href": "https://github.com/estruyf/vscode-front-matter"
@@ -71,8 +70,7 @@
"**/.frontmatter/config/*.json": "jsonc"
}
},
"keybindings": [
{
"keybindings": [{
"command": "frontMatter.dashboard",
"key": "alt+d"
},
@@ -96,23 +94,19 @@
}
],
"viewsContainers": {
"activitybar": [
{
"id": "frontmatter-explorer",
"title": "FM",
"icon": "$(fm-logo)"
}
]
"activitybar": [{
"id": "frontmatter-explorer",
"title": "FM",
"icon": "$(fm-logo)"
}]
},
"views": {
"frontmatter-explorer": [
{
"id": "frontMatter.explorer",
"name": "Front Matter",
"icon": "$(fm-logo)",
"type": "webview"
}
]
"frontmatter-explorer": [{
"id": "frontMatter.explorer",
"name": "Front Matter",
"icon": "$(fm-logo)",
"type": "webview"
}]
},
"configuration": {
"title": "%settings.configuration.title%",
@@ -180,8 +174,7 @@
"frontMatter.content.defaultFileType": {
"type": "string",
"default": "md",
"oneOf": [
{
"oneOf": [{
"enum": [
"md",
"mdx"
@@ -197,8 +190,7 @@
"frontMatter.content.defaultSorting": {
"type": "string",
"default": "",
"oneOf": [
{
"oneOf": [{
"enum": [
"LastModifiedAsc",
"LastModifiedDesc",
@@ -550,8 +542,7 @@
"categories"
],
"markdownDescription": "%setting.frontMatter.content.filters.markdownDescription%",
"items": [
{
"items": [{
"type": "string",
"enum": [
"contentFolders",
@@ -577,6 +568,7 @@
"default": [],
"markdownDescription": "%setting.frontMatter.custom.scripts.markdownDescription%",
"items": {
"$id": "#customscript",
"type": "object",
"properties": {
"id": {
@@ -624,8 +616,7 @@
"command": {
"$id": "#scriptCommand",
"type": "string",
"anyOf": [
{
"anyOf": [{
"enum": [
"node",
"bash",
@@ -821,8 +812,7 @@
"title",
"file"
],
"anyOf": [
{
"anyOf": [{
"required": [
"schema"
]
@@ -876,8 +866,7 @@
"id",
"path"
],
"anyOf": [
{
"anyOf": [{
"required": [
"schema"
]
@@ -1118,29 +1107,26 @@
}
}
},
"default": [
{
"name": "default",
"fileTypes": null,
"fields": [
{
"title": "Title",
"name": "title",
"type": "string"
},
{
"title": "Caption",
"name": "caption",
"type": "string"
},
{
"title": "Alt text",
"name": "alt",
"type": "string"
}
]
}
],
"default": [{
"name": "default",
"fileTypes": null,
"fields": [{
"title": "Title",
"name": "title",
"type": "string"
},
{
"title": "Caption",
"name": "caption",
"type": "string"
},
{
"title": "Alt text",
"name": "alt",
"type": "string"
}
]
}],
"scope": "Media"
},
"frontMatter.media.supportedMimeTypes": {
@@ -1376,8 +1362,7 @@
"default": "",
"description": "%setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.taxonomyId.description%",
"not": {
"anyOf": [
{
"anyOf": [{
"const": ""
},
{
@@ -1564,6 +1549,9 @@
"description": "%setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.when.properties.caseSensitive.description%"
}
}
},
"action": {
"$ref": "#customscript"
}
},
"additionalProperties": false,
@@ -1571,8 +1559,7 @@
"type",
"name"
],
"allOf": [
{
"allOf": [{
"if": {
"properties": {
"type": {
@@ -1784,51 +1771,48 @@
"fields"
]
},
"default": [
{
"name": "default",
"pageBundle": false,
"fields": [
{
"title": "Title",
"name": "title",
"type": "string"
},
{
"title": "Description",
"name": "description",
"type": "string"
},
{
"title": "Publishing date",
"name": "date",
"type": "datetime",
"default": "{{now}}",
"isPublishDate": true
},
{
"title": "Content preview",
"name": "preview",
"type": "image"
},
{
"title": "Is in draft",
"name": "draft",
"type": "boolean"
},
{
"title": "Tags",
"name": "tags",
"type": "tags"
},
{
"title": "Categories",
"name": "categories",
"type": "categories"
}
]
}
],
"default": [{
"name": "default",
"pageBundle": false,
"fields": [{
"title": "Title",
"name": "title",
"type": "string"
},
{
"title": "Description",
"name": "description",
"type": "string"
},
{
"title": "Publishing date",
"name": "date",
"type": "datetime",
"default": "{{now}}",
"isPublishDate": true
},
{
"title": "Content preview",
"name": "preview",
"type": "image"
},
{
"title": "Is in draft",
"name": "draft",
"type": "boolean"
},
{
"title": "Tags",
"name": "tags",
"type": "tags"
},
{
"title": "Categories",
"name": "categories",
"type": "categories"
}
]
}],
"scope": "Taxonomy"
},
"frontMatter.taxonomy.customTaxonomy": {
@@ -1841,8 +1825,7 @@
"type": "string",
"description": "%setting.frontMatter.taxonomy.customTaxonomy.items.properties.id.description%",
"not": {
"anyOf": [
{
"anyOf": [{
"const": ""
},
{
@@ -2042,8 +2025,7 @@
}
}
},
"commands": [
{
"commands": [{
"command": "frontMatter.project.switch",
"title": "%command.frontMatter.project.switch%",
"category": "Front Matter",
@@ -2375,21 +2357,16 @@
}
}
],
"submenus": [
{
"id": "frontmatter.submenu",
"label": "Front Matter"
}
],
"submenus": [{
"id": "frontmatter.submenu",
"label": "Front Matter"
}],
"menus": {
"webview/context": [
{
"command": "workbench.action.webview.openDeveloperTools",
"when": "frontMatter:isDevelopment"
}
],
"editor/title": [
{
"webview/context": [{
"command": "workbench.action.webview.openDeveloperTools",
"when": "frontMatter:isDevelopment"
}],
"editor/title": [{
"command": "frontMatter.markup.heading",
"group": "navigation@-133",
"when": "frontMatter:file:isValid == true && frontMatter:markdown:wysiwyg"
@@ -2475,14 +2452,11 @@
"when": "resourceFilename == 'frontmatter.json'"
}
],
"explorer/context": [
{
"submenu": "frontmatter.submenu",
"group": "frontmatter@1"
}
],
"frontmatter.submenu": [
{
"explorer/context": [{
"submenu": "frontmatter.submenu",
"group": "frontmatter@1"
}],
"frontmatter.submenu": [{
"command": "frontMatter.createFromTemplate",
"when": "explorerResourceIsFolder",
"group": "frontmatter@1"
@@ -2498,8 +2472,7 @@
"group": "frontmatter@3"
}
],
"commandPalette": [
{
"commandPalette": [{
"command": "frontMatter.init",
"when": "frontMatterCanInit"
},
@@ -2676,8 +2649,7 @@
"when": "frontMatter:file:isValid == true"
}
],
"view/title": [
{
"view/title": [{
"command": "frontMatter.docs",
"group": "navigation@-1",
"when": "view == frontMatter.explorer"
@@ -2714,16 +2686,13 @@
}
]
},
"languages": [
{
"id": "frontmatter.project.output",
"mimetypes": [
"text/x-code-output"
]
}
],
"grammars": [
{
"languages": [{
"id": "frontmatter.project.output",
"mimetypes": [
"text/x-code-output"
]
}],
"grammars": [{
"path": "./syntaxes/hugo.tmLanguage.json",
"scopeName": "frontmatter.markdown.hugo",
"injectTo": [
@@ -2736,48 +2705,45 @@
"path": "./syntaxes/frontmatter-output.tmLanguage.json"
}
],
"walkthroughs": [
{
"id": "frontmatter.welcome",
"title": "Get started with Front Matter",
"description": "Discover the features of Front Matter and learn how to use the CMS for your SSG or static site.",
"steps": [
{
"id": "frontmatter.welcome.init",
"title": "Get started",
"description": "Initial steps to get started.\n[Open dashboard](command:frontMatter.dashboard)",
"media": {
"markdown": "assets/walkthrough/get-started.md"
},
"completionEvents": [
"onContext:frontMatterInitialized"
]
"walkthroughs": [{
"id": "frontmatter.welcome",
"title": "Get started with Front Matter",
"description": "Discover the features of Front Matter and learn how to use the CMS for your SSG or static site.",
"steps": [{
"id": "frontmatter.welcome.init",
"title": "Get started",
"description": "Initial steps to get started.\n[Open dashboard](command:frontMatter.dashboard)",
"media": {
"markdown": "assets/walkthrough/get-started.md"
},
{
"id": "frontmatter.welcome.documentation",
"title": "Documentation",
"description": "Check out the documentation for Front Matter.\n[View our documentation](https://frontmatter.codes/docs)",
"media": {
"markdown": "assets/walkthrough/documentation.md"
},
"completionEvents": [
"onLink:https://frontmatter.codes/docs"
]
"completionEvents": [
"onContext:frontMatterInitialized"
]
},
{
"id": "frontmatter.welcome.documentation",
"title": "Documentation",
"description": "Check out the documentation for Front Matter.\n[View our documentation](https://frontmatter.codes/docs)",
"media": {
"markdown": "assets/walkthrough/documentation.md"
},
{
"id": "frontmatter.welcome.supporter",
"title": "Support the project",
"description": "Become a supporter.\n[Support the project](https://github.com/sponsors/estruyf)",
"media": {
"markdown": "assets/walkthrough/support-the-project.md"
},
"completionEvents": [
"onLink:https://github.com/sponsors/estruyf"
]
}
]
}
]
"completionEvents": [
"onLink:https://frontmatter.codes/docs"
]
},
{
"id": "frontmatter.welcome.supporter",
"title": "Support the project",
"description": "Become a supporter.\n[Support the project](https://github.com/sponsors/estruyf)",
"media": {
"markdown": "assets/walkthrough/support-the-project.md"
},
"completionEvents": [
"onLink:https://github.com/sponsors/estruyf"
]
}
]
}]
},
"scripts": {
"dev:ext": "npm run clean && npm run localization:generate && npm-run-all --parallel watch:*",
@@ -2905,4 +2871,4 @@
"dependencies": {
"@radix-ui/react-dropdown-menu": "^2.0.6"
}
}
}
+14 -6
View File
@@ -67,11 +67,11 @@ export class CustomScript {
* @param path
* @returns
*/
private static async singleRun(
public static async singleRun(
wsPath: string,
script: ICustomScript,
path: string | null = null
): Promise<void> {
): Promise<any> {
let articlePath: string | null = path;
let article: ParsedFrontMatter | null | undefined = null;
@@ -99,7 +99,7 @@ export class CustomScript {
articlePath as string,
script
);
await CustomScript.showOutput(output, script, articlePath);
return await CustomScript.showOutput(output, script, articlePath);
}
);
} else {
@@ -133,7 +133,7 @@ export class CustomScript {
title: l10n.t(LocalizationKey.helpersCustomScriptExecuting, script.title),
cancellable: false
},
async (progress, token) => {
async (_, __) => {
for await (const folder of folders) {
if (folder.lastModified.length > 0) {
for await (const file of folder.lastModified) {
@@ -266,15 +266,21 @@ export class CustomScript {
output: string | null,
script: ICustomScript,
articlePath?: string | null
): Promise<void> {
): Promise<any> {
if (output) {
try {
const data: {
frontmatter?: { [key: string]: any };
fmAction?: 'open' | 'copyMediaMetadata' | 'copyMediaMetadataAndDelete' | 'deleteMedia';
fmAction?:
| 'open'
| 'copyMediaMetadata'
| 'copyMediaMetadataAndDelete'
| 'deleteMedia'
| 'fieldAction';
fmPath?: string;
fmSourcePath?: string;
fmDestinationPath?: string;
fmFieldValue?: any;
} = JSON.parse(output);
if (data.frontmatter) {
@@ -326,6 +332,8 @@ export class CustomScript {
await MediaHelpers.deleteFile(data.fmSourcePath);
} else if (data.fmAction === 'deleteMedia' && data.fmPath) {
await MediaHelpers.deleteFile(data.fmPath);
} else if (data.fmAction === 'fieldAction') {
return data.fmFieldValue || undefined;
}
} else {
Logger.error(`No frontmatter found.`);
+28 -1
View File
@@ -1,5 +1,6 @@
import { Folders } from '../../commands';
import { SETTING_CUSTOM_SCRIPTS } from '../../constants';
import { CustomScript, Settings } from '../../helpers';
import { CustomScript, Notifications, Settings } from '../../helpers';
import { CustomScript as ICustomScript, PostMessageData } from '../../models';
import { CommandToCode } from '../../panelWebView/CommandToCode';
import { BaseListener } from './BaseListener';
@@ -16,6 +17,32 @@ export class ScriptListener extends BaseListener {
case CommandToCode.runCustomScript:
this.runCustomScript(msg);
break;
case CommandToCode.runFieldAction:
this.runFieldAction(msg);
break;
}
}
private static async runFieldAction({ command, payload, requestId }: PostMessageData) {
if (!payload || !requestId || !command) {
return;
}
const script = payload as ICustomScript;
if (script.script) {
const wsFolder = Folders.getWorkspaceFolder();
if (!wsFolder) {
return;
}
const fieldValue = await CustomScript.singleRun(wsFolder.fsPath, script);
if (fieldValue) {
this.sendRequest(command, requestId, fieldValue);
} else {
Notifications.error('The script did not return a field value');
this.sendRequestError(command, requestId, 'The script did not return a field value');
}
}
}
+1
View File
@@ -1 +1,2 @@
export * from './localization.enum';
export * from './localize';
+8
View File
@@ -1416,6 +1416,14 @@ export enum LocalizationKey {
* Unkown field type: {0}
*/
panelFieldsWrapperFieldUnknown = 'panel.fields.wrapperField.unknown',
/**
* Custom action
*/
panelFieldsFieldCustomActionButtonTitle = 'panel.fields.fieldCustomAction.button.title',
/**
* Executing field action...
*/
panelFieldsFieldCustomActionExecuting = 'panel.fields.fieldCustomAction.executing',
/**
* Actions
*/
+5
View File
@@ -0,0 +1,5 @@
import * as l10n from '@vscode/l10n';
export const localize = (key: string, ...args: any[]): string => {
return l10n.t(key, ...args);
};
+3
View File
@@ -141,6 +141,9 @@ export interface Field {
// When clause
when?: WhenClause;
// Custom action
action?: CustomScript;
}
export interface NumberOptions {
+2 -1
View File
@@ -46,5 +46,6 @@ export enum CommandToCode {
copilotSuggestTaxonomy = 'copilot-suggest-taxonomy',
searchByType = 'search-by-type',
processMediaData = 'process-media-data',
isServerStarted = 'is-server-started'
isServerStarted = 'is-server-started',
runFieldAction = 'run-field-action'
}
@@ -0,0 +1,48 @@
import * as React from 'react';
import { CustomScript } from '../../../models';
import { messageHandler } from '@estruyf/vscode/dist/client';
import { CodeBracketIcon } from '@heroicons/react/24/outline';
import { CommandToCode } from '../../CommandToCode';
import { LocalizationKey, localize } from '../../../localization';
export interface IFieldCustomActionProps {
action: CustomScript;
disabled?: boolean;
triggerLoading?: (message?: string) => void;
onChange: (value: any) => void;
}
export const FieldCustomAction: React.FunctionComponent<IFieldCustomActionProps> = ({ action, disabled, triggerLoading, onChange }: React.PropsWithChildren<IFieldCustomActionProps>) => {
return (
<button
className="metadata_field__title__action inline-block text-[var(--vscode-editor-foreground)] disabled:opacity-50"
title={action?.title || localize(LocalizationKey.panelFieldsFieldCustomActionButtonTitle)}
type="button"
onClick={() => {
if (triggerLoading) {
triggerLoading(localize(LocalizationKey.panelFieldsFieldCustomActionExecuting));
}
messageHandler.request(CommandToCode.runFieldAction, {
...action
}).then((value: any) => {
onChange(value);
if (triggerLoading) {
triggerLoading();
}
}).catch(() => {
console.error('Error while running the custom action');
if (triggerLoading) {
triggerLoading();
}
});
}}
disabled={disabled}
>
<span className='sr-only'>{action?.title || localize(LocalizationKey.panelFieldsFieldCustomActionButtonTitle)}</span>
<CodeBracketIcon style={{ height: "16px", width: "16px" }} aria-hidden="true" />
</button>
);
};
@@ -1,6 +1,8 @@
import * as React from 'react';
import { useMemo } from 'react';
import { RequiredAsterix } from './RequiredAsterix';
import { CustomScript } from '../../../models';
import { FieldCustomAction } from './FieldCustomAction';
export interface IFieldTitleProps {
label: string | JSX.Element;
@@ -8,6 +10,10 @@ export interface IFieldTitleProps {
className?: string;
required?: boolean;
actionElement?: JSX.Element;
customAction?: CustomScript;
isDisabled?: boolean;
triggerLoading?: (message?: string) => void;
onChange?: (value: any) => void;
}
export const FieldTitle: React.FunctionComponent<IFieldTitleProps> = ({
@@ -16,6 +22,10 @@ export const FieldTitle: React.FunctionComponent<IFieldTitleProps> = ({
className,
required,
actionElement,
customAction,
isDisabled,
triggerLoading,
onChange,
}: React.PropsWithChildren<IFieldTitleProps>) => {
const Icon = useMemo(() => {
return icon ? React.cloneElement(icon, { style: { width: '16px', height: '16px' } }) : null;
@@ -29,7 +39,19 @@ export const FieldTitle: React.FunctionComponent<IFieldTitleProps> = ({
<RequiredAsterix required={required} />
</label>
{actionElement}
<div className="flex gap-4">
{
customAction && onChange && (
<FieldCustomAction
action={customAction}
disabled={isDisabled}
triggerLoading={triggerLoading}
onChange={onChange} />
)
}
{actionElement}
</div>
</div>
);
};
@@ -5,7 +5,7 @@ import { CommandToCode } from '../../CommandToCode';
import { TagType } from '../../TagType';
import Downshift from 'downshift';
import { AddIcon } from '../Icons/AddIcon';
import { BlockFieldData, CustomTaxonomyData } from '../../../models';
import { BlockFieldData, CustomScript, CustomTaxonomyData } from '../../../models';
import { useCallback, useEffect, useMemo } from 'react';
import { messageHandler, Messenger } from '@estruyf/vscode/dist/client';
import { FieldMessage } from '../Fields/FieldMessage';
@@ -13,8 +13,7 @@ import { FieldTitle } from '../Fields/FieldTitle';
import { useRecoilValue } from 'recoil';
import { PanelSettingsAtom } from '../../state';
import { SparklesIcon } from '@heroicons/react/24/outline';
import * as l10n from '@vscode/l10n';
import { LocalizationKey } from '../../../localization';
import { LocalizationKey, localize } from '../../../localization';
import useDropdownStyle from '../../hooks/useDropdownStyle';
import { CopilotIcon } from '../Icons';
@@ -37,6 +36,7 @@ export interface ITagPickerProps {
limit?: number;
required?: boolean;
renderAsString?: boolean;
action?: CustomScript;
}
const TagPicker: React.FunctionComponent<ITagPickerProps> = ({
@@ -56,7 +56,8 @@ const TagPicker: React.FunctionComponent<ITagPickerProps> = ({
blockData,
limit,
required,
renderAsString
renderAsString,
action
}: React.PropsWithChildren<ITagPickerProps>) => {
const [selected, setSelected] = React.useState<string[]>([]);
const [inputValue, setInputValue] = React.useState<string>('');
@@ -65,7 +66,7 @@ const TagPicker: React.FunctionComponent<ITagPickerProps> = ({
const { getDropdownStyle } = useDropdownStyle(inputRef as any);
const dsRef = React.useRef<Downshift<string> | null>(null);
const settings = useRecoilValue(PanelSettingsAtom);
const [loading, setLoading] = React.useState<boolean>(false);
const [loading, setLoading] = React.useState<string | undefined>(undefined);
/**
* Removes an option
@@ -249,25 +250,29 @@ const TagPicker: React.FunctionComponent<ITagPickerProps> = ({
[options, inputRef, selected, freeform]
);
const updateTaxonomy = (values: string[]) => {
if (values && values instanceof Array && values.length > 0) {
const uniqValues = Array.from(new Set([...selected, ...values]));
setSelected(uniqValues);
sendUpdate(uniqValues);
setInputValue('');
}
}
const suggestTaxonomy = useCallback(
(aiType: 'ai' | 'copilot', type: TagType) => {
setLoading(true);
setLoading(localize(LocalizationKey.panelTagPickerAiGenerating));
const command =
aiType === 'ai' ? CommandToCode.aiSuggestTaxonomy : CommandToCode.copilotSuggestTaxonomy;
messageHandler
.request<string[]>(command, type)
.then((values) => {
setLoading(false);
if (values && values instanceof Array && values.length > 0) {
const uniqValues = Array.from(new Set([...selected, ...values]));
setSelected(uniqValues);
sendUpdate(uniqValues);
setInputValue('');
}
setLoading(undefined);
updateTaxonomy(values)
})
.catch(() => {
setLoading(false);
setLoading(undefined);
});
},
[selected]
@@ -286,13 +291,13 @@ const TagPicker: React.FunctionComponent<ITagPickerProps> = ({
const inputPlaceholder = useMemo((): string => {
if (checkIsDisabled()) {
return l10n.t(
return localize(
LocalizationKey.panelTagPickerInputPlaceholderDisabled,
`${limit} ${label || type.toLowerCase()}`
);
}
return l10n.t(LocalizationKey.panelTagPickerInputPlaceholderEmpty, label || type.toLowerCase());
return localize(LocalizationKey.panelTagPickerInputPlaceholderEmpty, label || type.toLowerCase());
}, [label, type, checkIsDisabled]);
const showRequiredState = useMemo(() => {
@@ -305,17 +310,17 @@ const TagPicker: React.FunctionComponent<ITagPickerProps> = ({
}
return (
<div className="flex gap-4">
<>
{settings?.aiEnabled && (
<button
className="metadata_field__title__action"
title={l10n.t(
title={localize(
LocalizationKey.panelTagPickerAiSuggest,
label?.toLowerCase() || type.toLowerCase()
)}
type="button"
onClick={() => suggestTaxonomy('ai', type)}
disabled={loading}
disabled={!!loading}
>
<SparklesIcon />
</button>
@@ -324,18 +329,18 @@ const TagPicker: React.FunctionComponent<ITagPickerProps> = ({
{settings?.copilotEnabled && (
<button
className="metadata_field__title__action"
title={l10n.t(
title={localize(
LocalizationKey.panelTagPickerCopilotSuggest,
label?.toLowerCase() || type.toLowerCase()
)}
type="button"
onClick={() => suggestTaxonomy('copilot', type)}
disabled={loading}
disabled={!!loading}
>
<CopilotIcon />
</button>
)}
</div>
</>
);
}, [settings?.aiEnabled, settings?.copilotEnabled, label, type]);
@@ -376,7 +381,7 @@ const TagPicker: React.FunctionComponent<ITagPickerProps> = ({
<>
{` `}
<span style={{ fontWeight: 'lighter' }}>
({l10n.t(LocalizationKey.panelTagPickerLimit, limit)})
({localize(LocalizationKey.panelTagPickerLimit, limit)})
</span>
</>
) : (
@@ -387,12 +392,16 @@ const TagPicker: React.FunctionComponent<ITagPickerProps> = ({
actionElement={actionElement}
icon={icon}
required={required}
isDisabled={!!loading}
customAction={action}
triggerLoading={(message) => setLoading(message)}
onChange={updateTaxonomy}
/>
<div className="relative">
{loading && (
<div className="metadata_field__loading">
{l10n.t(LocalizationKey.panelTagPickerAiGenerating)}
{loading}
</div>
)}
@@ -418,9 +427,8 @@ const TagPicker: React.FunctionComponent<ITagPickerProps> = ({
<>
<div
{...getRootProps(undefined, { suppressRefError: true })}
className={`article__tags__input ${freeform ? 'freeform' : ''} ${
showRequiredState ? 'required' : ''
}`}
className={`article__tags__input ${freeform ? 'freeform' : ''} ${showRequiredState ? 'required' : ''
}`}
>
<input
{...getInputProps({
@@ -443,7 +451,7 @@ const TagPicker: React.FunctionComponent<ITagPickerProps> = ({
{freeform && (
<button
className={`article__tags__input__button`}
title={l10n.t(LocalizationKey.panelTagPickerUnkown)}
title={localize(LocalizationKey.panelTagPickerUnkown)}
disabled={!inputValue || checkIsDisabled()}
onClick={() => insertUnkownTag(closeMenu)}
>
@@ -2,14 +2,13 @@ import { PencilIcon, SparklesIcon } from '@heroicons/react/24/outline';
import * as React from 'react';
import { useCallback, useEffect, useMemo } from 'react';
import { useRecoilState } from 'recoil';
import { BaseFieldProps, PanelSettings } from '../../../models';
import { BaseFieldProps, CustomScript, PanelSettings } from '../../../models';
import { RequiredFieldsAtom } from '../../state';
import { FieldTitle } from './FieldTitle';
import { FieldMessage } from './FieldMessage';
import { messageHandler } from '@estruyf/vscode/dist/client';
import { CommandToCode } from '../../CommandToCode';
import * as l10n from '@vscode/l10n';
import { LocalizationKey } from '../../../localization';
import { LocalizationKey, localize } from '../../../localization';
import { useDebounce } from '../../../hooks/useDebounce';
import { CopilotIcon } from '../Icons';
@@ -23,6 +22,7 @@ export interface ITextFieldProps extends BaseFieldProps<string> {
name: string;
placeholder?: string;
settings: PanelSettings;
action?: CustomScript;
onChange: (txtValue: string) => void;
}
@@ -40,11 +40,12 @@ export const TextField: React.FunctionComponent<ITextFieldProps> = ({
name,
settings,
onChange,
action,
required
}: React.PropsWithChildren<ITextFieldProps>) => {
const [, setRequiredFields] = useRecoilState(RequiredFieldsAtom);
const [text, setText] = React.useState<string | null | undefined>(undefined);
const [loading, setLoading] = React.useState<boolean>(false);
const [loading, setLoading] = React.useState<string | undefined>(undefined);
const [lastUpdated, setLastUpdated] = React.useState<number | null>(null);
const debouncedText = useDebounce<string | null | undefined>(text, DEBOUNCE_TIME);
@@ -93,13 +94,14 @@ export const TextField: React.FunctionComponent<ITextFieldProps> = ({
}, [showRequiredState, isValid]);
const suggestDescription = (type: 'ai' | 'copilot') => {
setLoading(true);
setLoading(localize(LocalizationKey.panelFieldsTextFieldAiGenerate));
messageHandler
.request<string>(
type === 'copilot' ? CommandToCode.copilotSuggestDescription : CommandToCode.aiSuggestDescription
)
.then((suggestion) => {
setLoading(false);
setLoading(undefined);
if (suggestion) {
setText(suggestion);
@@ -107,7 +109,7 @@ export const TextField: React.FunctionComponent<ITextFieldProps> = ({
}
})
.catch(() => {
setLoading(false);
setLoading(undefined);
});
};
@@ -117,14 +119,14 @@ export const TextField: React.FunctionComponent<ITextFieldProps> = ({
}
return (
<div className="flex gap-4">
<>
{settings?.aiEnabled && (
<button
className="metadata_field__title__action inline-block text-[var(--vscode-editor-foreground)] disabled:opacity-50"
title={l10n.t(LocalizationKey.panelFieldsTextFieldAiMessage, label?.toLowerCase())}
title={localize(LocalizationKey.panelFieldsTextFieldAiMessage, label?.toLowerCase())}
type="button"
onClick={() => suggestDescription('ai')}
disabled={loading}
disabled={!!loading}
>
<SparklesIcon />
</button>
@@ -133,17 +135,17 @@ export const TextField: React.FunctionComponent<ITextFieldProps> = ({
{settings?.copilotEnabled && (
<button
className="metadata_field__title__action inline-block text-[var(--vscode-editor-foreground)] disabled:opacity-50"
title={l10n.t(LocalizationKey.panelFieldsTextFieldCopilotMessage, label?.toLowerCase())}
title={localize(LocalizationKey.panelFieldsTextFieldCopilotMessage, label?.toLowerCase())}
type="button"
onClick={() => suggestDescription('copilot')}
disabled={loading}
disabled={!!loading}
>
<CopilotIcon />
</button>
)}
</div>
</>
);
}, [settings?.aiEnabled, name]);
}, [settings?.aiEnabled, settings?.copilotEnabled, name, action, loading]);
useEffect(() => {
if (text !== value && (lastUpdated === null || Date.now() - DEBOUNCE_TIME > lastUpdated)) {
@@ -165,18 +167,22 @@ export const TextField: React.FunctionComponent<ITextFieldProps> = ({
actionElement={actionElement}
icon={<PencilIcon />}
required={required}
isDisabled={!!loading}
customAction={action}
triggerLoading={(message) => setLoading(message)}
onChange={onTextChange}
/>
<div className='relative'>
{loading && (
<div className="metadata_field__loading">
{l10n.t(LocalizationKey.panelFieldsTextFieldAiGenerate)}
{loading}
</div>
)}
{wysiwyg ? (
<React.Suspense
fallback={<div>{l10n.t(LocalizationKey.panelFieldsTextFieldLoading)}</div>}
fallback={<div>{localize(LocalizationKey.panelFieldsTextFieldLoading)}</div>}
>
<WysiwygField text={text || ''} onChange={onTextChange} />
</React.Suspense>
@@ -206,7 +212,7 @@ export const TextField: React.FunctionComponent<ITextFieldProps> = ({
{limit && limit > 0 && (text || '').length > limit && (
<div className={`metadata_field__limit`}>
{l10n.t(LocalizationKey.panelFieldsTextFieldLimit, `${(text || '').length}/${limit}`)}
{localize(LocalizationKey.panelFieldsTextFieldLimit, `${(text || '').length}/${limit}`)}
</div>
)}
@@ -216,6 +216,7 @@ export const WrapperField: React.FunctionComponent<IWrapperFieldProps> = ({
value={(fieldValue as string) || null}
required={!!field.required}
settings={settings}
action={field.action}
/>
</FieldBoundary>
);
@@ -307,6 +308,7 @@ export const WrapperField: React.FunctionComponent<IWrapperFieldProps> = ({
limit={field.taxonomyLimit}
renderAsString={field.singleValueAsString}
required={!!field.required}
action={field.action}
/>
</FieldBoundary>
);