Merge branch 'dev' into issue/666

This commit is contained in:
Elio Struyf
2024-02-13 09:13:50 +01:00
68 changed files with 1200 additions and 339 deletions
+2 -2
View File
@@ -13,8 +13,8 @@ jobs:
name: Beta
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 18
registry-url: https://registry.npmjs.org/
+2 -2
View File
@@ -13,8 +13,8 @@ jobs:
name: Stable
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 18
registry-url: https://registry.npmjs.org/
+4
View File
@@ -5,14 +5,18 @@
### ✨ New features
- [#731](https://github.com/estruyf/vscode-front-matter/issues/731): Added the ability to map/unmap taxonomy to multiple pages at once
- [#746](https://github.com/estruyf/vscode-front-matter/issues/746): Placeholder support added to to the `slug` field
- [#749](https://github.com/estruyf/vscode-front-matter/issues/749): Ability to set your own filters on the content dashboard with the `frontMatter.content.filters` setting
### 🎨 Enhancements
- [#673](https://github.com/estruyf/vscode-front-matter/pull/673): Added git settings to the welcome view and settings view
- [#727](https://github.com/estruyf/vscode-front-matter/pull/727): Updated Japanese translations thanks to [mayumihara](https://github.com/mayumih387)
- [#737](https://github.com/estruyf/vscode-front-matter/issues/737): Optimize the grid layout of the content and media dashboards
- [#739](https://github.com/estruyf/vscode-front-matter/pull/739): New Git settings to disable and require a commit message
- [#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
- [#752](https://github.com/estruyf/vscode-front-matter/issues/752): Placeholder support in default `list` field values
### ⚡️ Optimizations
+11
View File
@@ -50,6 +50,11 @@
"settings.diagnostic": "Diagnostic",
"settings.diagnostic.description": "You can run the diagnostics to check the whole Front Matter CMS configuration.",
"settings.diagnostic.link": "Run full diagnostics",
"settings.git": "Git synchronization",
"settings.git.enabled": "Enable Git synchronization to easily sync your changes with your repository.",
"settings.git.commitMessage": "Commit message",
"settings.git.submoduleInfo": "When working with Git submodules, you can refer to the submodule settings in the documentation.",
"settings.git.submoduleLink": "Read more about Git submodules",
"settings.commonSettings.website.title": "Website and SSG settings",
"settings.commonSettings.previewUrl": "Preview URL",
@@ -278,6 +283,8 @@
"dashboard.steps.stepsToGetStarted.contentFolders.information.description": "You can also perform this action by right-clicking on the folder in the explorer view, and selecting register folder",
"dashboard.steps.stepsToGetStarted.tags.name": "Import all tags and categories (optional)",
"dashboard.steps.stepsToGetStarted.tags.description": "Now that Front Matter knows all the content folders. Would you like to import all tags and categories from the available content?",
"dashboard.steps.stepsToGetStarted.git.name": "Do you want to enable Git synchronization?",
"dashboard.steps.stepsToGetStarted.git.description": "Enable Git synchronization to eaily sync your changes with your repository.",
"dashboard.steps.stepsToGetStarted.showDashboard.name": "Show the dashboard",
"dashboard.steps.stepsToGetStarted.showDashboard.description": "Once all actions are completed, the dashboard can be loaded.",
"dashboard.steps.stepsToGetStarted.template.name": "Use a configuration template",
@@ -332,6 +339,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",
+1 -1
View File
@@ -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",
+30 -1
View File
@@ -887,6 +887,22 @@
"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.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%",
@@ -1654,6 +1670,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": "",
@@ -1892,6 +1916,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%",
@@ -2610,7 +2639,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",
+6 -1
View File
@@ -211,6 +211,7 @@
"setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.when.properties.caseSensitive.description": "Specify if the comparison is case sensitive. Default: true",
"setting.frontMatter.taxonomy.contentTypes.items.properties.pageBundle.description": "Specify if you want to create a folder when creating new content.",
"setting.frontMatter.taxonomy.contentTypes.items.properties.previewPath.description": "Defines a custom preview path for the content type.",
"setting.frontMatter.taxonomy.contentTypes.items.properties.slugTemplate.description": "Defines a custom slug template for the content type.",
"setting.frontMatter.taxonomy.contentTypes.items.properties.template.description": "An optional template that can be used for creating new content.",
"setting.frontMatter.taxonomy.contentTypes.items.properties.postScript.description": "An optional post script that can be used after new content creation.",
"setting.frontMatter.taxonomy.contentTypes.items.properties.filePrefix.description": "Defines a prefix for the file name.",
@@ -237,6 +238,7 @@
"setting.frontMatter.taxonomy.seoTitleLength.markdownDescription": "Specifies the optimal title length for SEO (set to `-1` to turn it off). [Check in the docs](https://frontmatter.codes/docs/settings/overview#frontmatter.taxonomy.seotitlelength)",
"setting.frontMatter.taxonomy.slugPrefix.markdownDescription": "Specify a prefix for the slug. [Check in the docs](https://frontmatter.codes/docs/settings/overview#frontmatter.taxonomy.slugprefix)",
"setting.frontMatter.taxonomy.slugSuffix.markdownDescription": "Specify a suffix for the slug. [Check in the docs](https://frontmatter.codes/docs/settings/overview#frontmatter.taxonomy.slugsuffix)",
"setting.frontMatter.taxonomy.slugTemplate.markdownDescription": "Defines a custom slug template for the content you will create. [Check in the docs](https://frontmatter.codes/docs/settings/overview#frontmatter.taxonomy.slugtemplate)",
"setting.frontMatter.taxonomy.tags.markdownDescription": "Specifies the tags which can be used in the Front Matter. [Check in the docs](https://frontmatter.codes/docs/settings/overview#frontmatter.taxonomy.tags)",
"setting.frontMatter.telemetry.disable.markdownDescription": "Specify if you want to disable the telemetry. [Check in the docs](https://frontmatter.codes/docs/settings/overview#frontmatter.telemetry.disable)",
"setting.frontMatter.templates.enabled.markdownDescription": "Specify if you want to use templates. [Check in the docs](https://frontmatter.codes/docs/settings/overview#frontmatter.templates.enabled)",
@@ -251,5 +253,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)"
}
+27 -14
View File
@@ -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,
+57 -3
View File
@@ -3,11 +3,13 @@ import {
CONTEXT,
ExtensionState,
SETTING_EXPERIMENTAL,
SETTING_EXTENSIBILITY_SCRIPTS
SETTING_EXTENSIBILITY_SCRIPTS,
COMMAND_NAME,
TelemetryEvent
} from '../constants';
import { join } from 'path';
import { commands, Uri, ViewColumn, Webview, WebviewPanel, window } from 'vscode';
import { DashboardSettings, Logger, Settings as SettingsHelper } from '../helpers';
import { DashboardSettings, Logger, Settings as SettingsHelper, Telemetry } from '../helpers';
import { DashboardCommand } from '../dashboardWebView/DashboardCommand';
import { Extension } from '../helpers/Extension';
import { WebviewHelper } from '@estruyf/vscode';
@@ -31,6 +33,8 @@ 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';
import { NavigationType } from '../dashboardWebView/models';
export class Dashboard {
private static webview: WebviewPanel | null = null;
@@ -51,6 +55,56 @@ export class Dashboard {
}
}
public static registerCommands() {
const subscriptions = Extension.getInstance().subscriptions;
subscriptions.push(
commands.registerCommand(COMMAND_NAME.dashboard, (data?: DashboardData) => {
Telemetry.send(TelemetryEvent.openContentDashboard);
if (!data) {
Dashboard.open({ type: NavigationType.Contents });
} else {
Dashboard.open(data);
}
})
);
subscriptions.push(
commands.registerCommand(COMMAND_NAME.dashboardMedia, () => {
Telemetry.send(TelemetryEvent.openMediaDashboard);
Dashboard.open({ type: NavigationType.Media });
})
);
subscriptions.push(
commands.registerCommand(COMMAND_NAME.dashboardSnippets, () => {
Telemetry.send(TelemetryEvent.openSnippetsDashboard);
Dashboard.open({ type: NavigationType.Snippets });
})
);
subscriptions.push(
commands.registerCommand(COMMAND_NAME.dashboardData, () => {
Telemetry.send(TelemetryEvent.openDataDashboard);
Dashboard.open({ type: NavigationType.Data });
})
);
subscriptions.push(
commands.registerCommand(COMMAND_NAME.dashboardTaxonomy, () => {
Telemetry.send(TelemetryEvent.openTaxonomyDashboard);
Dashboard.open({ type: NavigationType.Taxonomy });
})
);
subscriptions.push(
commands.registerCommand(COMMAND_NAME.dashboardClose, () => {
Telemetry.send(TelemetryEvent.closeDashboard);
Dashboard.close();
})
);
}
/**
* Open or reveal the dashboard
*/
@@ -204,7 +258,7 @@ export class Dashboard {
* @param msg
*/
public static postWebviewMessage(msg: {
command: DashboardCommand;
command: DashboardCommand | DashboardMessage;
requestId?: string;
payload?: unknown;
error?: unknown;
+2 -2
View File
@@ -13,7 +13,7 @@ import { ContentFolder, FileInfo, FolderInfo, StaticFolder } from '../models';
import uniqBy = require('lodash.uniqby');
import { Template } from './Template';
import { Notifications } from '../helpers/Notifications';
import { Logger, processKnownPlaceholders, Settings } from '../helpers';
import { Logger, Settings, processTimePlaceholders } from '../helpers';
import { existsSync } from 'fs';
import { format } from 'date-fns';
import { Dashboard } from './Dashboard';
@@ -377,7 +377,7 @@ export class Folders {
let folderPath: string | undefined = Folders.absWsFolder(folder, wsFolder);
if (folderPath.includes(`{{`) && folderPath.includes(`}}`)) {
const dateFormat = Settings.get(SETTING_DATE_FORMAT) as string;
folderPath = processKnownPlaceholders(folderPath, undefined, dateFormat);
folderPath = processTimePlaceholders(folderPath, dateFormat);
} else {
if (folderPath && !existsSync(folderPath)) {
Notifications.errorShowOnce(
+3 -3
View File
@@ -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.<field>}}
pathname = processFmPlaceholders(pathname, article?.data);
pathname = article?.data ? processFmPlaceholders(pathname, article?.data) : pathname;
try {
const articleDate = ArticleHelper.getDate(article);
+12 -3
View File
@@ -1,13 +1,22 @@
export const GeneralCommands = {
toWebview: {
setMode: 'setMode',
gitSyncingStart: 'gitSyncingStart',
gitSyncingEnd: 'gitSyncingEnd',
git: {
syncingStart: 'gitSyncingStart',
syncingEnd: 'gitSyncingEnd',
branchName: 'gitBranchName'
},
setLocalization: 'setLocalization'
},
toVSCode: {
openLink: 'openLink',
gitSync: 'gitSync',
git: {
isRepo: 'gitIsRepo',
sync: 'gitSync',
fetch: 'getFetch',
getBranch: 'getBranch',
selectBranch: 'gitSelectBranch'
},
getLocalization: 'getLocalization',
openOnWebsite: 'openOnWebsite'
}
+3
View File
@@ -0,0 +1,3 @@
export const GIT_CONFIG = {
defaultCommitMessage: 'Synced by Front Matter'
};
+2
View File
@@ -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';
+2 -1
View File
@@ -49,5 +49,6 @@ export const TelemetryEvent = {
webviewTaxonomyDashboard: 'webviewTaxonomyDashboard',
// Git
gitSync: 'gitSync'
gitSync: 'gitSync',
gitFetch: 'gitFetch'
};
+1
View File
@@ -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';
+3
View File
@@ -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';
@@ -100,6 +101,8 @@ 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_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';
+1
View File
@@ -54,6 +54,7 @@ export enum DashboardMessage {
insertSnippet = 'insertSnippet',
addSnippet = 'addSnippet',
updateSnippet = 'updateSnippet',
updateSnippetPlaceholders = 'updateSnippetPlaceholders',
// Taxonomy dashboard
getTaxonomyData = 'getTaxonomyData',
@@ -164,7 +164,7 @@ export const Header: React.FunctionComponent<IHeaderProps> = ({
<Searchbox />
<div className={`flex items-center justify-end space-x-4 flex-1`}>
<SyncButton />
{/* <SyncButton /> */}
<ChoiceButton
title={l10n.t(LocalizationKey.dashboardHeaderHeaderCreateContent)}
@@ -18,15 +18,15 @@ export const SyncButton: React.FunctionComponent<ISyncButtonProps> = (
const [isSyncing, setIsSyncing] = useState(false);
const pull = () => {
Messenger.send(GeneralCommands.toVSCode.gitSync);
Messenger.send(GeneralCommands.toVSCode.git.sync);
};
const messageListener = (message: MessageEvent<EventData<any>>) => {
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);
}
};
+10 -2
View File
@@ -42,10 +42,12 @@ import { LocalizationKey } from '../../../localization';
export interface IItemProps {
media: MediaInfo;
index: number;
}
export const Item: React.FunctionComponent<IItemProps> = ({
media
media,
index
}: React.PropsWithChildren<IItemProps>) => {
const [, setLightbox] = useRecoilState(LightboxAtom);
const [showAlert, setShowAlert] = useState(false);
@@ -66,7 +68,7 @@ export const Item: React.FunctionComponent<IItemProps> = ({
const [referenceElement, setReferenceElement] = useState<any>(null);
const [popperElement, setPopperElement] = useState<any>(null);
const { styles, attributes } = usePopper(referenceElement, popperElement, {
const { styles, attributes, update } = usePopper(referenceElement, popperElement, {
placement: 'bottom-end',
strategy: 'fixed'
});
@@ -400,6 +402,12 @@ export const Item: React.FunctionComponent<IItemProps> = ({
setMediaData(undefined);
};
useEffect(() => {
if (update) {
update();
}
}, [update, index]);
useEffect(() => {
const name = basename(parseWinPath(media.fsPath) || '');
if (name !== filename) {
@@ -250,8 +250,8 @@ export const Media: React.FunctionComponent<IMediaProps> = (
)}
<List>
{allMedia.map((file) => (
<Item key={file.fsPath} media={file} />
{allMedia.map((file, idx) => (
<Item key={file.fsPath} media={file} index={idx} />
))}
</List>
</div>
@@ -18,8 +18,7 @@ export const ActionMenuButton: React.FunctionComponent<IActionMenuButtonProps> =
ref={ref || null}
onClick={(e: React.MouseEvent<HTMLButtonElement>) => 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' : ''}`}
>
<span className="sr-only">{title}</span>
<EllipsisVerticalIcon className="w-4 h-4" aria-hidden="true" />
@@ -6,15 +6,17 @@ import { useRecoilValue } from 'recoil';
import { SettingsSelector } from '../../state';
import { SettingsInput } from './SettingsInput';
import { VSCodeButton } from '@vscode/webview-ui-toolkit/react';
import { FrameworkDetectors, SETTING_FRAMEWORK_START, SETTING_PREVIEW_HOST, SETTING_WEBSITE_URL } from '../../../constants';
import { DOCS_SUBMODULES, FrameworkDetectors, GIT_CONFIG, SETTING_FRAMEWORK_START, SETTING_GIT_COMMIT_MSG, SETTING_GIT_ENABLED, SETTING_PREVIEW_HOST, SETTING_WEBSITE_URL } from '../../../constants';
import { messageHandler } from '@estruyf/vscode/dist/client';
import { DashboardMessage } from '../../DashboardMessage';
import { SettingsCheckbox } from './SettingsCheckbox';
import { ChevronRightIcon } from '@heroicons/react/24/outline';
export interface ICommonSettingsProps { }
interface Config {
name: string;
value: string;
value: string | boolean;
}
export const CommonSettings: React.FunctionComponent<ICommonSettingsProps> = (props: React.PropsWithChildren<ICommonSettingsProps>) => {
@@ -22,7 +24,7 @@ export const CommonSettings: React.FunctionComponent<ICommonSettingsProps> = (pr
const [config, setConfig] = React.useState<Config[]>([]);
const [updated, setUpdated] = React.useState<boolean>(false);
const onSettingChange = React.useCallback((name: string, value: string) => {
const onSettingChange = React.useCallback((name: string, value: string | boolean) => {
setConfig((prev) => {
const setting = prev.find((c) => c.name === name);
if (setting) {
@@ -39,7 +41,13 @@ export const CommonSettings: React.FunctionComponent<ICommonSettingsProps> = (pr
}, [config]);
const retrieveSettings = () => {
messageHandler.request<Config[]>(DashboardMessage.getSettings, [SETTING_PREVIEW_HOST, SETTING_WEBSITE_URL, SETTING_FRAMEWORK_START]).then((config) => {
messageHandler.request<Config[]>(DashboardMessage.getSettings, [
SETTING_PREVIEW_HOST,
SETTING_WEBSITE_URL,
SETTING_FRAMEWORK_START,
SETTING_GIT_ENABLED,
SETTING_GIT_COMMIT_MSG,
]).then((config) => {
setConfig(config);
setUpdated(false);
});
@@ -65,6 +73,42 @@ export const CommonSettings: React.FunctionComponent<ICommonSettingsProps> = (pr
<Startup settings={settings} />
</div>
<div className='py-4'>
<h2 className='text-xl mb-2'>{l10n.t(LocalizationKey.settingsGit)}</h2>
<div className='space-y-2'>
<SettingsCheckbox
label={l10n.t(LocalizationKey.settingsGitEnabled)}
name={SETTING_GIT_ENABLED}
value={(config.find((c) => c.name === SETTING_GIT_ENABLED)?.value || false) as boolean}
onChange={onSettingChange}
/>
<SettingsInput
label={l10n.t(LocalizationKey.settingsGitCommitMessage)}
name={SETTING_GIT_COMMIT_MSG}
value={(config.find((c) => c.name === SETTING_GIT_COMMIT_MSG)?.value || "") as string}
placeholder={GIT_CONFIG.defaultCommitMessage}
onChange={onSettingChange}
/>
<p className={`text-[var(--frontmatter-secondary-text)] flex items-center`}>
<ChevronRightIcon className='h-4 w-4 inline' />
<span>
{l10n.t(LocalizationKey.settingsGitSubmoduleInfo)}&nbsp;
</span>
<a
href={DOCS_SUBMODULES}
title={l10n.t(LocalizationKey.settingsGitSubmoduleLink)}
className='text-[var(--vscode-textLink-foreground)] hover:text-[var(--vscode-textLink-activeForeground)]'>
{l10n.t(LocalizationKey.settingsGitSubmoduleLink)}
</a>
</p>
</div>
</div>
<div className='py-4'>
<h2 className='text-xl mb-2'>{l10n.t(LocalizationKey.settingsCommonSettingsWebsiteTitle)}</h2>
@@ -72,21 +116,21 @@ export const CommonSettings: React.FunctionComponent<ICommonSettingsProps> = (pr
<SettingsInput
label={l10n.t(LocalizationKey.settingsCommonSettingsPreviewUrl)}
name={SETTING_PREVIEW_HOST}
value={config.find((c) => c.name === SETTING_PREVIEW_HOST)?.value || ""}
value={(config.find((c) => c.name === SETTING_PREVIEW_HOST)?.value || "") as string}
onChange={onSettingChange}
/>
<SettingsInput
label={l10n.t(LocalizationKey.settingsCommonSettingsWebsiteUrl)}
name={SETTING_WEBSITE_URL}
value={config.find((c) => c.name === SETTING_WEBSITE_URL)?.value || ""}
value={(config.find((c) => c.name === SETTING_WEBSITE_URL)?.value || "") as string}
onChange={onSettingChange}
/>
<SettingsInput
label={l10n.t(LocalizationKey.settingsCommonSettingsStartCommand)}
name={SETTING_FRAMEWORK_START}
value={config.find((c) => 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 || ""}
/>
@@ -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<ISettingsCheckboxProps> = ({
label,
name,
value,
onChange
}: React.PropsWithChildren<ISettingsCheckboxProps>) => {
const [isEnabled, setIsEnabled] = React.useState(false);
const updateValue = (value: boolean) => {
setIsEnabled(value);
onChange(name, value);
}
React.useEffect(() => {
setIsEnabled(value);
}, [value]);
return (
<VSCodeCheckbox
onChange={(e: React.ChangeEvent<HTMLInputElement>) => updateValue(e.target.checked)}
checked={isEnabled}>
{label}
</VSCodeCheckbox>
);
};
@@ -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<ISettingsInputProps> = ({
label,
name,
value,
placeholder,
onChange,
fallback
}: React.PropsWithChildren<ISettingsInputProps>) => {
@@ -24,6 +26,7 @@ export const SettingsInput: React.FunctionComponent<ISettingsInputProps> = ({
boxShadow: 'none'
}}
value={value || fallback || ""}
placeholder={placeholder}
onInput={(e: React.ChangeEvent<HTMLInputElement>) => onChange(name, e.target.value)}>
{label}
</VSCodeTextField>
@@ -260,6 +260,7 @@ export const Item: React.FunctionComponent<IItemProps> = ({
ref={formRef}
snippetKey={snippetKey}
snippet={snippet}
filePath={viewData?.data?.filePath}
fieldInfo={viewData?.data?.snippetInfo?.fields}
selection={viewData?.data?.selection} />
</FormDialog>
@@ -1,8 +1,7 @@
import { Messenger } from '@estruyf/vscode/dist/client';
import { Messenger, messageHandler } from '@estruyf/vscode/dist/client';
import * as React from 'react';
import { useCallback, useEffect, useImperativeHandle, useMemo, useState } from 'react';
import { useRecoilValue } from 'recoil';
import { processKnownPlaceholders } from '../../../helpers/PlaceholderHelper';
import { SnippetParser } from '../../../helpers/SnippetParser';
import { Snippet, SnippetField, SnippetInfoField, SnippetSpecialPlaceholders } from '../../../models';
import { DashboardMessage } from '../../DashboardMessage';
@@ -14,6 +13,7 @@ export interface ISnippetFormProps {
snippetKey?: string;
snippet: Snippet;
selection: string | undefined;
filePath?: string;
fieldInfo?: SnippetInfoField[];
mediaData?: any;
onInsert?: (mediaData: any) => void;
@@ -24,7 +24,7 @@ export interface SnippetFormHandle {
}
const SnippetForm: React.ForwardRefRenderFunction<SnippetFormHandle, ISnippetFormProps> = (
{ snippetKey, snippet, selection, fieldInfo, mediaData, onInsert },
{ snippetKey, snippet, selection, filePath, fieldInfo, mediaData, onInsert },
ref
) => {
const viewData = useRecoilValue(ViewDataSelector);
@@ -41,20 +41,19 @@ const SnippetForm: React.ForwardRefRenderFunction<SnippetFormHandle, ISnippetFor
);
const insertPlaceholderValues = useCallback(
(value: SnippetSpecialPlaceholders) => {
async (value: SnippetSpecialPlaceholders) => {
if (value === 'FM_SELECTED_TEXT') {
return selection || '';
}
value = processKnownPlaceholders(
value = await messageHandler.request<string>(DashboardMessage.updateSnippetPlaceholders, {
value,
viewData?.data?.fileTitle || '',
settings?.date.format || ''
);
filePath
});
return value;
},
[selection]
[selection, filePath]
);
const insertValueFromMedia = useCallback(
@@ -128,7 +127,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`);
@@ -147,7 +146,7 @@ ${snippetBody}
if (idx > -1) {
allFields.push({
...field,
value: insertPlaceholderValues(field.default || '')
value: await insertPlaceholderValues(field.default || '')
});
}
}
@@ -167,6 +166,10 @@ ${snippetBody}
}
setFields(allFields);
}, [snippet, insertPlaceholderValues, insertValueFromMedia]);
useEffect(() => {
processFields();
}, [snippet]);
return (
@@ -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<IStepsToGetStartedProps>
const [loading, setLoading] = useState<boolean>(false);
const [framework, setFramework] = useState<string | null>(null);
const [taxImported, setTaxImported] = useState<boolean>(false);
const [isGitEnabled, setIsGitEnabled] = useState<boolean>(false);
const [isGitRepo, setIsGitRepo] = useState<boolean>(false);
const [templates, setTemplates] = useState<Template[]>([]);
const [astroCollectionsStatus, setAstroCollectionsStatus] = useState<Status>(Status.Optional);
@@ -73,6 +76,15 @@ export const StepsToGetStarted: React.FunctionComponent<IStepsToGetStartedProps>
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<IStepsToGetStartedProps>
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: (
<div className='mt-1'>
<VSCodeCheckbox
onChange={(e: React.ChangeEvent<HTMLInputElement>) => updateSetting(SETTING_GIT_ENABLED, e.target.checked)}
checked={isGitEnabled}>
{l10n.t(LocalizationKey.dashboardStepsStepsToGetStartedGitDescription)}
</VSCodeCheckbox>
</div>
),
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<IStepsToGetStartedProps>
: 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<IStepsToGetStartedProps>
}
}, [settings.crntFramework, settings.framework]);
React.useEffect(() => {
messageHandler.request<boolean>(GeneralCommands.toVSCode.git.isRepo).then((result) => {
setIsGitRepo(result);
});
setIsGitEnabled(settings.git?.actions || false);
}, [settings.git?.actions]);
React.useEffect(() => {
const fetchTemplates = async () => {
try {
+1 -1
View File
@@ -20,7 +20,7 @@ import { DataFile } from '../../models/DataFile';
export interface Settings {
projects: Project[];
project: Project;
git: GitSettings;
git: GitSettings | undefined;
beta: boolean;
initialized: boolean;
wsFolder: string;
+1 -45
View File
@@ -87,51 +87,7 @@ export async function activate(context: vscode.ExtensionContext) {
// Pages dashboard
Dashboard.init();
subscriptions.push(
vscode.commands.registerCommand(COMMAND_NAME.dashboard, (data?: DashboardData) => {
Telemetry.send(TelemetryEvent.openContentDashboard);
if (!data) {
Dashboard.open({ type: NavigationType.Contents });
} else {
Dashboard.open(data);
}
})
);
subscriptions.push(
vscode.commands.registerCommand(COMMAND_NAME.dashboardMedia, (data?: DashboardData) => {
Telemetry.send(TelemetryEvent.openMediaDashboard);
Dashboard.open({ type: NavigationType.Media });
})
);
subscriptions.push(
vscode.commands.registerCommand(COMMAND_NAME.dashboardSnippets, (data?: DashboardData) => {
Telemetry.send(TelemetryEvent.openSnippetsDashboard);
Dashboard.open({ type: NavigationType.Snippets });
})
);
subscriptions.push(
vscode.commands.registerCommand(COMMAND_NAME.dashboardData, (data?: DashboardData) => {
Telemetry.send(TelemetryEvent.openDataDashboard);
Dashboard.open({ type: NavigationType.Data });
})
);
subscriptions.push(
vscode.commands.registerCommand(COMMAND_NAME.dashboardTaxonomy, (data?: DashboardData) => {
Telemetry.send(TelemetryEvent.openTaxonomyDashboard);
Dashboard.open({ type: NavigationType.Taxonomy });
})
);
subscriptions.push(
vscode.commands.registerCommand(COMMAND_NAME.dashboardClose, (data?: DashboardData) => {
Telemetry.send(TelemetryEvent.closeDashboard);
Dashboard.close();
})
);
Dashboard.registerCommands();
if (!extension.getVersion().usedVersion) {
vscode.commands.executeCommand(COMMAND_NAME.dashboard);
+38 -6
View File
@@ -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;
+71 -16
View File
@@ -1,6 +1,13 @@
import { ModeListener } from './../listeners/general/ModeListener';
import { PagesListener } from './../listeners/dashboard';
import { ArticleHelper, CustomScript, Logger, Settings } from '.';
import {
ArticleHelper,
CustomScript,
Logger,
Settings,
processArticlePlaceholdersFromData,
processTimePlaceholders
} from '.';
import {
DefaultFieldValues,
EXTENSION_NAME,
@@ -26,7 +33,6 @@ import { Questions } from './Questions';
import { Notifications } from './Notifications';
import { DEFAULT_CONTENT_TYPE_NAME } from '../constants/ContentType';
import { Telemetry } from './Telemetry';
import { processKnownPlaceholders } from './PlaceholderHelper';
import { basename } from 'path';
import { ParsedFrontMatter } from '../parsers';
import { encodeEmoji, existsAsync, fieldWhenClause, writeFileAsync } from '../utils';
@@ -299,17 +305,17 @@ export class ContentType {
Telemetry.send(TelemetryEvent.addMissingFields);
const content = ArticleHelper.getCurrent();
const article = ArticleHelper.getCurrent();
if (!content || !content.data) {
if (!article || !article.data) {
Notifications.warning(
l10n.t(LocalizationKey.helpersContentTypeAddMissingFieldsNoFrontMatterWarning)
);
return;
}
const contentType = ArticleHelper.getContentType(content);
const updatedFields = ContentType.generateFields(content.data, contentType.fields);
const contentType = ArticleHelper.getContentType(article);
const updatedFields = ContentType.generateFields(article.data, contentType.fields);
const contentTypes = ContentType.getAll() || [];
const index = contentTypes.findIndex((ct) => ct.name === contentType.name);
@@ -927,7 +933,8 @@ export class ContentType {
titleValue,
templateData?.data || {},
newFilePath,
!!contentType.clearEmpty
!!contentType.clearEmpty,
contentType
);
const article: ParsedFrontMatter = {
@@ -982,6 +989,7 @@ export class ContentType {
data: any,
filePath: string,
clearEmpty: boolean,
contentType: IContentType,
isRoot: boolean = true
): Promise<any> {
if (obj.fields) {
@@ -995,9 +1003,13 @@ export class ContentType {
if (field.name === 'title') {
if (field.default) {
data[field.name] = processKnownPlaceholders(
field.default,
titleValue,
data[field.name] = processArticlePlaceholdersFromData(
field.default as string,
data,
contentType
);
data[field.name] = processTimePlaceholders(
data[field.name],
field.dateFormat || dateFormat
);
data[field.name] = await ArticleHelper.processCustomPlaceholders(
@@ -1018,6 +1030,7 @@ export class ContentType {
{},
filePath,
clearEmpty,
contentType,
false
);
@@ -1026,18 +1039,33 @@ export class ContentType {
}
} else {
const defaultValue = field.default;
console.log(field.name, defaultValue, Array.isArray(defaultValue));
if (typeof defaultValue === 'string') {
data[field.name] = processKnownPlaceholders(
data[field.name] = await ContentType.processFieldPlaceholders(
defaultValue,
titleValue,
field.dateFormat || dateFormat
);
data[field.name] = await ArticleHelper.processCustomPlaceholders(
data[field.name],
data,
contentType,
field.dateFormat || dateFormat,
titleValue,
filePath
);
} else if (defaultValue && Array.isArray(defaultValue)) {
let defaultValues = [];
for (let value of defaultValue as string[]) {
if (typeof value === 'string') {
value = await ContentType.processFieldPlaceholders(
value,
data,
contentType,
field.dateFormat || dateFormat,
titleValue,
filePath
);
}
defaultValues.push(value);
}
data[field.name] = defaultValues;
} else if (typeof defaultValue !== 'undefined') {
data[field.name] = defaultValue;
} else {
@@ -1082,6 +1110,7 @@ export class ContentType {
}
break;
case 'string':
case 'slug':
case 'image':
case 'file':
default:
@@ -1100,6 +1129,32 @@ export class ContentType {
return data;
}
/**
* Processes the field placeholders in the given value.
*
* @param defaultValue - The default value for the field.
* @param data - The data object containing the field values.
* @param contentType - The content type object.
* @param dateFormat - The date format string.
* @param title - The title string.
* @param filePath - The file path string.
* @returns The processed value with field placeholders replaced.
*/
private static async processFieldPlaceholders(
defaultValue: string,
data: any,
contentType: IContentType,
dateFormat: string,
title: string,
filePath: string
) {
let value = processArticlePlaceholdersFromData(defaultValue, data, contentType);
value = processTimePlaceholders(value, dateFormat);
value = await ArticleHelper.processCustomPlaceholders(value, title, filePath);
return value;
}
/**
* Verify if the content type feature is enabled
* @returns
+1 -5
View File
@@ -88,16 +88,12 @@ export class DashboardSettings {
const ext = Extension.getInstance();
const wsFolder = Folders.getWorkspaceFolder();
const isInitialized = await Project.isInitialized();
const gitActions = Settings.get<boolean>(SETTING_GIT_ENABLED);
const pagination = Settings.get<boolean | number>(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(),
+1 -7
View File
@@ -31,7 +31,6 @@ import {
SETTING_SLUG_UPDATE_FILE_NAME,
SETTING_TAXONOMY_CUSTOM,
SETTING_TAXONOMY_FIELD_GROUPS,
SETTING_GIT_ENABLED,
SETTING_SEO_TITLE_FIELD
} from '../constants';
import { GitListener } from '../listeners/general';
@@ -49,14 +48,9 @@ import { Folders } from '../commands';
export class PanelSettings {
public static async get(): Promise<IPanelSettings> {
const gitActions = Settings.get<boolean>(SETTING_GIT_ENABLED);
return {
aiEnabled: Settings.get<boolean>(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,
+35 -6
View File
@@ -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<string>(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 '';
}
/**
+2 -1
View File
@@ -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';
+53
View File
@@ -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<string> => {
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;
};
+1 -1
View File
@@ -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;
@@ -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');
+6 -1
View File
@@ -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,
+2 -2
View File
@@ -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);
}
}
+33 -2
View File
@@ -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);
}
}
+233 -43
View File
@@ -1,9 +1,18 @@
import {
COMMAND_NAME,
CONTEXT,
GIT_CONFIG,
SETTING_DATE_FORMAT,
SETTING_GIT_COMMIT_MSG,
SETTING_GIT_DISABLED_BRANCHES,
SETTING_GIT_ENABLED,
SETTING_GIT_REQUIRES_COMMIT_MSG,
SETTING_GIT_SUBMODULE_BRANCH,
SETTING_GIT_SUBMODULE_FOLDER,
SETTING_GIT_SUBMODULE_PULL,
SETTING_GIT_SUBMODULE_PUSH
} from './../../constants/settings';
SETTING_GIT_SUBMODULE_PUSH,
TelemetryEvent
} from './../../constants';
import { Settings } from './../../helpers/SettingsHelper';
import { Dashboard } from '../../commands/Dashboard';
import { PanelProvider } from '../../panelWebView/PanelProvider';
@@ -12,29 +21,58 @@ import {
Extension,
Logger,
Notifications,
processKnownPlaceholders,
parseWinPath,
processTimePlaceholders,
Telemetry
} from '../../helpers';
import { GeneralCommands } from './../../constants/GeneralCommands';
import simpleGit, { SimpleGit } from 'simple-git';
import {
COMMAND_NAME,
CONTEXT,
SETTING_DATE_FORMAT,
SETTING_GIT_COMMIT_MSG,
SETTING_GIT_ENABLED,
TelemetryEvent
} from '../../constants';
import { Folders } from '../../commands/Folders';
import { commands } from 'vscode';
import { 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: Event<GitAPIState>;
onDidOpenRepository: Event<GitRepository>;
onDidCloseRepository: Event<GitRepository>;
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;
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<boolean>(SETTING_GIT_ENABLED);
if (gitActions) {
return {
isGitRepo: gitActions ? await GitListener.isGitRepository() : false,
actions: gitActions || false,
disabledBranches: gitActions
? Settings.get<string[]>(SETTING_GIT_DISABLED_BRANCHES) || []
: [],
requiresCommitMessage: gitActions
? Settings.get<string[]>(SETTING_GIT_REQUIRES_COMMIT_MSG) || []
: []
};
}
return;
}
/**
* Initialize the listener
@@ -65,34 +103,68 @@ export class GitListener {
*/
public static process(msg: PostMessageData) {
switch (msg.command) {
case GeneralCommands.toVSCode.gitSync:
this.sync();
case GeneralCommands.toVSCode.git.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);
break;
case GeneralCommands.toVSCode.git.selectBranch:
this.selectBranch();
case GeneralCommands.toVSCode.git.isRepo:
this.checkIsGitRepo(msg.command, msg.requestId);
break;
}
}
/**
* Run the sync
*/
public static async sync() {
try {
this.sendMsg(GeneralCommands.toWebview.gitSyncingStart, {});
public static async checkIsGitRepo(command: string, requestId?: string) {
if (!command || !requestId) {
return;
}
Telemetry.send(TelemetryEvent.gitSync);
const isRepo = await GitListener.isGitRepository();
Dashboard.postWebviewMessage({ command: command as any, payload: isRepo, requestId });
}
/**
* Selects the current branch in the Git repository.
* @returns {Promise<void>} A promise that resolves when the branch command has been executed.
*/
public static async selectBranch(): Promise<void> {
const workspaceFolder = Folders.getWorkspaceFolder();
await commands.executeCommand('git.checkout', workspaceFolder);
}
/**
* 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 {
this.sendMsg(GeneralCommands.toWebview.git.syncingStart, isSync ? 'syncing' : 'fetching');
Telemetry.send(isSync ? TelemetryEvent.gitSync : TelemetryEvent.gitFetch);
await this.pull();
await this.push();
this.sendMsg(GeneralCommands.toWebview.gitSyncingEnd, {});
if (isSync) {
await this.push(commitMsg);
}
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, {});
}
}
/**
* 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();
@@ -106,12 +178,15 @@ export class GitListener {
Logger.warning(`Current workspace is not a GIT repository`);
}
GitListener.vscodeGitProvider();
return isRepo;
}
/**
* 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();
@@ -146,15 +221,18 @@ 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() {
let commitMsg = Settings.get<string>(SETTING_GIT_COMMIT_MSG);
private static async push(commitMsg?: string) {
commitMsg =
commitMsg || Settings.get<string>(SETTING_GIT_COMMIT_MSG) || 'Synced by Front Matter';
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);
}
@@ -175,7 +253,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) {
@@ -202,7 +280,7 @@ export class GitListener {
'git',
'commit',
'-m',
commitMsg || 'Synced by Front Matter'
commitMsg || GIT_CONFIG.defaultCommitMessage
]);
await git.subModule(['foreach', 'git', 'push']);
}
@@ -222,16 +300,18 @@ 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();
}
/**
* 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) {
@@ -258,9 +338,100 @@ export class GitListener {
}
/**
* Send the message to the webview
* @param command
* @param payload
* Initializes the VS Code Git provider and sets up event listeners for repository changes.
* @returns {Promise<void>} A promise that resolves when the Git provider is initialized.
*/
private static async vscodeGitProvider(): Promise<void> {
if (!GitListener.gitAPI) {
const extension = extensions.getExtension('vscode.git');
/**
* Logic from: https://github.com/microsoft/vscode/blob/main/extensions/github/src/extension.ts
* initializeGitExtension
*/
if (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);
});
GitListener.gitAPI.onDidCloseRepository((repo: GitRepository) => {
Logger.info(`Closed repo: ${repo?.state?.HEAD?.name}`);
});
}
}
}
/**
* 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;
}
this.sendRequest(command, requestId, GitListener.repository?.state?.HEAD.name);
}
private static listenToRepo(repositories: GitRepository[] | undefined) {
if (!repositories) {
return;
}
if (repositories && repositories.length === 1) {
GitListener.triggerBranchChange(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.triggerBranchChange(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) {
if (repo.state.HEAD.name !== GitListener.branchName) {
GitListener.branchName = repo.state.HEAD.name;
GitListener.repository = repo;
this.sendMsg(GeneralCommands.toWebview.git.branchName, GitListener.branchName);
repo.state.onDidChange(() => {
GitListener.triggerBranchChange(repo);
});
}
}
}
/**
* 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;
@@ -270,4 +441,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 });
}
}
+14 -5
View File
@@ -1,6 +1,6 @@
import { Article } from '../../commands';
import { ArticleHelper } from '../../helpers';
import { PostMessageData } from '../../models';
import { Command } from '../../panelWebView/Command';
import { CommandToCode } from '../../panelWebView/CommandToCode';
import { BaseListener } from './BaseListener';
@@ -17,7 +17,7 @@ export class ArticleListener extends BaseListener {
Article.updateSlug();
break;
case CommandToCode.generateSlug:
this.generateSlug(msg.payload);
this.generateSlug(msg.command, msg.payload, msg.requestId);
break;
case CommandToCode.updateLastMod:
Article.setLastModifiedDate();
@@ -32,10 +32,19 @@ export class ArticleListener extends BaseListener {
* Generate a slug
* @param title
*/
private static generateSlug(title: string) {
const slug = Article.generateSlug(title);
private static generateSlug(
command: CommandToCode,
{ title, slugTemplate }: { title: string; slugTemplate?: string },
requestId?: string
) {
if (!command || !requestId) {
return;
}
const article = ArticleHelper.getFrontMatterFromCurrentDocument();
const slug = Article.generateSlug(title, article, slugTemplate);
if (slug) {
this.sendMsg(Command.updatedSlug, slug);
this.sendRequest(command, requestId, slug);
}
}
}
+45 -10
View File
@@ -6,7 +6,16 @@ import { Command } from '../../panelWebView/Command';
import { CommandToCode } from '../../panelWebView/CommandToCode';
import { BaseListener } from './BaseListener';
import { authentication, commands, window } from 'vscode';
import { ArticleHelper, ContentType, Extension, Logger, Settings } from '../../helpers';
import {
ArticleHelper,
Extension,
Logger,
Settings,
ContentType,
processArticlePlaceholdersFromData,
processTimePlaceholders,
processFmPlaceholders
} from '../../helpers';
import {
COMMAND_NAME,
DefaultFields,
@@ -20,8 +29,7 @@ import {
} from '../../constants';
import { Article, Preview } from '../../commands';
import { ParsedFrontMatter } from '../../parsers';
import { processKnownPlaceholders } from '../../helpers/PlaceholderHelper';
import { Field, Mode, PostMessageData } from '../../models';
import { Field, Mode, PostMessageData, ContentType as IContentType } from '../../models';
import { encodeEmoji, fieldWhenClause } from '../../utils';
import { PanelProvider } from '../../panelWebView/PanelProvider';
import { MessageHandlerData } from '@estruyf/vscode';
@@ -66,7 +74,11 @@ export class DataListener extends BaseListener {
this.isServerStarted(msg.command, msg?.requestId);
break;
case CommandToCode.updatePlaceholder:
this.updatePlaceholder(msg?.payload?.field, msg?.payload?.value, msg?.payload?.title);
this.updatePlaceholder(
msg.command,
msg.payload as { field: string; value: string; data: { [key: string]: any } },
msg.requestId
);
break;
case CommandToCode.generateContentType:
commands.executeCommand(COMMAND_NAME.generateContentType);
@@ -211,7 +223,11 @@ export class DataListener extends BaseListener {
if (keys.length > 0 && contentTypes && wsFolder) {
// Get the current content type
const contentType = ArticleHelper.getContentType(updatedMetadata);
const contentType = ArticleHelper.getContentType({
content: '',
data: updatedMetadata,
path: filePath
});
let slugField;
if (contentType) {
ImageHelper.processImageFields(updatedMetadata, contentType.fields);
@@ -597,18 +613,37 @@ export class DataListener extends BaseListener {
* @param value
* @param title
*/
private static async updatePlaceholder(field: string, value: string, title: string) {
if (field && value) {
private static async updatePlaceholder(
command: CommandToCode,
articleData: {
field: string;
value: string;
data: { [key: string]: any };
contentType?: IContentType;
},
requestId?: string
) {
if (!command || !requestId || !articleData) {
return;
}
let { field, value, data, contentType } = articleData;
value = value || '';
if (field) {
const crntFile = window.activeTextEditor?.document;
const dateFormat = Settings.get(SETTING_DATE_FORMAT) as string;
value = processKnownPlaceholders(value, title || '', dateFormat);
value =
data && contentType ? processArticlePlaceholdersFromData(value, data, contentType) : value;
value = processTimePlaceholders(value, dateFormat);
value = processFmPlaceholders(value, data);
value = await ArticleHelper.processCustomPlaceholders(
value,
title || '',
data.title || '',
crntFile?.uri.fsPath || ''
);
}
this.sendMsg(Command.updatePlaceholder, { field, value });
this.sendRequest(Command.updatePlaceholder, requestId, { field, value });
}
}
+40
View File
@@ -191,6 +191,26 @@ export enum LocalizationKey {
* Run full diagnostics
*/
settingsDiagnosticLink = 'settings.diagnostic.link',
/**
* Git synchronization
*/
settingsGit = 'settings.git',
/**
* Enable Git synchronization to easily sync your changes with your repository.
*/
settingsGitEnabled = 'settings.git.enabled',
/**
* Commit message
*/
settingsGitCommitMessage = 'settings.git.commitMessage',
/**
* When working with Git submodules, you can refer to the submodule settings in the documentation.
*/
settingsGitSubmoduleInfo = 'settings.git.submoduleInfo',
/**
* Read more about Git submodules
*/
settingsGitSubmoduleLink = 'settings.git.submoduleLink',
/**
* Website and SSG settings
*/
@@ -919,6 +939,14 @@ export enum LocalizationKey {
* Now that Front Matter knows all the content folders. Would you like to import all tags and categories from the available content?
*/
dashboardStepsStepsToGetStartedTagsDescription = 'dashboard.steps.stepsToGetStarted.tags.description',
/**
* Do you want to enable Git synchronization?
*/
dashboardStepsStepsToGetStartedGitName = 'dashboard.steps.stepsToGetStarted.git.name',
/**
* Enable Git synchronization to eaily sync your changes with your repository.
*/
dashboardStepsStepsToGetStartedGitDescription = 'dashboard.steps.stepsToGetStarted.git.description',
/**
* Show the dashboard
*/
@@ -1095,6 +1123,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
*/
+2
View File
@@ -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;
+34
View File
@@ -0,0 +1,34 @@
import { Event } from 'vscode';
export type GitAPIState = 'uninitialized' | 'initialized';
export interface GitRepository {
state: GitRepositoryState;
rootUri: {
fsPath: string;
path: string;
};
repository: {
getBranches: () => Promise<GitBranch[]>;
};
}
export interface GitRepositoryState {
HEAD: GitBranch;
onDidChange: Event<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;
}
+2
View File
@@ -1,4 +1,6 @@
export interface GitSettings {
isGitRepo: boolean;
actions: boolean;
disabledBranches: string[];
requiresCommitMessage: string[];
}
+3 -2
View File
@@ -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[];
@@ -59,6 +59,7 @@ export interface ContentType {
fileType?: 'md' | 'mdx' | string;
previewPath?: string | null;
slugTemplate?: string;
pageBundle?: boolean;
defaultFileName?: string;
template?: string;
@@ -105,7 +106,7 @@ export interface Field {
isPreviewImage?: boolean;
hidden?: boolean;
taxonomyId?: string;
default?: string;
default?: string | number | string[] | boolean;
fields?: Field[];
fieldGroup?: string | string[];
dataType?: string | string[];
+1
View File
@@ -12,6 +12,7 @@ export * from './DataFolder';
export * from './DataType';
export * from './DraftField';
export * from './Framework';
export * from './GitRepository';
export * from './GitSettings';
export * from './LoadingType';
export * from './MediaContentType';
-1
View File
@@ -10,6 +10,5 @@ export enum Command {
sendMediaUrl = 'sendMediaUrl',
updatePlaceholder = 'updatePlaceholder',
dataFileEntries = 'dataFileEntries',
updatedSlug = 'updatedSlug',
serverStarted = 'server-started'
}
+29 -22
View File
@@ -83,7 +83,7 @@ export const ViewPanel: React.FunctionComponent<IViewPanelProps> = (
);
}
if (loading || !localeReady) {
if (loading && !localeReady) {
return <Spinner />;
}
@@ -116,37 +116,44 @@ export const ViewPanel: React.FunctionComponent<IViewPanelProps> = (
<div className={`ext_actions`}>
<GitAction settings={settings} />
<CustomView metadata={metadata} />
{!loading && (<CustomView metadata={metadata} />)}
<FeatureFlag features={mode?.features || [...allPanelValues]} flag={FEATURE_FLAG.panel.globalSettings}>
<GlobalSettings settings={settings} />
</FeatureFlag>
{settings && settings.seo && (
<FeatureFlag features={mode?.features || []} flag={FEATURE_FLAG.panel.seo}>
<SeoStatus
seo={settings.seo}
data={metadata}
focusElm={focusElm}
unsetFocus={unsetFocus}
/>
</FeatureFlag>
)}
{settings && metadata && (
{
!loading && settings && settings.seo && (
<FeatureFlag features={mode?.features || []} flag={FEATURE_FLAG.panel.seo}>
<SeoStatus
seo={settings.seo}
data={metadata}
focusElm={focusElm}
unsetFocus={unsetFocus}
/>
</FeatureFlag>
)
}
{!loading && settings && metadata && (
<FeatureFlag features={mode?.features || []} flag={FEATURE_FLAG.panel.actions}>
<Actions metadata={metadata} settings={settings} scripts={settings.scripts} />
</FeatureFlag>
)}
<FeatureFlag features={mode?.features || []} flag={FEATURE_FLAG.panel.metadata}>
<Metadata
settings={settings}
metadata={metadata}
focusElm={focusElm}
unsetFocus={unsetFocus}
features={mode?.features || []}
/>
</FeatureFlag>
{
!loading && (
<FeatureFlag features={mode?.features || []} flag={FEATURE_FLAG.panel.metadata}>
<Metadata
settings={settings}
metadata={metadata}
focusElm={focusElm}
unsetFocus={unsetFocus}
features={mode?.features || []}
/>
</FeatureFlag>
)
}
<FeatureFlag features={mode?.features || []} flag={FEATURE_FLAG.panel.recentlyModified}>
<FolderAndFiles data={folderAndFiles} />
+6 -5
View File
@@ -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<HTMLButtonElement>) => void;
@@ -11,12 +11,13 @@ const ActionButton: React.FunctionComponent<IActionButtonProps> = ({
className,
onClick,
disabled,
title
title,
children
}: React.PropsWithChildren<IActionButtonProps>) => {
return (
<div className={`article__action`}>
<button onClick={onClick} className={className || ''} disabled={disabled}>
{title}
<div className={`article__action w-full`}>
<button type="button" title={title} onClick={onClick} className={className || ''} disabled={disabled}>
{children}
</button>
</div>
);
@@ -28,6 +28,8 @@ export const OpenOnWebsiteAction: React.FunctionComponent<IOpenOnWebsiteActionPr
return (
<ActionButton
title={l10n.t(LocalizationKey.commonOpenOnWebsite)}
onClick={open} />
onClick={open}>
{l10n.t(LocalizationKey.commonOpenOnWebsite)}
</ActionButton>
);
};
+5 -1
View File
@@ -16,7 +16,11 @@ const CustomScript: React.FunctionComponent<ICustomScriptProps> = ({
Messenger.send(CommandToCode.runCustomScript, { title, script });
};
return <ActionButton onClick={runCustomScript} title={title} />;
return (
<ActionButton onClick={runCustomScript} title={title}>
{title}
</ActionButton>
);
};
CustomScript.displayName = 'CustomScript';
@@ -1,10 +1,8 @@
import { Messenger } from '@estruyf/vscode/dist/client';
import { EventData } from '@estruyf/vscode/dist/models';
import { Messenger, messageHandler } from '@estruyf/vscode/dist/client';
import { LinkIcon, ArrowPathIcon } from '@heroicons/react/24/outline';
import * as React from 'react';
import { useCallback, useEffect, useMemo } from 'react';
import { useEffect, useMemo } from 'react';
import { BaseFieldProps } from '../../../models';
import { Command } from '../../Command';
import { CommandToCode } from '../../CommandToCode';
import { FieldTitle } from './FieldTitle';
import { FieldMessage } from './FieldMessage';
@@ -14,6 +12,7 @@ import { LocalizationKey } from '../../../localization';
export interface ISlugFieldProps extends BaseFieldProps<string> {
titleValue: string | null;
editable?: boolean;
slugTemplate?: string;
onChange: (txtValue: string) => void;
}
@@ -23,6 +22,7 @@ export const SlugField: React.FunctionComponent<ISlugFieldProps> = ({
editable,
value,
titleValue,
slugTemplate,
onChange,
required
}: React.PropsWithChildren<ISlugFieldProps>) => {
@@ -38,16 +38,6 @@ export const SlugField: React.FunctionComponent<ISlugFieldProps> = ({
Messenger.send(CommandToCode.updateSlug);
};
const messageListener = useCallback(
(message: MessageEvent<EventData<any>>) => {
const { command, payload } = message.data;
if (command === Command.updatedSlug) {
setSlug(payload?.slugWithPrefixAndSuffix);
}
},
[text]
);
const showRequiredState = useMemo(() => {
return required && !text;
}, [required, text]);
@@ -60,17 +50,18 @@ export const SlugField: React.FunctionComponent<ISlugFieldProps> = ({
useEffect(() => {
if (titleValue) {
Messenger.send(CommandToCode.generateSlug, titleValue);
messageHandler.request<{ slug: string; slugWithPrefixAndSuffix: string; }>(CommandToCode.generateSlug, {
title: titleValue,
slugTemplate
}).then((slug) => {
if (slug.slugWithPrefixAndSuffix) {
setSlug(slug.slugWithPrefixAndSuffix);
}
}).catch((_) => {
setSlug(null);
});
}
}, [titleValue]);
useEffect(() => {
Messenger.listen(messageListener);
return () => {
Messenger.unlisten(messageListener);
};
}, []);
}, [titleValue, slugTemplate]);
return (
<div className={`metadata_field`}>
@@ -20,6 +20,7 @@ export interface ITextFieldProps extends BaseFieldProps<string> {
limit: number | undefined;
rows?: number;
name: string;
placeholder?: string;
settings: PanelSettings;
onChange: (txtValue: string) => void;
}
@@ -27,6 +28,7 @@ export interface ITextFieldProps extends BaseFieldProps<string> {
const WysiwygField = React.lazy(() => import('./WysiwygField'));
export const TextField: React.FunctionComponent<ITextFieldProps> = ({
placeholder,
singleLine,
wysiwyg,
limit,
@@ -154,6 +156,7 @@ export const TextField: React.FunctionComponent<ITextFieldProps> = ({
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<ITextFieldProps> = ({
rows={rows || 2}
value={text || ''}
onChange={(e) => onTextChange(e.currentTarget.value)}
placeholder={placeholder}
style={{
border
}}
@@ -1,9 +1,8 @@
import { Messenger } from '@estruyf/vscode/dist/client';
import { messageHandler } from '@estruyf/vscode/dist/client';
import * as React from 'react';
import { useCallback, useEffect, useState } from 'react';
import { DateHelper } from '../../../helpers/DateHelper';
import { BlockFieldData, CustomPanelViewResult, Field, PanelSettings } from '../../../models';
import { Command } from '../../Command';
import { BlockFieldData, ContentType, CustomPanelViewResult, Field, PanelSettings } from '../../../models';
import { CommandToCode } from '../../CommandToCode';
import { TagType } from '../../TagType';
import { DataBlockField } from '../DataBlock';
@@ -41,6 +40,7 @@ export interface IWrapperFieldProps {
parentFields: string[];
metadata: IMetadata;
settings: PanelSettings;
contentType: ContentType | null;
blockData: BlockFieldData | undefined;
focusElm: TagType | null;
parentBlock: string | null | undefined;
@@ -63,6 +63,7 @@ export const WrapperField: React.FunctionComponent<IWrapperFieldProps> = ({
parentFields,
metadata,
settings,
contentType,
blockData,
focusElm,
parentBlock,
@@ -76,21 +77,6 @@ export const WrapperField: React.FunctionComponent<IWrapperFieldProps> = ({
html: (data: any, onChange: (value: any) => void) => Promise<CustomPanelViewResult | undefined>;
}[]>([]);
const listener = useCallback(
(event: any) => {
const message = event.data;
if (message.command === Command.updatePlaceholder) {
const data = message.payload;
if (data.field === field.name) {
setFieldValue(data.value);
onSendUpdate(field.name, data.value, parentFields);
}
}
},
[field]
);
const getDate = (date: string | Date): Date | null => {
const parsedDate = DateHelper.tryParse(date, settings?.date?.format);
return parsedDate || (date as Date | null);
@@ -126,11 +112,18 @@ export const WrapperField: React.FunctionComponent<IWrapperFieldProps> = ({
// Check if the field value contains a placeholder
if (value && typeof value === 'string' && value.includes(`{{`) && value.includes(`}}`)) {
window.addEventListener('message', listener);
Messenger.send(CommandToCode.updatePlaceholder, {
messageHandler.request<{ field: string; value: any; }>(CommandToCode.updatePlaceholder, {
field: field.name,
title: metadata['title'],
value
value,
data: metadata,
contentType
}).then((data) => {
if (data.field === field.name) {
setFieldValue(data.value);
onSendUpdate(field.name, data.value, parentFields);
}
}).catch((err) => {
console.error(err);
});
} else {
// Did not contain a placeholder, so value can be set
@@ -138,10 +131,6 @@ export const WrapperField: React.FunctionComponent<IWrapperFieldProps> = ({
setFieldValue(value || null);
}
}
return () => {
window.removeEventListener('message', listener);
};
}, [field, parent]);
useEffect(() => {
@@ -372,7 +361,7 @@ export const WrapperField: React.FunctionComponent<IWrapperFieldProps> = ({
let draftValue = parent[field.name];
if (!draftValue && typeof parent[field.name] === 'undefined' && field.default) {
draftValue = field.default;
draftValue = field.default as string;
onSendUpdate(field.name, draftValue, parentFields);
}
@@ -509,6 +498,7 @@ export const WrapperField: React.FunctionComponent<IWrapperFieldProps> = ({
description={field.description}
titleValue={metadata.title as string}
value={fieldValue}
slugTemplate={contentType?.slugTemplate}
required={!!field.required}
editable={field.editable}
onChange={onFieldChange}
+136 -19
View File
@@ -1,6 +1,6 @@
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 { 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,21 +17,91 @@ export interface IGitActionProps {
export const GitAction: React.FunctionComponent<IGitActionProps> = ({
settings
}: React.PropsWithChildren<IGitActionProps>) => {
const [isSyncing, setIsSyncing] = useState(false);
const [commitMessage, setCommitMessage] = useState<string | undefined>(undefined);
const [crntBanch, setCrntBranch] = useState<string | undefined>(undefined);
const [isSyncing, setIsSyncing] = useState<"syncing" | "fetching" | "idle">("idle");
const pull = () => {
Messenger.send(GeneralCommands.toVSCode.gitSync);
const sync = () => {
Messenger.send(GeneralCommands.toVSCode.git.sync, commitMessage);
};
const messageListener = (message: MessageEvent<EventData<any>>) => {
const { command } = message.data;
const fetch = () => {
Messenger.send(GeneralCommands.toVSCode.git.fetch);
};
if (command === GeneralCommands.toWebview.gitSyncingStart) {
setIsSyncing(true);
} else if (command === GeneralCommands.toWebview.gitSyncingEnd) {
setIsSyncing(false);
const selectBranch = () => {
messageHandler.send(GeneralCommands.toVSCode.git.selectBranch)
}
const messageListener = React.useCallback((message: MessageEvent<EventData<any>>) => {
const { command, payload } = message.data;
if (command === GeneralCommands.toWebview.git.syncingStart) {
setIsSyncing(payload || "syncing");
} else if (command === GeneralCommands.toWebview.git.syncingStart) {
setIsSyncing("syncing");
} else if (command === GeneralCommands.toWebview.git.syncingEnd) {
setCommitMessage(undefined);
setIsSyncing("idle");
} else if (command === GeneralCommands.toWebview.git.branchName) {
setCrntBranch(payload || undefined);
}
};
}, []);
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;
}
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]);
const fetchBranch = React.useCallback(() => {
messageHandler.request<string>(GeneralCommands.toVSCode.git.getBranch).then((branch) => {
setCrntBranch(branch);
});
}, []);
useEffect(() => {
Messenger.listen(messageListener);
@@ -38,7 +109,11 @@ export const GitAction: React.FunctionComponent<IGitActionProps> = ({
return () => {
Messenger.unlisten(messageListener);
};
}, []);
}, [messageListener]);
useEffect(() => {
fetchBranch();
}, [fetchBranch]);
if (!settings?.git?.actions || !settings?.git.isGitRepo) {
return null;
@@ -46,17 +121,59 @@ export const GitAction: React.FunctionComponent<IGitActionProps> = ({
return (
<div className="git_actions">
<ActionButton
onClick={pull}
title={
<h2 className='text-[11px] flex justify-between items-center mb-4'>
<span className='uppercase'>
{l10n.t(LocalizationKey.panelGitGitActionTitle)}
</span>
<button
className='inline-flex items-center w-auto p-0 bg-inherit text-[var(--vscode-sideBarTitle-foreground)] hover:bg-inherit hover:text-[var(--vscode-sideBarTitle-foreground-hover)]'
title='Select Branch'
onClick={selectBranch}>
<BranchIcon className='w-4 h-4' aria-hidden="true" />
<span className='ml-1'>{crntBanch}</span>
</button>
</h2>
<div className='space-y-4'>
<input
type='text'
className='rounded-md disabled:opacity-50 disabled:cursor-not-allowed'
placeholder={l10n.t(LocalizationKey.panelGitGitActionInputPlaceholder)}
style={{
...isCommitRequired
}}
value={commitMessage || ''}
onChange={(e) => setCommitMessage(e.target.value)}
disabled={isCommitDisabed}
/>
<ActionButton
disabled={isSyncDisabled || isSyncing !== "idle"}
onClick={sync}
title={l10n.t(LocalizationKey.commonSync)}
>
<div className="git_actions__sync">
<ArrowPathIcon className={isSyncing ? 'animate-reverse-spin' : ''} />
<ArrowPathIcon className={isSyncing === "syncing" ? 'animate-spin' : ''} aria-hidden="true" />
<span>
{l10n.t(LocalizationKey.commonSync)}
</span>
</div>
}
/>
</ActionButton>
<ActionButton
disabled={!crntBanch || isSyncing !== "idle"}
onClick={fetch}
title={l10n.t(LocalizationKey.panelGitGitActionButtonFetch)}
>
<div className="git_actions__fetch">
<ArrowDownTrayIcon className={isSyncing === "fetching" ? 'animate-bounce' : ''} aria-hidden="true" />
<span>
{l10n.t(LocalizationKey.panelGitGitActionButtonFetch)}
</span>
</div>
</ActionButton>
</div>
</div>
);
};
@@ -0,0 +1,15 @@
import * as React from 'react';
export interface IBranchIconProps {
className?: string;
}
export const BranchIcon: React.FunctionComponent<IBranchIconProps> = ({
className
}: React.PropsWithChildren<IBranchIconProps>) => {
return (
<svg width="24" height="24" viewBox="0 0 24 24" className={className || ""} fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M21.0067 8.22168C21.0102 7.52792 20.8205 6.84689 20.4589 6.25485C20.0971 5.66281 19.5778 5.18315 18.959 4.86957C18.3401 4.556 17.6461 4.42091 16.9548 4.47941C16.2635 4.53793 15.6022 4.78773 15.0448 5.20085C14.4875 5.61397 14.0561 6.17409 13.7991 6.8185C13.5421 7.4629 13.4695 8.16613 13.5895 8.84944C13.7096 9.53274 14.0174 10.1692 14.4787 10.6874C14.94 11.2056 15.5365 11.5852 16.2012 11.7836C15.9558 12.2824 15.576 12.703 15.1047 12.9979C14.6334 13.2929 14.0892 13.4505 13.5331 13.4532H10.5437C9.43702 13.4571 8.37138 13.8727 7.55427 14.6191V7.39809C8.46159 7.21288 9.26783 6.69737 9.81668 5.95151C10.3655 5.20565 10.6178 4.28256 10.5248 3.36121C10.4317 2.43987 9.99985 1.5859 9.31292 0.964873C8.62599 0.343845 7.73295 0 6.80691 0C5.88087 0 4.98783 0.343845 4.3009 0.964873C3.61397 1.5859 3.18211 2.43987 3.08904 3.36121C2.99596 4.28256 3.24831 5.20565 3.79715 5.95151C4.34599 6.69737 5.15223 7.21288 6.05955 7.39809V16.5159C5.15393 16.6891 4.34299 17.1877 3.77969 17.9176C3.21639 18.6476 2.93968 19.5585 3.00173 20.4785C3.06379 21.3984 3.46033 22.2639 4.11656 22.9115C4.77279 23.5592 5.64335 23.9444 6.56403 23.9944C7.48472 24.0445 8.39187 23.7558 9.1144 23.183C9.83693 22.6102 10.3249 21.7928 10.4862 20.885C10.6475 19.9771 10.4712 19.0417 9.99023 18.255C9.50932 17.4683 8.75717 16.8848 7.87564 16.6145C8.12152 16.1162 8.50142 15.6963 8.97272 15.4019C9.44401 15.1074 9.98803 14.9503 10.5437 14.9479H13.5331C14.4658 14.9436 15.3739 14.6486 16.1311 14.1039C16.8882 13.5592 17.4566 12.792 17.7572 11.9091C18.6531 11.7914 19.476 11.3528 20.0735 10.6748C20.671 9.9968 21.0025 9.12533 21.0067 8.22168ZM4.56483 3.73752C4.56483 3.29408 4.69633 2.8606 4.94269 2.4919C5.18906 2.12319 5.53922 1.83581 5.9489 1.66611C6.3586 1.49642 6.8094 1.45202 7.24432 1.53854C7.67924 1.62504 8.07874 1.83857 8.3923 2.15214C8.70586 2.4657 8.9194 2.8652 9.00591 3.30012C9.09241 3.73504 9.04802 4.18585 8.87832 4.59553C8.70862 5.00521 8.42125 5.35539 8.05254 5.60175C7.68383 5.84811 7.25035 5.9796 6.80691 5.9796C6.21227 5.9796 5.642 5.74339 5.22152 5.32291C4.80105 4.90245 4.56483 4.33216 4.56483 3.73752ZM9.04899 20.1794C9.04899 20.6229 8.91749 21.0563 8.67113 21.425C8.42476 21.7937 8.0746 22.0811 7.66492 22.2508C7.25523 22.4205 6.80442 22.4649 6.36951 22.3784C5.93458 22.292 5.53509 22.0784 5.22152 21.7648C4.90796 21.4512 4.69443 21.0517 4.60791 20.6169C4.52141 20.1819 4.5658 19.7311 4.7355 19.3214C4.9052 18.9117 5.19258 18.5615 5.56128 18.3152C5.92999 18.0689 6.36347 17.9373 6.80691 17.9373C7.40155 17.9373 7.97183 18.1736 8.3923 18.594C8.81277 19.0145 9.04899 19.5848 9.04899 20.1794ZM17.2699 10.4638C16.8265 10.4638 16.393 10.3322 16.0243 10.0859C15.6556 9.83954 15.3683 9.48937 15.1986 9.07969C15.0289 8.67 14.9844 8.2192 15.0709 7.78427C15.1574 7.34935 15.3709 6.94985 15.6845 6.63629C15.9981 6.32273 16.3976 6.10919 16.8325 6.02268C17.2674 5.93617 17.7183 5.98058 18.1279 6.15027C18.5377 6.31997 18.8878 6.60734 19.1341 6.97605C19.3805 7.34476 19.512 7.77823 19.512 8.22168C19.512 8.81632 19.2757 9.3866 18.8553 9.80706C18.4348 10.2275 17.8645 10.4638 17.2699 10.4638Z" fill="currentcolor" />
</svg>
);
};
+1
View File
@@ -66,6 +66,7 @@ const Metadata: React.FunctionComponent<IMetadataProps> = ({
parent={parent}
parentFields={parentFields}
metadata={metadata}
contentType={contentType}
settings={settings}
blockData={blockData}
parentBlock={parentBlock}
+5 -1
View File
@@ -12,7 +12,11 @@ const Preview: React.FunctionComponent<IPreviewProps> = (_: React.PropsWithChild
Messenger.send(CommandToCode.openPreview);
};
return <ActionButton onClick={open} title={l10n.t(LocalizationKey.panelPreviewTitle)} />;
return (
<ActionButton onClick={open} title={l10n.t(LocalizationKey.panelPreviewTitle)}>
{l10n.t(LocalizationKey.panelPreviewTitle)}
</ActionButton>
);
};
Preview.displayName = 'Preview';
@@ -23,7 +23,9 @@ const PublishAction: React.FunctionComponent<IPublishActionProps> = (
onClick={publish}
className={`${draft ? '' : 'secondary'}`}
title={draft ? l10n.t(LocalizationKey.panelPublishActionPublish) : l10n.t(LocalizationKey.panelPublishActionUnpublish)}
/>
>
{draft ? l10n.t(LocalizationKey.panelPublishActionPublish) : l10n.t(LocalizationKey.panelPublishActionUnpublish)}
</ActionButton>
);
};
+5 -1
View File
@@ -14,7 +14,11 @@ const SlugAction: React.FunctionComponent<
Messenger.send(CommandToCode.updateSlug);
};
return <ActionButton onClick={optimize} title={l10n.t(LocalizationKey.panelSlugActionTitle)} />;
return (
<ActionButton onClick={optimize} title={l10n.t(LocalizationKey.panelSlugActionTitle)}>
{l10n.t(LocalizationKey.panelSlugActionTitle)}
</ActionButton>
);
};
SlugAction.displayName = 'SlugAction';
+4 -2
View File
@@ -4,10 +4,10 @@
/* Animations */
@keyframes spin {
from {
transform: rotate(360deg);
transform: rotate(0deg);
}
to {
transform: rotate(0deg);
transform: rotate(360deg);
}
}
@@ -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;
+4 -1
View File
@@ -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
};