diff --git a/CHANGELOG.md b/CHANGELOG.md index 929fa79c..eb187e55 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,10 +5,12 @@ ### ✨ 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 - [#749](https://github.com/estruyf/vscode-front-matter/issues/749): Ability to set your own filters on the content dashboard with the `frontMatter.content.filters` setting ### 🎨 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 - [#739](https://github.com/estruyf/vscode-front-matter/pull/739): New Git settings to disable and require a commit message diff --git a/l10n/bundle.l10n.json b/l10n/bundle.l10n.json index 62acddf4..39e98d97 100644 --- a/l10n/bundle.l10n.json +++ b/l10n/bundle.l10n.json @@ -50,6 +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": "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", @@ -278,6 +283,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/package.json b/package.json index 57bb9ae1..ac054b33 100644 --- a/package.json +++ b/package.json @@ -1604,6 +1604,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": "", @@ -1842,6 +1850,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%", diff --git a/package.nls.json b/package.nls.json index 67d9cc19..c5fa5524 100644 --- a/package.nls.json +++ b/package.nls.json @@ -211,6 +211,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.", @@ -237,6 +238,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 ca2ad508..b4aad4dc 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 9059ba02..7d1f4a16 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'; @@ -377,7 +377,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 18d05571..f0252f1d 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/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/constants/settings.ts b/src/constants/settings.ts index c50552da..8387adb1 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/SettingsView/CommonSettings.tsx b/src/dashboardWebView/components/SettingsView/CommonSettings.tsx index 33c2ce91..c1d5d55c 100644 --- a/src/dashboardWebView/components/SettingsView/CommonSettings.tsx +++ b/src/dashboardWebView/components/SettingsView/CommonSettings.tsx @@ -6,15 +6,17 @@ 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 { 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 { } interface Config { name: string; - value: string; + value: string | boolean; } export const CommonSettings: React.FunctionComponent = (props: React.PropsWithChildren) => { @@ -22,7 +24,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 +41,13 @@ 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, + SETTING_GIT_COMMIT_MSG, + ]).then((config) => { setConfig(config); setUpdated(false); }); @@ -65,6 +73,42 @@ export const CommonSettings: React.FunctionComponent = (pr +
+

{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)} + +

+
+
+

{l10n.t(LocalizationKey.settingsCommonSettingsWebsiteTitle)}

@@ -72,21 +116,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/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/dashboardWebView/components/SnippetsView/Item.tsx b/src/dashboardWebView/components/SnippetsView/Item.tsx index 0e7a5da5..8d4bbc5a 100644 --- a/src/dashboardWebView/components/SnippetsView/Item.tsx +++ b/src/dashboardWebView/components/SnippetsView/Item.tsx @@ -260,6 +260,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 bcb8c785..dab80763 100644 --- a/src/dashboardWebView/components/SnippetsView/SnippetForm.tsx +++ b/src/dashboardWebView/components/SnippetsView/SnippetForm.tsx @@ -1,8 +1,7 @@ -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'; @@ -14,6 +13,7 @@ export interface ISnippetFormProps { snippetKey?: string; snippet: Snippet; selection: string | undefined; + filePath?: string; fieldInfo?: SnippetInfoField[]; mediaData?: any; onInsert?: (mediaData: any) => void; @@ -24,7 +24,7 @@ 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); @@ -41,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( @@ -124,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`); @@ -143,7 +142,7 @@ ${snippetBody} if (idx > -1) { allFields.push({ ...field, - value: insertPlaceholderValues(field.default || '') + value: await insertPlaceholderValues(field.default || '') }); } } @@ -163,6 +162,10 @@ ${snippetBody} } setFields(allFields); + }, [snippet, insertPlaceholderValues, insertValueFromMedia]); + + useEffect(() => { + processFields(); }, [snippet]); return ( 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/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 900e28b0..be3a83e5 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'; @@ -299,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); @@ -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/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/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 3c620ee8..8c7fc7b6 100644 --- a/src/listeners/general/GitListener.ts +++ b/src/listeners/general/GitListener.ts @@ -1,11 +1,18 @@ import { + COMMAND_NAME, + CONTEXT, + GIT_CONFIG, + SETTING_DATE_FORMAT, + SETTING_GIT_COMMIT_MSG, SETTING_GIT_DISABLED_BRANCHES, + SETTING_GIT_ENABLED, SETTING_GIT_REQUIRES_COMMIT_MSG, 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'; @@ -15,19 +22,11 @@ import { Logger, Notifications, parseWinPath, - processKnownPlaceholders, + processTimePlaceholders, Telemetry } 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 { Event, commands, extensions } from 'vscode'; import { GitAPIState, GitRepository, PostMessageData } from '../../models'; @@ -115,10 +114,21 @@ export class GitListener { break; case GeneralCommands.toVSCode.git.selectBranch: this.selectBranch(); + case GeneralCommands.toVSCode.git.isRepo: + this.checkIsGitRepo(msg.command, msg.requestId); 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 }); + } + /** * Selects the current branch in the Git repository. * @returns {Promise} A promise that resolves when the branch command has been executed. @@ -222,7 +232,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); } @@ -243,7 +253,7 @@ export class GitListener { // Check if anything changed if (status.files.length > 0) { await subGit.raw(['add', '.', '-A']); - await subGit.commit(commitMsg); + await subGit.commit(commitMsg || GIT_CONFIG.defaultCommitMessage); } await subGit.push(); } catch (e) { @@ -265,7 +275,13 @@ export class GitListener { // First line is the submodule folder name if (lines.length > 1) { await git.subModule(['foreach', 'git', 'add', '.', '-A']); - await git.subModule(['foreach', 'git', 'commit', '-m', commitMsg]); + await git.subModule([ + 'foreach', + 'git', + 'commit', + '-m', + commitMsg || GIT_CONFIG.defaultCommitMessage + ]); await git.subModule(['foreach', 'git', 'push']); } } catch (e) { @@ -284,7 +300,7 @@ export class GitListener { if (status.files.length > 0) { await git.raw(['add', '.', '-A']); - await git.commit(commitMsg); + await git.commit(commitMsg || GIT_CONFIG.defaultCommitMessage); } await git.push(); diff --git a/src/listeners/panel/ArticleListener.ts b/src/listeners/panel/ArticleListener.ts index 4c6cbd47..3bd09dca 100644 --- a/src/listeners/panel/ArticleListener.ts +++ b/src/listeners/panel/ArticleListener.ts @@ -1,6 +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'; @@ -17,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(); @@ -32,10 +32,19 @@ export class ArticleListener extends BaseListener { * Generate a slug * @param title */ - private static generateSlug(title: string) { - const slug = Article.generateSlug(title); + 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 7388a3e4..8af8a125 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); @@ -211,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); @@ -597,18 +613,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/localization/localization.enum.ts b/src/localization/localization.enum.ts index 0823797b..c62db1be 100644 --- a/src/localization/localization.enum.ts +++ b/src/localization/localization.enum.ts @@ -191,6 +191,26 @@ export enum LocalizationKey { * Run full diagnostics */ settingsDiagnosticLink = 'settings.diagnostic.link', + /** + * Git synchronization + */ + settingsGit = 'settings.git', + /** + * Enable Git synchronization to easily sync your changes with your repository. + */ + 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 */ @@ -919,6 +939,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 */ 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 f6839bb2..561e44e7 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/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 22d39b27..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'; @@ -14,6 +12,7 @@ import { LocalizationKey } from '../../../localization'; export interface ISlugFieldProps extends BaseFieldProps { titleValue: string | null; editable?: boolean; + slugTemplate?: string; onChange: (txtValue: string) => void; } @@ -23,6 +22,7 @@ export const SlugField: React.FunctionComponent = ({ editable, value, titleValue, + slugTemplate, onChange, required }: React.PropsWithChildren) => { @@ -38,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]); @@ -60,17 +50,18 @@ export const SlugField: React.FunctionComponent = ({ useEffect(() => { if (titleValue) { - Messenger.send(CommandToCode.generateSlug, titleValue); + messageHandler.request<{ slug: string; slugWithPrefixAndSuffix: string; }>(CommandToCode.generateSlug, { + title: titleValue, + slugTemplate + }).then((slug) => { + if (slug.slugWithPrefixAndSuffix) { + setSlug(slug.slugWithPrefixAndSuffix); + } + }).catch((_) => { + setSlug(null); + }); } - }, [titleValue]); - - useEffect(() => { - Messenger.listen(messageListener); - - return () => { - Messenger.unlisten(messageListener); - }; - }, []); + }, [titleValue, slugTemplate]); return (
diff --git a/src/panelWebView/components/Fields/WrapperField.tsx b/src/panelWebView/components/Fields/WrapperField.tsx index 43d30226..6d3c29d5 100644 --- a/src/panelWebView/components/Fields/WrapperField.tsx +++ b/src/panelWebView/components/Fields/WrapperField.tsx @@ -1,9 +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 { Command } from '../../Command'; +import { BlockFieldData, ContentType, CustomPanelViewResult, Field, PanelSettings } from '../../../models'; import { CommandToCode } from '../../CommandToCode'; import { TagType } from '../../TagType'; import { DataBlockField } from '../DataBlock'; @@ -41,6 +40,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 +63,7 @@ export const WrapperField: React.FunctionComponent = ({ parentFields, metadata, settings, + contentType, blockData, focusElm, parentBlock, @@ -76,21 +77,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 +112,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 +131,6 @@ export const WrapperField: React.FunctionComponent = ({ setFieldValue(value || null); } } - - return () => { - window.removeEventListener('message', listener); - }; }, [field, parent]); useEffect(() => { @@ -509,6 +498,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 };