From 83cf0eb8f595f2f94ed63fdc3c5a8e1fa3bc9093 Mon Sep 17 00:00:00 2001 From: Elio Struyf Date: Mon, 29 Jan 2024 16:58:25 +0100 Subject: [PATCH 1/4] #746 - Placeholder support in slug --- package.json | 296 ++++++++---------- package.nls.json | 2 + src/commands/Article.ts | 41 ++- src/commands/Dashboard.ts | 3 +- src/commands/Folders.ts | 4 +- src/commands/Preview.ts | 6 +- src/constants/settings.ts | 1 + src/dashboardWebView/DashboardMessage.ts | 1 + .../components/SnippetsView/Item.tsx | 1 + .../components/SnippetsView/SnippetForm.tsx | 33 +- src/helpers/ArticleHelper.ts | 44 ++- src/helpers/ContentType.ts | 29 +- src/helpers/SlugHelper.ts | 41 ++- src/helpers/index.ts | 3 +- src/helpers/processArticlePlaceholders.ts | 53 ++++ src/helpers/processFmPlaceholders.ts | 2 +- ...erHelper.ts => processTimePlaceholders.ts} | 19 +- src/listeners/dashboard/BaseListener.ts | 7 +- src/listeners/dashboard/SnippetListener.ts | 35 ++- src/listeners/general/GitListener.ts | 4 +- src/listeners/panel/ArticleListener.ts | 6 +- src/listeners/panel/DataListener.ts | 49 ++- src/models/DashboardData.ts | 2 + src/models/PanelSettings.ts | 1 + .../components/Fields/SlugField.tsx | 9 +- .../components/Fields/WrapperField.tsx | 41 +-- src/panelWebView/components/Metadata.tsx | 1 + src/services/PagesParser.ts | 5 +- 28 files changed, 460 insertions(+), 279 deletions(-) create mode 100644 src/helpers/processArticlePlaceholders.ts rename src/helpers/{PlaceholderHelper.ts => processTimePlaceholders.ts} (76%) diff --git a/package.json b/package.json index 47c622f1..7789c97e 100644 --- a/package.json +++ b/package.json @@ -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" }, @@ -90,23 +88,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%", @@ -174,8 +168,7 @@ "frontMatter.content.defaultFileType": { "type": "string", "default": "md", - "oneOf": [ - { + "oneOf": [{ "enum": [ "md", "mdx" @@ -191,8 +184,7 @@ "frontMatter.content.defaultSorting": { "type": "string", "default": "", - "oneOf": [ - { + "oneOf": [{ "enum": [ "LastModifiedAsc", "LastModifiedDesc", @@ -544,8 +536,7 @@ "command": { "$id": "#scriptCommand", "type": "string", - "anyOf": [ - { + "anyOf": [{ "enum": [ "node", "bash", @@ -752,8 +743,7 @@ "title", "file" ], - "anyOf": [ - { + "anyOf": [{ "required": [ "schema" ] @@ -807,8 +797,7 @@ "id", "path" ], - "anyOf": [ - { + "anyOf": [{ "required": [ "schema" ] @@ -1209,8 +1198,7 @@ "default": "", "description": "%setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.taxonomyId.description%", "not": { - "anyOf": [ - { + "anyOf": [{ "const": "" }, { @@ -1404,8 +1392,7 @@ "type", "name" ], - "allOf": [ - { + "allOf": [{ "if": { "properties": { "type": { @@ -1576,6 +1563,14 @@ "default": null, "description": "%setting.frontMatter.taxonomy.contentTypes.items.properties.previewPath.description%" }, + "slugTemplate": { + "type": [ + "null", + "string" + ], + "default": null, + "description": "%setting.frontMatter.content.pageFolders.items.properties.slugTemplate.description%" + }, "template": { "type": "string", "default": "", @@ -1605,51 +1600,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": { @@ -1662,8 +1654,7 @@ "type": "string", "description": "%setting.frontMatter.taxonomy.customTaxonomy.items.properties.id.description%", "not": { - "anyOf": [ - { + "anyOf": [{ "const": "" }, { @@ -1818,6 +1809,11 @@ "markdownDescription": "%setting.frontMatter.taxonomy.slugSuffix.markdownDescription%", "scope": "Taxonomy" }, + "frontMatter.taxonomy.slugTemplate": { + "type": "string", + "markdownDescription": "%setting.frontMatter.taxonomy.slugTemplate.markdownDescription%", + "scope": "Taxonomy" + }, "frontMatter.taxonomy.tags": { "type": "array", "markdownDescription": "%setting.frontMatter.taxonomy.tags.markdownDescription%", @@ -1855,8 +1851,7 @@ } } }, - "commands": [ - { + "commands": [{ "command": "frontMatter.project.switch", "title": "%command.frontMatter.project.switch%", "category": "Front Matter", @@ -2173,21 +2168,16 @@ "category": "Front Matter" } ], - "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" @@ -2268,14 +2258,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" @@ -2291,8 +2278,7 @@ "group": "frontmatter@3" } ], - "commandPalette": [ - { + "commandPalette": [{ "command": "frontMatter.init", "when": "frontMatterCanInit" }, @@ -2441,8 +2427,7 @@ "when": "frontMatter:file:isValid == true" } ], - "view/title": [ - { + "view/title": [{ "command": "frontMatter.chatbot", "group": "navigation@0", "when": "view == frontMatter.explorer" @@ -2474,57 +2459,52 @@ } ] }, - "grammars": [ - { - "path": "./syntaxes/hugo.tmLanguage.json", - "scopeName": "frontmatter.markdown.hugo", - "injectTo": [ - "text.html.markdown" - ] - } - ], - "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" - ] + "grammars": [{ + "path": "./syntaxes/hugo.tmLanguage.json", + "scopeName": "frontmatter.markdown.hugo", + "injectTo": [ + "text.html.markdown" + ] + }], + "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:*", @@ -2660,4 +2640,4 @@ "vsce": { "dependencies": false } -} +} \ No newline at end of file diff --git a/package.nls.json b/package.nls.json index de0ecc3a..604d0807 100644 --- a/package.nls.json +++ b/package.nls.json @@ -210,6 +210,7 @@ "setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.when.properties.caseSensitive.description": "Specify if the comparison is case sensitive. Default: true", "setting.frontMatter.taxonomy.contentTypes.items.properties.pageBundle.description": "Specify if you want to create a folder when creating new content.", "setting.frontMatter.taxonomy.contentTypes.items.properties.previewPath.description": "Defines a custom preview path for the content type.", + "setting.frontMatter.taxonomy.contentTypes.items.properties.slugTemplate.description": "Defines a custom slug template for the content type.", "setting.frontMatter.taxonomy.contentTypes.items.properties.template.description": "An optional template that can be used for creating new content.", "setting.frontMatter.taxonomy.contentTypes.items.properties.postScript.description": "An optional post script that can be used after new content creation.", "setting.frontMatter.taxonomy.contentTypes.items.properties.filePrefix.description": "Defines a prefix for the file name.", @@ -236,6 +237,7 @@ "setting.frontMatter.taxonomy.seoTitleLength.markdownDescription": "Specifies the optimal title length for SEO (set to `-1` to turn it off). [Check in the docs](https://frontmatter.codes/docs/settings/overview#frontmatter.taxonomy.seotitlelength)", "setting.frontMatter.taxonomy.slugPrefix.markdownDescription": "Specify a prefix for the slug. [Check in the docs](https://frontmatter.codes/docs/settings/overview#frontmatter.taxonomy.slugprefix)", "setting.frontMatter.taxonomy.slugSuffix.markdownDescription": "Specify a suffix for the slug. [Check in the docs](https://frontmatter.codes/docs/settings/overview#frontmatter.taxonomy.slugsuffix)", + "setting.frontMatter.taxonomy.slugTemplate.markdownDescription": "Defines a custom slug template for the content you will create. [Check in the docs](https://frontmatter.codes/docs/settings/overview#frontmatter.taxonomy.slugtemplate)", "setting.frontMatter.taxonomy.tags.markdownDescription": "Specifies the tags which can be used in the Front Matter. [Check in the docs](https://frontmatter.codes/docs/settings/overview#frontmatter.taxonomy.tags)", "setting.frontMatter.telemetry.disable.markdownDescription": "Specify if you want to disable the telemetry. [Check in the docs](https://frontmatter.codes/docs/settings/overview#frontmatter.telemetry.disable)", "setting.frontMatter.templates.enabled.markdownDescription": "Specify if you want to use templates. [Check in the docs](https://frontmatter.codes/docs/settings/overview#frontmatter.templates.enabled)", diff --git a/src/commands/Article.ts b/src/commands/Article.ts index 7936b622..6ae42773 100644 --- a/src/commands/Article.ts +++ b/src/commands/Article.ts @@ -15,18 +15,23 @@ import { import * as vscode from 'vscode'; import { CustomPlaceholder, Field } from '../models'; import { format } from 'date-fns'; -import { ArticleHelper, Settings, SlugHelper } from '../helpers'; +import { + ArticleHelper, + Settings, + SlugHelper, + processArticlePlaceholdersFromData, + processTimePlaceholders +} from '../helpers'; import { Notifications } from '../helpers/Notifications'; import { extname, basename, parse, dirname } from 'path'; import { COMMAND_NAME, DefaultFields } from '../constants'; -import { DashboardData, SnippetRange } from '../models/DashboardData'; +import { DashboardData, SnippetInfo, SnippetRange } from '../models/DashboardData'; import { DateHelper } from '../helpers/DateHelper'; import { parseWinPath } from '../helpers/parseWinPath'; import { Telemetry } from '../helpers/Telemetry'; import { ParsedFrontMatter } from '../parsers'; import { MediaListener } from '../listeners/panel'; import { NavigationType } from '../dashboardWebView/models'; -import { processKnownPlaceholders } from '../helpers/PlaceholderHelper'; import { Position } from 'vscode'; import { SNIPPET } from '../constants/Snippet'; import * as l10n from '@vscode/l10n'; @@ -124,7 +129,7 @@ export class Article { /** * Generate the new slug */ - public static generateSlug(title: string) { + public static generateSlug(title: string, article?: ParsedFrontMatter, slugTemplate?: string) { if (!title) { return; } @@ -132,13 +137,15 @@ export class Article { const prefix = Settings.get(SETTING_SLUG_PREFIX) as string; const suffix = Settings.get(SETTING_SLUG_SUFFIX) as string; - const slug = SlugHelper.createSlug(title); + if (article?.data) { + const slug = SlugHelper.createSlug(title, article?.data, slugTemplate); - if (slug) { - return { - slug, - slugWithPrefixAndSuffix: `${prefix}${slug}${suffix}` - }; + if (slug) { + return { + slug, + slugWithPrefixAndSuffix: `${prefix}${slug}${suffix}` + }; + } } return undefined; @@ -168,7 +175,7 @@ export class Article { const titleField = 'title'; const articleTitle: string = article.data[titleField]; - const slugInfo = Article.generateSlug(articleTitle); + const slugInfo = Article.generateSlug(articleTitle, article, contentType.slugTemplate); if (slugInfo && slugInfo.slug && slugInfo.slugWithPrefixAndSuffix) { article.data['slug'] = slugInfo.slugWithPrefixAndSuffix; @@ -192,9 +199,13 @@ export class Article { ); for (const pField of customPlaceholderFields) { article.data[pField.name] = customPlaceholder.value; - article.data[pField.name] = processKnownPlaceholders( + article.data[pField.name] = processArticlePlaceholdersFromData( + article.data[pField.name], + article.data, + contentType + ); + article.data[pField.name] = processTimePlaceholders( article.data[pField.name], - articleTitle, dateFormat ); } @@ -388,7 +399,7 @@ export class Article { snippetStartBeforePos = linesBeforeSelection.length - snippetStartBeforePos - 1; } - let snippetInfo: { id: string; fields: any[] } | undefined = undefined; + let snippetInfo: SnippetInfo | undefined = undefined; let range: SnippetRange | undefined = undefined; if ( snippetEndAfterPos > -1 && @@ -412,6 +423,7 @@ export class Article { } const article = ArticleHelper.getFrontMatter(editor); + const contentType = article ? ArticleHelper.getContentType(article) : undefined; await vscode.commands.executeCommand(COMMAND_NAME.dashboard, { type: NavigationType.Snippets, @@ -419,6 +431,7 @@ export class Article { fileTitle: article?.data.title || '', filePath: editor.document.uri.fsPath, fieldName: basename(editor.document.uri.fsPath), + contentType, position, range, selection: selectionText, diff --git a/src/commands/Dashboard.ts b/src/commands/Dashboard.ts index 7b5001bb..9484bef1 100644 --- a/src/commands/Dashboard.ts +++ b/src/commands/Dashboard.ts @@ -31,6 +31,7 @@ import { GitListener, ModeListener } from '../listeners/general'; import { Folders } from './Folders'; import * as l10n from '@vscode/l10n'; import { LocalizationKey } from '../localization'; +import { DashboardMessage } from '../dashboardWebView/DashboardMessage'; export class Dashboard { private static webview: WebviewPanel | null = null; @@ -204,7 +205,7 @@ export class Dashboard { * @param msg */ public static postWebviewMessage(msg: { - command: DashboardCommand; + command: DashboardCommand | DashboardMessage; requestId?: string; payload?: unknown; error?: unknown; diff --git a/src/commands/Folders.ts b/src/commands/Folders.ts index 10f8513b..44cd5e77 100644 --- a/src/commands/Folders.ts +++ b/src/commands/Folders.ts @@ -13,7 +13,7 @@ import { ContentFolder, FileInfo, FolderInfo, StaticFolder } from '../models'; import uniqBy = require('lodash.uniqby'); import { Template } from './Template'; import { Notifications } from '../helpers/Notifications'; -import { Logger, processKnownPlaceholders, Settings } from '../helpers'; +import { Logger, Settings, processTimePlaceholders } from '../helpers'; import { existsSync } from 'fs'; import { format } from 'date-fns'; import { Dashboard } from './Dashboard'; @@ -378,7 +378,7 @@ export class Folders { let folderPath: string | undefined = Folders.absWsFolder(folder, wsFolder); if (folderPath.includes(`{{`) && folderPath.includes(`}}`)) { const dateFormat = Settings.get(SETTING_DATE_FORMAT) as string; - folderPath = processKnownPlaceholders(folderPath, undefined, dateFormat); + folderPath = processTimePlaceholders(folderPath, dateFormat); } else { if (folderPath && !existsSync(folderPath)) { Notifications.errorShowOnce( diff --git a/src/commands/Preview.ts b/src/commands/Preview.ts index 835b6030..4da897c6 100644 --- a/src/commands/Preview.ts +++ b/src/commands/Preview.ts @@ -14,7 +14,7 @@ import { import { ArticleHelper } from './../helpers/ArticleHelper'; import { join, parse } from 'path'; import { commands, env, Uri, ViewColumn, window, WebviewPanel, extensions } from 'vscode'; -import { Extension, parseWinPath, processKnownPlaceholders, Settings } from '../helpers'; +import { Extension, parseWinPath, processTimePlaceholders, Settings } from '../helpers'; import { ContentFolder, ContentType, PreviewSettings } from '../models'; import { format } from 'date-fns'; import { DateHelper } from '../helpers/DateHelper'; @@ -294,7 +294,7 @@ export class Preview { if (pathname) { // Known placeholders const dateFormat = Settings.get(SETTING_DATE_FORMAT) as string; - pathname = processKnownPlaceholders(pathname, article?.data?.title, dateFormat); + pathname = processTimePlaceholders(pathname, dateFormat); // Custom placeholders pathname = await ArticleHelper.processCustomPlaceholders( @@ -318,7 +318,7 @@ export class Preview { } // Support front matter placeholders - {{fm.}} - pathname = processFmPlaceholders(pathname, article?.data); + pathname = article?.data ? processFmPlaceholders(pathname, article?.data) : pathname; try { const articleDate = ArticleHelper.getDate(article); diff --git a/src/constants/settings.ts b/src/constants/settings.ts index ff9b7398..3a343e4d 100644 --- a/src/constants/settings.ts +++ b/src/constants/settings.ts @@ -25,6 +25,7 @@ export const SETTING_TAXONOMY_CONTENT_TYPES = 'taxonomy.contentTypes'; export const SETTING_SLUG_PREFIX = 'taxonomy.slugPrefix'; export const SETTING_SLUG_SUFFIX = 'taxonomy.slugSuffix'; +export const SETTING_SLUG_TEMPLATE = 'taxonomy.slugTemplate'; export const SETTING_SLUG_UPDATE_FILE_NAME = 'taxonomy.alignFilename'; export const SETTING_INDENT_ARRAY = 'taxonomy.indentArrays'; diff --git a/src/dashboardWebView/DashboardMessage.ts b/src/dashboardWebView/DashboardMessage.ts index 9aa31534..7d658e9f 100644 --- a/src/dashboardWebView/DashboardMessage.ts +++ b/src/dashboardWebView/DashboardMessage.ts @@ -54,6 +54,7 @@ export enum DashboardMessage { insertSnippet = 'insertSnippet', addSnippet = 'addSnippet', updateSnippet = 'updateSnippet', + updateSnippetPlaceholders = 'updateSnippetPlaceholders', // Taxonomy dashboard getTaxonomyData = 'getTaxonomyData', diff --git a/src/dashboardWebView/components/SnippetsView/Item.tsx b/src/dashboardWebView/components/SnippetsView/Item.tsx index f6e694ed..777a32f6 100644 --- a/src/dashboardWebView/components/SnippetsView/Item.tsx +++ b/src/dashboardWebView/components/SnippetsView/Item.tsx @@ -282,6 +282,7 @@ export const Item: React.FunctionComponent = ({ ref={formRef} snippetKey={snippetKey} snippet={snippet} + filePath={viewData?.data?.filePath} fieldInfo={viewData?.data?.snippetInfo?.fields} selection={viewData?.data?.selection} /> diff --git a/src/dashboardWebView/components/SnippetsView/SnippetForm.tsx b/src/dashboardWebView/components/SnippetsView/SnippetForm.tsx index dcc10dd7..dab80763 100644 --- a/src/dashboardWebView/components/SnippetsView/SnippetForm.tsx +++ b/src/dashboardWebView/components/SnippetsView/SnippetForm.tsx @@ -1,12 +1,10 @@ -import { Messenger } from '@estruyf/vscode/dist/client'; +import { Messenger, messageHandler } from '@estruyf/vscode/dist/client'; import * as React from 'react'; import { useCallback, useEffect, useImperativeHandle, useMemo, useState } from 'react'; import { useRecoilValue } from 'recoil'; -import { processKnownPlaceholders } from '../../../helpers/PlaceholderHelper'; import { SnippetParser } from '../../../helpers/SnippetParser'; import { Snippet, SnippetField, SnippetInfoField, SnippetSpecialPlaceholders } from '../../../models'; import { DashboardMessage } from '../../DashboardMessage'; -import useThemeColors from '../../hooks/useThemeColors'; import { SettingsAtom, ViewDataSelector } from '../../state'; import { SnippetInputField } from './SnippetInputField'; import { SNIPPET } from '../../../constants/Snippet'; @@ -15,6 +13,7 @@ export interface ISnippetFormProps { snippetKey?: string; snippet: Snippet; selection: string | undefined; + filePath?: string; fieldInfo?: SnippetInfoField[]; mediaData?: any; onInsert?: (mediaData: any) => void; @@ -25,13 +24,12 @@ export interface SnippetFormHandle { } const SnippetForm: React.ForwardRefRenderFunction = ( - { snippetKey, snippet, selection, fieldInfo, mediaData, onInsert }, + { snippetKey, snippet, selection, filePath, fieldInfo, mediaData, onInsert }, ref ) => { const viewData = useRecoilValue(ViewDataSelector); const [fields, setFields] = useState([]); const settings = useRecoilValue(SettingsAtom); - const { getColors } = useThemeColors(); const onTextChange = useCallback( (field: SnippetField, value: string) => { @@ -43,20 +41,19 @@ const SnippetForm: React.ForwardRefRenderFunction { + async (value: SnippetSpecialPlaceholders) => { if (value === 'FM_SELECTED_TEXT') { return selection || ''; } - value = processKnownPlaceholders( + value = await messageHandler.request(DashboardMessage.updateSnippetPlaceholders, { value, - viewData?.data?.fileTitle || '', - settings?.date.format || '' - ); + filePath + }); return value; }, - [selection] + [selection, filePath] ); const insertValueFromMedia = useCallback( @@ -126,7 +123,7 @@ ${snippetBody} } })); - useEffect(() => { + const processFields = useCallback(async () => { // Get all placeholder variables from the snippet const body = typeof snippet.body === 'string' ? snippet.body : snippet.body.join(`\n`); @@ -145,7 +142,7 @@ ${snippetBody} if (idx > -1) { allFields.push({ ...field, - value: insertPlaceholderValues(field.default || '') + value: await insertPlaceholderValues(field.default || '') }); } } @@ -165,15 +162,15 @@ ${snippetBody} } setFields(allFields); + }, [snippet, insertPlaceholderValues, insertValueFromMedia]); + + useEffect(() => { + processFields(); }, [snippet]); return (
-
+      
         {snippetBody}
       
diff --git a/src/helpers/ArticleHelper.ts b/src/helpers/ArticleHelper.ts index 1e3d65ac..03c8de9d 100644 --- a/src/helpers/ArticleHelper.ts +++ b/src/helpers/ArticleHelper.ts @@ -23,7 +23,17 @@ import { } from '../constants'; import { DumpOptions } from 'js-yaml'; import { FrontMatterParser, ParsedFrontMatter } from '../parsers'; -import { ContentType, Extension, Logger, Settings, SlugHelper, isValidFile, parseWinPath } from '.'; +import { + ContentType, + Extension, + Logger, + Settings, + SlugHelper, + isValidFile, + parseWinPath, + processArticlePlaceholdersFromPath, + processTimePlaceholders +} from '.'; import { format, parse } from 'date-fns'; import { Notifications } from './Notifications'; import { Article } from '../commands'; @@ -37,7 +47,6 @@ import { DEFAULT_FILE_TYPES } from '../constants/DefaultFileTypes'; import { fromMarkdown } from 'mdast-util-from-markdown'; import { Link, Parent } from 'mdast-util-from-markdown/lib'; import { Content } from 'mdast'; -import { processKnownPlaceholders } from './PlaceholderHelper'; import { CustomScript } from './CustomScript'; import { Folders } from '../commands/Folders'; import { existsAsync, readFileAsync } from '../utils'; @@ -57,6 +66,19 @@ export class ArticleHelper { return ArticleHelper.getFrontMatterFromDocument(editor.document); } + /** + * Retrieves the front matter from the current active document. + * @returns The front matter object if found, otherwise undefined. + */ + public static getFrontMatterFromCurrentDocument() { + const editor = vscode.window.activeTextEditor; + if (!editor) { + return; + } + + return ArticleHelper.getFrontMatterFromDocument(editor.document); + } + /** * Get the contents of the specified document * @@ -524,7 +546,12 @@ export class ArticleHelper { * @param title * @returns */ - public static async updatePlaceholders(data: any, title: string, filePath: string) { + public static async updatePlaceholders( + data: any, + title: string, + filePath: string, + slugTemplate?: string + ) { const dateFormat = Settings.get(SETTING_DATE_FORMAT) as string; const fmData = Object.assign({}, data); @@ -536,10 +563,11 @@ export class ArticleHelper { } if (fieldName === 'slug' && (fieldValue === null || fieldValue === '')) { - fmData[fieldName] = SlugHelper.createSlug(title); + fmData[fieldName] = SlugHelper.createSlug(title, fmData, slugTemplate); } - fmData[fieldName] = processKnownPlaceholders(fmData[fieldName], title, dateFormat); + fmData[fieldName] = await processArticlePlaceholdersFromPath(fmData[fieldName], filePath); + fmData[fieldName] = processTimePlaceholders(fmData[fieldName], dateFormat); fmData[fieldName] = await this.processCustomPlaceholders(fmData[fieldName], title, filePath); } @@ -597,7 +625,11 @@ export class ArticleHelper { } const regex = new RegExp(`{{${placeholder.id}}}`, 'g'); - const updatedValue = processKnownPlaceholders(placeHolderValue, title, dateFormat); + let updatedValue = filePath + ? await processArticlePlaceholdersFromPath(placeHolderValue, filePath) + : placeHolderValue; + + updatedValue = processTimePlaceholders(updatedValue, dateFormat); if (value === `{{${placeholder.id}}}`) { value = updatedValue; diff --git a/src/helpers/ContentType.ts b/src/helpers/ContentType.ts index f187fbcc..98f4abb4 100644 --- a/src/helpers/ContentType.ts +++ b/src/helpers/ContentType.ts @@ -1,6 +1,13 @@ import { ModeListener } from './../listeners/general/ModeListener'; import { PagesListener } from './../listeners/dashboard'; -import { ArticleHelper, CustomScript, Logger, Settings } from '.'; +import { + ArticleHelper, + CustomScript, + Logger, + Settings, + processArticlePlaceholdersFromData, + processTimePlaceholders +} from '.'; import { DefaultFieldValues, EXTENSION_NAME, @@ -26,7 +33,6 @@ import { Questions } from './Questions'; import { Notifications } from './Notifications'; import { DEFAULT_CONTENT_TYPE_NAME } from '../constants/ContentType'; import { Telemetry } from './Telemetry'; -import { processKnownPlaceholders } from './PlaceholderHelper'; import { basename } from 'path'; import { ParsedFrontMatter } from '../parsers'; import { encodeEmoji, existsAsync, fieldWhenClause, writeFileAsync } from '../utils'; @@ -927,7 +933,8 @@ export class ContentType { titleValue, templateData?.data || {}, newFilePath, - !!contentType.clearEmpty + !!contentType.clearEmpty, + contentType ); const article: ParsedFrontMatter = { @@ -982,6 +989,7 @@ export class ContentType { data: any, filePath: string, clearEmpty: boolean, + contentType: IContentType, isRoot: boolean = true ): Promise { if (obj.fields) { @@ -995,9 +1003,9 @@ export class ContentType { if (field.name === 'title') { if (field.default) { - data[field.name] = processKnownPlaceholders( - field.default, - titleValue, + data[field.name] = processArticlePlaceholdersFromData(field.default, data, contentType); + data[field.name] = processTimePlaceholders( + data[field.name], field.dateFormat || dateFormat ); data[field.name] = await ArticleHelper.processCustomPlaceholders( @@ -1018,6 +1026,7 @@ export class ContentType { {}, filePath, clearEmpty, + contentType, false ); @@ -1028,9 +1037,13 @@ export class ContentType { const defaultValue = field.default; if (typeof defaultValue === 'string') { - data[field.name] = processKnownPlaceholders( + data[field.name] = processArticlePlaceholdersFromData( defaultValue, - titleValue, + data, + contentType + ); + data[field.name] = processTimePlaceholders( + data[field.name], field.dateFormat || dateFormat ); data[field.name] = await ArticleHelper.processCustomPlaceholders( diff --git a/src/helpers/SlugHelper.ts b/src/helpers/SlugHelper.ts index de3b7177..6cf4ac36 100644 --- a/src/helpers/SlugHelper.ts +++ b/src/helpers/SlugHelper.ts @@ -1,4 +1,6 @@ -import { stopWords, charMap } from '../constants'; +import { Settings } from '.'; +import { stopWords, charMap, SETTING_DATE_FORMAT, SETTING_SLUG_TEMPLATE } from '../constants'; +import { processTimePlaceholders, processFmPlaceholders } from '.'; export class SlugHelper { /** @@ -6,13 +8,41 @@ export class SlugHelper { * * @param articleTitle */ - public static createSlug(articleTitle: string): string | null { + public static createSlug( + articleTitle: string, + articleData: { [key: string]: any }, + slugTemplate?: string + ): string | null { if (!articleTitle) { return null; } - // Remove punctuation from input string, and split it into words. - let cleanTitle = this.removePunctuation(articleTitle).trim(); + if (!slugTemplate) { + slugTemplate = Settings.get(SETTING_SLUG_TEMPLATE) || undefined; + } + + if (slugTemplate) { + if (slugTemplate.includes('{{title}}')) { + const regex = new RegExp('{{title}}', 'g'); + slugTemplate = slugTemplate.replace(regex, SlugHelper.slugify(articleTitle)); + } + + const dateFormat = Settings.get(SETTING_DATE_FORMAT) as string; + articleTitle = processTimePlaceholders(slugTemplate, dateFormat); + articleTitle = processFmPlaceholders(articleTitle, articleData); + return articleTitle; + } + + return SlugHelper.slugify(articleTitle); + } + + /** + * Converts a title into a slug by removing punctuation, stop words, and replacing characters. + * @param title - The title to be slugified. + * @returns The slugified version of the title. + */ + public static slugify(title: string): string { + let cleanTitle = this.removePunctuation(title).trim(); if (cleanTitle) { cleanTitle = cleanTitle.toLowerCase(); // Split into words @@ -23,8 +53,7 @@ export class SlugHelper { cleanTitle = this.replaceCharacters(cleanTitle); return cleanTitle; } - - return null; + return ''; } /** diff --git a/src/helpers/index.ts b/src/helpers/index.ts index b2fc5964..7d58a086 100644 --- a/src/helpers/index.ts +++ b/src/helpers/index.ts @@ -15,7 +15,6 @@ export * from './MediaHelpers'; export * from './MediaLibrary'; export * from './Notifications'; export * from './PanelSettings'; -export * from './PlaceholderHelper'; export * from './Questions'; export * from './Sanitize'; export * from './SeoHelper'; @@ -32,5 +31,7 @@ export * from './getTaxonomyField'; export * from './isValidFile'; export * from './openFileInEditor'; export * from './parseWinPath'; +export * from './processArticlePlaceholders'; export * from './processFmPlaceholders'; export * from './processPathPlaceholders'; +export * from './processTimePlaceholders'; diff --git a/src/helpers/processArticlePlaceholders.ts b/src/helpers/processArticlePlaceholders.ts new file mode 100644 index 00000000..30709564 --- /dev/null +++ b/src/helpers/processArticlePlaceholders.ts @@ -0,0 +1,53 @@ +import { ContentType } from '../models'; +import { ArticleHelper } from './ArticleHelper'; +import { SlugHelper } from './SlugHelper'; + +export const processArticlePlaceholdersFromData = ( + value: string, + data: { [key: string]: any }, + contentType: ContentType +): string => { + if (value.includes('{{title}}') && data.title) { + const regex = new RegExp('{{title}}', 'g'); + value = value.replace(regex, data.title || ''); + } + + if (value.includes('{{slug}}')) { + const regex = new RegExp('{{slug}}', 'g'); + value = value.replace( + regex, + SlugHelper.createSlug(data.title || '', data, contentType.slugTemplate) || '' + ); + } + + return value; +}; + +export const processArticlePlaceholdersFromPath = async ( + value: string, + filePath: string +): Promise => { + const article = await ArticleHelper.getFrontMatterByPath(filePath); + if (!article) { + return value; + } + + if (value.includes('{{title}}')) { + const regex = new RegExp('{{title}}', 'g'); + value = value.replace(regex, article.data.title || ''); + } + + if (value.includes('{{slug}}') && filePath) { + const contentType = article ? ArticleHelper.getContentType(article) : undefined; + if (contentType) { + const regex = new RegExp('{{slug}}', 'g'); + value = value.replace( + regex, + SlugHelper.createSlug(article.data.title || '', article.data, contentType.slugTemplate) || + '' + ); + } + } + + return value; +}; diff --git a/src/helpers/processFmPlaceholders.ts b/src/helpers/processFmPlaceholders.ts index e1133496..398c7120 100644 --- a/src/helpers/processFmPlaceholders.ts +++ b/src/helpers/processFmPlaceholders.ts @@ -1,6 +1,6 @@ import { format } from 'date-fns'; -export const processFmPlaceholders = (value: string, fmData: any) => { +export const processFmPlaceholders = (value: string, fmData: { [key: string]: any }) => { // Example: {{fm.date}} or {{fm.date | dateFormat 'DD.MM.YYYY'}} if (value && value.includes('{{fm.')) { const regex = /{{fm.[^}]*}}/g; diff --git a/src/helpers/PlaceholderHelper.ts b/src/helpers/processTimePlaceholders.ts similarity index 76% rename from src/helpers/PlaceholderHelper.ts rename to src/helpers/processTimePlaceholders.ts index 59626dc8..33ca60f7 100644 --- a/src/helpers/PlaceholderHelper.ts +++ b/src/helpers/processTimePlaceholders.ts @@ -1,29 +1,14 @@ import { format } from 'date-fns'; import { DateHelper } from './DateHelper'; -import { SlugHelper } from './SlugHelper'; /** - * Replace the known placeholders + * Replace the time placeholders * @param value * @param title * @returns */ -export const processKnownPlaceholders = ( - value: string, - title: string | undefined, - dateFormat: string -) => { +export const processTimePlaceholders = (value: string, dateFormat?: string) => { if (value && typeof value === 'string') { - if (value.includes('{{title}}')) { - const regex = new RegExp('{{title}}', 'g'); - value = value.replace(regex, title || ''); - } - - if (value.includes('{{slug}}')) { - const regex = new RegExp('{{slug}}', 'g'); - value = value.replace(regex, SlugHelper.createSlug(title || '') || ''); - } - if (value.includes('{{now}}')) { const regex = new RegExp('{{now}}', 'g'); diff --git a/src/listeners/dashboard/BaseListener.ts b/src/listeners/dashboard/BaseListener.ts index 4e9d6682..d8a0a080 100644 --- a/src/listeners/dashboard/BaseListener.ts +++ b/src/listeners/dashboard/BaseListener.ts @@ -1,5 +1,6 @@ import { Dashboard } from '../../commands/Dashboard'; import { DashboardCommand } from '../../dashboardWebView/DashboardCommand'; +import { DashboardMessage } from '../../dashboardWebView/DashboardMessage'; import { Logger } from '../../helpers/Logger'; import { PostMessageData } from '../../models'; @@ -20,7 +21,11 @@ export abstract class BaseListener { }); } - public static sendRequest(command: DashboardCommand, requestId: string, payload: any) { + public static sendRequest( + command: DashboardCommand | DashboardMessage, + requestId: string, + payload: any + ) { Dashboard.postWebviewMessage({ command, requestId, diff --git a/src/listeners/dashboard/SnippetListener.ts b/src/listeners/dashboard/SnippetListener.ts index 56572285..744dc631 100644 --- a/src/listeners/dashboard/SnippetListener.ts +++ b/src/listeners/dashboard/SnippetListener.ts @@ -1,9 +1,16 @@ import { EditorHelper } from '@estruyf/vscode'; import { window, Range, Position } from 'vscode'; import { Dashboard } from '../../commands/Dashboard'; -import { SETTING_CONTENT_SNIPPETS, TelemetryEvent } from '../../constants'; +import { SETTING_CONTENT_SNIPPETS, SETTING_DATE_FORMAT, TelemetryEvent } from '../../constants'; import { DashboardMessage } from '../../dashboardWebView/DashboardMessage'; -import { Notifications, Settings, Telemetry } from '../../helpers'; +import { + ArticleHelper, + Notifications, + Settings, + Telemetry, + processArticlePlaceholdersFromPath, + processTimePlaceholders +} from '../../helpers'; import { PostMessageData, Snippets } from '../../models'; import { BaseListener } from './BaseListener'; import { SettingsListener } from './SettingsListener'; @@ -25,6 +32,9 @@ export class SnippetListener extends BaseListener { Telemetry.send(TelemetryEvent.insertContentSnippet); this.insertSnippet(msg.payload); break; + case DashboardMessage.updateSnippetPlaceholders: + this.updateSnippetPlaceholders(msg.command, msg.payload, msg.requestId); + break; } } @@ -124,4 +134,25 @@ export class SnippetListener extends BaseListener { }); } } + + private static async updateSnippetPlaceholders( + command: DashboardMessage, + data: { value: string; filePath: string }, + requestId?: string + ) { + if (!data.value || !command || !requestId) { + return; + } + + let value = data.value; + + if (data.filePath) { + value = await processArticlePlaceholdersFromPath(data.value, data.filePath); + } + + const dateFormat = Settings.get(SETTING_DATE_FORMAT) as string; + value = processTimePlaceholders(value, dateFormat); + + this.sendRequest(command, requestId, value); + } } diff --git a/src/listeners/general/GitListener.ts b/src/listeners/general/GitListener.ts index e4e76537..0defcfdb 100644 --- a/src/listeners/general/GitListener.ts +++ b/src/listeners/general/GitListener.ts @@ -12,7 +12,7 @@ import { Extension, Logger, Notifications, - processKnownPlaceholders, + processTimePlaceholders, Telemetry } from '../../helpers'; import { GeneralCommands } from './../../constants/GeneralCommands'; @@ -154,7 +154,7 @@ export class GitListener { if (commitMsg) { const dateFormat = Settings.get(SETTING_DATE_FORMAT) as string; - commitMsg = processKnownPlaceholders(commitMsg, undefined, dateFormat); + commitMsg = processTimePlaceholders(commitMsg, dateFormat); commitMsg = await ArticleHelper.processCustomPlaceholders(commitMsg, undefined, undefined); } diff --git a/src/listeners/panel/ArticleListener.ts b/src/listeners/panel/ArticleListener.ts index 4c6cbd47..c49a1694 100644 --- a/src/listeners/panel/ArticleListener.ts +++ b/src/listeners/panel/ArticleListener.ts @@ -1,4 +1,5 @@ import { Article } from '../../commands'; +import { ArticleHelper } from '../../helpers'; import { PostMessageData } from '../../models'; import { Command } from '../../panelWebView/Command'; import { CommandToCode } from '../../panelWebView/CommandToCode'; @@ -32,8 +33,9 @@ export class ArticleListener extends BaseListener { * Generate a slug * @param title */ - private static generateSlug(title: string) { - const slug = Article.generateSlug(title); + private static generateSlug({ title, slugTemplate }: { title: string; slugTemplate?: string }) { + const article = ArticleHelper.getFrontMatterFromCurrentDocument(); + const slug = Article.generateSlug(title, article, slugTemplate); if (slug) { this.sendMsg(Command.updatedSlug, slug); } diff --git a/src/listeners/panel/DataListener.ts b/src/listeners/panel/DataListener.ts index 7388a3e4..c9205db2 100644 --- a/src/listeners/panel/DataListener.ts +++ b/src/listeners/panel/DataListener.ts @@ -6,7 +6,16 @@ import { Command } from '../../panelWebView/Command'; import { CommandToCode } from '../../panelWebView/CommandToCode'; import { BaseListener } from './BaseListener'; import { authentication, commands, window } from 'vscode'; -import { ArticleHelper, ContentType, Extension, Logger, Settings } from '../../helpers'; +import { + ArticleHelper, + Extension, + Logger, + Settings, + ContentType, + processArticlePlaceholdersFromData, + processTimePlaceholders, + processFmPlaceholders +} from '../../helpers'; import { COMMAND_NAME, DefaultFields, @@ -20,8 +29,7 @@ import { } from '../../constants'; import { Article, Preview } from '../../commands'; import { ParsedFrontMatter } from '../../parsers'; -import { processKnownPlaceholders } from '../../helpers/PlaceholderHelper'; -import { Field, Mode, PostMessageData } from '../../models'; +import { Field, Mode, PostMessageData, ContentType as IContentType } from '../../models'; import { encodeEmoji, fieldWhenClause } from '../../utils'; import { PanelProvider } from '../../panelWebView/PanelProvider'; import { MessageHandlerData } from '@estruyf/vscode'; @@ -66,7 +74,11 @@ export class DataListener extends BaseListener { this.isServerStarted(msg.command, msg?.requestId); break; case CommandToCode.updatePlaceholder: - this.updatePlaceholder(msg?.payload?.field, msg?.payload?.value, msg?.payload?.title); + this.updatePlaceholder( + msg.command, + msg.payload as { field: string; value: string; data: { [key: string]: any } }, + msg.requestId + ); break; case CommandToCode.generateContentType: commands.executeCommand(COMMAND_NAME.generateContentType); @@ -597,18 +609,37 @@ export class DataListener extends BaseListener { * @param value * @param title */ - private static async updatePlaceholder(field: string, value: string, title: string) { - if (field && value) { + private static async updatePlaceholder( + command: CommandToCode, + articleData: { + field: string; + value: string; + data: { [key: string]: any }; + contentType?: IContentType; + }, + requestId?: string + ) { + if (!command || !requestId || !articleData) { + return; + } + + let { field, value, data, contentType } = articleData; + + value = value || ''; + if (field) { const crntFile = window.activeTextEditor?.document; const dateFormat = Settings.get(SETTING_DATE_FORMAT) as string; - value = processKnownPlaceholders(value, title || '', dateFormat); + value = + data && contentType ? processArticlePlaceholdersFromData(value, data, contentType) : value; + value = processTimePlaceholders(value, dateFormat); + value = processFmPlaceholders(value, data); value = await ArticleHelper.processCustomPlaceholders( value, - title || '', + data.title || '', crntFile?.uri.fsPath || '' ); } - this.sendMsg(Command.updatePlaceholder, { field, value }); + this.sendRequest(Command.updatePlaceholder, requestId, { field, value }); } } diff --git a/src/models/DashboardData.ts b/src/models/DashboardData.ts index 8be82d4a..ca25175b 100644 --- a/src/models/DashboardData.ts +++ b/src/models/DashboardData.ts @@ -1,6 +1,7 @@ import { Position } from 'vscode'; import { NavigationType } from '../dashboardWebView/models'; import { BlockFieldData } from './BlockFieldData'; +import { ContentType } from '.'; export interface DashboardData { type: NavigationType; @@ -12,6 +13,7 @@ export interface ViewData { fieldName?: string; position?: Position; fileTitle?: string; + contentType?: ContentType; selection?: string; range?: SnippetRange; snippetInfo?: SnippetInfo; diff --git a/src/models/PanelSettings.ts b/src/models/PanelSettings.ts index 7b6bd2c9..7d0f07a9 100644 --- a/src/models/PanelSettings.ts +++ b/src/models/PanelSettings.ts @@ -59,6 +59,7 @@ export interface ContentType { fileType?: 'md' | 'mdx' | string; previewPath?: string | null; + slugTemplate?: string; pageBundle?: boolean; defaultFileName?: string; template?: string; diff --git a/src/panelWebView/components/Fields/SlugField.tsx b/src/panelWebView/components/Fields/SlugField.tsx index 22d39b27..09e96bee 100644 --- a/src/panelWebView/components/Fields/SlugField.tsx +++ b/src/panelWebView/components/Fields/SlugField.tsx @@ -14,6 +14,7 @@ import { LocalizationKey } from '../../../localization'; export interface ISlugFieldProps extends BaseFieldProps { titleValue: string | null; editable?: boolean; + slugTemplate?: string; onChange: (txtValue: string) => void; } @@ -23,6 +24,7 @@ export const SlugField: React.FunctionComponent = ({ editable, value, titleValue, + slugTemplate, onChange, required }: React.PropsWithChildren) => { @@ -60,9 +62,12 @@ export const SlugField: React.FunctionComponent = ({ useEffect(() => { if (titleValue) { - Messenger.send(CommandToCode.generateSlug, titleValue); + Messenger.send(CommandToCode.generateSlug, { + title: titleValue, + slugTemplate + }); } - }, [titleValue]); + }, [titleValue, slugTemplate]); useEffect(() => { Messenger.listen(messageListener); diff --git a/src/panelWebView/components/Fields/WrapperField.tsx b/src/panelWebView/components/Fields/WrapperField.tsx index 43d30226..7df0723c 100644 --- a/src/panelWebView/components/Fields/WrapperField.tsx +++ b/src/panelWebView/components/Fields/WrapperField.tsx @@ -1,8 +1,8 @@ -import { Messenger } from '@estruyf/vscode/dist/client'; +import { messageHandler } from '@estruyf/vscode/dist/client'; import * as React from 'react'; import { useCallback, useEffect, useState } from 'react'; import { DateHelper } from '../../../helpers/DateHelper'; -import { BlockFieldData, CustomPanelViewResult, Field, PanelSettings } from '../../../models'; +import { BlockFieldData, ContentType, CustomPanelViewResult, Field, PanelSettings } from '../../../models'; import { Command } from '../../Command'; import { CommandToCode } from '../../CommandToCode'; import { TagType } from '../../TagType'; @@ -41,6 +41,7 @@ export interface IWrapperFieldProps { parentFields: string[]; metadata: IMetadata; settings: PanelSettings; + contentType: ContentType | null; blockData: BlockFieldData | undefined; focusElm: TagType | null; parentBlock: string | null | undefined; @@ -63,6 +64,7 @@ export const WrapperField: React.FunctionComponent = ({ parentFields, metadata, settings, + contentType, blockData, focusElm, parentBlock, @@ -76,21 +78,6 @@ export const WrapperField: React.FunctionComponent = ({ html: (data: any, onChange: (value: any) => void) => Promise; }[]>([]); - const listener = useCallback( - (event: any) => { - const message = event.data; - - if (message.command === Command.updatePlaceholder) { - const data = message.payload; - if (data.field === field.name) { - setFieldValue(data.value); - onSendUpdate(field.name, data.value, parentFields); - } - } - }, - [field] - ); - const getDate = (date: string | Date): Date | null => { const parsedDate = DateHelper.tryParse(date, settings?.date?.format); return parsedDate || (date as Date | null); @@ -126,11 +113,18 @@ export const WrapperField: React.FunctionComponent = ({ // Check if the field value contains a placeholder if (value && typeof value === 'string' && value.includes(`{{`) && value.includes(`}}`)) { - window.addEventListener('message', listener); - Messenger.send(CommandToCode.updatePlaceholder, { + messageHandler.request<{ field: string; value: any; }>(CommandToCode.updatePlaceholder, { field: field.name, - title: metadata['title'], - value + value, + data: metadata, + contentType + }).then((data) => { + if (data.field === field.name) { + setFieldValue(data.value); + onSendUpdate(field.name, data.value, parentFields); + } + }).catch((err) => { + console.error(err); }); } else { // Did not contain a placeholder, so value can be set @@ -138,10 +132,6 @@ export const WrapperField: React.FunctionComponent = ({ setFieldValue(value || null); } } - - return () => { - window.removeEventListener('message', listener); - }; }, [field, parent]); useEffect(() => { @@ -509,6 +499,7 @@ export const WrapperField: React.FunctionComponent = ({ description={field.description} titleValue={metadata.title as string} value={fieldValue} + slugTemplate={contentType?.slugTemplate} required={!!field.required} editable={field.editable} onChange={onFieldChange} diff --git a/src/panelWebView/components/Metadata.tsx b/src/panelWebView/components/Metadata.tsx index 0f029611..fd220f09 100644 --- a/src/panelWebView/components/Metadata.tsx +++ b/src/panelWebView/components/Metadata.tsx @@ -66,6 +66,7 @@ const Metadata: React.FunctionComponent = ({ parent={parent} parentFields={parentFields} metadata={metadata} + contentType={contentType} settings={settings} blockData={blockData} parentBlock={parentBlock} diff --git a/src/services/PagesParser.ts b/src/services/PagesParser.ts index 57ef1552..06e95227 100644 --- a/src/services/PagesParser.ts +++ b/src/services/PagesParser.ts @@ -233,7 +233,10 @@ export class PagesParser { // Make sure these are always set title: escapedTitle, description: escapedDescription, - slug: article?.data.slug || Article.generateSlug(escapedTitle)?.slugWithPrefixAndSuffix, + slug: + article?.data.slug || + Article.generateSlug(escapedTitle, article, contentType.slugTemplate) + ?.slugWithPrefixAndSuffix, date: article?.data[dateField] || '', draft: article?.data.draft }; From d59d9a98d532f0531d769b46b12109c974771faa Mon Sep 17 00:00:00 2001 From: Elio Struyf Date: Tue, 30 Jan 2024 21:45:01 +0100 Subject: [PATCH 2/4] #746 - placeholder support for slug field --- CHANGELOG.md | 1 + src/helpers/ContentType.ts | 8 ++--- src/listeners/panel/ArticleListener.ts | 15 ++++++--- src/listeners/panel/DataListener.ts | 6 +++- src/panelWebView/Command.ts | 1 - .../components/Fields/SlugField.tsx | 32 ++++++------------- .../components/Fields/WrapperField.tsx | 1 - 7 files changed, 30 insertions(+), 34 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ca1d6db..2cc3fa2c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### ✨ New features - [#731](https://github.com/estruyf/vscode-front-matter/issues/731): Added the ability to map/unmap taxonomy to multiple pages at once +- [#746](https://github.com/estruyf/vscode-front-matter/issues/746): Placeholder support added to to the `slug` field ### 🎨 Enhancements diff --git a/src/helpers/ContentType.ts b/src/helpers/ContentType.ts index 98f4abb4..e4affa7f 100644 --- a/src/helpers/ContentType.ts +++ b/src/helpers/ContentType.ts @@ -305,17 +305,17 @@ export class ContentType { Telemetry.send(TelemetryEvent.addMissingFields); - const content = ArticleHelper.getCurrent(); + const article = ArticleHelper.getCurrent(); - if (!content || !content.data) { + if (!article || !article.data) { Notifications.warning( l10n.t(LocalizationKey.helpersContentTypeAddMissingFieldsNoFrontMatterWarning) ); return; } - const contentType = ArticleHelper.getContentType(content); - const updatedFields = ContentType.generateFields(content.data, contentType.fields); + const contentType = ArticleHelper.getContentType(article); + const updatedFields = ContentType.generateFields(article.data, contentType.fields); const contentTypes = ContentType.getAll() || []; const index = contentTypes.findIndex((ct) => ct.name === contentType.name); diff --git a/src/listeners/panel/ArticleListener.ts b/src/listeners/panel/ArticleListener.ts index c49a1694..3bd09dca 100644 --- a/src/listeners/panel/ArticleListener.ts +++ b/src/listeners/panel/ArticleListener.ts @@ -1,7 +1,6 @@ import { Article } from '../../commands'; import { ArticleHelper } from '../../helpers'; import { PostMessageData } from '../../models'; -import { Command } from '../../panelWebView/Command'; import { CommandToCode } from '../../panelWebView/CommandToCode'; import { BaseListener } from './BaseListener'; @@ -18,7 +17,7 @@ export class ArticleListener extends BaseListener { Article.updateSlug(); break; case CommandToCode.generateSlug: - this.generateSlug(msg.payload); + this.generateSlug(msg.command, msg.payload, msg.requestId); break; case CommandToCode.updateLastMod: Article.setLastModifiedDate(); @@ -33,11 +32,19 @@ export class ArticleListener extends BaseListener { * Generate a slug * @param title */ - private static generateSlug({ title, slugTemplate }: { title: string; slugTemplate?: string }) { + private static generateSlug( + command: CommandToCode, + { title, slugTemplate }: { title: string; slugTemplate?: string }, + requestId?: string + ) { + if (!command || !requestId) { + return; + } + const article = ArticleHelper.getFrontMatterFromCurrentDocument(); const slug = Article.generateSlug(title, article, slugTemplate); if (slug) { - this.sendMsg(Command.updatedSlug, slug); + this.sendRequest(command, requestId, slug); } } } diff --git a/src/listeners/panel/DataListener.ts b/src/listeners/panel/DataListener.ts index c9205db2..8af8a125 100644 --- a/src/listeners/panel/DataListener.ts +++ b/src/listeners/panel/DataListener.ts @@ -223,7 +223,11 @@ export class DataListener extends BaseListener { if (keys.length > 0 && contentTypes && wsFolder) { // Get the current content type - const contentType = ArticleHelper.getContentType(updatedMetadata); + const contentType = ArticleHelper.getContentType({ + content: '', + data: updatedMetadata, + path: filePath + }); let slugField; if (contentType) { ImageHelper.processImageFields(updatedMetadata, contentType.fields); diff --git a/src/panelWebView/Command.ts b/src/panelWebView/Command.ts index 733bb104..4840847b 100644 --- a/src/panelWebView/Command.ts +++ b/src/panelWebView/Command.ts @@ -10,6 +10,5 @@ export enum Command { sendMediaUrl = 'sendMediaUrl', updatePlaceholder = 'updatePlaceholder', dataFileEntries = 'dataFileEntries', - updatedSlug = 'updatedSlug', serverStarted = 'server-started' } diff --git a/src/panelWebView/components/Fields/SlugField.tsx b/src/panelWebView/components/Fields/SlugField.tsx index 09e96bee..2222ff34 100644 --- a/src/panelWebView/components/Fields/SlugField.tsx +++ b/src/panelWebView/components/Fields/SlugField.tsx @@ -1,10 +1,8 @@ -import { Messenger } from '@estruyf/vscode/dist/client'; -import { EventData } from '@estruyf/vscode/dist/models'; +import { Messenger, messageHandler } from '@estruyf/vscode/dist/client'; import { LinkIcon, ArrowPathIcon } from '@heroicons/react/24/outline'; import * as React from 'react'; -import { useCallback, useEffect, useMemo } from 'react'; +import { useEffect, useMemo } from 'react'; import { BaseFieldProps } from '../../../models'; -import { Command } from '../../Command'; import { CommandToCode } from '../../CommandToCode'; import { FieldTitle } from './FieldTitle'; import { FieldMessage } from './FieldMessage'; @@ -40,16 +38,6 @@ export const SlugField: React.FunctionComponent = ({ Messenger.send(CommandToCode.updateSlug); }; - const messageListener = useCallback( - (message: MessageEvent>) => { - const { command, payload } = message.data; - if (command === Command.updatedSlug) { - setSlug(payload?.slugWithPrefixAndSuffix); - } - }, - [text] - ); - const showRequiredState = useMemo(() => { return required && !text; }, [required, text]); @@ -62,21 +50,19 @@ export const SlugField: React.FunctionComponent = ({ useEffect(() => { if (titleValue) { - Messenger.send(CommandToCode.generateSlug, { + messageHandler.request<{ slug: string; slugWithPrefixAndSuffix: string; }>(CommandToCode.generateSlug, { title: titleValue, slugTemplate + }).then((slug) => { + if (slug.slugWithPrefixAndSuffix) { + setSlug(slug.slugWithPrefixAndSuffix); + } + }).catch((_) => { + setSlug(null); }); } }, [titleValue, slugTemplate]); - useEffect(() => { - Messenger.listen(messageListener); - - return () => { - Messenger.unlisten(messageListener); - }; - }, []); - return (
} required={required} /> diff --git a/src/panelWebView/components/Fields/WrapperField.tsx b/src/panelWebView/components/Fields/WrapperField.tsx index 7df0723c..6d3c29d5 100644 --- a/src/panelWebView/components/Fields/WrapperField.tsx +++ b/src/panelWebView/components/Fields/WrapperField.tsx @@ -3,7 +3,6 @@ import * as React from 'react'; import { useCallback, useEffect, useState } from 'react'; import { DateHelper } from '../../../helpers/DateHelper'; import { BlockFieldData, ContentType, CustomPanelViewResult, Field, PanelSettings } from '../../../models'; -import { Command } from '../../Command'; import { CommandToCode } from '../../CommandToCode'; import { TagType } from '../../TagType'; import { DataBlockField } from '../DataBlock'; From accb069babb265300fa86d36e8a263ad5d96385b Mon Sep 17 00:00:00 2001 From: Elio Struyf Date: Mon, 5 Feb 2024 17:11:17 +0100 Subject: [PATCH 3/4] #673 - git enablement --- l10n/bundle.l10n.json | 4 ++ src/constants/GeneralCommands.ts | 1 + .../SettingsView/CommonSettings.tsx | 33 ++++++++++++---- .../SettingsView/SettingsCheckbox.tsx | 35 +++++++++++++++++ .../components/Steps/StepsToGetStarted.tsx | 39 ++++++++++++++++++- src/listeners/dashboard/SettingsListener.ts | 4 +- src/listeners/general/GitListener.ts | 12 ++++++ src/localization/localization.enum.ts | 16 ++++++++ 8 files changed, 133 insertions(+), 11 deletions(-) create mode 100644 src/dashboardWebView/components/SettingsView/SettingsCheckbox.tsx diff --git a/l10n/bundle.l10n.json b/l10n/bundle.l10n.json index 3ee4a2e7..25d3f0ef 100644 --- a/l10n/bundle.l10n.json +++ b/l10n/bundle.l10n.json @@ -50,6 +50,8 @@ "settings.diagnostic": "Diagnostic", "settings.diagnostic.description": "You can run the diagnostics to check the whole Front Matter CMS configuration.", "settings.diagnostic.link": "Run full diagnostics", + "settings.git.enabled": "Git synchronization", + "settings.git.enabled.description": "Enable Git synchronization to easily sync your changes with your repository.", "settings.commonSettings.website.title": "Website and SSG settings", "settings.commonSettings.previewUrl": "Preview URL", @@ -278,6 +280,8 @@ "dashboard.steps.stepsToGetStarted.contentFolders.information.description": "You can also perform this action by right-clicking on the folder in the explorer view, and selecting register folder", "dashboard.steps.stepsToGetStarted.tags.name": "Import all tags and categories (optional)", "dashboard.steps.stepsToGetStarted.tags.description": "Now that Front Matter knows all the content folders. Would you like to import all tags and categories from the available content?", + "dashboard.steps.stepsToGetStarted.git.name": "Do you want to enable Git synchronization?", + "dashboard.steps.stepsToGetStarted.git.description": "Enable Git synchronization to eaily sync your changes with your repository.", "dashboard.steps.stepsToGetStarted.showDashboard.name": "Show the dashboard", "dashboard.steps.stepsToGetStarted.showDashboard.description": "Once all actions are completed, the dashboard can be loaded.", "dashboard.steps.stepsToGetStarted.template.name": "Use a configuration template", diff --git a/src/constants/GeneralCommands.ts b/src/constants/GeneralCommands.ts index ccb77129..bd641174 100644 --- a/src/constants/GeneralCommands.ts +++ b/src/constants/GeneralCommands.ts @@ -8,6 +8,7 @@ export const GeneralCommands = { toVSCode: { openLink: 'openLink', gitSync: 'gitSync', + gitIsRepo: 'gitIsRepo', getLocalization: 'getLocalization', openOnWebsite: 'openOnWebsite' } diff --git a/src/dashboardWebView/components/SettingsView/CommonSettings.tsx b/src/dashboardWebView/components/SettingsView/CommonSettings.tsx index 33c2ce91..75acd3a9 100644 --- a/src/dashboardWebView/components/SettingsView/CommonSettings.tsx +++ b/src/dashboardWebView/components/SettingsView/CommonSettings.tsx @@ -6,15 +6,16 @@ import { useRecoilValue } from 'recoil'; import { SettingsSelector } from '../../state'; import { SettingsInput } from './SettingsInput'; import { VSCodeButton } from '@vscode/webview-ui-toolkit/react'; -import { FrameworkDetectors, SETTING_FRAMEWORK_START, SETTING_PREVIEW_HOST, SETTING_WEBSITE_URL } from '../../../constants'; +import { FrameworkDetectors, SETTING_FRAMEWORK_START, SETTING_GIT_ENABLED, SETTING_PREVIEW_HOST, SETTING_WEBSITE_URL } from '../../../constants'; import { messageHandler } from '@estruyf/vscode/dist/client'; import { DashboardMessage } from '../../DashboardMessage'; +import { SettingsCheckbox } from './SettingsCheckbox'; export interface ICommonSettingsProps { } interface Config { name: string; - value: string; + value: string | boolean; } export const CommonSettings: React.FunctionComponent = (props: React.PropsWithChildren) => { @@ -22,7 +23,7 @@ export const CommonSettings: React.FunctionComponent = (pr const [config, setConfig] = React.useState([]); const [updated, setUpdated] = React.useState(false); - const onSettingChange = React.useCallback((name: string, value: string) => { + const onSettingChange = React.useCallback((name: string, value: string | boolean) => { setConfig((prev) => { const setting = prev.find((c) => c.name === name); if (setting) { @@ -39,7 +40,12 @@ export const CommonSettings: React.FunctionComponent = (pr }, [config]); const retrieveSettings = () => { - messageHandler.request(DashboardMessage.getSettings, [SETTING_PREVIEW_HOST, SETTING_WEBSITE_URL, SETTING_FRAMEWORK_START]).then((config) => { + messageHandler.request(DashboardMessage.getSettings, [ + SETTING_PREVIEW_HOST, + SETTING_WEBSITE_URL, + SETTING_FRAMEWORK_START, + SETTING_GIT_ENABLED + ]).then((config) => { setConfig(config); setUpdated(false); }); @@ -65,6 +71,19 @@ export const CommonSettings: React.FunctionComponent = (pr
+
+

{l10n.t(LocalizationKey.settingsGitEnabled)}

+ +
+ c.name === SETTING_GIT_ENABLED)?.value || false) as boolean} + onChange={onSettingChange} + /> +
+
+

{l10n.t(LocalizationKey.settingsCommonSettingsWebsiteTitle)}

@@ -72,21 +91,21 @@ export const CommonSettings: React.FunctionComponent = (pr c.name === SETTING_PREVIEW_HOST)?.value || ""} + value={(config.find((c) => c.name === SETTING_PREVIEW_HOST)?.value || "") as string} onChange={onSettingChange} /> c.name === SETTING_WEBSITE_URL)?.value || ""} + value={(config.find((c) => c.name === SETTING_WEBSITE_URL)?.value || "") as string} onChange={onSettingChange} /> c.name === SETTING_FRAMEWORK_START)?.value || ""} + value={(config.find((c) => c.name === SETTING_FRAMEWORK_START)?.value || "") as string} onChange={onSettingChange} fallback={FrameworkDetectors.find((f) => f.framework.name === settings?.crntFramework)?.commands.start || ""} /> diff --git a/src/dashboardWebView/components/SettingsView/SettingsCheckbox.tsx b/src/dashboardWebView/components/SettingsView/SettingsCheckbox.tsx new file mode 100644 index 00000000..71f2ee67 --- /dev/null +++ b/src/dashboardWebView/components/SettingsView/SettingsCheckbox.tsx @@ -0,0 +1,35 @@ +import { VSCodeCheckbox } from '@vscode/webview-ui-toolkit/react'; +import * as React from 'react'; + +export interface ISettingsCheckboxProps { + label: string; + name: string; + value: boolean; + onChange: (key: string, value: boolean) => void; +} + +export const SettingsCheckbox: React.FunctionComponent = ({ + label, + name, + value, + onChange +}: React.PropsWithChildren) => { + const [isEnabled, setIsEnabled] = React.useState(false); + + const updateValue = (value: boolean) => { + setIsEnabled(value); + onChange(name, value); + } + + React.useEffect(() => { + setIsEnabled(value); + }, [value]); + + return ( + ) => updateValue(e.target.checked)} + checked={isEnabled}> + {label} + + ); +}; \ No newline at end of file diff --git a/src/dashboardWebView/components/Steps/StepsToGetStarted.tsx b/src/dashboardWebView/components/Steps/StepsToGetStarted.tsx index 5b873d64..2e1c3fc5 100644 --- a/src/dashboardWebView/components/Steps/StepsToGetStarted.tsx +++ b/src/dashboardWebView/components/Steps/StepsToGetStarted.tsx @@ -13,11 +13,12 @@ import { FrameworkDetectors } from '../../../constants/FrameworkDetectors'; import * as l10n from '@vscode/l10n'; import { LocalizationKey } from '../../../localization'; import { SelectItem } from './SelectItem'; -import { Templates } from '../../../constants'; +import { GeneralCommands, SETTING_GIT_ENABLED, Templates } from '../../../constants'; import { TemplateItem } from './TemplateItem'; import { Spinner } from '../Common/Spinner'; import { AstroContentTypes } from '../Configuration/Astro/AstroContentTypes'; import { ContentFolders } from '../Configuration/Common/ContentFolders'; +import { VSCodeCheckbox } from '@vscode/webview-ui-toolkit/react'; export interface IStepsToGetStartedProps { settings: Settings; @@ -29,6 +30,8 @@ export const StepsToGetStarted: React.FunctionComponent const [loading, setLoading] = useState(false); const [framework, setFramework] = useState(null); const [taxImported, setTaxImported] = useState(false); + const [isGitEnabled, setIsGitEnabled] = useState(false); + const [isGitRepo, setIsGitRepo] = useState(false); const [templates, setTemplates] = useState([]); const [astroCollectionsStatus, setAstroCollectionsStatus] = useState(Status.Optional); @@ -73,6 +76,15 @@ export const StepsToGetStarted: React.FunctionComponent setTaxImported(true); }; + const updateSetting = (name: string, value: any) => { + setIsGitEnabled(value); + Messenger.send(DashboardMessage.updateSetting, { + name, + value, + global: true + }); + } + const crntTemplates = useMemo(() => { if (!templates || templates.length === 0 || !settings.crntFramework) { return []; @@ -230,6 +242,21 @@ export const StepsToGetStarted: React.FunctionComponent show: settings.crntFramework === 'astro' || framework === 'astro', status: settings.initialized && settings.staticFolder && settings.staticFolder !== "/" ? Status.Completed : Status.NotStarted, }, + { + id: `welcome-git`, + name: l10n.t(LocalizationKey.dashboardStepsStepsToGetStartedGitName), + description: ( +
+ ) => updateSetting(SETTING_GIT_ENABLED, e.target.checked)} + checked={isGitEnabled}> + {l10n.t(LocalizationKey.dashboardStepsStepsToGetStartedGitDescription)} + +
+ ), + show: isGitRepo, + status: settings.git.actions ? Status.Completed : Status.NotStarted + }, { id: `welcome-import`, name: l10n.t(LocalizationKey.dashboardStepsStepsToGetStartedTagsName), @@ -257,7 +284,7 @@ export const StepsToGetStarted: React.FunctionComponent : undefined } ] - ), [settings, framework, taxImported, templates, astroCollectionsStatus]); + ), [settings, framework, taxImported, templates, astroCollectionsStatus, isGitRepo]); React.useEffect(() => { if (settings.crntFramework || settings.framework?.name) { @@ -265,6 +292,14 @@ export const StepsToGetStarted: React.FunctionComponent } }, [settings.crntFramework, settings.framework]); + React.useEffect(() => { + messageHandler.request(GeneralCommands.toVSCode.gitIsRepo).then((result) => { + setIsGitRepo(result); + }); + + setIsGitEnabled(settings.git.actions); + }, [settings.git.actions]); + React.useEffect(() => { const fetchTemplates = async () => { try { diff --git a/src/listeners/dashboard/SettingsListener.ts b/src/listeners/dashboard/SettingsListener.ts index 74492212..030f1102 100644 --- a/src/listeners/dashboard/SettingsListener.ts +++ b/src/listeners/dashboard/SettingsListener.ts @@ -127,9 +127,9 @@ export class SettingsListener extends BaseListener { * Update a setting from the dashboard * @param data */ - private static async update(data: { name: string; value: any }) { + private static async update(data: { name: string; value: any; global?: boolean }) { if (data.name) { - await Settings.update(data.name, data.value); + await Settings.update(data.name, data.value, data.global); this.getSettings(true); } } diff --git a/src/listeners/general/GitListener.ts b/src/listeners/general/GitListener.ts index e4e76537..32813c07 100644 --- a/src/listeners/general/GitListener.ts +++ b/src/listeners/general/GitListener.ts @@ -65,12 +65,24 @@ export class GitListener { */ public static process(msg: PostMessageData) { switch (msg.command) { + case GeneralCommands.toVSCode.gitIsRepo: + this.checkIsGitRepo(msg.command, msg.requestId); + break; case GeneralCommands.toVSCode.gitSync: this.sync(); break; } } + public static async checkIsGitRepo(command: string, requestId?: string) { + if (!command || !requestId) { + return; + } + + const isRepo = await GitListener.isGitRepository(); + Dashboard.postWebviewMessage({ command: command as any, payload: isRepo, requestId }); + } + /** * Run the sync */ diff --git a/src/localization/localization.enum.ts b/src/localization/localization.enum.ts index 8fac4179..bcc85994 100644 --- a/src/localization/localization.enum.ts +++ b/src/localization/localization.enum.ts @@ -191,6 +191,14 @@ export enum LocalizationKey { * Run full diagnostics */ settingsDiagnosticLink = 'settings.diagnostic.link', + /** + * Git synchronization + */ + settingsGitEnabled = 'settings.git.enabled', + /** + * Enable Git synchronization to easily sync your changes with your repository. + */ + settingsGitEnabledDescription = 'settings.git.enabled.description', /** * Website and SSG settings */ @@ -919,6 +927,14 @@ export enum LocalizationKey { * Now that Front Matter knows all the content folders. Would you like to import all tags and categories from the available content? */ dashboardStepsStepsToGetStartedTagsDescription = 'dashboard.steps.stepsToGetStarted.tags.description', + /** + * Do you want to enable Git synchronization? + */ + dashboardStepsStepsToGetStartedGitName = 'dashboard.steps.stepsToGetStarted.git.name', + /** + * Enable Git synchronization to eaily sync your changes with your repository. + */ + dashboardStepsStepsToGetStartedGitDescription = 'dashboard.steps.stepsToGetStarted.git.description', /** * Show the dashboard */ From 62b9f124943bc1bc4d0ce5a251b6079d832fca2e Mon Sep 17 00:00:00 2001 From: Elio Struyf Date: Mon, 5 Feb 2024 19:28:16 +0100 Subject: [PATCH 4/4] #673 - git settings --- CHANGELOG.md | 1 + l10n/bundle.l10n.json | 7 ++-- src/constants/Git.ts | 3 ++ src/constants/Links.ts | 2 ++ src/constants/index.ts | 1 + .../SettingsView/CommonSettings.tsx | 33 ++++++++++++++++--- .../components/SettingsView/SettingsInput.tsx | 3 ++ src/listeners/general/GitListener.ts | 25 +++++++------- src/localization/localization.enum.ts | 16 +++++++-- 9 files changed, 70 insertions(+), 21 deletions(-) create mode 100644 src/constants/Git.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index dd6e33ab..49ad50de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ ### 🎨 Enhancements +- [#673](https://github.com/estruyf/vscode-front-matter/pull/673): Added git settings to the welcome view and settings view - [#727](https://github.com/estruyf/vscode-front-matter/pull/727): Updated Japanese translations thanks to [mayumihara](https://github.com/mayumih387) - [#737](https://github.com/estruyf/vscode-front-matter/issues/737): Optimize the grid layout of the content and media dashboards - [#741](https://github.com/estruyf/vscode-front-matter/issues/741): Added message on the content dashboard when content is processed diff --git a/l10n/bundle.l10n.json b/l10n/bundle.l10n.json index 25d3f0ef..b145ea1d 100644 --- a/l10n/bundle.l10n.json +++ b/l10n/bundle.l10n.json @@ -50,8 +50,11 @@ "settings.diagnostic": "Diagnostic", "settings.diagnostic.description": "You can run the diagnostics to check the whole Front Matter CMS configuration.", "settings.diagnostic.link": "Run full diagnostics", - "settings.git.enabled": "Git synchronization", - "settings.git.enabled.description": "Enable Git synchronization to easily sync your changes with your repository.", + "settings.git": "Git synchronization", + "settings.git.enabled": "Enable Git synchronization to easily sync your changes with your repository.", + "settings.git.commitMessage": "Commit message", + "settings.git.submoduleInfo": "When working with Git submodules, you can refer to the submodule settings in the documentation.", + "settings.git.submoduleLink": "Read more about Git submodules", "settings.commonSettings.website.title": "Website and SSG settings", "settings.commonSettings.previewUrl": "Preview URL", diff --git a/src/constants/Git.ts b/src/constants/Git.ts new file mode 100644 index 00000000..0d9dfd69 --- /dev/null +++ b/src/constants/Git.ts @@ -0,0 +1,3 @@ +export const GIT_CONFIG = { + defaultCommitMessage: 'Synced by Front Matter' +}; diff --git a/src/constants/Links.ts b/src/constants/Links.ts index 72c07f04..1bea4ebe 100644 --- a/src/constants/Links.ts +++ b/src/constants/Links.ts @@ -8,3 +8,5 @@ export const DOCUMENTATION_SETTINGS_LINK = 'https://frontmatter.codes/docs/setti export const SENTRY_LINK = 'https://1ac45704bbe74264a7b4674bdc2abf48@o1022172.ingest.sentry.io/5988293'; + +export const DOCS_SUBMODULES = 'https://frontmatter.codes/docs/git-integration#git-submodules'; diff --git a/src/constants/index.ts b/src/constants/index.ts index ca2aea05..ee247c3b 100644 --- a/src/constants/index.ts +++ b/src/constants/index.ts @@ -7,6 +7,7 @@ export * from './ExtensionState'; export * from './Features'; export * from './FrameworkDetectors'; export * from './GeneralCommands'; +export * from './Git'; export * from './Links'; export * from './LocalStore'; export * from './Navigation'; diff --git a/src/dashboardWebView/components/SettingsView/CommonSettings.tsx b/src/dashboardWebView/components/SettingsView/CommonSettings.tsx index 75acd3a9..c1d5d55c 100644 --- a/src/dashboardWebView/components/SettingsView/CommonSettings.tsx +++ b/src/dashboardWebView/components/SettingsView/CommonSettings.tsx @@ -6,10 +6,11 @@ import { useRecoilValue } from 'recoil'; import { SettingsSelector } from '../../state'; import { SettingsInput } from './SettingsInput'; import { VSCodeButton } from '@vscode/webview-ui-toolkit/react'; -import { FrameworkDetectors, SETTING_FRAMEWORK_START, SETTING_GIT_ENABLED, SETTING_PREVIEW_HOST, SETTING_WEBSITE_URL } from '../../../constants'; +import { DOCS_SUBMODULES, FrameworkDetectors, GIT_CONFIG, SETTING_FRAMEWORK_START, SETTING_GIT_COMMIT_MSG, SETTING_GIT_ENABLED, SETTING_PREVIEW_HOST, SETTING_WEBSITE_URL } from '../../../constants'; import { messageHandler } from '@estruyf/vscode/dist/client'; import { DashboardMessage } from '../../DashboardMessage'; import { SettingsCheckbox } from './SettingsCheckbox'; +import { ChevronRightIcon } from '@heroicons/react/24/outline'; export interface ICommonSettingsProps { } @@ -44,7 +45,8 @@ export const CommonSettings: React.FunctionComponent = (pr SETTING_PREVIEW_HOST, SETTING_WEBSITE_URL, SETTING_FRAMEWORK_START, - SETTING_GIT_ENABLED + SETTING_GIT_ENABLED, + SETTING_GIT_COMMIT_MSG, ]).then((config) => { setConfig(config); setUpdated(false); @@ -72,15 +74,38 @@ export const CommonSettings: React.FunctionComponent = (pr
-

{l10n.t(LocalizationKey.settingsGitEnabled)}

+

{l10n.t(LocalizationKey.settingsGit)}

c.name === SETTING_GIT_ENABLED)?.value || false) as boolean} onChange={onSettingChange} /> + + c.name === SETTING_GIT_COMMIT_MSG)?.value || "") as string} + placeholder={GIT_CONFIG.defaultCommitMessage} + onChange={onSettingChange} + /> + +

+ + + + {l10n.t(LocalizationKey.settingsGitSubmoduleInfo)}  + + + + {l10n.t(LocalizationKey.settingsGitSubmoduleLink)} + +

diff --git a/src/dashboardWebView/components/SettingsView/SettingsInput.tsx b/src/dashboardWebView/components/SettingsView/SettingsInput.tsx index 6785cc37..74730746 100644 --- a/src/dashboardWebView/components/SettingsView/SettingsInput.tsx +++ b/src/dashboardWebView/components/SettingsView/SettingsInput.tsx @@ -5,6 +5,7 @@ export interface ISettingsInputProps { label: string; name: string; value: string; + placeholder?: string; onChange: (key: string, value: string) => void; fallback?: string; } @@ -13,6 +14,7 @@ export const SettingsInput: React.FunctionComponent = ({ label, name, value, + placeholder, onChange, fallback }: React.PropsWithChildren) => { @@ -24,6 +26,7 @@ export const SettingsInput: React.FunctionComponent = ({ boxShadow: 'none' }} value={value || fallback || ""} + placeholder={placeholder} onInput={(e: React.ChangeEvent) => onChange(name, e.target.value)}> {label} diff --git a/src/listeners/general/GitListener.ts b/src/listeners/general/GitListener.ts index 32813c07..7a288b30 100644 --- a/src/listeners/general/GitListener.ts +++ b/src/listeners/general/GitListener.ts @@ -1,9 +1,16 @@ import { + COMMAND_NAME, + CONTEXT, + GIT_CONFIG, + SETTING_DATE_FORMAT, + SETTING_GIT_COMMIT_MSG, + SETTING_GIT_ENABLED, SETTING_GIT_SUBMODULE_BRANCH, SETTING_GIT_SUBMODULE_FOLDER, SETTING_GIT_SUBMODULE_PULL, - SETTING_GIT_SUBMODULE_PUSH -} from './../../constants/settings'; + SETTING_GIT_SUBMODULE_PUSH, + TelemetryEvent +} from './../../constants'; import { Settings } from './../../helpers/SettingsHelper'; import { Dashboard } from '../../commands/Dashboard'; import { PanelProvider } from '../../panelWebView/PanelProvider'; @@ -17,14 +24,6 @@ import { } from '../../helpers'; import { GeneralCommands } from './../../constants/GeneralCommands'; import simpleGit, { SimpleGit } from 'simple-git'; -import { - COMMAND_NAME, - CONTEXT, - SETTING_DATE_FORMAT, - SETTING_GIT_COMMIT_MSG, - SETTING_GIT_ENABLED, - TelemetryEvent -} from '../../constants'; import { Folders } from '../../commands/Folders'; import { commands } from 'vscode'; import { PostMessageData } from '../../models'; @@ -187,7 +186,7 @@ export class GitListener { // Check if anything changed if (status.files.length > 0) { await subGit.raw(['add', '.', '-A']); - await subGit.commit(commitMsg || 'Synced by Front Matter'); + await subGit.commit(commitMsg || GIT_CONFIG.defaultCommitMessage); } await subGit.push(); } catch (e) { @@ -214,7 +213,7 @@ export class GitListener { 'git', 'commit', '-m', - commitMsg || 'Synced by Front Matter' + commitMsg || GIT_CONFIG.defaultCommitMessage ]); await git.subModule(['foreach', 'git', 'push']); } @@ -234,7 +233,7 @@ export class GitListener { if (status.files.length > 0) { await git.raw(['add', '.', '-A']); - await git.commit(commitMsg || 'Synced by Front Matter'); + await git.commit(commitMsg || GIT_CONFIG.defaultCommitMessage); } await git.push(); diff --git a/src/localization/localization.enum.ts b/src/localization/localization.enum.ts index bcc85994..15053bc7 100644 --- a/src/localization/localization.enum.ts +++ b/src/localization/localization.enum.ts @@ -194,11 +194,23 @@ export enum LocalizationKey { /** * Git synchronization */ - settingsGitEnabled = 'settings.git.enabled', + settingsGit = 'settings.git', /** * Enable Git synchronization to easily sync your changes with your repository. */ - settingsGitEnabledDescription = 'settings.git.enabled.description', + settingsGitEnabled = 'settings.git.enabled', + /** + * Commit message + */ + settingsGitCommitMessage = 'settings.git.commitMessage', + /** + * When working with Git submodules, you can refer to the submodule settings in the documentation. + */ + settingsGitSubmoduleInfo = 'settings.git.submoduleInfo', + /** + * Read more about Git submodules + */ + settingsGitSubmoduleLink = 'settings.git.submoduleLink', /** * Website and SSG settings */