From c4055eb37cb3160168359bc51d8c84601e708ca6 Mon Sep 17 00:00:00 2001 From: Elio Struyf Date: Thu, 21 Apr 2022 21:18:24 +0200 Subject: [PATCH 1/5] Content type generation --- package.json | 9 +++ src/commands/Article.ts | 19 +----- src/constants/Extension.ts | 3 + src/extension.ts | 4 ++ src/helpers/ArticleHelper.ts | 17 +++++ src/helpers/ContentType.ts | 124 ++++++++++++++++++++++++++++++++++- 6 files changed, 157 insertions(+), 19 deletions(-) diff --git a/package.json b/package.json index 1b1592e9..1e76debb 100644 --- a/package.json +++ b/package.json @@ -1206,6 +1206,11 @@ "title": "Authenticate", "category": "Front matter" }, + { + "command": "frontMatter.generate.contenttype", + "title": "Generate content type from current file", + "category": "Front matter" + }, { "command": "frontMatter.markup.blockquote", "title": "Blockquote", @@ -1681,6 +1686,10 @@ { "command": "frontMatter.generateSlug", "when": "frontMatter:file:isValid == true" + }, + { + "command": "frontMatter.generate.contenttype", + "when": "frontMatter:file:isValid == true" } ], "view/title": [ diff --git a/src/commands/Article.ts b/src/commands/Article.ts index 4015443b..90f41f24 100644 --- a/src/commands/Article.ts +++ b/src/commands/Article.ts @@ -30,7 +30,7 @@ export class Article { return; } - const article = Article.getCurrent(); + const article = ArticleHelper.getCurrent(); if (!article) { return; @@ -375,23 +375,6 @@ export class Article { } as DashboardData); } - /** - * Get the current article - */ - private static getCurrent(): ParsedFrontMatter | undefined { - const editor = vscode.window.activeTextEditor; - if (!editor) { - return; - } - - const article = ArticleHelper.getFrontMatter(editor); - if (!article) { - return; - } - - return article; - } - /** * Update the article date and return it * @param article diff --git a/src/constants/Extension.ts b/src/constants/Extension.ts index c3331a7f..a55eb954 100644 --- a/src/constants/Extension.ts +++ b/src/constants/Extension.ts @@ -53,4 +53,7 @@ export const COMMAND_NAME = { orderedlist: getCommandName("markup.orderedlist"), taskList: getCommandName("markup.tasklist"), options: getCommandName("markup.options"), + + // Content types + generateContentType: getCommandName("generate.contenttype"), }; \ No newline at end of file diff --git a/src/extension.ts b/src/extension.ts index 25cc9ccb..b761dfba 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -154,6 +154,10 @@ export async function activate(context: vscode.ExtensionContext) { const createByTemplate = vscode.commands.registerCommand(COMMAND_NAME.createByTemplate, Folders.create); const createContent = vscode.commands.registerCommand(COMMAND_NAME.createContent, Content.create); + subscriptions.push( + vscode.commands.registerCommand(COMMAND_NAME.generateContentType, ContentType.generate) + ); + // Initialize command Template.init(); const projectInit = vscode.commands.registerCommand(COMMAND_NAME.init, async (cb: Function) => { diff --git a/src/helpers/ArticleHelper.ts b/src/helpers/ArticleHelper.ts index 1418bf48..b998df62 100644 --- a/src/helpers/ArticleHelper.ts +++ b/src/helpers/ArticleHelper.ts @@ -44,6 +44,23 @@ export class ArticleHelper { return ArticleHelper.parseFile(fileContents, document.fileName); } + /** + * Get the current article + */ + public static getCurrent(): ParsedFrontMatter | undefined { + const editor = vscode.window.activeTextEditor; + if (!editor) { + return; + } + + const article = ArticleHelper.getFrontMatter(editor); + if (!article) { + return; + } + + return article; + } + /** * Retrieve the file's front matter by its path * @param filePath diff --git a/src/helpers/ContentType.ts b/src/helpers/ContentType.ts index db032352..e089a240 100644 --- a/src/helpers/ContentType.ts +++ b/src/helpers/ContentType.ts @@ -2,7 +2,7 @@ import { PagesListener } from './../listeners/dashboard'; import { ArticleHelper, Settings } from "."; import { SETTING_CONTENT_DRAFT_FIELD, SETTING_DATE_FORMAT, SETTING_TAXONOMY_CONTENT_TYPES, TelemetryEvent } from "../constants"; import { ContentType as IContentType, DraftField, Field } from '../models'; -import { Uri, commands } from 'vscode'; +import { Uri, commands, window } from 'vscode'; import { Folders } from "../commands/Folders"; import { Questions } from "./Questions"; import { writeFileSync } from "fs"; @@ -92,6 +92,128 @@ export class ContentType { return Settings.get(SETTING_TAXONOMY_CONTENT_TYPES); } + /** + * Generate a content type + */ + public static async generate() { + const content = ArticleHelper.getCurrent(); + + if (!content || !content.data) { + Notifications.warning(`No front matter data found to generate a content type.`); + return; + } + + const answer = await window.showInputBox({ + ignoreFocusOut: true, + placeHolder: "Enter the name of the content type to generate", + prompt: "Enter the name of the content type to generate", + title: "Generate Content Type", + validateInput: (value: string) => { + if (!value) { + return "Please enter a name for the content type"; + } + + const contentTypes = ContentType.getAll(); + if (contentTypes && contentTypes.find(ct => ct.name.toLowerCase() === value.toLowerCase())) { + return "A content type with this name already exists"; + } + + return null; + } + }); + + if (!answer) { + Notifications.warning(`You didn't specify a name for the content type.`); + return; + } + + const fields = ContentType.generateFields(content.data); + + const newContentType: IContentType = { + name: answer, + fields + }; + + const contentTypes = ContentType.getAll() || []; + contentTypes.push(newContentType); + + Settings.update(SETTING_TAXONOMY_CONTENT_TYPES, contentTypes, true); + Notifications.info(`Content type ${answer} has been generated.`); + } + + /** + * Generate the fields from the data + * @param data + * @param fields + * @returns + */ + private static generateFields(data: any, fields: any[] = []) { + for (const field in data) { + const fieldData = data[field]; + + if (fieldData && fieldData instanceof Array && fieldData.length > 0 && typeof fieldData[0] === "string") { + if (field.toLowerCase() === "tag" || field.toLowerCase() === "tags") { + fields.push({ + title: field, + name: field, + type: "tags", + } as Field); + } else if (field.toLowerCase() === "category" || field.toLowerCase() === "categories") { + fields.push({ + title: field, + name: field, + type: "categories", + } as Field); + } else { + fields.push({ + title: field, + name: field, + type: "choice", + choices: fieldData + } as Field); + } + } else if (fieldData && fieldData instanceof Array && fieldData.length > 0 && typeof fieldData[0] === "object") { + const newFields = ContentType.generateFields(fieldData); + fields.push({ + title: field, + name: field, + type: "block", + fields: newFields + } as Field); + } else if (fieldData && fieldData instanceof Object) { + const newFields = ContentType.generateFields(fieldData); + fields.push({ + title: field, + name: field, + type: "fields", + fields: newFields + } as Field); + } else { + if (!isNaN(new Date(fieldData).getDate())) { + fields.push({ + title: field, + name: field, + type: "datetime" + } as Field); + } else if (field.toLowerCase() === "draft") { + fields.push({ + title: field, + name: field, + type: "draft" + } as Field); + } else { + fields.push({ + title: field, + name: field, + type: typeof fieldData + } as Field); + } + } + } + + return fields; + } + /** * Create a new file with the specified content type * @param contentType From dee28397cbe96ad3948a494153b2b7e089fb1088 Mon Sep 17 00:00:00 2001 From: Elio Struyf Date: Fri, 22 Apr 2022 11:18:42 +0200 Subject: [PATCH 2/5] Override default content type --- src/helpers/ContentType.ts | 70 +++++++++++++++++++++++------------ src/helpers/SettingsHelper.ts | 2 +- 2 files changed, 48 insertions(+), 24 deletions(-) diff --git a/src/helpers/ContentType.ts b/src/helpers/ContentType.ts index e089a240..0834bf4a 100644 --- a/src/helpers/ContentType.ts +++ b/src/helpers/ContentType.ts @@ -5,7 +5,7 @@ import { ContentType as IContentType, DraftField, Field } from '../models'; import { Uri, commands, window } from 'vscode'; import { Folders } from "../commands/Folders"; import { Questions } from "./Questions"; -import { writeFileSync } from "fs"; +import { existsSync, writeFileSync } from "fs"; import { Notifications } from "./Notifications"; import { DEFAULT_CONTENT_TYPE_NAME } from "../constants/ContentType"; import { Telemetry } from './Telemetry'; @@ -103,42 +103,64 @@ export class ContentType { return; } - const answer = await window.showInputBox({ + const override = await window.showQuickPick(["Yes", "No"], { + placeHolder: "Do you want to override the default content type?", ignoreFocusOut: true, - placeHolder: "Enter the name of the content type to generate", - prompt: "Enter the name of the content type to generate", - title: "Generate Content Type", - validateInput: (value: string) => { - if (!value) { - return "Please enter a name for the content type"; - } - - const contentTypes = ContentType.getAll(); - if (contentTypes && contentTypes.find(ct => ct.name.toLowerCase() === value.toLowerCase())) { - return "A content type with this name already exists"; - } - - return null; - } + title: "Override default content type" }); - if (!answer) { - Notifications.warning(`You didn't specify a name for the content type.`); - return; + let contentTypeName: string | undefined = `default`; + + if (override === "No") { + contentTypeName = await window.showInputBox({ + ignoreFocusOut: true, + placeHolder: "Enter the name of the content type to generate", + prompt: "Enter the name of the content type to generate", + title: "Generate Content Type", + validateInput: (value: string) => { + if (!value) { + return "Please enter a name for the content type"; + } + + const contentTypes = ContentType.getAll(); + if (contentTypes && contentTypes.find(ct => ct.name.toLowerCase() === value.toLowerCase())) { + return "A content type with this name already exists"; + } + + return null; + } + }); + + if (!contentTypeName) { + Notifications.warning(`You didn't specify a name for the content type.`); + return; + } } const fields = ContentType.generateFields(content.data); const newContentType: IContentType = { - name: answer, + name: contentTypeName, fields }; const contentTypes = ContentType.getAll() || []; - contentTypes.push(newContentType); + + if (override === "Yes") { + const index = contentTypes.findIndex(ct => ct.name === contentTypeName); + contentTypes[index].fields = fields; + } else { + contentTypes.push(newContentType); + } Settings.update(SETTING_TAXONOMY_CONTENT_TYPES, contentTypes, true); - Notifications.info(`Content type ${answer} has been generated.`); + + const configPath = Settings.projectConfigPath; + const notificationAction = await Notifications.info(`Content type ${contentTypeName} has been ${override === "Yes" ? `updated` : `generated`}.`, configPath && existsSync(configPath) ? `Open settings` : undefined); + + if (notificationAction === "Open settings" && configPath && existsSync(configPath)) { + commands.executeCommand('vscode.open', Uri.file(configPath)); + } } /** @@ -201,6 +223,8 @@ export class ContentType { name: field, type: "draft" } as Field); + } else if (field.toLowerCase() === "slug") { + // Do nothing } else { fields.push({ title: field, diff --git a/src/helpers/SettingsHelper.ts b/src/helpers/SettingsHelper.ts index 7382aed3..0c93a2d7 100644 --- a/src/helpers/SettingsHelper.ts +++ b/src/helpers/SettingsHelper.ts @@ -328,7 +328,7 @@ export class Settings { * Get the project config path * @returns */ - private static get projectConfigPath() { + public static get projectConfigPath() { const wsFolder = Folders.getWorkspaceFolder(); if (wsFolder) { const fmConfig = join(wsFolder.fsPath, Settings.globalFile); From 17a98fba68881e1e223dc3d9a203fc74e61ac093 Mon Sep 17 00:00:00 2001 From: Elio Struyf Date: Mon, 25 Apr 2022 14:57:59 +0200 Subject: [PATCH 3/5] Add content type create, update, setting --- CHANGELOG.md | 1 + package.json | 22 +++- src/constants/Extension.ts | 4 +- src/constants/TelemetryEvent.ts | 5 + src/extension.ts | 16 ++- src/helpers/ContentType.ts | 107 +++++++++++++++++- src/hooks/useContentType.tsx | 2 +- src/listeners/panel/DataListener.ts | 6 + src/panelWebView/CommandToCode.ts | 3 + .../ContentType/ContentTypeValidator.tsx | 74 ++++++++++++ src/panelWebView/components/Metadata.tsx | 8 +- src/panelWebView/styles.css | 27 +++++ 12 files changed, 263 insertions(+), 12 deletions(-) create mode 100644 src/panelWebView/components/ContentType/ContentTypeValidator.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f4887f1..c14c4e06 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ ### 🐞 Fixes +- Updated JSON schema link to supported version by VS Code (draft-07) - Hide the view mode action from the Front Matter panel if no custom modes are defined - Fix in decode base64 uploaded video files - Fix for a lightbox on other types of documents (pdf, etc.) diff --git a/package.json b/package.json index 1e76debb..0b13de08 100644 --- a/package.json +++ b/package.json @@ -1207,10 +1207,20 @@ "category": "Front matter" }, { - "command": "frontMatter.generate.contenttype", + "command": "frontMatter.contenttype.generate", "title": "Generate content type from current file", "category": "Front matter" }, + { + "command": "frontMatter.contenttype.addMissingFields", + "title": "Add missing fields from front matter to content type", + "category": "Front matter" + }, + { + "command": "frontMatter.contenttype.setContentType", + "title": "Set the content type to use for the current file", + "category": "Front matter" + }, { "command": "frontMatter.markup.blockquote", "title": "Blockquote", @@ -1688,7 +1698,15 @@ "when": "frontMatter:file:isValid == true" }, { - "command": "frontMatter.generate.contenttype", + "command": "frontMatter.contenttype.generate", + "when": "frontMatter:file:isValid == true" + }, + { + "command": "frontMatter.contenttype.addMissingFields", + "when": "frontMatter:file:isValid == true" + }, + { + "command": "frontMatter.contenttype.setContentType", "when": "frontMatter:file:isValid == true" } ], diff --git a/src/constants/Extension.ts b/src/constants/Extension.ts index a55eb954..8cfe890d 100644 --- a/src/constants/Extension.ts +++ b/src/constants/Extension.ts @@ -55,5 +55,7 @@ export const COMMAND_NAME = { options: getCommandName("markup.options"), // Content types - generateContentType: getCommandName("generate.contenttype"), + generateContentType: getCommandName("contenttype.generate"), + addMissingFields: getCommandName("contenttype.addMissingFields"), + setContentType: getCommandName("contenttype.setContentType"), }; \ No newline at end of file diff --git a/src/constants/TelemetryEvent.ts b/src/constants/TelemetryEvent.ts index b04b0232..0c6c43cd 100644 --- a/src/constants/TelemetryEvent.ts +++ b/src/constants/TelemetryEvent.ts @@ -26,6 +26,11 @@ export const TelemetryEvent = { updateMediaMetadata: 'updateMediaMetadata', openExplorerView: 'openExplorerView', + // Content types + generateContentType: 'generateContentType', + addMissingFields: 'addMissingFields', + setContentType: 'setContentType', + // Custom scripts runCustomScript: 'runCustomScript', runMediaScript: 'runMediaScript', diff --git a/src/extension.ts b/src/extension.ts index b761dfba..92356929 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -158,6 +158,14 @@ export async function activate(context: vscode.ExtensionContext) { vscode.commands.registerCommand(COMMAND_NAME.generateContentType, ContentType.generate) ); + subscriptions.push( + vscode.commands.registerCommand(COMMAND_NAME.addMissingFields, ContentType.addMissingFields) + ); + + subscriptions.push( + vscode.commands.registerCommand(COMMAND_NAME.setContentType, ContentType.setContentType) + ); + // Initialize command Template.init(); const projectInit = vscode.commands.registerCommand(COMMAND_NAME.init, async (cb: Function) => { @@ -197,7 +205,13 @@ export async function activate(context: vscode.ExtensionContext) { subscriptions.push(vscode.window.onDidChangeActiveTextEditor(() => triggerShowDraftStatus(`onDidChangeActiveTextEditor`))); subscriptions.push(vscode.window.onDidChangeTextEditorSelection((e) => { if (e.kind === vscode.TextEditorSelectionChangeKind.Mouse) { - triggerShowDraftStatus(`onDidChangeTextEditorSelection`); + statusDebouncer(() => triggerShowDraftStatus(`onDidChangeTextEditorSelection`), 200); + } + })); + subscriptions.push(vscode.workspace.onDidChangeTextDocument((TextDocumentChangeEvent) => { + const filePath = TextDocumentChangeEvent.document.uri.fsPath; + if (filePath && !filePath.toLowerCase().startsWith(`extension-output`)) { + statusDebouncer(() => triggerShowDraftStatus(`onDidChangeTextEditorSelection`), 200); } })); diff --git a/src/helpers/ContentType.ts b/src/helpers/ContentType.ts index 0834bf4a..829ac785 100644 --- a/src/helpers/ContentType.ts +++ b/src/helpers/ContentType.ts @@ -10,6 +10,7 @@ import { Notifications } from "./Notifications"; import { DEFAULT_CONTENT_TYPE_NAME } from "../constants/ContentType"; import { Telemetry } from './Telemetry'; import { processKnownPlaceholders } from './PlaceholderHelper'; +import { basename } from 'path'; export class ContentType { @@ -96,8 +97,13 @@ export class ContentType { * Generate a content type */ public static async generate() { + Telemetry.send(TelemetryEvent.generateContentType); + const content = ArticleHelper.getCurrent(); + const editor = window.activeTextEditor; + const filePath = editor?.document.uri.fsPath; + if (!content || !content.data) { Notifications.warning(`No front matter data found to generate a content type.`); return; @@ -108,10 +114,12 @@ export class ContentType { ignoreFocusOut: true, title: "Override default content type" }); + const overrideBool = override === "Yes"; let contentTypeName: string | undefined = `default`; - if (override === "No") { + // Ask for the new content type name + if (!overrideBool) { contentTypeName = await window.showInputBox({ ignoreFocusOut: true, placeHolder: "Enter the name of the content type to generate", @@ -137,16 +145,43 @@ export class ContentType { } } + // Ask if the content type needs to be used as a page bundle + let pageBundle = false; + const fileName = filePath ? basename(filePath) : undefined; + if (fileName?.startsWith(`index.`)) { + const pageBundleAnswer = await window.showQuickPick(["Yes", "No"], { + placeHolder: "Do you want to use this content type as a page bundle?", + ignoreFocusOut: true, + title: "Use as page bundle" + }); + pageBundle = pageBundleAnswer === "Yes"; + } + const fields = ContentType.generateFields(content.data); + if (!overrideBool && !fields.some(f => f.name === "type")) { + fields.push({ + name: "type", + type: "string", + default: contentTypeName, + hidden: true + } as Field); + } + + // Update the type field in the page + if (!overrideBool && editor) { + content.data["type"] = contentTypeName; + ArticleHelper.update(editor, content); + } const newContentType: IContentType = { name: contentTypeName, + pageBundle, fields }; const contentTypes = ContentType.getAll() || []; - if (override === "Yes") { + if (overrideBool) { const index = contentTypes.findIndex(ct => ct.name === contentTypeName); contentTypes[index].fields = fields; } else { @@ -156,11 +191,71 @@ export class ContentType { Settings.update(SETTING_TAXONOMY_CONTENT_TYPES, contentTypes, true); const configPath = Settings.projectConfigPath; - const notificationAction = await Notifications.info(`Content type ${contentTypeName} has been ${override === "Yes" ? `updated` : `generated`}.`, configPath && existsSync(configPath) ? `Open settings` : undefined); + const notificationAction = await Notifications.info(`Content type ${contentTypeName} has been ${overrideBool ? `updated` : `generated`}.`, configPath && existsSync(configPath) ? `Open settings` : undefined); if (notificationAction === "Open settings" && configPath && existsSync(configPath)) { commands.executeCommand('vscode.open', Uri.file(configPath)); - } + } + } + + /** + * Add missing fields to the content type + */ + public static async addMissingFields() { + Telemetry.send(TelemetryEvent.addMissingFields); + + const content = ArticleHelper.getCurrent(); + + if (!content || !content.data) { + Notifications.warning(`No front matter data found to add missing fields.`); + return; + } + + const contentType = ArticleHelper.getContentType(content?.data); + const updatedFields = ContentType.generateFields(content.data, contentType.fields); + + const contentTypes = ContentType.getAll() || []; + const index = contentTypes.findIndex(ct => ct.name === contentType.name); + contentTypes[index].fields = updatedFields; + + Settings.update(SETTING_TAXONOMY_CONTENT_TYPES, contentTypes, true); + + const configPath = Settings.projectConfigPath; + const notificationAction = await Notifications.info(`Content type ${contentType.name} has been updated.`, configPath && existsSync(configPath) ? `Open settings` : undefined); + + if (notificationAction === "Open settings" && configPath && existsSync(configPath)) { + commands.executeCommand('vscode.open', Uri.file(configPath)); + } + } + + /** + * Set the content type to be used for the current file + */ + public static async setContentType() { + Telemetry.send(TelemetryEvent.setContentType); + + const content = ArticleHelper.getCurrent(); + const contentTypes = ContentType.getAll() || []; + + if (!content || !content.data) { + Notifications.warning(`No front matter data found to set the content type.`); + return; + } + + const ctAnswer = await window.showQuickPick(contentTypes.map(ct => ct.name), { + title: "Select the content type", + ignoreFocusOut: true, + placeHolder: "Which content type would you like to use?" + }); + + if (!ctAnswer) { + return; + } + + content.data.type = ctAnswer; + + const editor = window.activeTextEditor; + ArticleHelper.update(editor!, content); } /** @@ -173,6 +268,10 @@ export class ContentType { for (const field in data) { const fieldData = data[field]; + if (fields.some(f => f.name === field)) { + continue; + } + if (fieldData && fieldData instanceof Array && fieldData.length > 0 && typeof fieldData[0] === "string") { if (field.toLowerCase() === "tag" || field.toLowerCase() === "tags") { fields.push({ diff --git a/src/hooks/useContentType.tsx b/src/hooks/useContentType.tsx index 02111a63..f474bee4 100644 --- a/src/hooks/useContentType.tsx +++ b/src/hooks/useContentType.tsx @@ -21,7 +21,7 @@ export default function useContentType(settings: PanelSettings | Settings | unde setContentType(ct || DEFAULT_CONTENT_TYPE) } - }, [settings?.contentTypes, metadata?.data]); + }, [settings?.contentTypes, metadata?.type]); return contentType; } \ No newline at end of file diff --git a/src/listeners/panel/DataListener.ts b/src/listeners/panel/DataListener.ts index faf5ba59..f696ada5 100644 --- a/src/listeners/panel/DataListener.ts +++ b/src/listeners/panel/DataListener.ts @@ -43,6 +43,12 @@ export class DataListener extends BaseListener { case CommandToCode.updatePlaceholder: this.updatePlaceholder(msg?.data?.field, msg?.data?.value, msg?.data?.title); break; + case CommandToCode.generateContentType: + commands.executeCommand(COMMAND_NAME.generateContentType); + case CommandToCode.addMissingFields: + commands.executeCommand(COMMAND_NAME.addMissingFields); + case CommandToCode.setContentType: + commands.executeCommand(COMMAND_NAME.setContentType); } } diff --git a/src/panelWebView/CommandToCode.ts b/src/panelWebView/CommandToCode.ts index c0946944..30ab1986 100644 --- a/src/panelWebView/CommandToCode.ts +++ b/src/panelWebView/CommandToCode.ts @@ -34,4 +34,7 @@ export enum CommandToCode { getImageUrl = "get-image-url", updatePlaceholder = "update-placeholder", getMode = "get-mode", + generateContentType = "generate-content-type", + addMissingFields = "add-missing-fields", + setContentType = "set-content-type", } \ No newline at end of file diff --git a/src/panelWebView/components/ContentType/ContentTypeValidator.tsx b/src/panelWebView/components/ContentType/ContentTypeValidator.tsx new file mode 100644 index 00000000..b50c758a --- /dev/null +++ b/src/panelWebView/components/ContentType/ContentTypeValidator.tsx @@ -0,0 +1,74 @@ +import { VSCodeButton, VSCodeDivider } from '@vscode/webview-ui-toolkit/react'; +import * as React from 'react'; +import { useMemo } from 'react'; +import { MessageHelper } from '../../../helpers/MessageHelper'; +import { Field } from '../../../models'; +import { CommandToCode } from '../../CommandToCode'; +import { IMetadata } from '../Metadata'; +import { VsLabel } from '../VscodeComponents'; + +export interface IContentTypeValidatorProps { + fields: Field[]; + metadata: IMetadata +} + +const fieldsToIgnore = [`filePath`, `articleDetails`, `slug`]; + +export const ContentTypeValidator: React.FunctionComponent = ({ fields, metadata}: React.PropsWithChildren) => { + + const isValid = useMemo(() => { + const metadataFields = Object.keys(metadata).filter(key => !fieldsToIgnore.includes(key)); + + for (const mField of metadataFields) { + if (!fields.find(field => field.name === mField)) { + return false; + } + } + + return true; + }, [fields, metadata]); + + + const generateContentType = () => { + MessageHelper.sendMessage(CommandToCode.generateContentType); + }; + + const addMissingFields = () => { + MessageHelper.sendMessage(CommandToCode.addMissingFields); + }; + + const setContentType = () => { + MessageHelper.sendMessage(CommandToCode.setContentType); + }; + + + if (isValid) { + return null; + } + + return ( +
+ +
+ + + Content type +
+
+ +

We noticed field differences between the content type and the front matter data.

+ +

Would you like to generate or update the content type for this page?

+ +
+ Generate content type + + Add missing fields + + Set content type +
+ + +
+ ); +}; \ No newline at end of file diff --git a/src/panelWebView/components/Metadata.tsx b/src/panelWebView/components/Metadata.tsx index d866ef7e..1ae6ce24 100644 --- a/src/panelWebView/components/Metadata.tsx +++ b/src/panelWebView/components/Metadata.tsx @@ -4,12 +4,10 @@ import { CommandToCode } from '../CommandToCode'; import { MessageHelper } from '../../helpers/MessageHelper'; import { TagType } from '../TagType'; import { Collapsible } from './Collapsible'; -import { SymbolKeywordIcon } from './Icons/SymbolKeywordIcon'; -import { TagPicker } from './TagPicker'; import "react-datepicker/dist/react-datepicker.css"; import useContentType from '../../hooks/useContentType'; -import FieldBoundary from './ErrorBoundary/FieldBoundary'; import { WrapperField } from './Fields/WrapperField'; +import { ContentTypeValidator } from './ContentType/ContentTypeValidator'; export interface IMetadata { [prop: string]: string[] | string | null | IMetadata; @@ -80,6 +78,10 @@ const Metadata: React.FunctionComponent = ({settings, metadata, return ( + + { renderFields(contentType?.fields || [], metadata) } diff --git a/src/panelWebView/styles.css b/src/panelWebView/styles.css index 4144d90d..29383c63 100644 --- a/src/panelWebView/styles.css +++ b/src/panelWebView/styles.css @@ -253,6 +253,33 @@ display: none; } +/* Metadata section - Content type */ +.metadata_field__alert svg { + color: var(--vscode-editorWarning-foreground) +} + +.hint { + margin-bottom: 1rem; +} + +.hint__buttons vscode-button { + display: block; + margin-bottom: .5rem; + text-align: center; +} + +.hint__buttons vscode-button:last-child { + margin-bottom: 0; +} + +vscode-divider { + margin-top: 1rem; +} + +.inline_hint { + color: var(--vscode-editorInlayHint-foreground); +} + /* File field */ .metadata_field__file__button.not_empty { display: flex; From f10d93c22ef671a00c7e16143a63954817e63dab Mon Sep 17 00:00:00 2001 From: Elio Struyf Date: Mon, 25 Apr 2022 20:47:44 +0200 Subject: [PATCH 4/5] Add new mode for the content type actions --- package.json | 1 + src/constants/Features.ts | 1 + src/helpers/ContentType.ts | 29 +++++++++++++++++++++++- src/listeners/general/ModeListener.ts | 24 ++++++++++++++++++++ src/panelWebView/ViewPanel.tsx | 3 ++- src/panelWebView/components/Metadata.tsx | 13 +++++++---- 6 files changed, 65 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 0b13de08..683dabf2 100644 --- a/package.json +++ b/package.json @@ -598,6 +598,7 @@ "panel.globalSettings", "panel.seo", "panel.actions", + "panel.contentType", "panel.metadata", "panel.recentlyModified", "panel.otherActions", diff --git a/src/constants/Features.ts b/src/constants/Features.ts index eecc4df9..e4aa8629 100644 --- a/src/constants/Features.ts +++ b/src/constants/Features.ts @@ -8,6 +8,7 @@ export const FEATURE_FLAG = { metadata: "panel.metadata", recentlyModified: "panel.recentlyModified", otherActions: "panel.otherActions", + contentType: "panel.contentType", }, dashboard: { snippets: { diff --git a/src/helpers/ContentType.ts b/src/helpers/ContentType.ts index 829ac785..7eb0526e 100644 --- a/src/helpers/ContentType.ts +++ b/src/helpers/ContentType.ts @@ -1,6 +1,7 @@ +import { ModeListener } from './../listeners/general/ModeListener'; import { PagesListener } from './../listeners/dashboard'; import { ArticleHelper, Settings } from "."; -import { SETTING_CONTENT_DRAFT_FIELD, SETTING_DATE_FORMAT, SETTING_TAXONOMY_CONTENT_TYPES, TelemetryEvent } from "../constants"; +import { FEATURE_FLAG, SETTING_CONTENT_DRAFT_FIELD, SETTING_DATE_FORMAT, SETTING_TAXONOMY_CONTENT_TYPES, TelemetryEvent } from "../constants"; import { ContentType as IContentType, DraftField, Field } from '../models'; import { Uri, commands, window } from 'vscode'; import { Folders } from "../commands/Folders"; @@ -97,6 +98,10 @@ export class ContentType { * Generate a content type */ public static async generate() { + if (!(await ContentType.verify())) { + return; + } + Telemetry.send(TelemetryEvent.generateContentType); const content = ArticleHelper.getCurrent(); @@ -202,6 +207,10 @@ export class ContentType { * Add missing fields to the content type */ public static async addMissingFields() { + if (!(await ContentType.verify())) { + return; + } + Telemetry.send(TelemetryEvent.addMissingFields); const content = ArticleHelper.getCurrent(); @@ -232,6 +241,10 @@ export class ContentType { * Set the content type to be used for the current file */ public static async setContentType() { + if (!(await ContentType.verify())) { + return; + } + Telemetry.send(TelemetryEvent.setContentType); const content = ArticleHelper.getCurrent(); @@ -412,4 +425,18 @@ export class ContentType { return data; } + + /** + * Verify if the content type feature is enabled + * @returns + */ + private static async verify() { + const hasFeature = await ModeListener.hasFeature(FEATURE_FLAG.panel.contentType); + if (!hasFeature) { + Notifications.warning(`The content type actions are not available in this mode.`); + return false; + } + + return true; + } } \ No newline at end of file diff --git a/src/listeners/general/ModeListener.ts b/src/listeners/general/ModeListener.ts index 636499d2..25989474 100644 --- a/src/listeners/general/ModeListener.ts +++ b/src/listeners/general/ModeListener.ts @@ -51,6 +51,30 @@ export class ModeListener extends BaseListener { } } + /** + * Check if the mode has the feature enabled + * @param feature + * @returns + */ + public static async hasFeature(feature: string) { + const modes = Settings.get(SETTING_GLOBAL_MODES); + + if (!modes || modes.length === 0) { + return true; + } + + const activeMode = ModeSwitch.getMode(); + if (activeMode) { + const mode = modes.find(m => m.id === activeMode); + return mode?.features.find(f => f === feature); + } + + return true; + } + + /** + * Reset the context + */ public static async resetEnablement() { await commands.executeCommand('setContext', CONTEXT.isSnippetsDashboardEnabled, true); await commands.executeCommand('setContext', CONTEXT.isDataDashboardEnabled, true); diff --git a/src/panelWebView/ViewPanel.tsx b/src/panelWebView/ViewPanel.tsx index 95f148d6..0cd56ead 100644 --- a/src/panelWebView/ViewPanel.tsx +++ b/src/panelWebView/ViewPanel.tsx @@ -70,7 +70,8 @@ export const ViewPanel: React.FunctionComponent = (props: React settings={settings} metadata={metadata} focusElm={focusElm} - unsetFocus={unsetFocus} /> + unsetFocus={unsetFocus} + features={mode?.features || []} /> diff --git a/src/panelWebView/components/Metadata.tsx b/src/panelWebView/components/Metadata.tsx index 1ae6ce24..267e92c3 100644 --- a/src/panelWebView/components/Metadata.tsx +++ b/src/panelWebView/components/Metadata.tsx @@ -8,6 +8,8 @@ import "react-datepicker/dist/react-datepicker.css"; import useContentType from '../../hooks/useContentType'; import { WrapperField } from './Fields/WrapperField'; import { ContentTypeValidator } from './ContentType/ContentTypeValidator'; +import { FeatureFlag } from '../../components/features/FeatureFlag'; +import { FEATURE_FLAG } from '../../constants'; export interface IMetadata { [prop: string]: string[] | string | null | IMetadata; @@ -16,10 +18,11 @@ export interface IMetadataProps { settings: PanelSettings | undefined; metadata: IMetadata; focusElm: TagType | null; + features: string[]; unsetFocus: () => void; } -const Metadata: React.FunctionComponent = ({settings, metadata, focusElm, unsetFocus}: React.PropsWithChildren) => { +const Metadata: React.FunctionComponent = ({settings, features, metadata, focusElm, unsetFocus}: React.PropsWithChildren) => { const contentType = useContentType(settings, metadata); const sendUpdate = (field: string | undefined, value: any, parents: string[]) => { @@ -78,9 +81,11 @@ const Metadata: React.FunctionComponent = ({settings, metadata, return ( - + + + { renderFields(contentType?.fields || [], metadata) From d161aa98a03895e733f9ceee27142b01c2c90d9e Mon Sep 17 00:00:00 2001 From: Elio Struyf Date: Tue, 26 Apr 2022 12:05:08 +0200 Subject: [PATCH 5/5] Updates for button colors --- assets/media/styles.css | 5 +++++ .../components/ContentType/ContentTypeValidator.tsx | 4 ++-- src/panelWebView/styles.css | 2 +- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/assets/media/styles.css b/assets/media/styles.css index b349f165..acd7388f 100644 --- a/assets/media/styles.css +++ b/assets/media/styles.css @@ -355,6 +355,11 @@ color: var(--vscode-button-secondaryForeground); } +.ext_link_block a:hover, +.ext_link_block button:hover { + background-color: var(--vscode-button-secondaryHoverBackground); +} + .table__cell { overflow: hidden; } diff --git a/src/panelWebView/components/ContentType/ContentTypeValidator.tsx b/src/panelWebView/components/ContentType/ContentTypeValidator.tsx index b50c758a..f084a9bf 100644 --- a/src/panelWebView/components/ContentType/ContentTypeValidator.tsx +++ b/src/panelWebView/components/ContentType/ContentTypeValidator.tsx @@ -58,10 +58,10 @@ export const ContentTypeValidator: React.FunctionComponentWe noticed field differences between the content type and the front matter data.

-

Would you like to generate or update the content type for this page?

+

Would you like to create, update, or set the content type for this content?

- Generate content type + Create content type Add missing fields diff --git a/src/panelWebView/styles.css b/src/panelWebView/styles.css index 29383c63..ba088adc 100644 --- a/src/panelWebView/styles.css +++ b/src/panelWebView/styles.css @@ -277,7 +277,7 @@ vscode-divider { } .inline_hint { - color: var(--vscode-editorInlayHint-foreground); + color: var(--vscode-sideBar-foreground); } /* File field */