mirror of
https://github.com/estruyf/vscode-front-matter.git
synced 2026-06-29 14:31:38 +02:00
#824 - Field actions
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.`);
|
||||
|
||||
@@ -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 +1,2 @@
|
||||
export * from './localization.enum';
|
||||
export * from './localize';
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import * as l10n from '@vscode/l10n';
|
||||
|
||||
export const localize = (key: string, ...args: any[]): string => {
|
||||
return l10n.t(key, ...args);
|
||||
};
|
||||
@@ -141,6 +141,9 @@ export interface Field {
|
||||
|
||||
// When clause
|
||||
when?: WhenClause;
|
||||
|
||||
// Custom action
|
||||
action?: CustomScript;
|
||||
}
|
||||
|
||||
export interface NumberOptions {
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user