From d45cd0d015eaeebbfed069fb63e035be8c071daf Mon Sep 17 00:00:00 2001 From: Elio Struyf Date: Fri, 19 Jan 2024 18:11:53 +0100 Subject: [PATCH 01/23] Git branching --- package.json | 291 ++++++++---------- src/constants/GeneralCommands.ts | 13 +- src/constants/settings.ts | 1 + .../components/Header/SyncButton.tsx | 6 +- src/dashboardWebView/models/Settings.ts | 2 +- src/helpers/DashboardSettings.ts | 6 +- src/helpers/PanelSettings.ts | 7 +- src/listeners/general/GitListener.ts | 163 +++++++++- src/localization/localization.enum.ts | 2 +- src/models/GitRepository.ts | 30 ++ src/models/GitSettings.ts | 1 + src/models/PanelSettings.ts | 2 +- src/models/index.ts | 1 + src/panelWebView/components/Git/GitAction.tsx | 54 +++- 14 files changed, 390 insertions(+), 189 deletions(-) create mode 100644 src/models/GitRepository.ts diff --git a/package.json b/package.json index 8957161e..bb9cf3a5 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" ] @@ -874,6 +863,14 @@ "markdownDescription": "%setting.frontMatter.git.commitMessage.markdownDescription%", "default": "Synced by Front Matter" }, + "frontMatter.git.disableOnBranches": { + "type": "array", + "markdownDescription": "%setting.frontMatter.git.disableOnBranches.markdownDescription%", + "default": [], + "items": { + "type": "string" + } + }, "frontMatter.git.submodule.pull": { "type": "boolean", "markdownDescription": "%setting.frontMatter.git.submodule.pull.markdownDescription%", @@ -1209,8 +1206,7 @@ "default": "", "description": "%setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.taxonomyId.description%", "not": { - "anyOf": [ - { + "anyOf": [{ "const": "" }, { @@ -1404,8 +1400,7 @@ "type", "name" ], - "allOf": [ - { + "allOf": [{ "if": { "properties": { "type": { @@ -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": "" }, { @@ -1855,8 +1846,7 @@ } } }, - "commands": [ - { + "commands": [{ "command": "frontMatter.project.switch", "title": "%command.frontMatter.project.switch%", "category": "Front Matter", @@ -2173,21 +2163,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 +2253,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 +2273,7 @@ "group": "frontmatter@3" } ], - "commandPalette": [ - { + "commandPalette": [{ "command": "frontMatter.init", "when": "frontMatterCanInit" }, @@ -2441,8 +2422,7 @@ "when": "frontMatter:file:isValid == true" } ], - "view/title": [ - { + "view/title": [{ "command": "frontMatter.chatbot", "group": "navigation@0", "when": "view == frontMatter.explorer" @@ -2474,57 +2454,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:*", @@ -2661,4 +2636,4 @@ "vsce": { "dependencies": false } -} +} \ No newline at end of file diff --git a/src/constants/GeneralCommands.ts b/src/constants/GeneralCommands.ts index ccb77129..0087cccf 100644 --- a/src/constants/GeneralCommands.ts +++ b/src/constants/GeneralCommands.ts @@ -1,13 +1,20 @@ export const GeneralCommands = { toWebview: { setMode: 'setMode', - gitSyncingStart: 'gitSyncingStart', - gitSyncingEnd: 'gitSyncingEnd', + git: { + syncingStart: 'gitSyncingStart', + syncingEnd: 'gitSyncingEnd', + branchInfo: 'gitBranchInfo' + }, setLocalization: 'setLocalization' }, toVSCode: { openLink: 'openLink', - gitSync: 'gitSync', + git: { + sync: 'gitSync', + getBranch: 'getBranch', + selectBranch: 'gitSelectBranch' + }, getLocalization: 'getLocalization', openOnWebsite: 'openOnWebsite' } diff --git a/src/constants/settings.ts b/src/constants/settings.ts index ff9b7398..328c57b5 100644 --- a/src/constants/settings.ts +++ b/src/constants/settings.ts @@ -98,6 +98,7 @@ export const SETTING_FRAMEWORK_START = 'framework.startCommand'; export const SETTING_SITE_BASEURL = 'site.baseURL'; export const SETTING_GIT_ENABLED = 'git.enabled'; +export const SETTING_GIT_DISABLED_BRANCHES = 'git.disableOnBranches'; export const SETTING_GIT_COMMIT_MSG = 'git.commitMessage'; export const SETTING_GIT_SUBMODULE_PULL = 'git.submodule.pull'; export const SETTING_GIT_SUBMODULE_PUSH = 'git.submodule.push'; diff --git a/src/dashboardWebView/components/Header/SyncButton.tsx b/src/dashboardWebView/components/Header/SyncButton.tsx index 791132cf..9df81574 100644 --- a/src/dashboardWebView/components/Header/SyncButton.tsx +++ b/src/dashboardWebView/components/Header/SyncButton.tsx @@ -20,15 +20,15 @@ export const SyncButton: React.FunctionComponent = ( const { getColors } = useThemeColors(); const pull = () => { - Messenger.send(GeneralCommands.toVSCode.gitSync); + Messenger.send(GeneralCommands.toVSCode.git.sync); }; const messageListener = (message: MessageEvent>) => { const { command } = message.data; - if (command === GeneralCommands.toWebview.gitSyncingStart) { + if (command === GeneralCommands.toWebview.git.syncingStart) { setIsSyncing(true); - } else if (command === GeneralCommands.toWebview.gitSyncingEnd) { + } else if (command === GeneralCommands.toWebview.git.syncingEnd) { setIsSyncing(false); } }; diff --git a/src/dashboardWebView/models/Settings.ts b/src/dashboardWebView/models/Settings.ts index f5ff7b8d..14194fb3 100644 --- a/src/dashboardWebView/models/Settings.ts +++ b/src/dashboardWebView/models/Settings.ts @@ -19,7 +19,7 @@ import { DataFile } from '../../models/DataFile'; export interface Settings { projects: Project[]; project: Project; - git: GitSettings; + git: GitSettings | undefined; beta: boolean; initialized: boolean; wsFolder: string; diff --git a/src/helpers/DashboardSettings.ts b/src/helpers/DashboardSettings.ts index fc277e2c..1f7ca5e0 100644 --- a/src/helpers/DashboardSettings.ts +++ b/src/helpers/DashboardSettings.ts @@ -79,16 +79,12 @@ export class DashboardSettings { const ext = Extension.getInstance(); const wsFolder = Folders.getWorkspaceFolder(); const isInitialized = await Project.isInitialized(); - const gitActions = Settings.get(SETTING_GIT_ENABLED); const pagination = Settings.get(SETTING_DASHBOARD_CONTENT_PAGINATION); const settings = { projects: Settings.getProjects(), project: Settings.getProject(), - git: { - isGitRepo: gitActions ? await GitListener.isGitRepository() : false, - actions: gitActions || false - }, + git: await GitListener.getSettings(), beta: ext.isBetaVersion(), wsFolder: wsFolder ? wsFolder.fsPath : '', staticFolder: Folders.getStaticFolderRelativePath(), diff --git a/src/helpers/PanelSettings.ts b/src/helpers/PanelSettings.ts index 121b4760..f022bbb0 100644 --- a/src/helpers/PanelSettings.ts +++ b/src/helpers/PanelSettings.ts @@ -49,14 +49,9 @@ import { Folders } from '../commands'; export class PanelSettings { public static async get(): Promise { - const gitActions = Settings.get(SETTING_GIT_ENABLED); - return { aiEnabled: Settings.get(SETTING_SPONSORS_AI_ENABLED) || false, - git: { - isGitRepo: gitActions ? await GitListener.isGitRepository() : false, - actions: gitActions || false - }, + git: await GitListener.getSettings(), seo: { title: (Settings.get(SETTING_SEO_TITLE_LENGTH) as number) || -1, slug: (Settings.get(SETTING_SEO_SLUG_LENGTH) as number) || -1, diff --git a/src/listeners/general/GitListener.ts b/src/listeners/general/GitListener.ts index e4e76537..6b9eabae 100644 --- a/src/listeners/general/GitListener.ts +++ b/src/listeners/general/GitListener.ts @@ -1,4 +1,5 @@ import { + SETTING_GIT_DISABLED_BRANCHES, SETTING_GIT_SUBMODULE_BRANCH, SETTING_GIT_SUBMODULE_FOLDER, SETTING_GIT_SUBMODULE_PULL, @@ -12,6 +13,7 @@ import { Extension, Logger, Notifications, + parseWinPath, processKnownPlaceholders, Telemetry } from '../../helpers'; @@ -26,15 +28,38 @@ import { TelemetryEvent } from '../../constants'; import { Folders } from '../../commands/Folders'; -import { commands } from 'vscode'; -import { PostMessageData } from '../../models'; +import { commands, extensions } from 'vscode'; +import { GitRepository, GitRepositoryState, PostMessageData } from '../../models'; import * as l10n from '@vscode/l10n'; import { LocalizationKey } from '../../localization'; export class GitListener { + private static gitAPI: { + onDidChangeState: (repo: any) => void; + onDidOpenRepository: (repo: any) => void; + onDidCloseRepository: (repo: any) => void; + getAPI: (version: number) => any; + repositories: GitRepository[]; + } | null = null; private static isRegistered: boolean = false; private static client: SimpleGit | null = null; private static subClient: SimpleGit | null = null; + private static repository: GitRepository | null = null; + + public static async getSettings() { + const gitActions = Settings.get(SETTING_GIT_ENABLED); + if (gitActions) { + return { + isGitRepo: gitActions ? await GitListener.isGitRepository() : false, + actions: gitActions || false, + disabledBranches: gitActions + ? Settings.get(SETTING_GIT_DISABLED_BRANCHES) || [] + : [] + }; + } + + return; + } /** * Initialize the listener @@ -65,28 +90,39 @@ export class GitListener { */ public static process(msg: PostMessageData) { switch (msg.command) { - case GeneralCommands.toVSCode.gitSync: + case GeneralCommands.toVSCode.git.sync: this.sync(); break; + case GeneralCommands.toVSCode.git.getBranch: + this.getBranch(msg.command, msg.requestId); + break; + case GeneralCommands.toVSCode.git.selectBranch: + this.selectBranch(); + break; } } + public static async selectBranch() { + const workspaceFolder = Folders.getWorkspaceFolder(); + await commands.executeCommand('git.checkout', workspaceFolder); + } + /** * Run the sync */ public static async sync() { try { - this.sendMsg(GeneralCommands.toWebview.gitSyncingStart, {}); + this.sendMsg(GeneralCommands.toWebview.git.syncingStart, {}); Telemetry.send(TelemetryEvent.gitSync); await this.pull(); await this.push(); - this.sendMsg(GeneralCommands.toWebview.gitSyncingEnd, {}); + this.sendMsg(GeneralCommands.toWebview.git.syncingEnd, {}); } catch (e) { Logger.error((e as Error).message); - this.sendMsg(GeneralCommands.toWebview.gitSyncingEnd, {}); + this.sendMsg(GeneralCommands.toWebview.git.syncingEnd, {}); } } @@ -106,6 +142,8 @@ export class GitListener { Logger.warning(`Current workspace is not a GIT repository`); } + GitListener.vscodeGitProvider(); + return isRepo; } @@ -257,6 +295,100 @@ export class GitListener { } } + private static async vscodeGitProvider() { + if (!GitListener.gitAPI) { + const wsFolder = Folders.getWorkspaceFolder(); + const extension = extensions.getExtension('vscode.git'); + + /** + * Logic from: https://github.com/microsoft/vscode/blob/main/extensions/github/src/extension.ts + * initializeGitExtension + */ + if (wsFolder && extension) { + const gitExtension = extension.isActive ? extension.exports : await extension.activate(); + + // Get version 1 of the API + GitListener.gitAPI = gitExtension.getAPI(1); + + if (!GitListener.gitAPI) { + return; + } + + GitListener.listenToRepo(GitListener.gitAPI?.repositories); + + GitListener.gitAPI.onDidChangeState(() => { + GitListener.listenToRepo(GitListener.gitAPI?.repositories); + }); + + GitListener.gitAPI?.onDidOpenRepository((repo: GitRepository) => { + GitListener.triggerBranchChange(repo); + + repo.state.onDidChange(() => { + GitListener.triggerBranchChange(repo); + }); + }); + + GitListener.gitAPI?.onDidCloseRepository((repo: any) => { + console.log(`Closed repo:`, repo); + }); + } + } + } + + private static async getBranch(command: string, requestId?: string) { + if (!command || !requestId) { + return; + } + + this.sendRequest(command, requestId, GitListener.repository?.state?.HEAD.name); + } + + private static listenToRepo(repositories: GitRepository[] | undefined) { + if (!repositories) { + return; + } + + if (repositories && repositories.length === 1) { + GitListener.repository = repositories[0]; + } else if (repositories && repositories.length > 1) { + const wsFolder = Folders.getWorkspaceFolder(); + if (wsFolder) { + const repo = repositories.find( + (repo) => parseWinPath(repo.rootUri.fsPath) === parseWinPath(wsFolder.fsPath) + ); + if (repo) { + GitListener.repository = repo; + } + } + } + + GitListener.repository?.state?.onDidChange(() => { + GitListener.triggerBranchChange(GitListener.repository); + }); + } + + /** + * Trigger the branch change + * @param repo + */ + private static async triggerBranchChange(repo: GitRepository | null) { + if (repo && repo.state) { + GitListener.repository = repo; + let branches = []; + + if (repo.repository.getBranches) { + const allBranches = await repo.repository.getBranches(); + if (allBranches && allBranches.length > 0) { + branches = allBranches.map((branch: any) => branch.name); + } + } + this.sendMsg(GeneralCommands.toWebview.git.branchInfo, { + crntBranch: GitListener.repository?.state?.HEAD.name, + branches + }); + } + } + /** * Send the message to the webview * @param command @@ -270,4 +402,23 @@ export class GitListener { Dashboard.postWebviewMessage({ command: command as any, payload }); } + + /** + * Sends a request to the webview panel. + * @param command - The command to send. + * @param requestId - The unique identifier for the request. + * @param payload - The payload to send with the request. + */ + private static sendRequest(command: string, requestId: string, payload: any) { + const extPath = Extension.getInstance().extensionPath; + const panel = PanelProvider.getInstance(extPath); + + panel.getWebview()?.postMessage({ + command, + requestId, + payload + }); + + Dashboard.postWebviewMessage({ command: command as any, requestId, payload }); + } } diff --git a/src/localization/localization.enum.ts b/src/localization/localization.enum.ts index 452cd2eb..54e508de 100644 --- a/src/localization/localization.enum.ts +++ b/src/localization/localization.enum.ts @@ -700,7 +700,7 @@ export enum LocalizationKey { */ dashboardMediaMediaFolderDefault = 'dashboard.media.media.folder.default', /** - * No media files to show. You can drag & drop new files by holding your [shift] key. + * No media files to show. You can drag&drop new files by holding your [shift] key. */ dashboardMediaMediaPlaceholder = 'dashboard.media.media.placeholder', /** diff --git a/src/models/GitRepository.ts b/src/models/GitRepository.ts new file mode 100644 index 00000000..178705a9 --- /dev/null +++ b/src/models/GitRepository.ts @@ -0,0 +1,30 @@ +export interface GitRepository { + state: GitRepositoryState; + rootUri: { + fsPath: string; + path: string; + }; + repository: { + getBranches: () => Promise; + }; +} + +export interface GitRepositoryState { + HEAD: GitBranch; + onDidChange: (listener: () => void) => void; +} + +export interface GitBranch { + type: number; + name: string; + upstream: Upstream; + commit: string; + ahead: number; + behind: number; +} + +export interface Upstream { + name: string; + remote: string; + commit: string; +} diff --git a/src/models/GitSettings.ts b/src/models/GitSettings.ts index e452ec54..e29a149b 100644 --- a/src/models/GitSettings.ts +++ b/src/models/GitSettings.ts @@ -1,4 +1,5 @@ export interface GitSettings { isGitRepo: boolean; actions: boolean; + disabledBranches: string[]; } diff --git a/src/models/PanelSettings.ts b/src/models/PanelSettings.ts index 7b6bd2c9..f6839bb2 100644 --- a/src/models/PanelSettings.ts +++ b/src/models/PanelSettings.ts @@ -5,7 +5,7 @@ import { DashboardData } from './DashboardData'; import { DataType } from './DataType'; export interface PanelSettings { - git: GitSettings; + git: GitSettings | undefined; seo: SEO; slug: Slug; tags: string[]; diff --git a/src/models/index.ts b/src/models/index.ts index 68886aa7..2104d824 100644 --- a/src/models/index.ts +++ b/src/models/index.ts @@ -12,6 +12,7 @@ export * from './DataFolder'; export * from './DataType'; export * from './DraftField'; export * from './Framework'; +export * from './GitRepository'; export * from './GitSettings'; export * from './MediaPaths'; export * from './Mode'; diff --git a/src/panelWebView/components/Git/GitAction.tsx b/src/panelWebView/components/Git/GitAction.tsx index 1c6d4242..53ba5164 100644 --- a/src/panelWebView/components/Git/GitAction.tsx +++ b/src/panelWebView/components/Git/GitAction.tsx @@ -1,4 +1,4 @@ -import { Messenger } from '@estruyf/vscode/dist/client'; +import { Messenger, messageHandler } from '@estruyf/vscode/dist/client'; import { EventData } from '@estruyf/vscode/dist/models'; import { ArrowPathIcon } from '@heroicons/react/24/outline'; import * as React from 'react'; @@ -16,22 +16,43 @@ export interface IGitActionProps { export const GitAction: React.FunctionComponent = ({ settings }: React.PropsWithChildren) => { + const [crntBanch, setCrntBranch] = useState(undefined); + const [branches, setBranches] = useState(undefined); const [isSyncing, setIsSyncing] = useState(false); const pull = () => { - Messenger.send(GeneralCommands.toVSCode.gitSync); + Messenger.send(GeneralCommands.toVSCode.git.sync); }; + const selectBranch = () => { + messageHandler.send(GeneralCommands.toVSCode.git.selectBranch) + } + const messageListener = (message: MessageEvent>) => { - const { command } = message.data; + const { command, payload } = message.data; - if (command === GeneralCommands.toWebview.gitSyncingStart) { + if (command === GeneralCommands.toWebview.git.syncingStart) { setIsSyncing(true); - } else if (command === GeneralCommands.toWebview.gitSyncingEnd) { + } else if (command === GeneralCommands.toWebview.git.syncingEnd) { setIsSyncing(false); + } else if (command === GeneralCommands.toWebview.git.branchInfo) { + setCrntBranch(payload.crntBranch || undefined); + setBranches(payload.branches || undefined); } }; + const isSyncDisabled = React.useMemo(() => { + if (!settings?.git?.disabledBranches || settings.git.disabledBranches.length === 0) { + return false; + } + + if (!crntBanch) { + return true; + } + + return settings.git.disabledBranches.includes(crntBanch); + }, [settings?.git?.disabledBranches, crntBanch]) + useEffect(() => { Messenger.listen(messageListener); @@ -40,13 +61,36 @@ export const GitAction: React.FunctionComponent = ({ }; }, []); + useEffect(() => { + messageHandler.request(GeneralCommands.toVSCode.git.getBranch).then((branch) => { + setCrntBranch(branch); + }); + }, []); + if (!settings?.git?.actions || !settings?.git.isGitRepo) { return null; } return (
+

+ + Git Actions + + + +

+ From 6150a3454794afba24bd5e25fc56e721db41788f Mon Sep 17 00:00:00 2001 From: Elio Struyf Date: Sun, 21 Jan 2024 17:27:56 +0100 Subject: [PATCH 02/23] Update git API --- src/listeners/general/GitListener.ts | 54 ++++++++++++++-------------- src/models/GitRepository.ts | 6 +++- 2 files changed, 31 insertions(+), 29 deletions(-) diff --git a/src/listeners/general/GitListener.ts b/src/listeners/general/GitListener.ts index 6b9eabae..3379fb68 100644 --- a/src/listeners/general/GitListener.ts +++ b/src/listeners/general/GitListener.ts @@ -28,16 +28,16 @@ import { TelemetryEvent } from '../../constants'; import { Folders } from '../../commands/Folders'; -import { commands, extensions } from 'vscode'; -import { GitRepository, GitRepositoryState, PostMessageData } from '../../models'; +import { Event, commands, extensions } from 'vscode'; +import { GitAPIState, GitRepository, PostMessageData } from '../../models'; import * as l10n from '@vscode/l10n'; import { LocalizationKey } from '../../localization'; export class GitListener { private static gitAPI: { - onDidChangeState: (repo: any) => void; - onDidOpenRepository: (repo: any) => void; - onDidCloseRepository: (repo: any) => void; + onDidChangeState: Event; + onDidOpenRepository: Event; + onDidCloseRepository: Event; getAPI: (version: number) => any; repositories: GitRepository[]; } | null = null; @@ -320,16 +320,12 @@ export class GitListener { GitListener.listenToRepo(GitListener.gitAPI?.repositories); }); - GitListener.gitAPI?.onDidOpenRepository((repo: GitRepository) => { + GitListener.gitAPI.onDidOpenRepository((repo: GitRepository) => { GitListener.triggerBranchChange(repo); - - repo.state.onDidChange(() => { - GitListener.triggerBranchChange(repo); - }); }); - GitListener.gitAPI?.onDidCloseRepository((repo: any) => { - console.log(`Closed repo:`, repo); + GitListener.gitAPI.onDidCloseRepository((repo: GitRepository) => { + Logger.info(`Closed repo: ${repo?.state?.HEAD?.name}`); }); } } @@ -349,7 +345,7 @@ export class GitListener { } if (repositories && repositories.length === 1) { - GitListener.repository = repositories[0]; + GitListener.triggerBranchChange(repositories[0]); } else if (repositories && repositories.length > 1) { const wsFolder = Folders.getWorkspaceFolder(); if (wsFolder) { @@ -357,14 +353,10 @@ export class GitListener { (repo) => parseWinPath(repo.rootUri.fsPath) === parseWinPath(wsFolder.fsPath) ); if (repo) { - GitListener.repository = repo; + GitListener.triggerBranchChange(repo); } } } - - GitListener.repository?.state?.onDidChange(() => { - GitListener.triggerBranchChange(GitListener.repository); - }); } /** @@ -373,19 +365,25 @@ export class GitListener { */ private static async triggerBranchChange(repo: GitRepository | null) { if (repo && repo.state) { - GitListener.repository = repo; - let branches = []; + if (repo.state.HEAD.name !== GitListener.repository?.state?.HEAD.name) { + GitListener.repository = repo; + let branches = []; - if (repo.repository.getBranches) { - const allBranches = await repo.repository.getBranches(); - if (allBranches && allBranches.length > 0) { - branches = allBranches.map((branch: any) => branch.name); + if (repo.repository.getBranches) { + const allBranches = await repo.repository.getBranches(); + if (allBranches && allBranches.length > 0) { + branches = allBranches.map((branch: any) => branch.name); + } } + this.sendMsg(GeneralCommands.toWebview.git.branchInfo, { + crntBranch: GitListener.repository?.state?.HEAD.name, + branches + }); + + repo.state.onDidChange(() => { + GitListener.triggerBranchChange(repo); + }); } - this.sendMsg(GeneralCommands.toWebview.git.branchInfo, { - crntBranch: GitListener.repository?.state?.HEAD.name, - branches - }); } } diff --git a/src/models/GitRepository.ts b/src/models/GitRepository.ts index 178705a9..0cb40ddc 100644 --- a/src/models/GitRepository.ts +++ b/src/models/GitRepository.ts @@ -1,3 +1,7 @@ +import { Event } from 'vscode'; + +export type GitAPIState = 'uninitialized' | 'initialized'; + export interface GitRepository { state: GitRepositoryState; rootUri: { @@ -11,7 +15,7 @@ export interface GitRepository { export interface GitRepositoryState { HEAD: GitBranch; - onDidChange: (listener: () => void) => void; + onDidChange: Event; } export interface GitBranch { From cf96923d96fb99a71736461a1c0afbe4e6529fbb Mon Sep 17 00:00:00 2001 From: Elio Struyf Date: Sun, 21 Jan 2024 17:33:58 +0100 Subject: [PATCH 03/23] Faster loading for panel --- src/panelWebView/ViewPanel.tsx | 51 +++++++++++++++++++--------------- 1 file changed, 29 insertions(+), 22 deletions(-) diff --git a/src/panelWebView/ViewPanel.tsx b/src/panelWebView/ViewPanel.tsx index de78eb23..2b32ae52 100644 --- a/src/panelWebView/ViewPanel.tsx +++ b/src/panelWebView/ViewPanel.tsx @@ -83,7 +83,7 @@ export const ViewPanel: React.FunctionComponent = ( ); } - if (loading || !localeReady) { + if (loading && !localeReady) { return ; } @@ -116,37 +116,44 @@ export const ViewPanel: React.FunctionComponent = (
- + {!loading && ()} - {settings && settings.seo && ( - - - - )} - {settings && metadata && ( + { + !loading && settings && settings.seo && ( + + + + ) + } + + {!loading && settings && metadata && ( )} - - - + { + !loading && ( + + + + ) + } From d22ebfa6ce9504a3e289fc86179ff042b762d5f6 Mon Sep 17 00:00:00 2001 From: Elio Struyf Date: Mon, 22 Jan 2024 11:45:11 +0100 Subject: [PATCH 04/23] Commit message input --- package.json | 8 ++ src/constants/GeneralCommands.ts | 3 +- src/constants/TelemetryEvent.ts | 3 +- src/constants/settings.ts | 1 + .../components/Header/Header.tsx | 2 +- src/helpers/PanelSettings.ts | 1 - src/listeners/general/GitListener.ts | 55 ++++---- src/models/GitSettings.ts | 1 + src/panelWebView/components/ActionButton.tsx | 2 +- .../components/Fields/TextField.tsx | 4 + src/panelWebView/components/Git/GitAction.tsx | 131 +++++++++++++----- .../components/Icons/BranchIcon.tsx | 15 ++ src/panelWebView/styles.css | 2 + 13 files changed, 163 insertions(+), 65 deletions(-) create mode 100644 src/panelWebView/components/Icons/BranchIcon.tsx diff --git a/package.json b/package.json index bb9cf3a5..9fc98088 100644 --- a/package.json +++ b/package.json @@ -871,6 +871,14 @@ "type": "string" } }, + "frontMatter.git.requiresCommitMessage": { + "type": "array", + "markdownDescription": "%setting.frontMatter.git.requiresCommitMessage.markdownDescription%", + "default": [], + "items": { + "type": "string" + } + }, "frontMatter.git.submodule.pull": { "type": "boolean", "markdownDescription": "%setting.frontMatter.git.submodule.pull.markdownDescription%", diff --git a/src/constants/GeneralCommands.ts b/src/constants/GeneralCommands.ts index 0087cccf..3deef171 100644 --- a/src/constants/GeneralCommands.ts +++ b/src/constants/GeneralCommands.ts @@ -4,7 +4,7 @@ export const GeneralCommands = { git: { syncingStart: 'gitSyncingStart', syncingEnd: 'gitSyncingEnd', - branchInfo: 'gitBranchInfo' + branchName: 'gitBranchName' }, setLocalization: 'setLocalization' }, @@ -12,6 +12,7 @@ export const GeneralCommands = { openLink: 'openLink', git: { sync: 'gitSync', + fetch: 'getFetch', getBranch: 'getBranch', selectBranch: 'gitSelectBranch' }, diff --git a/src/constants/TelemetryEvent.ts b/src/constants/TelemetryEvent.ts index e4a1dbc1..f717da6f 100644 --- a/src/constants/TelemetryEvent.ts +++ b/src/constants/TelemetryEvent.ts @@ -49,5 +49,6 @@ export const TelemetryEvent = { webviewTaxonomyDashboard: 'webviewTaxonomyDashboard', // Git - gitSync: 'gitSync' + gitSync: 'gitSync', + gitFetch: 'gitFetch' }; diff --git a/src/constants/settings.ts b/src/constants/settings.ts index 328c57b5..33d592cb 100644 --- a/src/constants/settings.ts +++ b/src/constants/settings.ts @@ -99,6 +99,7 @@ export const SETTING_SITE_BASEURL = 'site.baseURL'; export const SETTING_GIT_ENABLED = 'git.enabled'; export const SETTING_GIT_DISABLED_BRANCHES = 'git.disableOnBranches'; +export const SETTING_GIT_REQUIRES_COMMIT_MSG = 'git.requiresCommitMessage'; export const SETTING_GIT_COMMIT_MSG = 'git.commitMessage'; export const SETTING_GIT_SUBMODULE_PULL = 'git.submodule.pull'; export const SETTING_GIT_SUBMODULE_PUSH = 'git.submodule.push'; diff --git a/src/dashboardWebView/components/Header/Header.tsx b/src/dashboardWebView/components/Header/Header.tsx index bd606d42..38689280 100644 --- a/src/dashboardWebView/components/Header/Header.tsx +++ b/src/dashboardWebView/components/Header/Header.tsx @@ -173,7 +173,7 @@ export const Header: React.FunctionComponent = ({
- + {/* */} (SETTING_GIT_ENABLED); @@ -54,6 +56,9 @@ export class GitListener { actions: gitActions || false, disabledBranches: gitActions ? Settings.get(SETTING_GIT_DISABLED_BRANCHES) || [] + : [], + requiresCommitMessage: gitActions + ? Settings.get(SETTING_GIT_REQUIRES_COMMIT_MSG) || [] : [] }; } @@ -91,7 +96,10 @@ export class GitListener { public static process(msg: PostMessageData) { switch (msg.command) { case GeneralCommands.toVSCode.git.sync: - this.sync(); + this.sync(msg.payload); + break; + case GeneralCommands.toVSCode.git.fetch: + this.sync(undefined, false); break; case GeneralCommands.toVSCode.git.getBranch: this.getBranch(msg.command, msg.requestId); @@ -108,16 +116,19 @@ export class GitListener { } /** - * Run the sync + * Run the sync/fetch */ - public static async sync() { + public static async sync(commitMsg?: string, isSync: boolean = true) { try { - this.sendMsg(GeneralCommands.toWebview.git.syncingStart, {}); + this.sendMsg(GeneralCommands.toWebview.git.syncingStart, isSync ? 'syncing' : 'fetching'); - Telemetry.send(TelemetryEvent.gitSync); + Telemetry.send(isSync ? TelemetryEvent.gitSync : TelemetryEvent.gitFetch); await this.pull(); - await this.push(); + + if (isSync) { + await this.push(commitMsg); + } this.sendMsg(GeneralCommands.toWebview.git.syncingEnd, {}); } catch (e) { @@ -187,8 +198,9 @@ export class GitListener { * Push the changes to the remote * @returns */ - private static async push() { - let commitMsg = Settings.get(SETTING_GIT_COMMIT_MSG); + private static async push(commitMsg?: string) { + commitMsg = + commitMsg || Settings.get(SETTING_GIT_COMMIT_MSG) || 'Synced by Front Matter'; if (commitMsg) { const dateFormat = Settings.get(SETTING_DATE_FORMAT) as string; @@ -213,7 +225,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); } await subGit.push(); } catch (e) { @@ -235,13 +247,7 @@ 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 || 'Synced by Front Matter' - ]); + await git.subModule(['foreach', 'git', 'commit', '-m', commitMsg]); await git.subModule(['foreach', 'git', 'push']); } } catch (e) { @@ -260,7 +266,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); } await git.push(); @@ -365,20 +371,11 @@ export class GitListener { */ private static async triggerBranchChange(repo: GitRepository | null) { if (repo && repo.state) { - if (repo.state.HEAD.name !== GitListener.repository?.state?.HEAD.name) { + if (repo.state.HEAD.name !== GitListener.branchName) { + GitListener.branchName = repo.state.HEAD.name; GitListener.repository = repo; - let branches = []; - if (repo.repository.getBranches) { - const allBranches = await repo.repository.getBranches(); - if (allBranches && allBranches.length > 0) { - branches = allBranches.map((branch: any) => branch.name); - } - } - this.sendMsg(GeneralCommands.toWebview.git.branchInfo, { - crntBranch: GitListener.repository?.state?.HEAD.name, - branches - }); + this.sendMsg(GeneralCommands.toWebview.git.branchName, GitListener.branchName); repo.state.onDidChange(() => { GitListener.triggerBranchChange(repo); diff --git a/src/models/GitSettings.ts b/src/models/GitSettings.ts index e29a149b..f32c71d5 100644 --- a/src/models/GitSettings.ts +++ b/src/models/GitSettings.ts @@ -2,4 +2,5 @@ export interface GitSettings { isGitRepo: boolean; actions: boolean; disabledBranches: string[]; + requiresCommitMessage: string[]; } diff --git a/src/panelWebView/components/ActionButton.tsx b/src/panelWebView/components/ActionButton.tsx index 9acf2eef..71e7ee73 100644 --- a/src/panelWebView/components/ActionButton.tsx +++ b/src/panelWebView/components/ActionButton.tsx @@ -14,7 +14,7 @@ const ActionButton: React.FunctionComponent = ({ title }: React.PropsWithChildren) => { return ( -
+
diff --git a/src/panelWebView/components/Fields/TextField.tsx b/src/panelWebView/components/Fields/TextField.tsx index f55651f9..097228a4 100644 --- a/src/panelWebView/components/Fields/TextField.tsx +++ b/src/panelWebView/components/Fields/TextField.tsx @@ -20,6 +20,7 @@ export interface ITextFieldProps extends BaseFieldProps { limit: number | undefined; rows?: number; name: string; + placeholder?: string; settings: PanelSettings; onChange: (txtValue: string) => void; } @@ -27,6 +28,7 @@ export interface ITextFieldProps extends BaseFieldProps { const WysiwygField = React.lazy(() => import('./WysiwygField')); export const TextField: React.FunctionComponent = ({ + placeholder, singleLine, wysiwyg, limit, @@ -154,6 +156,7 @@ export const TextField: React.FunctionComponent = ({ className={`metadata_field__input`} value={text || ''} onChange={(e) => onTextChange(e.currentTarget.value)} + placeholder={placeholder} style={{ border }} @@ -164,6 +167,7 @@ export const TextField: React.FunctionComponent = ({ rows={rows || 2} value={text || ''} onChange={(e) => onTextChange(e.currentTarget.value)} + placeholder={placeholder} style={{ border }} diff --git a/src/panelWebView/components/Git/GitAction.tsx b/src/panelWebView/components/Git/GitAction.tsx index 53ba5164..1e3cefab 100644 --- a/src/panelWebView/components/Git/GitAction.tsx +++ b/src/panelWebView/components/Git/GitAction.tsx @@ -1,6 +1,6 @@ import { Messenger, messageHandler } from '@estruyf/vscode/dist/client'; import { EventData } from '@estruyf/vscode/dist/models'; -import { ArrowPathIcon } from '@heroicons/react/24/outline'; +import { ArrowDownTrayIcon, ArrowPathIcon } from '@heroicons/react/24/outline'; import * as React from 'react'; import { useEffect, useState } from 'react'; import { GeneralCommands } from '../../../constants'; @@ -8,6 +8,7 @@ import { PanelSettings } from '../../../models'; import { ActionButton } from '../ActionButton'; import * as l10n from '@vscode/l10n'; import { LocalizationKey } from '../../../localization'; +import { BranchIcon } from '../Icons/BranchIcon'; export interface IGitActionProps { settings: PanelSettings | undefined; @@ -16,12 +17,16 @@ export interface IGitActionProps { export const GitAction: React.FunctionComponent = ({ settings }: React.PropsWithChildren) => { + const [commitMessage, setCommitMessage] = useState(undefined); const [crntBanch, setCrntBranch] = useState(undefined); - const [branches, setBranches] = useState(undefined); - const [isSyncing, setIsSyncing] = useState(false); + const [isSyncing, setIsSyncing] = useState<"syncing" | "fetching" | "idle">("idle"); - const pull = () => { - Messenger.send(GeneralCommands.toVSCode.git.sync); + const sync = () => { + Messenger.send(GeneralCommands.toVSCode.git.sync, commitMessage); + }; + + const fetch = () => { + Messenger.send(GeneralCommands.toVSCode.git.fetch); }; const selectBranch = () => { @@ -32,26 +37,65 @@ export const GitAction: React.FunctionComponent = ({ const { command, payload } = message.data; if (command === GeneralCommands.toWebview.git.syncingStart) { - setIsSyncing(true); + setIsSyncing(payload || "syncing"); + } else if (command === GeneralCommands.toWebview.git.syncingStart) { + setIsSyncing("syncing"); } else if (command === GeneralCommands.toWebview.git.syncingEnd) { - setIsSyncing(false); - } else if (command === GeneralCommands.toWebview.git.branchInfo) { - setCrntBranch(payload.crntBranch || undefined); - setBranches(payload.branches || undefined); + setCommitMessage(undefined); + setIsSyncing("idle"); + } else if (command === GeneralCommands.toWebview.git.branchName) { + setCrntBranch(payload || undefined); } }; - const isSyncDisabled = React.useMemo(() => { - if (!settings?.git?.disabledBranches || settings.git.disabledBranches.length === 0) { - return false; + const isCommitRequired = React.useMemo(() => { + const requiresCommitMessage = settings?.git?.requiresCommitMessage || []; + + if (!crntBanch) { + return {}; } + if (requiresCommitMessage && requiresCommitMessage.includes(crntBanch) && !commitMessage) { + return { + border: '1px solid var(--vscode-inputValidation-errorBorder)' + }; + } + + return {}; + }, [settings?.git?.requiresCommitMessage, crntBanch, commitMessage]) + + const isCommitDisabed = React.useMemo(() => { + const disabledBranches = settings?.git?.disabledBranches || []; + if (!crntBanch) { return true; } - return settings.git.disabledBranches.includes(crntBanch); - }, [settings?.git?.disabledBranches, crntBanch]) + if (disabledBranches && disabledBranches.includes(crntBanch)) { + return true; + } + + return false; + }, [settings?.git?.disabledBranches, crntBanch, commitMessage]) + + const isSyncDisabled = React.useMemo(() => { + const disabledBranches = settings?.git?.disabledBranches || []; + const requiresCommitMessage = settings?.git?.requiresCommitMessage || []; + + if (!crntBanch) { + return true; + } + + if (disabledBranches && disabledBranches.includes(crntBanch)) { + return true; + } + + if (requiresCommitMessage && requiresCommitMessage.includes(crntBanch)) { + return !commitMessage; + } + + return false; + }, [settings?.git?.disabledBranches, settings?.git?.requiresCommitMessage, crntBanch, commitMessage]) useEffect(() => { Messenger.listen(messageListener); @@ -75,32 +119,57 @@ export const GitAction: React.FunctionComponent = ({

- Git Actions + Changes

- - - - {l10n.t(LocalizationKey.commonSync)} - -
- } - /> +
+ setCommitMessage(e.target.value)} + disabled={isCommitDisabed} + /> + + + + + {l10n.t(LocalizationKey.commonSync)} + +
+ } + /> + + + + + Fetch + +
+ } + /> +
); }; diff --git a/src/panelWebView/components/Icons/BranchIcon.tsx b/src/panelWebView/components/Icons/BranchIcon.tsx new file mode 100644 index 00000000..18a3c50e --- /dev/null +++ b/src/panelWebView/components/Icons/BranchIcon.tsx @@ -0,0 +1,15 @@ +import * as React from 'react'; + +export interface IBranchIconProps { + className?: string; +} + +export const BranchIcon: React.FunctionComponent = ({ + className +}: React.PropsWithChildren) => { + return ( + + + + ); +}; \ No newline at end of file diff --git a/src/panelWebView/styles.css b/src/panelWebView/styles.css index 163bec84..9d0a9a40 100644 --- a/src/panelWebView/styles.css +++ b/src/panelWebView/styles.css @@ -976,12 +976,14 @@ vscode-divider { } /* Git actions */ +.git_actions__fetch, .git_actions__sync { display: flex; align-items: center; justify-content: center; } +.git_actions__fetch svg, .git_actions__sync svg { height: 1.25rem; width: 1.25rem; From d70f983694031c4400984bbb991c7451cace0883 Mon Sep 17 00:00:00 2001 From: Elio Struyf Date: Tue, 23 Jan 2024 15:21:04 +0100 Subject: [PATCH 05/23] Optimizations --- src/listeners/general/GitListener.ts | 66 +++++++++++++------ src/panelWebView/components/Git/GitAction.tsx | 20 +++--- 2 files changed, 59 insertions(+), 27 deletions(-) diff --git a/src/listeners/general/GitListener.ts b/src/listeners/general/GitListener.ts index 9405b8e3..3c620ee8 100644 --- a/src/listeners/general/GitListener.ts +++ b/src/listeners/general/GitListener.ts @@ -48,6 +48,15 @@ export class GitListener { private static repository: GitRepository | null = null; private static branchName: string | null = null; + /** + * Retrieves the Git settings. + * @returns {Promise<{ + * isGitRepo: boolean, + * actions: boolean, + * disabledBranches: string[], + * requiresCommitMessage: string[] + * }>} The Git settings. + */ public static async getSettings() { const gitActions = Settings.get(SETTING_GIT_ENABLED); if (gitActions) { @@ -110,13 +119,19 @@ export class GitListener { } } - public static async selectBranch() { + /** + * Selects the current branch in the Git repository. + * @returns {Promise} A promise that resolves when the branch command has been executed. + */ + public static async selectBranch(): Promise { const workspaceFolder = Folders.getWorkspaceFolder(); await commands.executeCommand('git.checkout', workspaceFolder); } /** - * Run the sync/fetch + * Synchronizes the local repository with the remote repository. + * @param commitMsg The commit message for the push operation. + * @param isSync Determines whether to perform a sync operation (default: true) or a fetch operation. */ public static async sync(commitMsg?: string, isSync: boolean = true) { try { @@ -138,8 +153,8 @@ export class GitListener { } /** - * Check if the current workspace is a git repository - * @returns + * Checks if the current workspace is a Git repository. + * @returns A boolean indicating whether the current workspace is a Git repository. */ public static async isGitRepository() { const git = this.getClient(); @@ -159,8 +174,9 @@ export class GitListener { } /** - * Pull the changes from the remote - * @returns + * Pulls the latest changes from the remote repository. + * If submoduleFolder is specified, it checks out the submoduleBranch for the submodule located in that folder. + * If submodulePull is true, it also updates the submodules with the latest changes from the remote repository. */ private static async pull() { const git = this.getClient(); @@ -195,8 +211,10 @@ export class GitListener { } /** - * Push the changes to the remote - * @returns + * Pushes the changes to the remote repository. + * + * @param commitMsg The commit message to use. If not provided, it will use the default commit message or the one specified in the settings. + * @returns A promise that resolves when the push operation is completed. */ private static async push(commitMsg?: string) { commitMsg = @@ -273,9 +291,11 @@ export class GitListener { } /** - * Get the git client - * @param submoduleFolder - * @returns + * Retrieves the Git client instance based on the provided submodule folder. + * If no submodule folder is provided, it returns the main Git client instance. + * If a submodule folder is provided, it returns the submodule-specific Git client instance. + * @param submoduleFolder The path to the submodule folder. + * @returns The Git client instance or null if it cannot be retrieved. */ private static getClient(submoduleFolder: string = ''): SimpleGit | null { if (!submoduleFolder && this.client) { @@ -301,16 +321,19 @@ export class GitListener { } } - private static async vscodeGitProvider() { + /** + * Initializes the VS Code Git provider and sets up event listeners for repository changes. + * @returns {Promise} A promise that resolves when the Git provider is initialized. + */ + private static async vscodeGitProvider(): Promise { if (!GitListener.gitAPI) { - const wsFolder = Folders.getWorkspaceFolder(); const extension = extensions.getExtension('vscode.git'); /** * Logic from: https://github.com/microsoft/vscode/blob/main/extensions/github/src/extension.ts * initializeGitExtension */ - if (wsFolder && extension) { + if (extension) { const gitExtension = extension.isActive ? extension.exports : await extension.activate(); // Get version 1 of the API @@ -337,6 +360,11 @@ export class GitListener { } } + /** + * Retrieves the branch name and sends a request. + * @param command - The command to send. + * @param requestId - The ID of the request. + */ private static async getBranch(command: string, requestId?: string) { if (!command || !requestId) { return; @@ -366,8 +394,8 @@ export class GitListener { } /** - * Trigger the branch change - * @param repo + * Triggers a branch change event for the specified Git repository. + * @param repo The Git repository to monitor for branch changes. */ private static async triggerBranchChange(repo: GitRepository | null) { if (repo && repo.state) { @@ -385,9 +413,9 @@ export class GitListener { } /** - * Send the message to the webview - * @param command - * @param payload + * Sends a message to the panel and the dashboard. + * @param command - The command to send. + * @param payload - The payload to send with the command. */ private static sendMsg(command: string, payload: any) { const extPath = Extension.getInstance().extensionPath; diff --git a/src/panelWebView/components/Git/GitAction.tsx b/src/panelWebView/components/Git/GitAction.tsx index 1e3cefab..2838f882 100644 --- a/src/panelWebView/components/Git/GitAction.tsx +++ b/src/panelWebView/components/Git/GitAction.tsx @@ -33,7 +33,7 @@ export const GitAction: React.FunctionComponent = ({ messageHandler.send(GeneralCommands.toVSCode.git.selectBranch) } - const messageListener = (message: MessageEvent>) => { + const messageListener = React.useCallback((message: MessageEvent>) => { const { command, payload } = message.data; if (command === GeneralCommands.toWebview.git.syncingStart) { @@ -46,7 +46,7 @@ export const GitAction: React.FunctionComponent = ({ } else if (command === GeneralCommands.toWebview.git.branchName) { setCrntBranch(payload || undefined); } - }; + }, []); const isCommitRequired = React.useMemo(() => { const requiresCommitMessage = settings?.git?.requiresCommitMessage || []; @@ -95,7 +95,13 @@ export const GitAction: React.FunctionComponent = ({ } return false; - }, [settings?.git?.disabledBranches, settings?.git?.requiresCommitMessage, crntBanch, commitMessage]) + }, [settings?.git?.disabledBranches, settings?.git?.requiresCommitMessage, crntBanch, commitMessage]); + + const fetchBranch = React.useCallback(() => { + messageHandler.request(GeneralCommands.toVSCode.git.getBranch).then((branch) => { + setCrntBranch(branch); + }); + }, []); useEffect(() => { Messenger.listen(messageListener); @@ -103,13 +109,11 @@ export const GitAction: React.FunctionComponent = ({ return () => { Messenger.unlisten(messageListener); }; - }, []); + }, [messageListener]); useEffect(() => { - messageHandler.request(GeneralCommands.toVSCode.git.getBranch).then((branch) => { - setCrntBranch(branch); - }); - }, []); + fetchBranch(); + }, [fetchBranch]); if (!settings?.git?.actions || !settings?.git.isGitRepo) { return null; From b1380388b620bc3cf9e7a96866d4acd28c3496dc Mon Sep 17 00:00:00 2001 From: Elio Struyf Date: Tue, 23 Jan 2024 20:10:48 +0100 Subject: [PATCH 06/23] Issue: Open Preview button stops working #738 --- CHANGELOG.md | 1 + src/commands/Preview.ts | 3 +++ 2 files changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 39f0ecbb..f9c9c6d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ - [#721](https://github.com/estruyf/vscode-front-matter/issues/721): Fix keywords regex to support unicode characters - [#725](https://github.com/estruyf/vscode-front-matter/issues/725): Fix for opening menu of pinned items - [#730](https://github.com/estruyf/vscode-front-matter/issues/730): Add debounce to the input fields +- [#738](https://github.com/estruyf/vscode-front-matter/issues/738): Fix when re-opening the preview after closing it ## [9.4.0] - 2023-12-12 - [Release notes](https://beta.frontmatter.codes/updates/v9.4.0) diff --git a/src/commands/Preview.ts b/src/commands/Preview.ts index 0f28e902..835b6030 100644 --- a/src/commands/Preview.ts +++ b/src/commands/Preview.ts @@ -97,6 +97,9 @@ export class Preview { const cspSource = webView.webview.cspSource; webView.onDidDispose(() => { + if (crntFilePath && this.webviews[crntFilePath]) { + delete this.webviews[crntFilePath]; + } webView.dispose(); }); From 6e8421745861101ba73c0cb08458abeeefd195f6 Mon Sep 17 00:00:00 2001 From: Elio Struyf Date: Wed, 24 Jan 2024 09:53:09 +0100 Subject: [PATCH 07/23] #739 - localization keys + action button --- l10n/bundle.l10n.json | 4 ++ package.nls.json | 5 ++- src/localization/localization.enum.ts | 12 +++++ src/panelWebView/components/ActionButton.tsx | 9 ++-- .../Actions/OpenOnWebsiteAction.tsx | 4 +- src/panelWebView/components/CustomScript.tsx | 6 ++- src/panelWebView/components/Git/GitAction.tsx | 44 +++++++++---------- src/panelWebView/components/Preview.tsx | 6 ++- src/panelWebView/components/PublishAction.tsx | 4 +- src/panelWebView/components/SlugAction.tsx | 6 ++- 10 files changed, 68 insertions(+), 32 deletions(-) diff --git a/l10n/bundle.l10n.json b/l10n/bundle.l10n.json index 8a0bbae0..ab9f7d9e 100644 --- a/l10n/bundle.l10n.json +++ b/l10n/bundle.l10n.json @@ -329,6 +329,10 @@ "dashboard.configuration.astro.astroContentTypes.empty": "No Astro Content Collections found.", "dashboard.configuration.astro.astroContentTypes.description": "The following Astro Content Collections can be used to generate a content-type.", + "panel.git.gitAction.title": "Publish changes", + "panel.git.gitAction.input.placeholder": "Commit message", + "panel.git.gitAction.button.fetch": "Fetch", + "panel.contentType.contentTypeValidator.title": "Content-type", "panel.contentType.contentTypeValidator.hint": "We noticed field differences between the content-type and the front matter data. \n Would you like to create, update, or set the content-type for this content?", "panel.contentType.contentTypeValidator.button.create": "Create content-type", diff --git a/package.nls.json b/package.nls.json index de0ecc3a..f96cfdbe 100644 --- a/package.nls.json +++ b/package.nls.json @@ -250,5 +250,8 @@ "command.frontMatter.settings.refresh": "Refresh Front Matter Settings", "setting.frontMatter.config.dynamicFilePath.markdownDescription": "Specify the path to the dynamic config file (ex: [[workspace]]/config.js). [Check in the docs](https://frontmatter.codes/docs/settings/overview#frontmatter.config.dynamicfilepath)", "setting.frontMatter.taxonomy.contentTypes.items.properties.allowAsSubContent.description": "Specify if the content type can be used as sub content.", - "setting.frontMatter.taxonomy.contentTypes.items.properties.isSubContent.description": "Specify if the content type is sub content." + "setting.frontMatter.taxonomy.contentTypes.items.properties.isSubContent.description": "Specify if the content type is sub content.", + + "setting.frontMatter.git.disableOnBranches.markdownDescription": "Specify the branches on which you want to disable the Git actions. [Check in the docs](https://frontmatter.codes/docs/settings/overview#frontmatter.git.disableonbranches)", + "setting.frontMatter.git.requiresCommitMessage.markdownDescription": "Specify if you want to require a commit message when publishing your changes for a specified branch. [Check in the docs](https://frontmatter.codes/docs/settings/overview#frontmatter.git.requirescommitmessage)" } \ No newline at end of file diff --git a/src/localization/localization.enum.ts b/src/localization/localization.enum.ts index 54e508de..0d379de2 100644 --- a/src/localization/localization.enum.ts +++ b/src/localization/localization.enum.ts @@ -1087,6 +1087,18 @@ export enum LocalizationKey { * The following Astro Content Collections can be used to generate a content-type. */ dashboardConfigurationAstroAstroContentTypesDescription = 'dashboard.configuration.astro.astroContentTypes.description', + /** + * Publish changes + */ + panelGitGitActionTitle = 'panel.git.gitAction.title', + /** + * Commit message + */ + panelGitGitActionInputPlaceholder = 'panel.git.gitAction.input.placeholder', + /** + * Fetch + */ + panelGitGitActionButtonFetch = 'panel.git.gitAction.button.fetch', /** * Content-type */ diff --git a/src/panelWebView/components/ActionButton.tsx b/src/panelWebView/components/ActionButton.tsx index 71e7ee73..9097ac03 100644 --- a/src/panelWebView/components/ActionButton.tsx +++ b/src/panelWebView/components/ActionButton.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; export interface IActionButtonProps { - title: JSX.Element | string; + title: string; className?: string; disabled?: boolean; onClick: (e: React.SyntheticEvent) => void; @@ -11,12 +11,13 @@ const ActionButton: React.FunctionComponent = ({ className, onClick, disabled, - title + title, + children }: React.PropsWithChildren) => { return (
-
); diff --git a/src/panelWebView/components/Actions/OpenOnWebsiteAction.tsx b/src/panelWebView/components/Actions/OpenOnWebsiteAction.tsx index 1d3dc3a0..c8d6b2c7 100644 --- a/src/panelWebView/components/Actions/OpenOnWebsiteAction.tsx +++ b/src/panelWebView/components/Actions/OpenOnWebsiteAction.tsx @@ -28,6 +28,8 @@ export const OpenOnWebsiteAction: React.FunctionComponent + onClick={open}> + {l10n.t(LocalizationKey.commonOpenOnWebsite)} + ); }; \ No newline at end of file diff --git a/src/panelWebView/components/CustomScript.tsx b/src/panelWebView/components/CustomScript.tsx index 11225982..a6c547d5 100644 --- a/src/panelWebView/components/CustomScript.tsx +++ b/src/panelWebView/components/CustomScript.tsx @@ -16,7 +16,11 @@ const CustomScript: React.FunctionComponent = ({ Messenger.send(CommandToCode.runCustomScript, { title, script }); }; - return ; + return ( + + {title} + + ); }; CustomScript.displayName = 'CustomScript'; diff --git a/src/panelWebView/components/Git/GitAction.tsx b/src/panelWebView/components/Git/GitAction.tsx index 2838f882..c8e71472 100644 --- a/src/panelWebView/components/Git/GitAction.tsx +++ b/src/panelWebView/components/Git/GitAction.tsx @@ -123,14 +123,14 @@ export const GitAction: React.FunctionComponent = ({

- Changes + {l10n.t(LocalizationKey.panelGitGitActionTitle)}

@@ -139,7 +139,7 @@ export const GitAction: React.FunctionComponent = ({ = ({ - - - {l10n.t(LocalizationKey.commonSync)} - -
- } - /> + title={l10n.t(LocalizationKey.commonSync)} + > +
+
+
- - - Fetch - -
- } - /> + title={l10n.t(LocalizationKey.panelGitGitActionButtonFetch)} + > +
+
+
); diff --git a/src/panelWebView/components/Preview.tsx b/src/panelWebView/components/Preview.tsx index 5b8629f1..60472e9e 100644 --- a/src/panelWebView/components/Preview.tsx +++ b/src/panelWebView/components/Preview.tsx @@ -12,7 +12,11 @@ const Preview: React.FunctionComponent = (_: React.PropsWithChild Messenger.send(CommandToCode.openPreview); }; - return ; + return ( + + {l10n.t(LocalizationKey.panelPreviewTitle)} + + ); }; Preview.displayName = 'Preview'; diff --git a/src/panelWebView/components/PublishAction.tsx b/src/panelWebView/components/PublishAction.tsx index d705363e..42f68fa0 100644 --- a/src/panelWebView/components/PublishAction.tsx +++ b/src/panelWebView/components/PublishAction.tsx @@ -23,7 +23,9 @@ const PublishAction: React.FunctionComponent = ( onClick={publish} className={`${draft ? '' : 'secondary'}`} title={draft ? l10n.t(LocalizationKey.panelPublishActionPublish) : l10n.t(LocalizationKey.panelPublishActionUnpublish)} - /> + > + {draft ? l10n.t(LocalizationKey.panelPublishActionPublish) : l10n.t(LocalizationKey.panelPublishActionUnpublish)} + ); }; diff --git a/src/panelWebView/components/SlugAction.tsx b/src/panelWebView/components/SlugAction.tsx index a7705a7a..880f8181 100644 --- a/src/panelWebView/components/SlugAction.tsx +++ b/src/panelWebView/components/SlugAction.tsx @@ -14,7 +14,11 @@ const SlugAction: React.FunctionComponent< Messenger.send(CommandToCode.updateSlug); }; - return ; + return ( + + {l10n.t(LocalizationKey.panelSlugActionTitle)} + + ); }; SlugAction.displayName = 'SlugAction'; From 7c4aa1d63ddee4be85d5172221db52885038d6ad Mon Sep 17 00:00:00 2001 From: Elio Struyf Date: Wed, 24 Jan 2024 09:57:08 +0100 Subject: [PATCH 08/23] Update changlog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f9c9c6d6..91336ae8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ ### 🎨 Enhancements - [#727](https://github.com/estruyf/vscode-front-matter/pull/727): Updated Japanese translations thanks to [mayumihara](https://github.com/mayumih387) +- [#739](https://github.com/estruyf/vscode-front-matter/pull/739): New Git settings to disable and require a commit message ### ⚡️ Optimizations From 83cf0eb8f595f2f94ed63fdc3c5a8e1fa3bc9093 Mon Sep 17 00:00:00 2001 From: Elio Struyf Date: Mon, 29 Jan 2024 16:58:25 +0100 Subject: [PATCH 09/23] #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 10/23] #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 11/23] #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 12/23] #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 */ From 3c29df54c195b1ae19af758746b919a92a366f4c Mon Sep 17 00:00:00 2001 From: Elio Struyf Date: Mon, 12 Feb 2024 12:36:08 +0100 Subject: [PATCH 13/23] Updated changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a4fff10..929fa79c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,8 +10,8 @@ ### 🎨 Enhancements - [#727](https://github.com/estruyf/vscode-front-matter/pull/727): Updated Japanese translations thanks to [mayumihara](https://github.com/mayumih387) -- [#739](https://github.com/estruyf/vscode-front-matter/pull/739): New Git settings to disable and require a commit message - [#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 - [#741](https://github.com/estruyf/vscode-front-matter/issues/741): Added message on the content dashboard when content is processed - [#747](https://github.com/estruyf/vscode-front-matter/issues/747): The `@frontmatter/extensibility` dependency now supports scripts for placeholders From 6cbf86f822a49bf6a66cacb6f3e5f7a5f698ede8 Mon Sep 17 00:00:00 2001 From: Elio Struyf Date: Mon, 12 Feb 2024 13:02:39 +0100 Subject: [PATCH 14/23] Add is repo --- src/constants/GeneralCommands.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/constants/GeneralCommands.ts b/src/constants/GeneralCommands.ts index 3deef171..e761c335 100644 --- a/src/constants/GeneralCommands.ts +++ b/src/constants/GeneralCommands.ts @@ -11,6 +11,7 @@ export const GeneralCommands = { toVSCode: { openLink: 'openLink', git: { + isRepo: 'gitIsRepo', sync: 'gitSync', fetch: 'getFetch', getBranch: 'getBranch', From a29a6600ab62662468edf96fa78aa4b00ebfbc49 Mon Sep 17 00:00:00 2001 From: Elio Struyf Date: Mon, 12 Feb 2024 13:05:26 +0100 Subject: [PATCH 15/23] Update welcome view --- .../components/Steps/StepsToGetStarted.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/dashboardWebView/components/Steps/StepsToGetStarted.tsx b/src/dashboardWebView/components/Steps/StepsToGetStarted.tsx index 2e1c3fc5..2be2adb3 100644 --- a/src/dashboardWebView/components/Steps/StepsToGetStarted.tsx +++ b/src/dashboardWebView/components/Steps/StepsToGetStarted.tsx @@ -255,7 +255,7 @@ export const StepsToGetStarted: React.FunctionComponent
), show: isGitRepo, - status: settings.git.actions ? Status.Completed : Status.NotStarted + status: settings.git?.actions ? Status.Completed : Status.NotStarted }, { id: `welcome-import`, @@ -293,12 +293,12 @@ export const StepsToGetStarted: React.FunctionComponent }, [settings.crntFramework, settings.framework]); React.useEffect(() => { - messageHandler.request(GeneralCommands.toVSCode.gitIsRepo).then((result) => { + messageHandler.request(GeneralCommands.toVSCode.git.isRepo).then((result) => { setIsGitRepo(result); }); - setIsGitEnabled(settings.git.actions); - }, [settings.git.actions]); + setIsGitEnabled(settings.git?.actions || false); + }, [settings.git?.actions]); React.useEffect(() => { const fetchTemplates = async () => { From c4d3f76510fb1849db7fab6e503d2587ac603035 Mon Sep 17 00:00:00 2001 From: Elio Struyf Date: Mon, 12 Feb 2024 13:43:26 +0100 Subject: [PATCH 16/23] Fix menu positioning --- package-lock.json | 2 +- package.json | 285 ++++++++++-------- .../components/Media/Item.tsx | 12 +- .../components/Media/Media.tsx | 4 +- .../components/Menu/ActionMenuButton.tsx | 3 +- 5 files changed, 173 insertions(+), 133 deletions(-) diff --git a/package-lock.json b/package-lock.json index 54476200..2cd78a77 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "@actions/core": "^1.10.0", "@bendera/vscode-webview-elements": "0.6.2", "@estruyf/vscode": "^1.1.0", - "@headlessui/react": "^1.7.17", + "@headlessui/react": "^1.7.18", "@heroicons/react": "^2.1.1", "@iarna/toml": "2.2.3", "@octokit/rest": "^18.12.0", diff --git a/package.json b/package.json index ac054b33..b379ba0c 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,8 @@ "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" @@ -70,7 +71,8 @@ "**/.frontmatter/config/*.json": "jsonc" } }, - "keybindings": [{ + "keybindings": [ + { "command": "frontMatter.dashboard", "key": "alt+d" }, @@ -88,19 +90,23 @@ } ], "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%", @@ -168,7 +174,8 @@ "frontMatter.content.defaultFileType": { "type": "string", "default": "md", - "oneOf": [{ + "oneOf": [ + { "enum": [ "md", "mdx" @@ -184,7 +191,8 @@ "frontMatter.content.defaultSorting": { "type": "string", "default": "", - "oneOf": [{ + "oneOf": [ + { "enum": [ "LastModifiedAsc", "LastModifiedDesc", @@ -561,7 +569,8 @@ "command": { "$id": "#scriptCommand", "type": "string", - "anyOf": [{ + "anyOf": [ + { "enum": [ "node", "bash", @@ -768,7 +777,8 @@ "title", "file" ], - "anyOf": [{ + "anyOf": [ + { "required": [ "schema" ] @@ -822,7 +832,8 @@ "id", "path" ], - "anyOf": [{ + "anyOf": [ + { "required": [ "schema" ] @@ -1239,7 +1250,8 @@ "default": "", "description": "%setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.taxonomyId.description%", "not": { - "anyOf": [{ + "anyOf": [ + { "const": "" }, { @@ -1433,7 +1445,8 @@ "type", "name" ], - "allOf": [{ + "allOf": [ + { "if": { "properties": { "type": { @@ -1641,48 +1654,51 @@ "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": { @@ -1695,7 +1711,8 @@ "type": "string", "description": "%setting.frontMatter.taxonomy.customTaxonomy.items.properties.id.description%", "not": { - "anyOf": [{ + "anyOf": [ + { "const": "" }, { @@ -1892,7 +1909,8 @@ } } }, - "commands": [{ + "commands": [ + { "command": "frontMatter.project.switch", "title": "%command.frontMatter.project.switch%", "category": "Front Matter", @@ -2209,16 +2227,21 @@ "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" @@ -2299,11 +2322,14 @@ "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" @@ -2319,7 +2345,8 @@ "group": "frontmatter@3" } ], - "commandPalette": [{ + "commandPalette": [ + { "command": "frontMatter.init", "when": "frontMatterCanInit" }, @@ -2468,7 +2495,8 @@ "when": "frontMatter:file:isValid == true" } ], - "view/title": [{ + "view/title": [ + { "command": "frontMatter.chatbot", "group": "navigation@0", "when": "view == frontMatter.explorer" @@ -2500,52 +2528,57 @@ } ] }, - "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" + "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" + ] }, - "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.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": [ - "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" - ] - } - ] - }] + { + "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:*", @@ -2573,7 +2606,7 @@ "@actions/core": "^1.10.0", "@bendera/vscode-webview-elements": "0.6.2", "@estruyf/vscode": "^1.1.0", - "@headlessui/react": "^1.7.17", + "@headlessui/react": "^1.7.18", "@heroicons/react": "^2.1.1", "@iarna/toml": "2.2.3", "@octokit/rest": "^18.12.0", @@ -2670,4 +2703,4 @@ "vsce": { "dependencies": false } -} \ No newline at end of file +} diff --git a/src/dashboardWebView/components/Media/Item.tsx b/src/dashboardWebView/components/Media/Item.tsx index dc45b7cc..a2092526 100644 --- a/src/dashboardWebView/components/Media/Item.tsx +++ b/src/dashboardWebView/components/Media/Item.tsx @@ -42,10 +42,12 @@ import { LocalizationKey } from '../../../localization'; export interface IItemProps { media: MediaInfo; + index: number; } export const Item: React.FunctionComponent = ({ - media + media, + index }: React.PropsWithChildren) => { const [, setLightbox] = useRecoilState(LightboxAtom); const [showAlert, setShowAlert] = useState(false); @@ -68,7 +70,7 @@ export const Item: React.FunctionComponent = ({ const [referenceElement, setReferenceElement] = useState(null); const [popperElement, setPopperElement] = useState(null); - const { styles, attributes } = usePopper(referenceElement, popperElement, { + const { styles, attributes, update } = usePopper(referenceElement, popperElement, { placement: 'bottom-end', strategy: 'fixed' }); @@ -404,6 +406,12 @@ export const Item: React.FunctionComponent = ({ setMediaData(undefined); }; + useEffect(() => { + if (update) { + update(); + } + }, [update, index]); + useEffect(() => { if (media.alt !== alt) { setAlt(media.alt); diff --git a/src/dashboardWebView/components/Media/Media.tsx b/src/dashboardWebView/components/Media/Media.tsx index 823830cb..cfab4915 100644 --- a/src/dashboardWebView/components/Media/Media.tsx +++ b/src/dashboardWebView/components/Media/Media.tsx @@ -250,8 +250,8 @@ export const Media: React.FunctionComponent = ( )} - {allMedia.map((file) => ( - + {allMedia.map((file, idx) => ( + ))} diff --git a/src/dashboardWebView/components/Menu/ActionMenuButton.tsx b/src/dashboardWebView/components/Menu/ActionMenuButton.tsx index 641056f6..96c0e526 100644 --- a/src/dashboardWebView/components/Menu/ActionMenuButton.tsx +++ b/src/dashboardWebView/components/Menu/ActionMenuButton.tsx @@ -18,8 +18,7 @@ export const ActionMenuButton: React.FunctionComponent = ref={ref || null} onClick={(e: React.MouseEvent) => e.stopPropagation()} disabled={disabled} - className={`group inline-flex justify-center text-sm font-medium text-[var(--vscode-tab-inactiveForeground)] hover:text-[var(--vscode-tab-activeForeground)] ${disabled ? 'opacity-50' : '' - }`} + className={`group inline-flex justify-center text-sm font-medium text-[var(--vscode-tab-inactiveForeground)] hover:text-[var(--vscode-tab-activeForeground)] ${disabled ? 'opacity-50' : ''}`} > {title}