diff --git a/CHANGELOG.md b/CHANGELOG.md index 789549ed..07284237 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### ✨ New features +- [#369](https://github.com/estruyf/vscode-front-matter/issues/369): New `required` property to specify if a content-type field is required - [#376](https://github.com/estruyf/vscode-front-matter/issues/376): Ability to run scripts after content was created - [#377](https://github.com/estruyf/vscode-front-matter/issues/377): Git sync actions added on panel and content dashboard (pull and push your changes to remote) - [#379](https://github.com/estruyf/vscode-front-matter/issues/377): New `frontMatter.config.reload` command to reload the configuration file + reinitialize its listeners diff --git a/assets/media/styles.css b/assets/media/styles.css index 0f0492bf..bc23546c 100644 --- a/assets/media/styles.css +++ b/assets/media/styles.css @@ -164,17 +164,6 @@ border: 1px solid rgba(0, 0, 0, .9); } -.article__tags__input.freeform { - position: relative; - outline: 1px solid var(--vscode-inputValidation-infoBorder); - outline-offset: -1px; -} - -.article__tags__input.freeform input { - padding-right: 35px; - border: 0; -} - .article__tags ul { color: var(--vscode-dropdown-foreground); background-color: var(--vscode-dropdown-background); @@ -382,162 +371,6 @@ input:checked + .field__toggle__slider:before { } /* Metadata */ -.metadata_field { - margin-bottom: 1rem; -} - -.vscode-dark .metadata_field__box { - background: rgba(255, 255, 255, 0.1); - border: 1px dashed rgba(255, 255, 255, 0.2); -} - -.vscode-light .metadata_field__box { - background: rgba(0, 0, 0, 0.1); - border: 1px dashed rgba(0, 0, 0, 0.2); -} - -.metadata_field__box { - background: rgba(255, 255, 255, 0.1); - border: 1px dashed rgba(255, 255, 255, 0.2); - margin-bottom: .5rem; - padding: .5rem 1rem; -} - -.metadata_field__label { - display: flex; - align-items: center; - margin-bottom: .5rem; -} - -.metadata_field__label.metadata_field__label_parent { - justify-content: center; -} - -.metadata_field__label svg { - margin-right: .5rem; -} - -.metadata_field__error { - color: var(--vscode-errorForeground); - display: flex; - justify-content: space-between; - align-items: center; -} - -.metadata_field__error button { - color: var(--vscode-button-secondaryForeground); - background-color: var(--vscode-button-secondaryBackground); - padding-left: 1rem; - padding-right: 1rem; - width: auto; -} - -.metadata_field__error button:hover { - background-color: var(--vscode-button-secondaryHoverBackground); -} - -.metadata_field__input, .metadata_field__input:focus, -.metadata_field__textarea, .metadata_field__textarea:focus { - outline: none; -} - -.metadata_field__limit { - color: var(--vscode-inputValidation-warningBorder); - margin-top: .25rem; -} - -.metadata_field__number { - border: 1px solid var(--vscode-inputValidation-infoBorder) !important; - outline: none !important; -} - -.metadata_field__choice__toggle { - color: var(--vscode-input-placeholderForeground); - border: 1px solid var(--vscode-inputValidation-infoBorder) !important; - outline: none !important; - width: 100%; - padding: var(--input-padding-vertical) var(--input-padding-horizontal); - background-color: var(--vscode-input-background); - - display: flex; - align-items: center; - position: relative; -} - -.metadata_field__choice__toggle:hover, -.metadata_field__choice__toggle:focus, -.metadata_field__choice__toggle:active, -.metadata_field__choice__toggle:disabled { - background-color: var(--vscode-input-background); -} - -.metadata_field__choice__toggle span { - margin-right: 1rem; -} - -.metadata_field__choice__toggle svg.icon { - height: 1rem; - width: 1rem; - margin-left: .25rem; - - position: absolute; - right: .25rem; -} - -.metadata_field__choice_list { - width: 90%; - margin: 0; - padding: 0; - z-index: 1; - position: absolute; - list-style: none; - overflow: auto; - max-height: 200px; - - color: var(--vscode-dropdown-foreground); - background-color: var(--vscode-dropdown-background); -} - -.metadata_field__choice_list.open { - border: 1px solid rgba(0, 0, 0, .9); -} - -.metadata_field__choice_list li { - padding: var(--input-padding-vertical) var(--input-padding-horizontal); - cursor: pointer; -} - -.metadata_field__choice_list li:active { - color: var(--vscode-button-foreground); - background-color: var(--vscode-button-background); -} - -.metadata_field__choice_list li[aria-selected="true"] { - color: var(--vscode-button-foreground); - background-color: var(--vscode-button-hoverBackground); -} - -.metadata_field__choice_list li[aria-disabled="true"] { - display: none; -} - -.metadata_field__choice_list__item { - opacity: 0.8; -} - -.metadata_field__choice__button { - margin-top: .5rem; - display: inline-flex; - align-items: center; - width: auto; - margin-right: .5rem; -} - -.metadata_field__choice__button_icon { - height: 1.25rem; - width: 1.25rem; - margin-left: .5rem; -} .metadata_field__datetime { display: flex; @@ -560,75 +393,6 @@ input:checked + .field__toggle__slider:before { background-color: var(--vscode-button-secondaryHoverBackground); } -.metadata_field__multiple_images { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 1rem; -} - -.metadata_field__preview_image img { - display: block; - margin: 0 auto; - max-height: 16rem; -} - -.metadata_field__file__button, -.metadata_field__preview_image__button { - background-color: transparent; - border: 1px dashed var(--vscode-button-background); - padding: 1.5rem; - filter: brightness(85%); -} - -.metadata_field__file__button:hover, -.metadata_field__preview_image__button:hover { - background-color: rgba(255, 255, 255, .1); - filter: brightness(100%); -} - -.metadata_field__file__button svg, -.metadata_field__preview_image__button svg { - color: var(--vscode-foreground); - display: block; - width: 3rem; - height: 3rem; - margin: 0 auto; -} - -.metadata_field__file__button span, -.metadata_field__preview_image__button span { - color: var(--vscode-foreground); - display: inline-block; - margin: 0 auto; - margin-top: .5rem; -} - -.vscode-light .metadata_field__preview_image__preview { - background: rgba(0, 0, 0, 0.1); -} - -.vscode-dark .metadata_field__preview_image__preview { - background: rgba(255, 255, 255, 0.1); -} - -.metadata_field__preview_image__preview { - background-color: var(--vscode-button-secondaryBackground); - display: flex; - flex-direction: column; - justify-content: flex-end; -} - -.metadata_field__preview_image__remove { - background-color: var(--vscode-inputValidation-errorBackground); - color: var(--vscode-inputValidation-errorForeground); -} - -.metadata_field__preview_image__remove:hover { - background-color: var(--vscode-inputValidation-errorBackground); - color: var(--vscode-inputValidation-errorForeground); - opacity: .9; -} - /* File list */ .file_list vscode-label { border-bottom: 1px solid var(--vscode-foreground); diff --git a/package.json b/package.json index ca98fe74..d048c10a 100644 --- a/package.json +++ b/package.json @@ -743,6 +743,14 @@ "markdownDescription": "Specifies the notifications you want to see. By default, all notifications types will be shown. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.global.notifications)", "scope": "Templates" }, + "frontMatter.global.notifications.disabled": { + "type": "array", + "default": [], + "markdownDescription": "This is an array with the notifications types that can be disabled for Front Matter CMS. [Check in the docs](https://frontmatter.codes/docs/settings#frontmatter.global.notification.types)", + "enum": [ + "requiredFieldValidation" + ] + }, "frontMatter.media.defaultSorting": { "type": "string", "default": "", @@ -1020,6 +1028,11 @@ "type": "boolean", "default": true, "description": "Specify if the field is editable" + }, + "required": { + "type": "boolean", + "default": false, + "description": "Specify if the field is required" } }, "additionalProperties": false, diff --git a/src/commands/StatusListener.ts b/src/commands/StatusListener.ts index de7c7723..239fb872 100644 --- a/src/commands/StatusListener.ts +++ b/src/commands/StatusListener.ts @@ -1,11 +1,13 @@ -import { CONTEXT, SETTING_SEO_DESCRIPTION_FIELD, SETTING_SEO_DESCRIPTION_LENGTH, SETTING_SEO_TITLE_LENGTH } from './../constants'; +import { ParsedFrontMatter } from './../parsers/FrontMatterParser'; +import { CONTEXT, NOTIFICATION_TYPE, SETTING_SEO_DESCRIPTION_FIELD, SETTING_SEO_DESCRIPTION_LENGTH, SETTING_SEO_TITLE_LENGTH } from './../constants'; import * as vscode from 'vscode'; -import { ArticleHelper, SeoHelper, Settings } from '../helpers'; +import { ArticleHelper, Notifications, SeoHelper, Settings } from '../helpers'; import { ExplorerView } from '../explorerView/ExplorerView'; import { DefaultFields } from '../constants'; import { ContentType } from '../helpers/ContentType'; import { DataListener } from '../listeners/panel'; import { commands } from 'vscode'; +import { Field } from '../models'; export class StatusListener { @@ -42,7 +44,7 @@ export class StatusListener { } } - // Check SEO for title and description length + // Check SEO and required fields if (article && article.data) { collection.clear(); @@ -58,6 +60,9 @@ export class StatusListener { if (article.data[fieldName] && descLength > -1) { SeoHelper.checkLength(editor, collection, article, fieldName, descLength); } + + // Check the required fields + StatusListener.verifyRequiredFields(editor, article, collection); } const panel = ExplorerView.getInstance(); @@ -80,4 +85,98 @@ export class StatusListener { frontMatterSB.hide(); } + + /** + * Verify the required fields + * @param article + * @param collection + */ + private static verifyRequiredFields(editor: vscode.TextEditor, article: ParsedFrontMatter, collection: vscode.DiagnosticCollection) { + // Check for missing fields + const emptyFields = ContentType.findEmptyRequiredFields(article); + const fieldsToReport = []; + + if (emptyFields && emptyFields.length > 0) { + const text = editor.document.getText(); + const markdown = ArticleHelper.stringifyFrontMatter("", article.data); + const editorSpaces = vscode.window.activeTextEditor?.options?.tabSize; + + const requiredDiagnostics: vscode.Diagnostic[] = []; + + for (const fields of emptyFields) { + let txtIdx = -1; + let fieldName = ""; + let level = 0; + + for (const field of fields) { + const totalSpaces = level * (typeof editorSpaces === "string" ? parseInt(editorSpaces) : editorSpaces || 2); + const crntIdx = StatusListener.findFieldLine(text, txtIdx, totalSpaces, field); + + if (crntIdx && crntIdx > txtIdx) { + txtIdx = crntIdx; + fieldName = field.name; + } + + ++level; + } + + if (txtIdx !== -1 && txtIdx < markdown.length) { + fieldsToReport.push(fields.map(f => f.title).join("/")); + + const posStart = editor.document.positionAt(txtIdx); + const posEnd = editor.document.positionAt(txtIdx + 1 + fieldName.length); + + const diagnostic: vscode.Diagnostic = { + code: '', + message: `This ${fields.map(f => f.name).join("/")} field is required to contain a value.`, + range: new vscode.Range(posStart, posEnd), + severity: vscode.DiagnosticSeverity.Error, + source: 'Front Matter' + }; + + requiredDiagnostics.push(diagnostic); + } + } + + if (collection.has(editor.document.uri)) { + const otherDiag = collection.get(editor.document.uri) || []; + collection.set(editor.document.uri, [...otherDiag, ...requiredDiagnostics]); + } else { + collection.set(editor.document.uri, [...requiredDiagnostics]); + } + + if (fieldsToReport.length > 0) { + Notifications.showIfNotDisabled(NOTIFICATION_TYPE.requiredFieldValidation, "ERROR_ONCE", `The following fields are required to contain a value: ${fieldsToReport.join(", ")}`); + } + } + } + + /** + * Find the line of the field + * @param text + * @param startIdx + * @param totalSpaces + * @param field + * @returns + */ + private static findFieldLine(text: string, startIdx: number, totalSpaces: number, field: Field): number | undefined { + const crntIdx = text.indexOf(field.name, startIdx === -1 ? 0 : startIdx); + + if (crntIdx > -1) { + // Find the linebreak before the current index + const txtFromStart = text.substring(0, crntIdx); + const splitLineBreaks = txtFromStart.split(/\r?\n/); + const lastLine = splitLineBreaks[splitLineBreaks.length - 1]; + + if (lastLine.length === totalSpaces) { + if (crntIdx > startIdx) { + return crntIdx; + } + } else { + return StatusListener.findFieldLine(text, crntIdx + field.name.length, totalSpaces, field); + } + } + + return; + } } \ No newline at end of file diff --git a/src/constants/DefaultFieldValues.ts b/src/constants/DefaultFieldValues.ts new file mode 100644 index 00000000..044fc5d6 --- /dev/null +++ b/src/constants/DefaultFieldValues.ts @@ -0,0 +1,5 @@ + + +export const DefaultFieldValues = { + faultyCustomPlaceholder: "" +} \ No newline at end of file diff --git a/src/constants/NotificationType.ts b/src/constants/NotificationType.ts new file mode 100644 index 00000000..b1368869 --- /dev/null +++ b/src/constants/NotificationType.ts @@ -0,0 +1,5 @@ + + +export const NOTIFICATION_TYPE = { + requiredFieldValidation: "requiredFieldValidation", +} \ No newline at end of file diff --git a/src/constants/index.ts b/src/constants/index.ts index 7bfde162..e268e063 100644 --- a/src/constants/index.ts +++ b/src/constants/index.ts @@ -1,4 +1,5 @@ export * from './ContentType'; +export * from './DefaultFieldValues'; export * from './DefaultFields'; export * from './DefaultFileTypes'; export * from './Extension'; @@ -9,6 +10,7 @@ export * from './GeneralCommands'; export * from './Links'; export * from './LocalStore'; export * from './Navigation'; +export * from './NotificationType'; export * from './PreviewCommands'; export * from './TelemetryEvent'; export * from './charCode'; diff --git a/src/constants/settings.ts b/src/constants/settings.ts index e8f7edd0..bdef70f5 100644 --- a/src/constants/settings.ts +++ b/src/constants/settings.ts @@ -3,6 +3,7 @@ export const EXTENSION_NAME = "Front Matter"; export const CONFIG_KEY = "frontMatter"; export const SETTING_GLOBAL_NOTIFICATIONS = "global.notifications"; +export const SETTING_GLOBAL_NOTIFICATIONS_DISABLED = "global.notifications.disabled"; export const SETTING_GLOBAL_MODES = "global.modes"; export const SETTING_GLOBAL_ACTIVE_MODE = "global.activeMode"; diff --git a/src/helpers/ArticleHelper.ts b/src/helpers/ArticleHelper.ts index b6ea919b..2fa1ba5c 100644 --- a/src/helpers/ArticleHelper.ts +++ b/src/helpers/ArticleHelper.ts @@ -5,7 +5,7 @@ import { MarkdownFoldingProvider } from './../providers/MarkdownFoldingProvider' import { DEFAULT_CONTENT_TYPE, DEFAULT_CONTENT_TYPE_NAME } from './../constants/ContentType'; import * as vscode from 'vscode'; import * as fs from "fs"; -import { DefaultFields, SETTING_CONTENT_DEFAULT_FILETYPE, SETTING_CONTENT_PLACEHOLDERS, SETTING_CONTENT_SUPPORTED_FILETYPES, SETTING_FILE_PRESERVE_CASING, SETTING_COMMA_SEPARATED_FIELDS, SETTING_DATE_FIELD, SETTING_DATE_FORMAT, SETTING_INDENT_ARRAY, SETTING_REMOVE_QUOTES, SETTING_SITE_BASEURL, SETTING_TAXONOMY_CONTENT_TYPES, SETTING_TEMPLATES_PREFIX, SETTING_MODIFIED_FIELD } from '../constants'; +import { DefaultFields, SETTING_CONTENT_DEFAULT_FILETYPE, SETTING_CONTENT_PLACEHOLDERS, SETTING_CONTENT_SUPPORTED_FILETYPES, SETTING_FILE_PRESERVE_CASING, SETTING_COMMA_SEPARATED_FIELDS, SETTING_DATE_FIELD, SETTING_DATE_FORMAT, SETTING_INDENT_ARRAY, SETTING_REMOVE_QUOTES, SETTING_SITE_BASEURL, SETTING_TAXONOMY_CONTENT_TYPES, SETTING_TEMPLATES_PREFIX, SETTING_MODIFIED_FIELD, DefaultFieldValues } from '../constants'; import { DumpOptions } from 'js-yaml'; import { FrontMatterParser, ParsedFrontMatter } from '../parsers'; import { Extension, Logger, Settings, SlugHelper } from '.'; @@ -443,7 +443,7 @@ export class ArticleHelper { Notifications.error(`Error while processing the ${placeholder.id} placeholder`); Logger.error((e as Error).message); - value = ""; + value = DefaultFieldValues.faultyCustomPlaceholder; } } diff --git a/src/helpers/ContentType.ts b/src/helpers/ContentType.ts index 4dffc537..7b64a2d9 100644 --- a/src/helpers/ContentType.ts +++ b/src/helpers/ContentType.ts @@ -1,7 +1,7 @@ import { ModeListener } from './../listeners/general/ModeListener'; import { PagesListener } from './../listeners/dashboard'; import { ArticleHelper, CustomScript, Settings } from "."; -import { FEATURE_FLAG, SETTING_CONTENT_DRAFT_FIELD, SETTING_DATE_FORMAT, SETTING_FRAMEWORK_ID, SETTING_TAXONOMY_CONTENT_TYPES, SETTING_TAXONOMY_FIELD_GROUPS, TelemetryEvent } from "../constants"; +import { DefaultFieldValues, FEATURE_FLAG, SETTING_CONTENT_DRAFT_FIELD, SETTING_DATE_FORMAT, SETTING_FRAMEWORK_ID, SETTING_TAXONOMY_CONTENT_TYPES, SETTING_TAXONOMY_FIELD_GROUPS, TelemetryEvent } from "../constants"; import { ContentType as IContentType, DraftField, Field, FieldGroup, FieldType, ScriptType } from '../models'; import { Uri, commands, window, ProgressLocation, workspace } from 'vscode'; import { Folders } from "../commands/Folders"; @@ -278,7 +278,7 @@ export class ContentType { * @param parents * @returns */ - public static getFieldValue(data: any, parents: string[]): string[] { + public static getFieldValue(data: any, parents: string[]): string | string[] { let fieldValue = []; let crntPageData = data; @@ -375,6 +375,51 @@ export class ContentType { return parents; } + + /** + * Find the required fields + */ + public static findEmptyRequiredFields(article: ParsedFrontMatter): Field[][] | undefined { + const contentType = ArticleHelper.getContentType(article.data); + if (!contentType) { + return; + } + + const allRequiredFields = ContentType.findRequiredFieldsDeep(contentType.fields); + + let emptyFields: Field[][] = []; + + for (const fields of allRequiredFields) { + const fieldValue = this.getFieldValue(article.data, fields.map(f => f.name)); + if ((fieldValue === null || fieldValue === undefined || fieldValue === "") || fieldValue.length === 0 || fieldValue === DefaultFieldValues.faultyCustomPlaceholder) { + emptyFields.push(fields); + } + } + + return emptyFields || []; + } + + /** + * Find all the required fields in the content type + * @param fields + * @param parents + * @returns + */ + private static findRequiredFieldsDeep(fields: Field[], parents: Field[][] = [], parentFields: Field[] = []): Field[][] { + for (const field of fields) { + if (field.required) { + parents.push([...parentFields, field]); + } + + if (field.type === "fields" && field.fields) { + this.findRequiredFieldsDeep(field.fields, parents, [...parentFields, field]); + } + } + + return parents; + } + + /** * Look for the preview image in the block field * @param field diff --git a/src/helpers/CustomScript.ts b/src/helpers/CustomScript.ts index ee9f3e67..5f2db7db 100644 --- a/src/helpers/CustomScript.ts +++ b/src/helpers/CustomScript.ts @@ -14,6 +14,7 @@ import { DashboardCommand } from '../dashboardWebView/DashboardCommand'; import { ParsedFrontMatter } from '../parsers'; import { TelemetryEvent } from '../constants/TelemetryEvent'; import { SETTING_CUSTOM_SCRIPTS } from '../constants'; +import { existsSync } from 'fs'; export class CustomScript { @@ -272,6 +273,12 @@ export class CustomScript { } const scriptPath = join(wsPath, script.script); + + if (!existsSync(scriptPath)) { + reject(new Error(`Script not found: ${scriptPath}`)); + return; + } + const fullScript = `${command} ${scriptPath} ${args}`; Logger.info(`Executing: ${fullScript}`); diff --git a/src/helpers/Notifications.ts b/src/helpers/Notifications.ts index a88aadcc..9f83941a 100644 --- a/src/helpers/Notifications.ts +++ b/src/helpers/Notifications.ts @@ -1,12 +1,20 @@ +import { SETTING_GLOBAL_NOTIFICATIONS_DISABLED } from './../constants/settings'; import { window } from "vscode"; import { EXTENSION_NAME, SETTING_GLOBAL_NOTIFICATIONS } from "../constants"; import { Logger } from "./Logger"; import { Settings } from "./SettingsHelper"; +type NotificationType = "INFO" | "WARNING" | "ERROR" | "ERROR_ONCE"; export class Notifications { private static notifications: string[] = []; + /** + * Show a notification to the user + * @param message + * @param items + * @returns + */ public static info(message: string, ...items: any): Thenable { Logger.info(`${EXTENSION_NAME}: ${message}`, "INFO"); @@ -17,6 +25,12 @@ export class Notifications { return Promise.resolve(undefined); } + /** + * Show a warning notification to the user + * @param message + * @param items + * @returns + */ public static warning(message: string, ...items: any): Thenable { Logger.info(`${EXTENSION_NAME}: ${message}`, "WARNING"); @@ -27,6 +41,12 @@ export class Notifications { return Promise.resolve(undefined); } + /** + * Show an error notification to the user + * @param message + * @param items + * @returns + */ public static error(message: string, ...items: any): Thenable { Logger.info(`${EXTENSION_NAME}: ${message}`, "ERROR"); @@ -37,6 +57,12 @@ export class Notifications { return Promise.resolve(undefined); } + /** + * Show an error notification to the user only once + * @param message + * @param items + * @returns + */ public static async errorShowOnce(message: string, ...items: any): Promise { if (this.notifications.includes(message)) { return; @@ -47,7 +73,40 @@ export class Notifications { return this.error(message, ...items); } - private static shouldShow(level: "INFO" | "WARNING" | "ERROR"): boolean { + /** + * Show the notification if not disabled + * @param type + * @param notificationType + * @param message + * @param items + * @returns + */ + public static async showIfNotDisabled(type: string, notificationType: NotificationType, message: string, ...items: any): Promise { + const disabledTypes = Settings.get(SETTING_GLOBAL_NOTIFICATIONS_DISABLED); + + if (disabledTypes && disabledTypes.includes(type)) { + return; + } + + switch (notificationType) { + case "WARNING": + return await Notifications.warning(message, ...items); + case "ERROR": + return await Notifications.error(message, ...items); + case "ERROR_ONCE": + return await Notifications.errorShowOnce(message, ...items); + case "INFO": + default: + return await Notifications.info(message, ...items); + } + } + + /** + * Check if the notification should be shown + * @param level + * @returns + */ + private static shouldShow(level: NotificationType): boolean { let levels = Settings.get(SETTING_GLOBAL_NOTIFICATIONS); if (!levels) { diff --git a/src/helpers/TaxonomyHelper.ts b/src/helpers/TaxonomyHelper.ts index b69c0715..1327f321 100644 --- a/src/helpers/TaxonomyHelper.ts +++ b/src/helpers/TaxonomyHelper.ts @@ -197,7 +197,10 @@ export class TaxonomyHelper { if (fieldNames.length > 0 && article && article.data) { const { data } = article; - let taxonomies: string[] = ContentType.getFieldValue(data, fieldNames); + let taxonomies: string| string[] = ContentType.getFieldValue(data, fieldNames); + if (typeof taxonomies === "string") { + taxonomies = taxonomies.split(`,`); + } if (taxonomies && taxonomies.length > 0) { const idx = taxonomies.findIndex(o => o === oldValue); @@ -300,8 +303,15 @@ export class TaxonomyHelper { if (oldFieldNames.length > 0 && newFieldNames.length > 0 && article && article.data) { const { data } = article; - let oldTaxonomies: string[] = ContentType.getFieldValue(data, oldFieldNames) || []; - let newTaxonomies: string[] = ContentType.getFieldValue(data, newFieldNames) || []; + let oldTaxonomies: string | string[] = ContentType.getFieldValue(data, oldFieldNames) || []; + let newTaxonomies: string | string[] = ContentType.getFieldValue(data, newFieldNames) || []; + + if (typeof oldTaxonomies === "string") { + oldTaxonomies = oldTaxonomies.split(","); + } + if (typeof newTaxonomies === "string") { + newTaxonomies = newTaxonomies.split(","); + } if (oldTaxonomies && oldTaxonomies.length > 0) { const idx = oldTaxonomies.findIndex(o => o === value); diff --git a/src/listeners/dashboard/PagesListener.ts b/src/listeners/dashboard/PagesListener.ts index a271f0c1..592b4db7 100644 --- a/src/listeners/dashboard/PagesListener.ts +++ b/src/listeners/dashboard/PagesListener.ts @@ -306,10 +306,12 @@ export class PagesListener extends BaseListener { } let tagParents = ContentType.findFieldByType(contentType.fields, "tags"); - page.fmTags = ContentType.getFieldValue(article.data, tagParents.length !== 0 ? tagParents : ["tags"]); + const tagsValue = ContentType.getFieldValue(article.data, tagParents.length !== 0 ? tagParents : ["tags"]); + page.fmTags = typeof tagsValue === "string" ? tagsValue.split(",") : []; let categoryParents = ContentType.findFieldByType(contentType.fields, "categories"); - page.fmCategories = ContentType.getFieldValue(article.data, categoryParents.length !== 0 ? categoryParents : ["categories"]); + const categoriesValue = ContentType.getFieldValue(article.data, categoryParents.length !== 0 ? categoryParents : ["categories"]); + page.fmCategories = typeof categoriesValue === "string" ? categoriesValue.split(",") : []; // Check if parent fields were retrieved, if not there was no image present if (previewFieldParents.length > 0) { diff --git a/src/models/BaseFieldProps.ts b/src/models/BaseFieldProps.ts new file mode 100644 index 00000000..fe7fd2a2 --- /dev/null +++ b/src/models/BaseFieldProps.ts @@ -0,0 +1,7 @@ + + +export interface BaseFieldProps { + label: string; + value: T | null; + required?: boolean; +} \ No newline at end of file diff --git a/src/models/PanelSettings.ts b/src/models/PanelSettings.ts index 57379266..79ec7db8 100644 --- a/src/models/PanelSettings.ts +++ b/src/models/PanelSettings.ts @@ -71,6 +71,7 @@ export interface Field { taxonomyLimit?: number; fileExtensions?: string[]; editable?: boolean; + required?: boolean; // Date fields isPublishDate?: boolean; diff --git a/src/models/index.ts b/src/models/index.ts index e484e46e..f42a401f 100644 --- a/src/models/index.ts +++ b/src/models/index.ts @@ -1,3 +1,4 @@ +export * from './BaseFieldProps'; export * from './BlockFieldData'; export * from './Choice'; export * from './ContentFolder'; diff --git a/src/panelWebView/components/DataBlock/DataBlockField.tsx b/src/panelWebView/components/DataBlock/DataBlockField.tsx index 9aa02775..63514928 100644 --- a/src/panelWebView/components/DataBlock/DataBlockField.tsx +++ b/src/panelWebView/components/DataBlock/DataBlockField.tsx @@ -2,11 +2,11 @@ import * as React from 'react'; import { useState, useCallback, useEffect, useMemo } from 'react'; import { BlockFieldData, Field, FieldGroup, PanelSettings } from '../../../models'; import { PencilIcon } from '@heroicons/react/outline'; -import { VsLabel } from '../VscodeComponents'; import { DataBlockRecords, DataBlockSelector } from '.'; import { SortEnd } from 'react-sortable-hoc'; import { arrayMoveImmutable } from 'array-move'; import { IMetadata } from '../Metadata'; +import { FieldTitle } from '../Fields/FieldTitle'; export interface IDataBlockFieldProps { label: string; @@ -25,9 +25,10 @@ export interface IDataBlockFieldProps { ) => (JSX.Element | null)[] | undefined; onSubmit: (data: any) => void; parentBlock: string | null | undefined; + required?: boolean; } -export const DataBlockField: React.FunctionComponent = ({ label, filePath, settings, field, parentFields = [], value, fieldsRenderer, onSubmit, parentBlock }: React.PropsWithChildren) => { +export const DataBlockField: React.FunctionComponent = ({ label, filePath, settings, field, parentFields = [], value, fieldsRenderer, onSubmit, parentBlock, required }: React.PropsWithChildren) => { const [ selectedIndex, setSelectedIndex ] = useState(null); const [ selectedGroup, setSelectedGroup ] = useState(null); const [ selectedBlockData, setSelectedBlockData ] = useState(null); @@ -229,12 +230,10 @@ export const DataBlockField: React.FunctionComponent = ({ return (
- - -
- {label} -
-
+ } + required={required} /> { (!hideSubBlock) ? ( diff --git a/src/panelWebView/components/Fields/ChoiceField.tsx b/src/panelWebView/components/Fields/ChoiceField.tsx index 87b46673..a5ffdd12 100644 --- a/src/panelWebView/components/Fields/ChoiceField.tsx +++ b/src/panelWebView/components/Fields/ChoiceField.tsx @@ -1,21 +1,21 @@ import {CheckIcon, ChevronDownIcon} from '@heroicons/react/outline'; import Downshift from 'downshift'; import * as React from 'react'; -import { useEffect } from 'react'; +import { useEffect, useMemo } from 'react'; +import { BaseFieldProps } from '../../../models'; import { Choice } from '../../../models/Choice'; -import { VsLabel } from '../VscodeComponents'; import { ChoiceButton } from './ChoiceButton'; +import { FieldTitle } from './FieldTitle'; +import { RequiredMessage } from './RequiredMessage'; -export interface IChoiceFieldProps { - label: string; - selected: string | string[]; +export interface IChoiceFieldProps extends BaseFieldProps { choices: string[] | Choice[]; multiSelect?: boolean; onChange: (value: string | string[]) => void; } -export const ChoiceField: React.FunctionComponent = ({label, selected, choices, multiSelect, onChange}: React.PropsWithChildren) => { - const [ crntSelected, setCrntSelected ] = React.useState(selected); +export const ChoiceField: React.FunctionComponent = ({ label, value, choices, multiSelect, onChange, required }: React.PropsWithChildren) => { + const [ crntSelected, setCrntSelected ] = React.useState(value); const dsRef = React.useRef | null>(null); const onValueChange = (txtValue: string) => { @@ -54,12 +54,6 @@ export const ChoiceField: React.FunctionComponent = ({label, } return ""; }; - - useEffect(() => { - if (crntSelected !== selected) { - setCrntSelected(selected); - } - }, [selected]); const availableChoices = !multiSelect ? choices : (choices as Array).filter((choice: string | Choice) => { const value = typeof choice === 'string' || typeof choice === 'number' ? choice : choice.id; @@ -73,13 +67,22 @@ export const ChoiceField: React.FunctionComponent = ({label, return true; }); + const showRequiredState = useMemo(() => { + return required && ((crntSelected instanceof Array && crntSelected.length === 0) || !crntSelected); + }, [required, crntSelected]); + + useEffect(() => { + if (crntSelected !== value) { + setCrntSelected(value); + } + }, [value]); + return ( -
- -
- {label} -
-
+
+ } + required={required} /> = ({label,
)} + + { crntSelected instanceof Array ? crntSelected.map((value: string) => ( diff --git a/src/panelWebView/components/Fields/DataFileField.tsx b/src/panelWebView/components/Fields/DataFileField.tsx index 4c128262..e33c68a2 100644 --- a/src/panelWebView/components/Fields/DataFileField.tsx +++ b/src/panelWebView/components/Fields/DataFileField.tsx @@ -5,9 +5,10 @@ import * as React from 'react'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { Command } from '../../Command'; import { CommandToCode } from '../../CommandToCode'; -import { VsLabel } from '../VscodeComponents'; import Downshift from 'downshift'; import { ChoiceButton } from './ChoiceButton'; +import { FieldTitle } from './FieldTitle'; +import { RequiredMessage } from './RequiredMessage'; export interface IDataFileFieldProps { label: string; @@ -16,10 +17,11 @@ export interface IDataFileFieldProps { dataFileValue?: string; selected: string | string[]; multiSelect?: boolean; + required?: boolean; onChange: (value: string | string[]) => void; } -export const DataFileField: React.FunctionComponent = ({ label, dataFileId, dataFileKey, dataFileValue, selected, multiSelect, onChange }: React.PropsWithChildren) => { +export const DataFileField: React.FunctionComponent = ({ label, dataFileId, dataFileKey, dataFileValue, selected, multiSelect, onChange, required }: React.PropsWithChildren) => { const [ dataEntries, setDataEntries ] = useState(null); const [ crntSelected, setCrntSelected ] = React.useState(); const dsRef = React.useRef | null>(null); @@ -91,6 +93,10 @@ export const DataFileField: React.FunctionComponent = ({ la return ""; }, [allChoices]); + const showRequiredState = useMemo(() => { + return required && ((crntSelected instanceof Array && crntSelected.length === 0) || !crntSelected); + }, [required, crntSelected]); + useEffect(() => { if (selected) { if (multiSelect) { @@ -120,12 +126,11 @@ export const DataFileField: React.FunctionComponent = ({ la }, []); return ( -
- -
- {label} -
-
+
+ } + required={required} /> = ({ la
)} + + { crntSelected instanceof Array ? crntSelected.map((value: string) => ( diff --git a/src/panelWebView/components/Fields/DateTimeField.tsx b/src/panelWebView/components/Fields/DateTimeField.tsx index ffa732e2..a76881ab 100644 --- a/src/panelWebView/components/Fields/DateTimeField.tsx +++ b/src/panelWebView/components/Fields/DateTimeField.tsx @@ -1,13 +1,13 @@ import * as React from 'react'; -import { VsLabel } from '../VscodeComponents'; import {ClockIcon} from '@heroicons/react/outline'; import DatePicker from 'react-datepicker'; -import { forwardRef } from 'react'; +import { forwardRef, useEffect, useMemo } from 'react'; import { DateHelper } from '../../../helpers/DateHelper'; +import { BaseFieldProps } from '../../../models'; +import { RequiredMessage } from './RequiredMessage'; +import { FieldTitle } from './FieldTitle'; -export interface IDateTimeFieldProps { - label: string; - date: Date | null; +export interface IDateTimeFieldProps extends BaseFieldProps { format?: string; onChange: (date: Date) => void; } @@ -22,7 +22,7 @@ const CustomInput = forwardRef(({ value, onClick } ) }); -export const DateTimeField: React.FunctionComponent = ({label, date, format, onChange}: React.PropsWithChildren) => { +export const DateTimeField: React.FunctionComponent = ({label, value, required, format, onChange}: React.PropsWithChildren) => { const [ dateValue, setDateValue ] = React.useState(null); const onDateChange = (date: Date) => { @@ -30,23 +30,26 @@ export const DateTimeField: React.FunctionComponent = ({lab onChange(date); }; - React.useEffect(() => { - const crntValue = DateHelper.tryParse(date, format); + const showRequiredState = useMemo(() => { + return required && !dateValue; + }, [required, dateValue]); + + useEffect(() => { + const crntValue = DateHelper.tryParse(value, format); const stateValue = DateHelper.tryParse(dateValue, format); if (crntValue?.toISOString() !== stateValue?.toISOString()) { - setDateValue(date); + setDateValue(value); } - }, [ date, dateValue ]); + }, [ value, dateValue ]); return (
- -
- {label} -
-
- + } + required={required} /> +
= ({lab now
+ +
); }; \ No newline at end of file diff --git a/src/panelWebView/components/Fields/DraftField.tsx b/src/panelWebView/components/Fields/DraftField.tsx index ddc5456f..6569f8ce 100644 --- a/src/panelWebView/components/Fields/DraftField.tsx +++ b/src/panelWebView/components/Fields/DraftField.tsx @@ -1,26 +1,24 @@ import * as React from 'react'; -import { RocketIcon } from '../Icons/RocketIcon'; -import { VsLabel } from '../VscodeComponents'; +import { BaseFieldProps } from '../../../models'; import { ChoiceField } from './ChoiceField'; import { Toggle } from './Toggle'; -export interface IDraftFieldProps { - label: string; +export interface IDraftFieldProps extends BaseFieldProps { type: "boolean" | "choice"; - value: boolean | string | null | undefined; choices?: string[]; onChanged: (value: string | boolean) => void; } -export const DraftField: React.FunctionComponent = ({ label, type, value, choices, onChanged }: React.PropsWithChildren) => { +export const DraftField: React.FunctionComponent = ({ label, type, value, choices, onChanged, required }: React.PropsWithChildren) => { if (type === "boolean") { return ( onChanged(checked)} /> ); } @@ -29,9 +27,10 @@ export const DraftField: React.FunctionComponent = ({ label, t return ( onChanged(value as string)} /> ); } diff --git a/src/panelWebView/components/Fields/FieldTitle.tsx b/src/panelWebView/components/Fields/FieldTitle.tsx new file mode 100644 index 00000000..54ce6329 --- /dev/null +++ b/src/panelWebView/components/Fields/FieldTitle.tsx @@ -0,0 +1,28 @@ +import * as React from 'react'; +import { useMemo } from 'react'; +import { VsLabel } from '../VscodeComponents'; +import { RequiredAsterix } from './RequiredAsterix'; + +export interface IFieldTitleProps { + label: string | JSX.Element; + icon?: JSX.Element; + className?: string; + required?: boolean; +} + +export const FieldTitle: React.FunctionComponent = ({ label, icon, className, required }: React.PropsWithChildren) => { + + const Icon = useMemo(() => { + return icon ? React.cloneElement(icon, { style: { width: "16px", height: "16px" } }) : null; + }, [icon]) + + return ( + +
+ {Icon} + {label} + +
+
+ ); +}; \ No newline at end of file diff --git a/src/panelWebView/components/Fields/FileField.tsx b/src/panelWebView/components/Fields/FileField.tsx index 5042f054..e59becfb 100644 --- a/src/panelWebView/components/Fields/FileField.tsx +++ b/src/panelWebView/components/Fields/FileField.tsx @@ -3,16 +3,15 @@ import { DocumentIcon, PaperClipIcon, TrashIcon } from '@heroicons/react/outline import { basename } from 'path'; import * as React from 'react'; import { useCallback, useMemo } from 'react'; -import { BlockFieldData } from '../../../models'; +import { BaseFieldProps, BlockFieldData } from '../../../models'; import { CommandToCode } from '../../CommandToCode'; -import { VsLabel } from '../VscodeComponents'; +import { FieldTitle } from './FieldTitle'; +import { RequiredMessage } from './RequiredMessage'; -export interface IFileFieldProps { - label: string; +export interface IFileFieldProps extends BaseFieldProps { fieldName: string; filePath: string; multiple?: boolean; - value: string | string[] | null; fileExtensions?: string[]; parents?: string[]; blockData?: BlockFieldData; @@ -35,7 +34,7 @@ const File = ({ value, onRemove }: { value: string, onRemove: (value: string) => ) } -export const FileField: React.FunctionComponent = ({ label, multiple, filePath, fileExtensions, fieldName, value, parents, blockData, onChange }: React.PropsWithChildren) => { +export const FileField: React.FunctionComponent = ({ label, multiple, filePath, fileExtensions, fieldName, value, parents, blockData, onChange, required }: React.PropsWithChildren) => { const selectFile = useCallback(() => { Messenger.send(CommandToCode.selectFile, { @@ -59,15 +58,18 @@ export const FileField: React.FunctionComponent = ({ label, mul return !value || (Array.isArray(value) && value.length === 0); }, [value]); + const showRequiredState = useMemo(() => { + return required && isEmpty; + }, [required, isEmpty]); + return (
- -
- {label} -
-
+ } + required={required} /> -
+
{ (isEmpty || multiple) && ( ) } + + { value && !Array.isArray(value) && ( diff --git a/src/panelWebView/components/Fields/ImageFallback.tsx b/src/panelWebView/components/Fields/ImageFallback.tsx index 0236ef18..e83771e0 100644 --- a/src/panelWebView/components/Fields/ImageFallback.tsx +++ b/src/panelWebView/components/Fields/ImageFallback.tsx @@ -27,7 +27,9 @@ export const ImageFallback: React.FunctionComponent = ({ sr

The image couldn't be loaded

+ }}> + The image couldn't be loaded +

); } diff --git a/src/panelWebView/components/Fields/ListField.tsx b/src/panelWebView/components/Fields/ListField.tsx index b286d33f..33812a8b 100644 --- a/src/panelWebView/components/Fields/ListField.tsx +++ b/src/panelWebView/components/Fields/ListField.tsx @@ -1,15 +1,15 @@ import { PencilIcon, TrashIcon, ViewListIcon } from '@heroicons/react/outline'; import * as React from 'react'; -import { useCallback, useEffect, useRef } from 'react'; -import { VsLabel } from '../VscodeComponents'; +import { useCallback, useEffect, useMemo, useRef } from 'react'; +import { BaseFieldProps } from '../../../models'; +import { FieldTitle } from './FieldTitle'; +import { RequiredMessage } from './RequiredMessage'; -export interface IListFieldProps { - label: string; - value: string[] | null; +export interface IListFieldProps extends BaseFieldProps { onChange: (value: string | string[]) => void; } -export const ListField: React.FunctionComponent = ({ label, value, onChange }: React.PropsWithChildren) => { +export const ListField: React.FunctionComponent = ({ label, value, required, onChange }: React.PropsWithChildren) => { const [ text, setText ] = React.useState(""); const [ list, setList ] = React.useState(null); const [ itemToEdit, setItemToEdit ] = React.useState(null); @@ -58,6 +58,10 @@ export const ListField: React.FunctionComponent = ({ label, val } }, [list]); + const showRequiredState = useMemo(() => { + return required && (!list || list?.length === 0); + }, [required, list]); + useEffect(() => { if (value) { if (typeof value === "string") { @@ -68,15 +72,12 @@ export const ListField: React.FunctionComponent = ({ label, val } }, [value]); - let isValid = true; - return ( -
- -
- {label} -
-
+
+ } + required={required} /> = ({ label, val if (e.key === "Enter") { onSaveForm(); } - }} - style={{ - border: "1px solid var(--vscode-inputValidation-infoBorder)" }} /> + +
+ +
); }; \ No newline at end of file diff --git a/src/panelWebView/components/Fields/RequiredAsterix.tsx b/src/panelWebView/components/Fields/RequiredAsterix.tsx new file mode 100644 index 00000000..7bebc35d --- /dev/null +++ b/src/panelWebView/components/Fields/RequiredAsterix.tsx @@ -0,0 +1,15 @@ +import * as React from 'react'; + +export interface IRequiredAsterixProps { + required?: boolean; +} + +export const RequiredAsterix: React.FunctionComponent = ({ required }: React.PropsWithChildren) => { + if (!required) { + return null; + } + + return ( + * + ); +}; \ No newline at end of file diff --git a/src/panelWebView/components/Fields/RequiredMessage.tsx b/src/panelWebView/components/Fields/RequiredMessage.tsx new file mode 100644 index 00000000..d54a2f3c --- /dev/null +++ b/src/panelWebView/components/Fields/RequiredMessage.tsx @@ -0,0 +1,19 @@ +import * as React from 'react'; + +export interface IRequiredMessageProps { + name: string; + show?: boolean; +} + +export const RequiredMessage: React.FunctionComponent = ({ name, show }: React.PropsWithChildren) => { + + if (!show) { + return null; + } + + return ( +
+ The {name} field is required. +
+ ); +}; \ No newline at end of file diff --git a/src/panelWebView/components/Fields/SlugField.tsx b/src/panelWebView/components/Fields/SlugField.tsx index d934a6ba..e657181f 100644 --- a/src/panelWebView/components/Fields/SlugField.tsx +++ b/src/panelWebView/components/Fields/SlugField.tsx @@ -2,28 +2,22 @@ import { Messenger } from '@estruyf/vscode/dist/client'; import { EventData } from '@estruyf/vscode/dist/models'; import {LinkIcon, RefreshIcon} from '@heroicons/react/outline'; import * as React from 'react'; -import { useCallback, useEffect } from 'react'; +import { useCallback, useEffect, useMemo } from 'react'; +import { BaseFieldProps } from '../../../models'; import { Command } from '../../Command'; import { CommandToCode } from '../../CommandToCode'; -import { VsLabel } from '../VscodeComponents'; +import { FieldTitle } from './FieldTitle'; +import { RequiredMessage } from './RequiredMessage'; -export interface ISlugFieldProps { - label: string; - value: string | null; +export interface ISlugFieldProps extends BaseFieldProps { titleValue: string | null; editable?: boolean; onChange: (txtValue: string) => void; } -export const SlugField: React.FunctionComponent = ({ label, editable, value, titleValue, onChange }: React.PropsWithChildren) => { +export const SlugField: React.FunctionComponent = ({ label, editable, value, titleValue, onChange, required }: React.PropsWithChildren) => { const [ text, setText ] = React.useState(value); const [ slug, setSlug ] = React.useState(value); - - useEffect(() => { - if (text !== value) { - setText(value); - } - }, [ value ]); const onTextChange = (txtValue: string) => { setText(txtValue); @@ -41,6 +35,16 @@ export const SlugField: React.FunctionComponent = ({ label, edi } }, [text]); + const showRequiredState = useMemo(() => { + return required && !text; + }, [required, text]); + + useEffect(() => { + if (text !== value) { + setText(value); + } + }, [ value ]); + useEffect(() => { if (titleValue) { Messenger.send(CommandToCode.generateSlug, titleValue); @@ -57,18 +61,20 @@ export const SlugField: React.FunctionComponent = ({ label, edi return (
- -
- {label} -
-
+ } + required={required} />
onTextChange(e.currentTarget.value)} /> + onChange={(e) => onTextChange(e.currentTarget.value)} + style={{ + borderColor: showRequiredState ? "var(--vscode-inputValidation-errorBorder)" : undefined, + }} />
+ +
); } \ No newline at end of file diff --git a/src/panelWebView/components/Fields/TextField.tsx b/src/panelWebView/components/Fields/TextField.tsx index d73af348..51e84c5c 100644 --- a/src/panelWebView/components/Fields/TextField.tsx +++ b/src/panelWebView/components/Fields/TextField.tsx @@ -1,10 +1,13 @@ import {PencilIcon} from '@heroicons/react/outline'; import * as React from 'react'; -import { VsLabel } from '../VscodeComponents'; +import { useCallback, useEffect, useMemo } from 'react'; +import { useRecoilState } from 'recoil'; +import { BaseFieldProps } from '../../../models'; +import { RequiredFieldsAtom } from '../../state'; +import { FieldTitle } from './FieldTitle'; +import { RequiredMessage } from './RequiredMessage'; -export interface ITextFieldProps { - label: string; - value: string | null; +export interface ITextFieldProps extends BaseFieldProps { singleLine: boolean | undefined; wysiwyg: boolean | undefined; limit: number | undefined; @@ -14,14 +17,9 @@ export interface ITextFieldProps { const WysiwygField = React.lazy(() => import('./WysiwygField')); -export const TextField: React.FunctionComponent = ({singleLine, wysiwyg, limit, label, value, rows, onChange}: React.PropsWithChildren) => { +export const TextField: React.FunctionComponent = ({singleLine, wysiwyg, limit, label, value, rows, onChange, required}: React.PropsWithChildren) => { + const [ requiredFields, setRequiredFields ] = useRecoilState(RequiredFieldsAtom); const [ text, setText ] = React.useState(value); - - React.useEffect(() => { - if (text !== value) { - setText(value); - } - }, [ value ]); const onTextChange = (txtValue: string) => { setText(txtValue); @@ -33,13 +31,49 @@ export const TextField: React.FunctionComponent = ({singleLine, isValid = ((text || "").length <= limit); } + const updateRequired = useCallback((isValid: boolean) => { + setRequiredFields((prev) => { + let clone = Object.assign([], prev); + + if (isValid) { + clone = clone.filter((item) => item !== label); + } else { + clone.push(label); + } + + return clone; + }); + }, [setRequiredFields]) + + const showRequiredState = useMemo(() => { + return required && !text; + }, [required, text]); + + const border = useMemo(() => { + if (showRequiredState) { + updateRequired(false); + return "1px solid var(--vscode-inputValidation-errorBorder)"; + } else if (!isValid) { + updateRequired(true); + return "1px solid var(--vscode-inputValidation-warningBorder)"; + } else { + updateRequired(true); + return "1px solid var(--vscode-inputValidation-infoBorder)"; + } + }, [showRequiredState, isValid]); + + useEffect(() => { + if (text !== value) { + setText(value); + } + }, [ value ]); + return (
- -
- {label} -
-
+ } + required={required} /> { wysiwyg ? ( @@ -53,7 +87,7 @@ export const TextField: React.FunctionComponent = ({singleLine, value={text || ""} onChange={(e) => onTextChange(e.currentTarget.value)} style={{ - border: isValid ? "1px solid var(--vscode-inputValidation-infoBorder)" : "1px solid var(--vscode-inputValidation-warningBorder)" + border }} /> ) : (