mirror of
https://github.com/estruyf/vscode-front-matter.git
synced 2026-07-06 09:51:29 +02:00
Merge branch 'dev' of github.com:estruyf/vscode-front-matter into dev
This commit is contained in:
+17
-1
@@ -10,7 +10,8 @@ import {
|
||||
SETTING_SLUG_PREFIX,
|
||||
SETTING_SLUG_SUFFIX,
|
||||
SETTING_CONTENT_PLACEHOLDERS,
|
||||
TelemetryEvent
|
||||
TelemetryEvent,
|
||||
SETTING_SLUG_TEMPLATE
|
||||
} from './../constants';
|
||||
import * as vscode from 'vscode';
|
||||
import { CustomPlaceholder, Field } from '../models';
|
||||
@@ -260,6 +261,21 @@ export class Article {
|
||||
return;
|
||||
}
|
||||
|
||||
const slugTemplate = Settings.get<string>(SETTING_SLUG_TEMPLATE);
|
||||
if (slugTemplate) {
|
||||
if (slugTemplate === '{{title}}') {
|
||||
const article = ArticleHelper.getFrontMatter(editor);
|
||||
if (article?.data?.title) {
|
||||
return article.data.title.toLowerCase().replace(/\s/g, '-');
|
||||
}
|
||||
} else {
|
||||
const article = ArticleHelper.getFrontMatter(editor);
|
||||
if (article?.data) {
|
||||
return SlugHelper.createSlug(article.data.title, article.data, slugTemplate);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const file = parseWinPath(editor.document.fileName);
|
||||
|
||||
if (!isValidFile(file)) {
|
||||
|
||||
@@ -358,7 +358,7 @@ export class Dashboard {
|
||||
version.usedVersion ? '' : `data-showWelcome="true"`
|
||||
} ${
|
||||
experimental ? `data-experimental="${experimental}"` : ''
|
||||
} data-webview-url="${webviewUrl}" ></div>
|
||||
} data-webview-url="${webviewUrl}" data-is-crash-disabled="${!Telemetry.isVscodeEnabled()}" ></div>
|
||||
|
||||
${(scriptsToLoad || [])
|
||||
.map((script) => {
|
||||
|
||||
+54
-33
@@ -99,7 +99,7 @@ export class Folders {
|
||||
}
|
||||
|
||||
const folders = Folders.get().filter((f) => !f.disableCreation);
|
||||
const location = folders.find((f) => f.title === selectedFolder);
|
||||
const location = folders.find((f) => f.path === selectedFolder.path);
|
||||
if (location) {
|
||||
const folderPath = Folders.getFolderPath(Uri.file(location.path));
|
||||
if (folderPath) {
|
||||
@@ -293,31 +293,6 @@ export class Folders {
|
||||
if (crntFolderInfo) {
|
||||
folderInfo.push(crntFolderInfo);
|
||||
}
|
||||
|
||||
// Process localization folders
|
||||
if (folder.defaultLocale) {
|
||||
const i18nConfig = folder.locales || Settings.get<I18nConfig[]>(SETTING_CONTENT_I18N);
|
||||
if (i18nConfig) {
|
||||
for (const i18n of i18nConfig) {
|
||||
if (i18n.locale !== folder.defaultLocale && i18n.path) {
|
||||
const i18nFolder = {
|
||||
...folder,
|
||||
path: join(folder.path, i18n.path),
|
||||
title: `${folder.title} (${i18n.title})`
|
||||
} as ContentFolder;
|
||||
|
||||
const crntFolderInfo = await Folders.getFilesByFolder(
|
||||
i18nFolder,
|
||||
supportedFiles,
|
||||
limit
|
||||
);
|
||||
if (crntFolderInfo) {
|
||||
folderInfo.push(crntFolderInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return folderInfo;
|
||||
@@ -333,11 +308,14 @@ export class Folders {
|
||||
public static get(): ContentFolder[] {
|
||||
const wsFolder = Folders.getWorkspaceFolder();
|
||||
let folders: ContentFolder[] = Settings.get(SETTING_CONTENT_PAGE_FOLDERS) as ContentFolder[];
|
||||
const i18nSettings = Settings.get<I18nConfig[]>(SETTING_CONTENT_I18N);
|
||||
|
||||
// Filter out folders without a path
|
||||
folders = folders.filter((f) => f.path);
|
||||
|
||||
const contentFolders = folders.map((folder) => {
|
||||
const contentFolders: ContentFolder[] = [];
|
||||
|
||||
folders.forEach((folder) => {
|
||||
if (!folder.title) {
|
||||
folder.title = basename(folder.path);
|
||||
}
|
||||
@@ -371,11 +349,52 @@ export class Folders {
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...folder,
|
||||
originalPath: folder.path,
|
||||
path: folderPath
|
||||
};
|
||||
// Check i18n
|
||||
if (folder.defaultLocale && (folder.locales || i18nSettings)) {
|
||||
const i18nConfig =
|
||||
folder.locales && folder.locales.length > 0 ? folder.locales : i18nSettings;
|
||||
|
||||
let defaultLocale;
|
||||
let sourcePath = folderPath;
|
||||
let localeFolders: ContentFolder[] = [];
|
||||
|
||||
if (i18nConfig && i18nConfig.length > 0) {
|
||||
for (const i18n of i18nConfig) {
|
||||
if (i18n.locale === folder.defaultLocale) {
|
||||
defaultLocale = i18n;
|
||||
} else if (i18n.locale !== folder.defaultLocale && i18n.path) {
|
||||
localeFolders.push({
|
||||
...folder,
|
||||
title: folder.title,
|
||||
originalPath: folder.path,
|
||||
locale: i18n.locale,
|
||||
localeTitle: i18n?.title || i18n.locale,
|
||||
localeSourcePath: sourcePath,
|
||||
path: parseWinPath(join(folderPath, i18n.path))
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
contentFolders.push({
|
||||
...folder,
|
||||
title: folder.title,
|
||||
locale: folder.defaultLocale,
|
||||
localeTitle: defaultLocale?.title || folder.defaultLocale,
|
||||
originalPath: folder.path,
|
||||
localeSourcePath: sourcePath,
|
||||
path: parseWinPath(join(folderPath, defaultLocale?.path || ''))
|
||||
});
|
||||
|
||||
contentFolders.push(...localeFolders);
|
||||
} else {
|
||||
contentFolders.push({
|
||||
...folder,
|
||||
locale: folder.defaultLocale,
|
||||
originalPath: folder.path,
|
||||
path: folderPath
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return contentFolders.filter((folder) => folder !== null) as ContentFolder[];
|
||||
@@ -651,7 +670,9 @@ export class Folders {
|
||||
return {
|
||||
title: folder.title,
|
||||
files: files.length,
|
||||
lastModified: fileStats
|
||||
lastModified: fileStats,
|
||||
locale: folder.locale,
|
||||
localeTitle: folder.localeTitle
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,8 +44,8 @@ export class StatusListener {
|
||||
commands.executeCommand('setContext', CONTEXT.isValidFile, true);
|
||||
|
||||
// Check i18n
|
||||
const isI18nDefault = await i18n.isDefaultLanguage(document.uri.fsPath);
|
||||
commands.executeCommand('setContext', CONTEXT.isI18nDefault, isI18nDefault);
|
||||
const isI18nEnabled = await i18n.isLocaleEnabled(document.uri.fsPath);
|
||||
commands.executeCommand('setContext', CONTEXT.isI18nEnabled, isI18nEnabled);
|
||||
|
||||
const article = editor
|
||||
? ArticleHelper.getFrontMatter(editor)
|
||||
@@ -88,7 +88,7 @@ export class StatusListener {
|
||||
}
|
||||
} else {
|
||||
commands.executeCommand('setContext', CONTEXT.isValidFile, false);
|
||||
commands.executeCommand('setContext', CONTEXT.isI18nDefault, false);
|
||||
commands.executeCommand('setContext', CONTEXT.isI18nEnabled, false);
|
||||
|
||||
const panel = PanelProvider.getInstance();
|
||||
if (panel && panel.visible) {
|
||||
|
||||
+103
-56
@@ -4,13 +4,12 @@ import {
|
||||
ContentType,
|
||||
Extension,
|
||||
FrameworkDetector,
|
||||
Logger,
|
||||
Notifications,
|
||||
Settings,
|
||||
openFileInEditor,
|
||||
parseWinPath
|
||||
} from '../helpers';
|
||||
import { COMMAND_NAME, ExtensionState, SETTING_CONTENT_I18N } from '../constants';
|
||||
import { COMMAND_NAME, SETTING_CONTENT_I18N } from '../constants';
|
||||
import { ContentFolder, Field, I18nConfig, ContentType as IContentType } from '../models';
|
||||
import { join, parse } from 'path';
|
||||
import { existsAsync } from '../utils';
|
||||
@@ -19,6 +18,7 @@ import { ParsedFrontMatter } from '../parsers';
|
||||
import { PagesListener } from '../listeners/dashboard';
|
||||
import * as l10n from '@vscode/l10n';
|
||||
import { LocalizationKey } from '../localization';
|
||||
import { Translations } from '../services/Translations';
|
||||
|
||||
export class i18n {
|
||||
private static processedFiles: {
|
||||
@@ -43,6 +43,30 @@ export class i18n {
|
||||
i18n.processedFiles = {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves all the I18nConfig settings.
|
||||
*
|
||||
* @returns An array of I18nConfig settings.
|
||||
*/
|
||||
public static getAll() {
|
||||
const i18nSettings = Settings.get<I18nConfig[]>(SETTING_CONTENT_I18N) || [];
|
||||
|
||||
const folders = Folders.get();
|
||||
if (folders) {
|
||||
for (const folder of folders) {
|
||||
if (folder.locales) {
|
||||
for (const locale of folder.locales) {
|
||||
if (!i18nSettings.some((i18n) => i18n.locale === locale.locale)) {
|
||||
i18nSettings.push(locale);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return i18nSettings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the I18nConfig settings from the application.
|
||||
* @returns An array of I18nConfig objects if settings are found, otherwise undefined.
|
||||
@@ -65,6 +89,25 @@ export class i18n {
|
||||
return pageFolder.locales;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the locale is enabled for the given file path.
|
||||
* @param filePath - The file path to check.
|
||||
* @returns A promise that resolves to a boolean indicating whether the locale is enabled or not.
|
||||
*/
|
||||
public static async isLocaleEnabled(filePath: string): Promise<boolean> {
|
||||
const i18nSettings = await i18n.getSettings(filePath);
|
||||
if (!i18nSettings) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const pageFolder = Folders.getPageFolderByFilePath(filePath);
|
||||
if (!pageFolder || !pageFolder.locale) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return i18nSettings.some((i18n) => i18n.locale === pageFolder.locale);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given file path corresponds to the default language.
|
||||
* @param filePath - The file path to check.
|
||||
@@ -84,6 +127,10 @@ export class i18n {
|
||||
const fileInfo = await i18n.getFileInfo(filePath);
|
||||
|
||||
if (pageFolder.path) {
|
||||
if (pageFolder.locale) {
|
||||
return pageFolder.locale === pageFolder.defaultLocale;
|
||||
}
|
||||
|
||||
let pageFolderPath = parseWinPath(pageFolder.path);
|
||||
if (!pageFolderPath.endsWith('/')) {
|
||||
pageFolderPath += '/';
|
||||
@@ -120,9 +167,10 @@ export class i18n {
|
||||
|
||||
if (
|
||||
pageFolder.path &&
|
||||
pageFolder.locale &&
|
||||
parseWinPath(fileInfo.dir).toLowerCase() === parseWinPath(pageFolderPath).toLowerCase()
|
||||
) {
|
||||
return i18nSettings.find((i18n) => i18n.locale === pageFolder?.defaultLocale);
|
||||
return i18nSettings.find((i18n) => i18n.locale === pageFolder?.locale);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -172,9 +220,9 @@ export class i18n {
|
||||
let pageFolder = Folders.getPageFolderByFilePath(filePath);
|
||||
const fileInfo = await i18n.getFileInfo(filePath);
|
||||
|
||||
if (pageFolder && pageFolder.defaultLocale) {
|
||||
if (pageFolder && pageFolder.defaultLocale && pageFolder.localeSourcePath) {
|
||||
for (const i18n of i18nSettings) {
|
||||
const translation = join(pageFolder.path, i18n.path || '', fileInfo.filename);
|
||||
const translation = join(pageFolder.localeSourcePath, i18n.path || '', fileInfo.filename);
|
||||
if (await existsAsync(translation)) {
|
||||
translations[i18n.locale] = {
|
||||
locale: i18n,
|
||||
@@ -224,20 +272,40 @@ export class i18n {
|
||||
fileUri = Uri.file(fileUri);
|
||||
}
|
||||
|
||||
const pageFolder = Folders.getPageFolderByFilePath(fileUri.fsPath);
|
||||
if (!pageFolder || !pageFolder.localeSourcePath) {
|
||||
Notifications.error(l10n.t(LocalizationKey.commandsI18nCreateErrorNoContentFolder));
|
||||
return;
|
||||
}
|
||||
|
||||
const i18nSettings = await i18n.getSettings(fileUri.fsPath);
|
||||
if (!i18nSettings) {
|
||||
Notifications.warning(l10n.t(LocalizationKey.commandsI18nCreateWarningNoConfig));
|
||||
return;
|
||||
}
|
||||
|
||||
const isDefaultLanguage = await i18n.isDefaultLanguage(fileUri.fsPath);
|
||||
if (!isDefaultLanguage) {
|
||||
Notifications.warning(l10n.t(LocalizationKey.commandsI18nCreateWarningNotDefaultLocale));
|
||||
const sourceLocale = await i18n.getLocale(fileUri.fsPath);
|
||||
if (!sourceLocale || !sourceLocale.locale) {
|
||||
Notifications.warning(l10n.t(LocalizationKey.commandsI18nCreateErrorNoLocaleDefinition));
|
||||
return;
|
||||
}
|
||||
|
||||
const translations = (await i18n.getTranslations(fileUri.fsPath)) || {};
|
||||
const targetLocales = i18nSettings.filter((i18nSetting) => {
|
||||
return (
|
||||
i18nSetting.path &&
|
||||
i18nSetting.locale !== sourceLocale.locale &&
|
||||
!translations[i18nSetting.locale]
|
||||
);
|
||||
});
|
||||
|
||||
if (targetLocales.length === 0) {
|
||||
Notifications.warning(l10n.t(LocalizationKey.commandsI18nCreateErrorNoLocales));
|
||||
return;
|
||||
}
|
||||
|
||||
const locale = await window.showQuickPick(
|
||||
i18nSettings.filter((i18n) => i18n.path).map((i18n) => i18n.title || i18n.locale),
|
||||
targetLocales.map((i18n) => i18n.title || i18n.locale),
|
||||
{
|
||||
title: l10n.t(LocalizationKey.commandsI18nCreateQuickPickTitle),
|
||||
placeHolder: l10n.t(LocalizationKey.commandsI18nCreateQuickPickPlaceHolder),
|
||||
@@ -249,10 +317,10 @@ export class i18n {
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedI18n = i18nSettings.find(
|
||||
const targetLocale = i18nSettings.find(
|
||||
(i18n) => i18n.title === locale || i18n.locale === locale
|
||||
);
|
||||
if (!selectedI18n || !selectedI18n.path) {
|
||||
if (!targetLocale || !targetLocale.path) {
|
||||
Notifications.warning(l10n.t(LocalizationKey.commandsI18nCreateWarningNoConfig));
|
||||
return;
|
||||
}
|
||||
@@ -280,7 +348,7 @@ export class i18n {
|
||||
pageBundleDir = join(parse(pageBundleDir).dir);
|
||||
}
|
||||
|
||||
const i18nDir = join(dir, selectedI18n.path, pageBundleDir);
|
||||
const i18nDir = join(pageFolder.localeSourcePath, targetLocale.path, pageBundleDir);
|
||||
|
||||
if (!(await existsAsync(i18nDir))) {
|
||||
await workspace.fs.createDirectory(Uri.file(i18nDir));
|
||||
@@ -290,7 +358,8 @@ export class i18n {
|
||||
article,
|
||||
fileUri.fsPath,
|
||||
contentType,
|
||||
selectedI18n,
|
||||
sourceLocale,
|
||||
targetLocale,
|
||||
i18nDir
|
||||
);
|
||||
|
||||
@@ -300,9 +369,8 @@ export class i18n {
|
||||
return;
|
||||
}
|
||||
|
||||
const sourceLocale = await i18n.getLocale(fileUri.fsPath);
|
||||
if (sourceLocale?.locale) {
|
||||
article = await i18n.translate(article, sourceLocale, selectedI18n);
|
||||
article = await i18n.translate(article, sourceLocale, targetLocale);
|
||||
}
|
||||
|
||||
const newFileUri = Uri.file(newFilePath);
|
||||
@@ -318,7 +386,7 @@ export class i18n {
|
||||
Notifications.info(
|
||||
l10n.t(
|
||||
LocalizationKey.commandsI18nCreateSuccessCreated,
|
||||
selectedI18n.title || selectedI18n.locale
|
||||
sourceLocale.title || sourceLocale.locale
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -336,12 +404,6 @@ export class i18n {
|
||||
targetLocale: I18nConfig
|
||||
) {
|
||||
return new Promise<ParsedFrontMatter>(async (resolve) => {
|
||||
const authKey = await Extension.getInstance().getSecret(ExtensionState.Secrets.DeeplApiKey);
|
||||
if (!authKey) {
|
||||
resolve(article);
|
||||
return;
|
||||
}
|
||||
|
||||
await window.withProgress(
|
||||
{
|
||||
location: ProgressLocation.Notification,
|
||||
@@ -349,43 +411,26 @@ export class i18n {
|
||||
cancellable: false
|
||||
},
|
||||
async () => {
|
||||
const title = article.data.title;
|
||||
const description = article.data.description;
|
||||
const content = article.content;
|
||||
|
||||
try {
|
||||
const body = JSON.stringify({
|
||||
text: [title, description, content],
|
||||
source_lang: sourceLocale.locale,
|
||||
target_lang: targetLocale.locale
|
||||
});
|
||||
const title = article.data.title || '';
|
||||
const description = article.data.description || '';
|
||||
const content = article.content || '';
|
||||
|
||||
let host = authKey.endsWith(':fx') ? 'api-free.deepl.com' : 'api.deepl.com';
|
||||
const text = [title, description, content];
|
||||
const translations = await Translations.translate(
|
||||
text,
|
||||
sourceLocale.locale,
|
||||
targetLocale.locale
|
||||
);
|
||||
|
||||
const response = await fetch(`https://${host}/v2/translate`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `DeepL-Auth-Key ${authKey}`,
|
||||
'User-Agent': `FrontMatterCMS/${Extension.getInstance().version}`,
|
||||
'Content-Type': 'application/json',
|
||||
'content-length': body.length.toString(),
|
||||
Accept: 'application/json'
|
||||
},
|
||||
body
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`DeepL: ${response.statusText}`);
|
||||
if (!translations || translations.length < 3) {
|
||||
resolve(article);
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
if (!data.translations || data.translations.length < 3) {
|
||||
throw new Error('DeepL: Invalid response');
|
||||
}
|
||||
|
||||
article.data.title = data.translations[0].text;
|
||||
article.data.description = data.translations[1].text;
|
||||
article.content = data.translations[2].text;
|
||||
article.data.title = article.data.title ? translations[0] : '';
|
||||
article.data.description = article.data.description ? translations[1] : '';
|
||||
article.content = article.content ? translations[2] : '';
|
||||
} catch (error) {
|
||||
Notifications.error(`${(error as Error).message}`);
|
||||
}
|
||||
@@ -460,7 +505,8 @@ export class i18n {
|
||||
* @param article - The parsed front matter of the article.
|
||||
* @param filePath - The path of the file containing the front matter.
|
||||
* @param contentType - The content type of the article.
|
||||
* @param i18nConfig - The configuration for internationalization.
|
||||
* @param sourceLocale - The source locale.
|
||||
* @param targetLocale - The target locale.
|
||||
* @param i18nDir - The directory where the i18n files are located.
|
||||
* @returns A Promise that resolves to the updated parsed front matter.
|
||||
*/
|
||||
@@ -468,7 +514,8 @@ export class i18n {
|
||||
article: ParsedFrontMatter,
|
||||
filePath: string,
|
||||
contentType: IContentType,
|
||||
i18nConfig: I18nConfig,
|
||||
sourceLocale: I18nConfig,
|
||||
targetLocale: I18nConfig,
|
||||
i18nDir: string
|
||||
): Promise<ParsedFrontMatter> {
|
||||
const imageFields = ContentType.findFieldsByTypeDeep(contentType.fields, 'image');
|
||||
|
||||
@@ -33,6 +33,12 @@ export const ExtensionState = {
|
||||
},
|
||||
|
||||
Secrets: {
|
||||
DeeplApiKey: `frontMatter:Secrets:DeeplApiKey`
|
||||
Deepl: {
|
||||
ApiKey: `frontMatter:Secrets:DeeplApiKey`
|
||||
},
|
||||
Azure: {
|
||||
TranslatorKey: `frontMatter:Secrets:AzureTranslatorKey`,
|
||||
TranslatorRegion: `frontMatter:Secrets:AzureTranslatorRegion`
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -21,6 +21,9 @@ export const GeneralCommands = {
|
||||
get: 'getSecret',
|
||||
set: 'setSecret'
|
||||
},
|
||||
content: {
|
||||
locales: 'getContentLocales'
|
||||
},
|
||||
runCommand: 'runCommand',
|
||||
getLocalization: 'getLocalization',
|
||||
openOnWebsite: 'openOnWebsite'
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export const SentryIgnore = [
|
||||
`ResizeObserver loop limit exceeded`,
|
||||
`Cannot read properties of undefined (reading 'unobserve')`,
|
||||
`TypeError: Cannot read properties of undefined (reading 'unobserve')`
|
||||
`TypeError: Cannot read properties of undefined (reading 'unobserve')`,
|
||||
`ResizeObserver loop completed with undelivered notifications.`
|
||||
];
|
||||
|
||||
@@ -8,7 +8,7 @@ export const CONTEXT = {
|
||||
isValidFile: 'frontMatter:file:isValid',
|
||||
isDevelopment: 'frontMatter:isDevelopment',
|
||||
|
||||
isI18nDefault: 'frontMatter:i18n:default',
|
||||
isI18nEnabled: 'frontMatter:i18n:enabled',
|
||||
|
||||
hasViewModes: 'frontMatter:has:modes',
|
||||
|
||||
|
||||
@@ -133,13 +133,3 @@ export const SETTING_CONTENT_FOLDERS = 'content.folders';
|
||||
* Use the `isPublishDate` property on the content type datetime field instead
|
||||
*/
|
||||
export const SETTING_DATE_FIELD = 'taxonomy.dateField';
|
||||
/**
|
||||
* @deprecated
|
||||
* Use the `isModifiedDate` property on the content type datetime field instead
|
||||
*/
|
||||
export const SETTING_MODIFIED_FIELD = 'taxonomy.modifiedField';
|
||||
/**
|
||||
* @deprecated
|
||||
* Use the `frontMatter.content.snippets` setting instead
|
||||
*/
|
||||
export const SETTING_DASHBOARD_MEDIA_SNIPPET = 'dashboard.mediaSnippet';
|
||||
|
||||
@@ -30,7 +30,7 @@ export interface IAppProps {
|
||||
export const App: React.FunctionComponent<IAppProps> = ({
|
||||
showWelcome
|
||||
}: React.PropsWithChildren<IAppProps>) => {
|
||||
const { pages, settings, localeReady } = useMessages();
|
||||
const { pages, settings } = useMessages();
|
||||
const view = useRecoilValue(DashboardViewSelector);
|
||||
const mode = useRecoilValue(ModeAtom);
|
||||
const [isDevMode, setIsDevMode] = useState(false);
|
||||
@@ -70,7 +70,7 @@ export const App: React.FunctionComponent<IAppProps> = ({
|
||||
}
|
||||
}, []);
|
||||
|
||||
if (!settings || !localeReady) {
|
||||
if (!settings) {
|
||||
return <Spinner />;
|
||||
}
|
||||
|
||||
|
||||
@@ -230,7 +230,7 @@ export const ContentActions: React.FunctionComponent<IContentActionsProps> = ({
|
||||
}
|
||||
|
||||
{
|
||||
locale && isDefaultLocale && (
|
||||
locale && (
|
||||
<DropdownMenuItem onClick={() => runCommand(COMMAND_NAME.i18n.create)}>
|
||||
<LanguageIcon className={`mr-2 h-4 w-4`} aria-hidden={true} />
|
||||
<span>{l10n.t(LocalizationKey.dashboardContentsContentActionsTranslationsCreate)}</span>
|
||||
|
||||
@@ -18,7 +18,7 @@ export const Filters: React.FunctionComponent<IFiltersProps> = (_: React.PropsWi
|
||||
const settings = useRecoilValue(SettingsSelector);
|
||||
const location = useLocation();
|
||||
|
||||
const otherFilters = useMemo(() => settings?.filters?.filter((filter) => filter !== "pageFolders" && filter !== "tags" && filter !== "categories"), [settings?.filters]);
|
||||
const otherFilters = useMemo(() => settings?.filters?.filter((filter) => filter !== "contentFolders" && filter !== "tags" && filter !== "categories"), [settings?.filters]);
|
||||
|
||||
const otherFilterValues = useMemo(() => {
|
||||
return otherFilters?.map((filter) => {
|
||||
@@ -77,7 +77,7 @@ export const Filters: React.FunctionComponent<IFiltersProps> = (_: React.PropsWi
|
||||
<LanguageFilter />
|
||||
|
||||
{
|
||||
settings?.filters?.includes("pageFolders") && (
|
||||
settings?.filters?.includes("contentFolders") && (
|
||||
<FoldersFilter />
|
||||
)
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { FolderAtom, SettingsSelector } from '../../state';
|
||||
import { MenuButton, MenuItem } from '../Menu';
|
||||
import * as l10n from '@vscode/l10n';
|
||||
import { LocalizationKey } from '../../../localization';
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger } from '../../../components/shadcn/Dropdown';
|
||||
import { DropdownMenu, DropdownMenuContent } from '../../../components/shadcn/Dropdown';
|
||||
|
||||
export interface IFoldersFilterProps { }
|
||||
|
||||
@@ -14,7 +14,11 @@ export const FoldersFilter: React.FunctionComponent<
|
||||
const DEFAULT_TYPE = l10n.t(LocalizationKey.dashboardHeaderFoldersDefault);
|
||||
const [crntFolder, setCrntFolder] = useRecoilState(FolderAtom);
|
||||
const settings = useRecoilValue(SettingsSelector);
|
||||
const contentFolders = settings?.contentFolders || [];
|
||||
|
||||
const contentFolders = React.useMemo(() => {
|
||||
return settings?.contentFolders
|
||||
.filter((folder, index, self) => index === self.findIndex((t) => t.originalPath === folder.originalPath)) || [];
|
||||
}, [settings?.contentFolders]);
|
||||
|
||||
if (contentFolders.length <= 1) {
|
||||
return null;
|
||||
|
||||
@@ -128,7 +128,7 @@ export const DetailsSlideOver: React.FunctionComponent<IDetailsSlideOverProps> =
|
||||
</div>
|
||||
|
||||
<div className="relative mt-6 flex-1 px-4 sm:px-6">
|
||||
<div className="absolute inset-0 px-4 sm:px-6 space-y-8">
|
||||
<div className="space-y-8">
|
||||
<div>
|
||||
{(isImageFile || isVideoFile) && (
|
||||
<div className={`block w-full aspect-w-10 aspect-h-7 overflow-hidden border rounded border-[var(--frontmatter-border)] bg-[var(--vscode-editor-background)]`}>
|
||||
|
||||
@@ -10,26 +10,64 @@ export interface IIntegrationsViewProps { }
|
||||
|
||||
export const IntegrationsView: React.FunctionComponent<IIntegrationsViewProps> = ({ }: React.PropsWithChildren<IIntegrationsViewProps>) => {
|
||||
const [deeplApiKey, setDeeplApiKey] = React.useState<string>('');
|
||||
const [azureApiKey, setAzureApiKey] = React.useState<string>('');
|
||||
const [azureRegion, setAzureRegion] = React.useState<string>('');
|
||||
const [crntDeeplApiKey, setCrntDeeplApiKey] = React.useState<string>('');
|
||||
const [crntAzureApiKey, setCrntAzureApiKey] = React.useState<string>('');
|
||||
const [crntAzureRegion, setCrntAzureRegion] = React.useState<string>('');
|
||||
|
||||
const onSave = React.useCallback(() => {
|
||||
messageHandler.request<string>(GeneralCommands.toVSCode.secrets.set, {
|
||||
key: ExtensionState.Secrets.DeeplApiKey,
|
||||
value: crntDeeplApiKey
|
||||
}).then((apiKey: string) => {
|
||||
setDeeplApiKey(apiKey);
|
||||
});
|
||||
}, [crntDeeplApiKey]);
|
||||
if (crntDeeplApiKey !== deeplApiKey) {
|
||||
messageHandler.request<string>(GeneralCommands.toVSCode.secrets.set, {
|
||||
key: ExtensionState.Secrets.Deepl.ApiKey,
|
||||
value: crntDeeplApiKey
|
||||
}).then((apiKey: string) => {
|
||||
setDeeplApiKey(apiKey);
|
||||
});
|
||||
}
|
||||
|
||||
const onChange = (_: string, value: string) => {
|
||||
setCrntDeeplApiKey(value);
|
||||
if (crntAzureApiKey !== azureApiKey) {
|
||||
messageHandler.request<string>(GeneralCommands.toVSCode.secrets.set, {
|
||||
key: ExtensionState.Secrets.Azure.TranslatorKey,
|
||||
value: crntAzureApiKey
|
||||
}).then((apiKey: string) => {
|
||||
setAzureApiKey(apiKey);
|
||||
});
|
||||
}
|
||||
|
||||
if (crntAzureRegion !== azureRegion) {
|
||||
messageHandler.request<string>(GeneralCommands.toVSCode.secrets.set, {
|
||||
key: ExtensionState.Secrets.Azure.TranslatorRegion,
|
||||
value: crntAzureRegion
|
||||
}).then((apiKey: string) => {
|
||||
setAzureRegion(apiKey);
|
||||
});
|
||||
}
|
||||
}, [crntDeeplApiKey, deeplApiKey, crntAzureApiKey, azureApiKey, crntAzureRegion, azureRegion]);
|
||||
|
||||
const onChange = (key: string, value: string) => {
|
||||
if (key === ExtensionState.Secrets.Deepl.ApiKey) {
|
||||
setCrntDeeplApiKey(value);
|
||||
} else if (key === ExtensionState.Secrets.Azure.TranslatorKey) {
|
||||
setCrntAzureApiKey(value);
|
||||
} else if (key === ExtensionState.Secrets.Azure.TranslatorRegion) {
|
||||
setCrntAzureRegion(value);
|
||||
}
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
messageHandler.request<string>(GeneralCommands.toVSCode.secrets.get, ExtensionState.Secrets.DeeplApiKey).then((apiKey: string) => {
|
||||
messageHandler.request<string>(GeneralCommands.toVSCode.secrets.get, ExtensionState.Secrets.Deepl.ApiKey).then((apiKey: string) => {
|
||||
setDeeplApiKey(apiKey);
|
||||
setCrntDeeplApiKey(apiKey);
|
||||
});
|
||||
messageHandler.request<string>(GeneralCommands.toVSCode.secrets.get, ExtensionState.Secrets.Azure.TranslatorKey).then((apiKey: string) => {
|
||||
setAzureApiKey(apiKey);
|
||||
setCrntAzureApiKey(apiKey);
|
||||
});
|
||||
messageHandler.request<string>(GeneralCommands.toVSCode.secrets.get, ExtensionState.Secrets.Azure.TranslatorRegion).then((apiKey: string) => {
|
||||
setAzureRegion(apiKey);
|
||||
setCrntAzureRegion(apiKey);
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
@@ -39,16 +77,37 @@ export const IntegrationsView: React.FunctionComponent<IIntegrationsViewProps> =
|
||||
|
||||
<SettingsInput
|
||||
label={l10n.t(LocalizationKey.settingsIntegrationsViewDeeplIntputLabel)}
|
||||
name={ExtensionState.Secrets.DeeplApiKey}
|
||||
name={ExtensionState.Secrets.Deepl.ApiKey}
|
||||
value={crntDeeplApiKey || ""}
|
||||
placeholder={l10n.t(LocalizationKey.settingsIntegrationsViewDeeplIntputPlaceholder)}
|
||||
onChange={onChange}
|
||||
/>
|
||||
|
||||
<h2 className='text-xl mb-2'>{l10n.t(LocalizationKey.settingsIntegrationsViewAzureTitle)}</h2>
|
||||
|
||||
<SettingsInput
|
||||
label={l10n.t(LocalizationKey.settingsIntegrationsViewAzureIntputLabel)}
|
||||
name={ExtensionState.Secrets.Azure.TranslatorKey}
|
||||
value={crntAzureApiKey || ""}
|
||||
placeholder={l10n.t(LocalizationKey.settingsIntegrationsViewAzureIntputPlaceholder)}
|
||||
onChange={onChange}
|
||||
/>
|
||||
<SettingsInput
|
||||
label={l10n.t(LocalizationKey.settingsIntegrationsViewAzureRegionLabel)}
|
||||
name={ExtensionState.Secrets.Azure.TranslatorRegion}
|
||||
value={crntAzureRegion || ""}
|
||||
placeholder={l10n.t(LocalizationKey.settingsIntegrationsViewAzureRegionPlaceholder)}
|
||||
onChange={onChange}
|
||||
/>
|
||||
|
||||
<div className={`mt-4 flex gap-2`}>
|
||||
<VSCodeButton
|
||||
onClick={onSave}
|
||||
disabled={deeplApiKey === crntDeeplApiKey}>
|
||||
disabled={
|
||||
deeplApiKey === crntDeeplApiKey &&
|
||||
azureApiKey === crntAzureApiKey &&
|
||||
azureRegion === crntAzureRegion
|
||||
}>
|
||||
{l10n.t(LocalizationKey.commonSave)}
|
||||
</VSCodeButton>
|
||||
</div>
|
||||
|
||||
@@ -15,7 +15,6 @@ import { Messenger } from '@estruyf/vscode/dist/client';
|
||||
import { EventData } from '@estruyf/vscode/dist/models';
|
||||
import { NavigationType } from '../models';
|
||||
import { GeneralCommands } from '../../constants';
|
||||
import * as l10n from '@vscode/l10n';
|
||||
|
||||
export default function useMessages() {
|
||||
const [loading, setLoading] = useRecoilState(LoadingAtom);
|
||||
@@ -25,7 +24,6 @@ export default function useMessages() {
|
||||
const [, setMode] = useRecoilState(ModeAtom);
|
||||
const [, setView] = useRecoilState(DashboardViewAtom);
|
||||
const [, setSearchReady] = useRecoilState(SearchReadyAtom);
|
||||
const [localeReady, setLocaleReady] = useState<boolean>(false);
|
||||
|
||||
const messageListener = (event: MessageEvent<EventData<any>>) => {
|
||||
const message = event.data;
|
||||
@@ -61,12 +59,6 @@ export default function useMessages() {
|
||||
case GeneralCommands.toWebview.setMode:
|
||||
setMode(message.payload);
|
||||
break;
|
||||
case GeneralCommands.toWebview.setLocalization:
|
||||
l10n.config({
|
||||
contents: message.payload
|
||||
});
|
||||
setLocaleReady(true);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -78,7 +70,6 @@ export default function useMessages() {
|
||||
Messenger.send(DashboardMessage.getTheme);
|
||||
Messenger.send(DashboardMessage.getData);
|
||||
Messenger.send(DashboardMessage.getMode);
|
||||
Messenger.send(GeneralCommands.toVSCode.getLocalization);
|
||||
|
||||
return () => {
|
||||
Messenger.unlisten(messageListener);
|
||||
@@ -89,7 +80,6 @@ export default function useMessages() {
|
||||
loading,
|
||||
pages,
|
||||
viewData,
|
||||
settings,
|
||||
localeReady
|
||||
settings
|
||||
};
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ import { DashboardMessage } from '../DashboardMessage';
|
||||
import { EventData } from '@estruyf/vscode/dist/models';
|
||||
import { parseWinPath } from '../../helpers/parseWinPath';
|
||||
import { sortPages } from '../../utils/sortPages';
|
||||
import { ExtensionState } from '../../constants';
|
||||
import { ExtensionState, GeneralCommands } from '../../constants';
|
||||
import { SortingOption } from '../models';
|
||||
import { I18nConfig } from '../../models';
|
||||
import { usePrevious } from '../../panelWebView/hooks/usePrevious';
|
||||
@@ -204,7 +204,7 @@ export default function usePages(pages: Page[]) {
|
||||
setTabInfo(draftTypes);
|
||||
|
||||
if (Object.keys(filters).length === 0) {
|
||||
const availableFilters = (settings?.filters || []).filter((f) => f !== 'pageFolders' && f !== 'tags' && f !== 'categories');
|
||||
const availableFilters = (settings?.filters || []).filter((f) => f !== 'contentFolders' && f !== 'tags' && f !== 'categories');
|
||||
if (availableFilters.length > 0) {
|
||||
const allFilters: { [filter: string]: string[]; } = {};
|
||||
for (const filter of availableFilters) {
|
||||
@@ -220,16 +220,6 @@ export default function usePages(pages: Page[]) {
|
||||
}
|
||||
}
|
||||
|
||||
if (tabPrevious !== tab || !locales || locales.length === 0) {
|
||||
// Store the locale information
|
||||
const config: I18nConfig[] = [];
|
||||
crntPages.forEach((page) => {
|
||||
if (page.fmLocale && !config.some(locale => locale.locale === page.fmLocale?.locale)) {
|
||||
config.push(page.fmLocale);
|
||||
}
|
||||
});
|
||||
setLocales(config);
|
||||
}
|
||||
|
||||
// Set the pages
|
||||
setPageItems(crntPages);
|
||||
@@ -276,6 +266,12 @@ export default function usePages(pages: Page[]) {
|
||||
} else {
|
||||
startPageProcessing();
|
||||
}
|
||||
|
||||
if (pages && pages.length > 0) {
|
||||
messageHandler.request<I18nConfig[]>(GeneralCommands.toVSCode.content.locales).then((config) => {
|
||||
setLocales(config || []);
|
||||
});
|
||||
}
|
||||
}, [settings?.draftField, pages, sorting, search, tag, category, locale, filters, folder]);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -3,8 +3,6 @@ import { render } from 'react-dom';
|
||||
import { RecoilRoot } from 'recoil';
|
||||
import { App } from './components/App';
|
||||
import * as Sentry from '@sentry/react';
|
||||
import { Integrations } from '@sentry/tracing';
|
||||
import { SENTRY_LINK, SentryIgnore } from '../constants';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import './styles.css';
|
||||
import { Preview } from './components/Preview';
|
||||
@@ -12,6 +10,8 @@ import { SettingsProvider } from './providers/SettingsProvider';
|
||||
import { CustomPanelViewResult } from '../models';
|
||||
import { Chatbot } from './components/Chatbot/Chatbot';
|
||||
import { updateCssVariables } from './utils';
|
||||
import { I10nProvider } from './providers/I10nProvider';
|
||||
import { SentryInit } from '../utils/sentryInit';
|
||||
|
||||
declare const acquireVsCodeApi: <T = unknown>() => {
|
||||
getState: () => T;
|
||||
@@ -50,7 +50,7 @@ export const routePaths: { [name: string]: string } = {
|
||||
settings: '/settings',
|
||||
};
|
||||
|
||||
const mutationObserver = new MutationObserver((mutationsList, observer) => {
|
||||
const mutationObserver = new MutationObserver((_, __) => {
|
||||
updateCssVariables();
|
||||
});
|
||||
|
||||
@@ -64,19 +64,13 @@ if (elm) {
|
||||
const url = elm?.getAttribute('data-url');
|
||||
const experimental = elm?.getAttribute('data-experimental');
|
||||
const webviewUrl = elm?.getAttribute('data-webview-url');
|
||||
const isCrashDisabled = elm?.getAttribute('data-is-crash-disabled');
|
||||
|
||||
updateCssVariables();
|
||||
mutationObserver.observe(document.body, { childList: false, attributes: true });
|
||||
|
||||
if (isProd === 'true') {
|
||||
Sentry.init({
|
||||
dsn: SENTRY_LINK,
|
||||
integrations: [new Integrations.BrowserTracing()],
|
||||
tracesSampleRate: 0, // No performance tracing required
|
||||
release: version || '',
|
||||
environment: environment || '',
|
||||
ignoreErrors: SentryIgnore
|
||||
});
|
||||
if (isProd === 'true' && isCrashDisabled === 'false') {
|
||||
Sentry.init(SentryInit(version, environment));
|
||||
|
||||
Sentry.setTag("type", "dashboard");
|
||||
if (document.body.getAttribute(`data-vscode-theme-id`)) {
|
||||
@@ -88,17 +82,21 @@ if (elm) {
|
||||
|
||||
if (type === 'preview') {
|
||||
render(
|
||||
<SettingsProvider experimental={experimental === 'true'} version={version || ""}>
|
||||
<Preview url={url} />
|
||||
</SettingsProvider>, elm);
|
||||
<I10nProvider>
|
||||
<SettingsProvider experimental={experimental === 'true'} version={version || ""}>
|
||||
<Preview url={url} />
|
||||
</SettingsProvider>
|
||||
</I10nProvider>, elm);
|
||||
} else if (type === 'chatbot') {
|
||||
render(
|
||||
<SettingsProvider
|
||||
aiUrl='https://frontmatter.codes'
|
||||
experimental={experimental === 'true'}
|
||||
version={version || ""}>
|
||||
<Chatbot />
|
||||
</SettingsProvider>, elm);
|
||||
<I10nProvider>
|
||||
<SettingsProvider
|
||||
aiUrl='https://frontmatter.codes'
|
||||
experimental={experimental === 'true'}
|
||||
version={version || ""}>
|
||||
<Chatbot />
|
||||
</SettingsProvider>
|
||||
</I10nProvider>, elm);
|
||||
} else {
|
||||
render(
|
||||
<RecoilRoot>
|
||||
@@ -106,9 +104,11 @@ if (elm) {
|
||||
initialEntries={Object.keys(routePaths).map((key: string) => routePaths[key]) as string[]}
|
||||
initialIndex={1}
|
||||
>
|
||||
<SettingsProvider experimental={experimental === 'true'} version={version || ""} webviewUrl={webviewUrl || ""}>
|
||||
<App showWelcome={!!welcome} />
|
||||
</SettingsProvider>
|
||||
<I10nProvider>
|
||||
<SettingsProvider experimental={experimental === 'true'} version={version || ""} webviewUrl={webviewUrl || ""}>
|
||||
<App showWelcome={!!welcome} />
|
||||
</SettingsProvider>
|
||||
</I10nProvider>
|
||||
</MemoryRouter>
|
||||
</RecoilRoot>,
|
||||
elm
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
CustomScript,
|
||||
CustomTaxonomy,
|
||||
DraftField,
|
||||
FilterType,
|
||||
Framework,
|
||||
GitSettings,
|
||||
MediaContentType,
|
||||
@@ -38,7 +39,7 @@ export interface Settings {
|
||||
framework: Framework | null | undefined;
|
||||
draftField: DraftField | null | undefined;
|
||||
customSorting: SortingSetting[] | undefined;
|
||||
filters: (string | { title: string; name: string })[] | undefined;
|
||||
filters: (FilterType | { title: string; name: string })[] | undefined;
|
||||
dashboardState: DashboardState;
|
||||
scripts: CustomScript[];
|
||||
dataFiles: DataFile[] | undefined;
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import * as React from 'react';
|
||||
import { messageHandler } from '@estruyf/vscode/dist/client';
|
||||
import { GeneralCommands } from '../../constants';
|
||||
import * as l10n from '@vscode/l10n';
|
||||
|
||||
interface I10nProviderProps { }
|
||||
|
||||
const I10nContext = React.createContext<I10nProviderProps | undefined>(undefined);
|
||||
|
||||
const I10nProvider: React.FunctionComponent<I10nProviderProps> = ({ children }: React.PropsWithChildren<I10nProviderProps>) => {
|
||||
const [localeReady, setLocaleReady] = React.useState<boolean>(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
messageHandler.request<any>(GeneralCommands.toVSCode.getLocalization).then((contents) => {
|
||||
if (contents) {
|
||||
l10n.config({
|
||||
contents
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
setLocaleReady(true);
|
||||
}, 0);
|
||||
}
|
||||
}).catch(() => {
|
||||
setLocaleReady(false);
|
||||
throw new Error('Error getting localization');
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<I10nContext.Provider value={{}}>
|
||||
{
|
||||
localeReady && children
|
||||
}
|
||||
</I10nContext.Provider>
|
||||
)
|
||||
};
|
||||
|
||||
const useI10nContext = (): I10nProviderProps => {
|
||||
const loadFunc = React.useContext(I10nContext);
|
||||
|
||||
if (loadFunc === undefined) {
|
||||
throw new Error('useI10nContext must be used within the I10nProvider');
|
||||
}
|
||||
|
||||
return loadFunc;
|
||||
};
|
||||
|
||||
I10nContext.displayName = 'I10nContext';
|
||||
I10nProvider.displayName = 'I10nProvider';
|
||||
|
||||
export { I10nProvider, useI10nContext };
|
||||
@@ -18,7 +18,6 @@ import {
|
||||
SETTING_SITE_BASEURL,
|
||||
SETTING_TAXONOMY_CONTENT_TYPES,
|
||||
SETTING_TEMPLATES_PREFIX,
|
||||
SETTING_MODIFIED_FIELD,
|
||||
DefaultFieldValues
|
||||
} from '../constants';
|
||||
import { DumpOptions } from 'js-yaml';
|
||||
@@ -387,11 +386,7 @@ export class ArticleHelper {
|
||||
const articleCt = ArticleHelper.getContentType(article);
|
||||
const modDateField = articleCt.fields.find((f) => f.isModifiedDate);
|
||||
|
||||
return (
|
||||
modDateField?.name ||
|
||||
(Settings.get(SETTING_MODIFIED_FIELD) as string) ||
|
||||
DefaultFields.LastModified
|
||||
);
|
||||
return modDateField?.name || DefaultFields.LastModified;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -95,7 +95,7 @@ export class ContentType {
|
||||
|
||||
const contentTypes = ContentType.getAll();
|
||||
const folders = Folders.get().filter((f) => !f.disableCreation);
|
||||
const folder = folders.find((f) => f.title === selectedFolder);
|
||||
const folder = folders.find((f) => f.path === selectedFolder.path);
|
||||
|
||||
if (!folder) {
|
||||
return;
|
||||
@@ -937,6 +937,13 @@ export class ContentType {
|
||||
contentType
|
||||
);
|
||||
|
||||
let isTypeSet = false;
|
||||
if (data.type) {
|
||||
isTypeSet = true;
|
||||
} else {
|
||||
data.type = contentType.name;
|
||||
}
|
||||
|
||||
const article: ParsedFrontMatter = {
|
||||
content: '',
|
||||
data: Object.assign({}, data),
|
||||
@@ -945,6 +952,10 @@ export class ContentType {
|
||||
|
||||
data = ArticleHelper.updateDates(article);
|
||||
|
||||
if (isTypeSet) {
|
||||
delete data.type;
|
||||
}
|
||||
|
||||
if (contentType.name !== DEFAULT_CONTENT_TYPE_NAME) {
|
||||
data['type'] = contentType.name;
|
||||
}
|
||||
|
||||
@@ -42,6 +42,7 @@ import {
|
||||
CustomScript,
|
||||
DEFAULT_MEDIA_CONTENT_TYPE,
|
||||
DraftField,
|
||||
FilterType,
|
||||
MediaContentType,
|
||||
Snippets,
|
||||
SortingSetting,
|
||||
@@ -111,7 +112,8 @@ export class DashboardSettings {
|
||||
draftField: Settings.get<DraftField>(SETTING_CONTENT_DRAFT_FIELD),
|
||||
customSorting: Settings.get<SortingSetting[]>(SETTING_CONTENT_SORTING),
|
||||
contentFolders: Folders.get(),
|
||||
filters: Settings.get<string[]>(SETTING_CONTENT_FILTERS),
|
||||
filters:
|
||||
Settings.get<(FilterType | { title: string; name: string })[]>(SETTING_CONTENT_FILTERS),
|
||||
crntFramework: Settings.get<string>(SETTING_FRAMEWORK_ID),
|
||||
framework: !isInitialized && wsFolder ? await FrameworkDetector.get(wsFolder.fsPath) : null,
|
||||
scripts: Settings.get<CustomScript[]>(SETTING_CUSTOM_SCRIPTS) || [],
|
||||
|
||||
@@ -15,20 +15,15 @@ import { Template } from '../commands/Template';
|
||||
import {
|
||||
EXTENSION_NAME,
|
||||
GITHUB_LINK,
|
||||
SETTING_DATE_FIELD,
|
||||
SETTING_MODIFIED_FIELD,
|
||||
EXTENSION_BETA_ID,
|
||||
EXTENSION_ID,
|
||||
ExtensionState,
|
||||
CONFIG_KEY,
|
||||
SETTING_CONTENT_PAGE_FOLDERS,
|
||||
SETTING_DASHBOARD_MEDIA_SNIPPET,
|
||||
SETTING_CONTENT_SNIPPETS,
|
||||
SETTING_TEMPLATES_ENABLED,
|
||||
SETTING_TAXONOMY_TAGS,
|
||||
SETTING_TAXONOMY_CATEGORIES
|
||||
} from '../constants';
|
||||
import { ContentFolder, Snippet, TaxonomyType } from '../models';
|
||||
import { ContentFolder, TaxonomyType } from '../models';
|
||||
import { Notifications } from './Notifications';
|
||||
import { Settings } from './SettingsHelper';
|
||||
import { TaxonomyHelper } from './TaxonomyHelper';
|
||||
@@ -202,64 +197,6 @@ export class Extension {
|
||||
await Settings.createTeamSettings();
|
||||
}
|
||||
|
||||
const hideDateDeprecation = await Extension.getInstance().getState<boolean>(
|
||||
ExtensionState.Updates.v7_0_0.dateFields,
|
||||
'workspace'
|
||||
);
|
||||
if (!hideDateDeprecation) {
|
||||
// Migration scripts can be written here
|
||||
const publishField = Settings.inspect(SETTING_DATE_FIELD);
|
||||
const modifiedField = Settings.inspect(SETTING_MODIFIED_FIELD);
|
||||
|
||||
// Check for extension deprecations
|
||||
if (
|
||||
publishField?.workspaceValue ||
|
||||
publishField?.globalValue ||
|
||||
publishField?.teamValue ||
|
||||
modifiedField?.workspaceValue ||
|
||||
modifiedField?.globalValue ||
|
||||
modifiedField?.teamValue
|
||||
) {
|
||||
Notifications.warning(
|
||||
l10n.t(
|
||||
LocalizationKey.helpersExtensionMigrateSettingsDeprecatedWarning,
|
||||
`${CONFIG_KEY}.${SETTING_DATE_FIELD}`,
|
||||
`${CONFIG_KEY}.${SETTING_MODIFIED_FIELD}`
|
||||
),
|
||||
l10n.t(LocalizationKey.helpersExtensionMigrateSettingsDeprecatedWarningHide),
|
||||
l10n.t(LocalizationKey.helpersExtensionMigrateSettingsDeprecatedWarningSeeGuide)
|
||||
).then(async (value) => {
|
||||
if (
|
||||
value ===
|
||||
l10n.t(LocalizationKey.helpersExtensionMigrateSettingsDeprecatedWarningSeeGuide)
|
||||
) {
|
||||
const isProd = this.isProductionMode;
|
||||
commands.executeCommand(
|
||||
'vscode.open',
|
||||
Uri.parse(
|
||||
`https://${
|
||||
isProd ? '' : 'beta.'
|
||||
}frontmatter.codes/docs/troubleshooting#publish-and-modified-date-migration`
|
||||
)
|
||||
);
|
||||
await Extension.getInstance().setState<boolean>(
|
||||
ExtensionState.Updates.v7_0_0.dateFields,
|
||||
true,
|
||||
'workspace'
|
||||
);
|
||||
} else if (
|
||||
value === l10n.t(LocalizationKey.helpersExtensionMigrateSettingsDeprecatedWarningHide)
|
||||
) {
|
||||
await Extension.getInstance().setState<boolean>(
|
||||
ExtensionState.Updates.v7_0_0.dateFields,
|
||||
true,
|
||||
'workspace'
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (major < 7) {
|
||||
const contentFolders: ContentFolder[] = Settings.get(
|
||||
SETTING_CONTENT_PAGE_FOLDERS
|
||||
@@ -282,28 +219,6 @@ export class Extension {
|
||||
}
|
||||
|
||||
if (major <= 7 && minor < 3) {
|
||||
const mediaSnippet = Settings.get<string[]>(SETTING_DASHBOARD_MEDIA_SNIPPET);
|
||||
if (mediaSnippet && mediaSnippet.length > 0) {
|
||||
let snippet = mediaSnippet.join(`\n`);
|
||||
|
||||
snippet = snippet.replace(`{mediaUrl}`, `[[&mediaUrl]]`);
|
||||
snippet = snippet.replace(`{mediaHeight}`, `[[mediaHeight]]`);
|
||||
snippet = snippet.replace(`{mediaWidth}`, `[[mediaWidth]]`);
|
||||
snippet = snippet.replace(`{caption}`, `[[&caption]]`);
|
||||
snippet = snippet.replace(`{alt}`, `[[alt]]`);
|
||||
snippet = snippet.replace(`{filename}`, `[[filename]]`);
|
||||
snippet = snippet.replace(`{title}`, `[[title]]`);
|
||||
|
||||
const snippets = Settings.get<Snippet[]>(SETTING_CONTENT_SNIPPETS) || ({} as any);
|
||||
snippets[`Media snippet (migrated)`] = {
|
||||
body: snippet.split(`\n`),
|
||||
isMediaSnippet: true,
|
||||
description: `Migrated media snippet from frontMatter.dashboard.mediaSnippet setting`
|
||||
};
|
||||
|
||||
await Settings.update(SETTING_CONTENT_SNIPPETS, snippets, true);
|
||||
}
|
||||
|
||||
const templates = await Template.getTemplates();
|
||||
if (templates && templates.length > 0) {
|
||||
const answer = await window.showQuickPick(
|
||||
|
||||
@@ -8,6 +8,12 @@ import { Logger } from './Logger';
|
||||
import { SponsorAi } from '../services/SponsorAI';
|
||||
import * as l10n from '@vscode/l10n';
|
||||
import { LocalizationKey } from '../localization';
|
||||
import { ContentFolder } from '../models';
|
||||
|
||||
interface FolderQuickPickItem extends QuickPickItem {
|
||||
path: string;
|
||||
locale?: string;
|
||||
}
|
||||
|
||||
export class Questions {
|
||||
/**
|
||||
@@ -124,13 +130,27 @@ export class Questions {
|
||||
*/
|
||||
public static async SelectContentFolder(
|
||||
showWarning: boolean = true
|
||||
): Promise<string | undefined> {
|
||||
): Promise<FolderQuickPickItem | undefined> {
|
||||
let folders = Folders.get().filter((f) => !f.disableCreation);
|
||||
|
||||
let selectedFolder: string | undefined;
|
||||
let selectedFolder: FolderQuickPickItem | undefined;
|
||||
if (folders.length > 1) {
|
||||
const folderOptions = folders.map((f: ContentFolder) => {
|
||||
if (f.locale) {
|
||||
return {
|
||||
label: `${f.title} (${f.localeTitle || f.locale})`,
|
||||
locale: f.locale,
|
||||
path: f.path
|
||||
} as FolderQuickPickItem;
|
||||
}
|
||||
return {
|
||||
label: f.title,
|
||||
path: f.path
|
||||
} as FolderQuickPickItem;
|
||||
});
|
||||
|
||||
selectedFolder = await window.showQuickPick(
|
||||
folders.map((f) => f.title),
|
||||
folderOptions,
|
||||
{
|
||||
title: l10n.t(LocalizationKey.helpersQuestionsSelectContentFolderQuickPickTitle),
|
||||
placeHolder: l10n.t(
|
||||
@@ -140,7 +160,10 @@ export class Questions {
|
||||
}
|
||||
);
|
||||
} else if (folders.length === 1) {
|
||||
selectedFolder = folders[0].title;
|
||||
selectedFolder = {
|
||||
label: folders[0].title,
|
||||
path: folders[0].path
|
||||
} as FolderQuickPickItem;
|
||||
} else {
|
||||
// When no page folders are found, the welcome dashboard is shown
|
||||
return;
|
||||
@@ -189,6 +212,11 @@ export class Questions {
|
||||
label: contentType.name
|
||||
}));
|
||||
|
||||
if (options.length === 0) {
|
||||
Notifications.error(LocalizationKey.helpersQuestionsSelectContentTypeQuickPickErrorNoContentTypes);
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedOption = await window.showQuickPick(options, {
|
||||
title: l10n.t(LocalizationKey.helpersQuestionsSelectContentTypeQuickPickTitle),
|
||||
placeHolder: l10n.t(LocalizationKey.helpersQuestionsSelectContentTypeQuickPickPlaceholder),
|
||||
|
||||
@@ -43,7 +43,8 @@ import {
|
||||
SETTING_CONFIG_DYNAMIC_FILE_PATH,
|
||||
SETTING_PROJECTS,
|
||||
SETTING_TAXONOMY_TAGS,
|
||||
SETTING_TAXONOMY_CATEGORIES
|
||||
SETTING_TAXONOMY_CATEGORIES,
|
||||
SETTING_CONTENT_FILTERS
|
||||
} from '../constants';
|
||||
import { Folders } from '../commands/Folders';
|
||||
import { join, basename, dirname, parse } from 'path';
|
||||
@@ -804,7 +805,8 @@ export class Settings {
|
||||
settingName === SETTING_GLOBAL_NOTIFICATIONS ||
|
||||
settingName === SETTING_GLOBAL_NOTIFICATIONS_DISABLED ||
|
||||
settingName === SETTING_MEDIA_SUPPORTED_MIMETYPES ||
|
||||
settingName === SETTING_COMMA_SEPARATED_FIELDS
|
||||
settingName === SETTING_COMMA_SEPARATED_FIELDS ||
|
||||
settingName === SETTING_CONTENT_FILTERS
|
||||
) {
|
||||
if (typeof originalConfig[key] === 'undefined') {
|
||||
Settings.globalConfig[key] = value;
|
||||
|
||||
@@ -24,6 +24,9 @@ export class SlugHelper {
|
||||
if (slugTemplate) {
|
||||
if (slugTemplate.includes('{{title}}')) {
|
||||
const regex = new RegExp('{{title}}', 'g');
|
||||
slugTemplate = slugTemplate.replace(regex, articleTitle.toLowerCase().replace(/\s/g, '-'));
|
||||
} else if (slugTemplate.includes('{{seoTitle}}')) {
|
||||
const regex = new RegExp('{{seoTitle}}', 'g');
|
||||
slugTemplate = slugTemplate.replace(regex, SlugHelper.slugify(articleTitle));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { workspace } from 'vscode';
|
||||
import { Extension, Settings } from '.';
|
||||
import { EXTENSION_BETA_ID, EXTENSION_ID, SETTING_TELEMETRY_DISABLE } from '../constants';
|
||||
|
||||
@@ -23,6 +24,24 @@ export class Telemetry {
|
||||
return Telemetry.instance;
|
||||
}
|
||||
|
||||
public static isVscodeEnabled(): boolean {
|
||||
const config = workspace.getConfiguration('telemetry');
|
||||
const isVscodeEnable = config.get<'off' | undefined>('enableTelemetry');
|
||||
return isVscodeEnable === 'off' ? false : true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if telemetry is enabled.
|
||||
* @returns {boolean} Returns true if telemetry is enabled, false otherwise.
|
||||
*/
|
||||
public static isEnabled(): boolean {
|
||||
const isVscodeEnable = Telemetry.isVscodeEnabled();
|
||||
|
||||
const isDisabled = Settings.get<boolean>(SETTING_TELEMETRY_DISABLE);
|
||||
|
||||
return isDisabled || isVscodeEnable ? false : true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send metrics to our own database
|
||||
* @param eventName
|
||||
@@ -30,8 +49,7 @@ export class Telemetry {
|
||||
* @returns
|
||||
*/
|
||||
public static send(eventName: string, properties?: any) {
|
||||
const isDisabled = Settings.get<boolean>(SETTING_TELEMETRY_DISABLE);
|
||||
if (isDisabled) {
|
||||
if (!Telemetry.isEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import { GeneralCommands } from '../../constants';
|
||||
import { PostMessageData } from '../../models';
|
||||
import { BaseListener } from './BaseListener';
|
||||
import { getLocalizationFile } from '../../utils/getLocalizationFile';
|
||||
import { i18n } from '../../commands/i18n';
|
||||
|
||||
export class LocalizationListener extends BaseListener {
|
||||
/**
|
||||
@@ -11,14 +12,29 @@ export class LocalizationListener extends BaseListener {
|
||||
public static process(msg: PostMessageData) {
|
||||
switch (msg.command) {
|
||||
case GeneralCommands.toVSCode.getLocalization:
|
||||
this.getLocalization();
|
||||
this.getLocalization(msg.command, msg.requestId);
|
||||
break;
|
||||
case GeneralCommands.toVSCode.content.locales:
|
||||
this.getContentLocales(msg.command, msg.requestId);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public static async getLocalization() {
|
||||
const fileContents = await getLocalizationFile();
|
||||
public static async getLocalization(command: string, requestId?: string) {
|
||||
if (!command || !requestId) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.sendMsg(GeneralCommands.toWebview.setLocalization as any, fileContents);
|
||||
const fileContents = await getLocalizationFile();
|
||||
this.sendRequest(command as any, requestId, fileContents);
|
||||
}
|
||||
|
||||
private static async getContentLocales(command: string, requestId?: string) {
|
||||
if (!command || !requestId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const config = i18n.getAll();
|
||||
this.sendRequest(command as any, requestId, config);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -306,7 +306,6 @@ export class DataListener extends BaseListener {
|
||||
}
|
||||
}
|
||||
|
||||
const dateFields = ContentType.findFieldsByTypeDeep(contentType.fields, 'datetime');
|
||||
const imageFields = ContentType.findFieldsByTypeDeep(contentType.fields, 'image');
|
||||
const fileFields = ContentType.findFieldsByTypeDeep(contentType.fields, 'file');
|
||||
const fieldsWithEmojiEncoding = contentType.fields.filter((f) => f.encodeEmoji);
|
||||
@@ -314,13 +313,6 @@ export class DataListener extends BaseListener {
|
||||
// Support multi-level fields
|
||||
const parentObj = DataListener.getParentObject(article.data, article, parents, blockData);
|
||||
|
||||
const dateFieldsArray = dateFields.find((f: Field[]) => {
|
||||
const lastField = f?.[f.length - 1];
|
||||
if (lastField) {
|
||||
return lastField.name === field;
|
||||
}
|
||||
});
|
||||
|
||||
// Check multi-image fields
|
||||
const multiImageFieldsArray = imageFields.find((f: Field[]) => {
|
||||
const lastField = f?.[f.length - 1];
|
||||
@@ -338,13 +330,7 @@ export class DataListener extends BaseListener {
|
||||
});
|
||||
|
||||
// Check date fields
|
||||
if (dateFieldsArray && dateFieldsArray.length > 0) {
|
||||
for (const dateField of dateFieldsArray) {
|
||||
if (field === dateField.name && value) {
|
||||
parentObj[field] = Article.formatDate(new Date(value), dateField.dateFormat);
|
||||
}
|
||||
}
|
||||
} else if (multiImageFieldsArray || multiFileFieldsArray) {
|
||||
if (multiImageFieldsArray || multiFileFieldsArray) {
|
||||
const fields =
|
||||
multiImageFieldsArray && multiImageFieldsArray.length > 0
|
||||
? multiImageFieldsArray
|
||||
|
||||
@@ -11,14 +11,17 @@ export class LocalizationListener extends BaseListener {
|
||||
public static process(msg: PostMessageData) {
|
||||
switch (msg.command) {
|
||||
case GeneralCommands.toVSCode.getLocalization:
|
||||
this.getLocalization();
|
||||
this.getLocalization(msg.command, msg.requestId);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public static async getLocalization() {
|
||||
const fileContents = await getLocalizationFile();
|
||||
public static async getLocalization(command: string, requestId?: string) {
|
||||
if (!command || !requestId) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.sendMsg(GeneralCommands.toWebview.setLocalization as any, fileContents);
|
||||
const fileContents = await getLocalizationFile();
|
||||
this.sendRequest(command, requestId, fileContents);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -248,13 +248,33 @@ export enum LocalizationKey {
|
||||
*/
|
||||
settingsIntegrationsViewDeeplTitle = 'settings.integrationsView.deepl.title',
|
||||
/**
|
||||
* Authentication key
|
||||
* API key
|
||||
*/
|
||||
settingsIntegrationsViewDeeplIntputLabel = 'settings.integrationsView.deepl.intput.label',
|
||||
/**
|
||||
* Enter your DeepL authentication key
|
||||
* Enter your Azure Translator API key
|
||||
*/
|
||||
settingsIntegrationsViewDeeplIntputPlaceholder = 'settings.integrationsView.deepl.intput.placeholder',
|
||||
/**
|
||||
* Azure AI Translator Service
|
||||
*/
|
||||
settingsIntegrationsViewAzureTitle = 'settings.integrationsView.azure.title',
|
||||
/**
|
||||
* Subscription key
|
||||
*/
|
||||
settingsIntegrationsViewAzureIntputLabel = 'settings.integrationsView.azure.intput.label',
|
||||
/**
|
||||
* Enter your Azure AI Translator - Subscription key
|
||||
*/
|
||||
settingsIntegrationsViewAzureIntputPlaceholder = 'settings.integrationsView.azure.intput.placeholder',
|
||||
/**
|
||||
* Region
|
||||
*/
|
||||
settingsIntegrationsViewAzureRegionLabel = 'settings.integrationsView.azure.region.label',
|
||||
/**
|
||||
* Enter your Azure AI Translator - Region. Example: westeurope
|
||||
*/
|
||||
settingsIntegrationsViewAzureRegionPlaceholder = 'settings.integrationsView.azure.region.placeholder',
|
||||
/**
|
||||
* Developer mode
|
||||
*/
|
||||
@@ -1725,9 +1745,17 @@ export enum LocalizationKey {
|
||||
*/
|
||||
commandsI18nCreateWarningNoConfig = 'commands.i18n.create.warning.noConfig',
|
||||
/**
|
||||
* The current file cannot be used for i18n content creation.
|
||||
* Could not retrieve the locale for the current file.
|
||||
*/
|
||||
commandsI18nCreateWarningNotDefaultLocale = 'commands.i18n.create.warning.notDefaultLocale',
|
||||
commandsI18nCreateErrorNoLocaleDefinition = 'commands.i18n.create.error.noLocaleDefinition',
|
||||
/**
|
||||
* Current file has been translated to all available languages.
|
||||
*/
|
||||
commandsI18nCreateErrorNoLocales = 'commands.i18n.create.error.noLocales',
|
||||
/**
|
||||
* Could not define a content folder for the current file.
|
||||
*/
|
||||
commandsI18nCreateErrorNoContentFolder = 'commands.i18n.create.error.noContentFolder',
|
||||
/**
|
||||
* The i18n translation already exists.
|
||||
*/
|
||||
@@ -2148,18 +2176,6 @@ export enum LocalizationKey {
|
||||
* {0} has been updated to v{1} — check out what's new!
|
||||
*/
|
||||
helpersExtensionGetVersionUpdateNotification = 'helpers.extension.getVersion.update.notification',
|
||||
/**
|
||||
* The "{0}" and "{1}" settings have been deprecated. Please use the "isPublishDate" and "isModifiedDate" datetime field properties instead.
|
||||
*/
|
||||
helpersExtensionMigrateSettingsDeprecatedWarning = 'helpers.extension.migrateSettings.deprecated.warning',
|
||||
/**
|
||||
* Hide
|
||||
*/
|
||||
helpersExtensionMigrateSettingsDeprecatedWarningHide = 'helpers.extension.migrateSettings.deprecated.warning.hide',
|
||||
/**
|
||||
* See migration guide
|
||||
*/
|
||||
helpersExtensionMigrateSettingsDeprecatedWarningSeeGuide = 'helpers.extension.migrateSettings.deprecated.warning.seeGuide',
|
||||
/**
|
||||
* {0} - Templates
|
||||
*/
|
||||
@@ -2280,6 +2296,10 @@ export enum LocalizationKey {
|
||||
* No content type was selected.
|
||||
*/
|
||||
helpersQuestionsSelectContentTypeNoSelectionWarning = 'helpers.questions.selectContentType.noSelection.warning',
|
||||
/**
|
||||
* There are no matching content types configured for this folder.
|
||||
*/
|
||||
helpersQuestionsSelectContentTypeQuickPickErrorNoContentTypes = 'helpers.questions.selectContentType.quickPick.error.noContentTypes',
|
||||
/**
|
||||
* Article {0} is longer than {1} characters (current length: {2}). For SEO reasons, it would be better to make it less than {1} characters.
|
||||
*/
|
||||
|
||||
@@ -12,6 +12,10 @@ export interface ContentFolder {
|
||||
originalPath?: string;
|
||||
$schema?: string;
|
||||
extended?: boolean;
|
||||
|
||||
locale?: string;
|
||||
localeTitle?: string;
|
||||
localeSourcePath?: string;
|
||||
defaultLocale?: string;
|
||||
locales: I18nConfig[];
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export type FilterType = 'contentFolders' | 'tags' | 'categories';
|
||||
@@ -191,6 +191,8 @@ export interface FolderInfo {
|
||||
title: string;
|
||||
files: number;
|
||||
lastModified: FileInfo[];
|
||||
locale?: string;
|
||||
localeTitle?: string;
|
||||
}
|
||||
|
||||
export interface FileInfo extends FileStat {
|
||||
|
||||
@@ -11,6 +11,7 @@ export * from './DataFile';
|
||||
export * from './DataFolder';
|
||||
export * from './DataType';
|
||||
export * from './DraftField';
|
||||
export * from './FilterType';
|
||||
export * from './Framework';
|
||||
export * from './GitRepository';
|
||||
export * from './GitSettings';
|
||||
|
||||
@@ -286,7 +286,9 @@ export class PanelProvider implements WebviewViewProvider, Disposable {
|
||||
<body>
|
||||
<div id="app" data-isProd="${isProd}" data-environment="${
|
||||
isBeta ? 'BETA' : 'main'
|
||||
}" data-version="${version.usedVersion}"></div>
|
||||
}" data-version="${
|
||||
version.usedVersion
|
||||
}" data-is-crash-disabled="${!Telemetry.isVscodeEnabled()}"></div>
|
||||
|
||||
${(scriptsToLoad || [])
|
||||
.map((script) => {
|
||||
|
||||
@@ -33,7 +33,6 @@ export const ViewPanel: React.FunctionComponent<IViewPanelProps> = (
|
||||
folderAndFiles,
|
||||
focusElm,
|
||||
unsetFocus,
|
||||
localeReady,
|
||||
mode
|
||||
} = useMessages();
|
||||
const prevMediaSelection = usePrevious(mediaSelecting);
|
||||
@@ -83,7 +82,7 @@ export const ViewPanel: React.FunctionComponent<IViewPanelProps> = (
|
||||
);
|
||||
}
|
||||
|
||||
if (loading && !localeReady) {
|
||||
if (loading) {
|
||||
return <Spinner />;
|
||||
}
|
||||
|
||||
|
||||
@@ -95,6 +95,13 @@ export const DataBlockField: React.FunctionComponent<IDataBlockFieldProps> = ({
|
||||
// Delete the field group to have it added at the end
|
||||
delete data['fieldGroup'];
|
||||
|
||||
// Remove the empty fields
|
||||
Object.keys(data).forEach((key) => {
|
||||
if (data[key] === undefined || data[key] === null || Object.keys(data[key]).length === 0) {
|
||||
delete data[key];
|
||||
}
|
||||
});
|
||||
|
||||
if (selectedIndex !== null && selectedIndex !== undefined && dataClone.length > 0) {
|
||||
dataClone[selectedIndex] = {
|
||||
...data,
|
||||
@@ -306,7 +313,7 @@ export const DataBlockField: React.FunctionComponent<IDataBlockFieldProps> = ({
|
||||
{selectedGroup?.fields &&
|
||||
fieldsRenderer(
|
||||
selectedGroup?.fields,
|
||||
selectedBlockData || {},
|
||||
Object.assign({}, selectedBlockData) || {},
|
||||
[...parentFields, field.name],
|
||||
{
|
||||
parentFields: [...parentFields, field.name],
|
||||
|
||||
@@ -11,7 +11,7 @@ import { LocalizationKey } from '../../../localization';
|
||||
|
||||
export interface IDateTimeFieldProps extends BaseFieldProps<Date | null> {
|
||||
format?: string;
|
||||
onChange: (date: Date) => void;
|
||||
onChange: (date: string) => void;
|
||||
}
|
||||
|
||||
type InputProps = JSX.IntrinsicElements['input'];
|
||||
@@ -37,7 +37,7 @@ export const DateTimeField: React.FunctionComponent<IDateTimeFieldProps> = ({
|
||||
|
||||
const onDateChange = React.useCallback((date: Date) => {
|
||||
setDateValue(date);
|
||||
onChange(date);
|
||||
onChange(DateHelper.format(date, format || DEFAULT_FORMAT) || "");
|
||||
}, [format, onChange]);
|
||||
|
||||
const showRequiredState = useMemo(() => {
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
import * as React from 'react';
|
||||
import { BlockFieldData, Field, PanelSettings } from '../../../models';
|
||||
import { IMetadata } from '../Metadata';
|
||||
import { FieldTitle } from './FieldTitle';
|
||||
|
||||
export interface IFieldCollectionProps {
|
||||
field: Field;
|
||||
parent: IMetadata;
|
||||
parentFields: string[];
|
||||
blockData: BlockFieldData | undefined;
|
||||
settings: PanelSettings;
|
||||
renderFields: (
|
||||
ctFields: Field[],
|
||||
parent: IMetadata,
|
||||
parentFields: string[],
|
||||
blockData?: BlockFieldData,
|
||||
onFieldUpdate?: (field: string | undefined, value: any, parents: string[]) => void,
|
||||
parentBlock?: string | null
|
||||
) => (JSX.Element | null)[] | undefined;
|
||||
onChange: (field: string | undefined, value: any, parents: string[]) => void;
|
||||
}
|
||||
|
||||
export const FieldCollection: React.FunctionComponent<IFieldCollectionProps> = ({
|
||||
field,
|
||||
parent,
|
||||
parentFields,
|
||||
blockData,
|
||||
settings,
|
||||
renderFields,
|
||||
onChange
|
||||
}: React.PropsWithChildren<IFieldCollectionProps>) => {
|
||||
const [fields, setFields] = React.useState<Field[]>([]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!settings.fieldGroups) {
|
||||
return
|
||||
}
|
||||
|
||||
const group = settings.fieldGroups.find((group) => group.id === field.fieldGroup);
|
||||
if (group) {
|
||||
setFields(group.fields);
|
||||
}
|
||||
}, [field, settings?.fieldGroups]);
|
||||
|
||||
if (!fields || fields.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`metadata_field__box`}>
|
||||
<FieldTitle
|
||||
className={`metadata_field__label_parent`}
|
||||
label={field.title || field.name}
|
||||
icon={undefined}
|
||||
required={field.required}
|
||||
/>
|
||||
|
||||
{field.description && (
|
||||
<p className={`metadata_field__description`}>{field.description}</p>
|
||||
)}
|
||||
|
||||
{renderFields(
|
||||
fields,
|
||||
parent,
|
||||
[...parentFields, field.name],
|
||||
blockData,
|
||||
onChange
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -26,7 +26,8 @@ import {
|
||||
PreviewImageField,
|
||||
PreviewImageValue,
|
||||
NumberField,
|
||||
CustomField
|
||||
CustomField,
|
||||
FieldCollection
|
||||
} from '.';
|
||||
import { fieldWhenClause } from '../../../utils/fieldWhenClause';
|
||||
import { ContentTypeRelationshipField } from './ContentTypeRelationshipField';
|
||||
@@ -521,6 +522,27 @@ export const WrapperField: React.FunctionComponent<IWrapperFieldProps> = ({
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} else if (field.type === "fieldCollection") {
|
||||
if (!parent[field.name]) {
|
||||
parent[field.name] = {};
|
||||
}
|
||||
|
||||
const subMetadata = parent[field.name] as IMetadata;
|
||||
|
||||
return (
|
||||
<FieldBoundary key={field.name} fieldName={field.title || field.name}>
|
||||
<FieldCollection
|
||||
key={field.name}
|
||||
field={field}
|
||||
parent={subMetadata}
|
||||
parentFields={parentFields}
|
||||
renderFields={renderFields}
|
||||
settings={settings}
|
||||
blockData={blockData}
|
||||
onChange={onSendUpdate}
|
||||
/>
|
||||
</FieldBoundary>
|
||||
);
|
||||
} else {
|
||||
console.warn(l10n.t(LocalizationKey.panelFieldsWrapperFieldUnknown, field.type));
|
||||
return null;
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
export * from './ChoiceButton';
|
||||
export * from './ChoiceField';
|
||||
export * from './ContentTypeRelationshipField';
|
||||
export * from './CustomField';
|
||||
export * from './DataFileField';
|
||||
export * from './DateTimeField';
|
||||
export * from './DraftField';
|
||||
export * from './FieldCollection';
|
||||
export * from './FieldMessage';
|
||||
export * from './FieldTitle';
|
||||
export * from './FileField';
|
||||
|
||||
@@ -29,7 +29,7 @@ const FolderAndFiles: React.FunctionComponent<IFolderAndFilesProps> = ({
|
||||
{folder.lastModified ? (
|
||||
<div key={`${folder.title}-${idx}`}>
|
||||
<FileList
|
||||
folderName={folder.title}
|
||||
folderName={folder.locale ? `${folder.title} (${folder.localeTitle || folder.locale})` : folder.title}
|
||||
totalFiles={folder.files}
|
||||
files={folder.lastModified}
|
||||
/>
|
||||
|
||||
@@ -10,13 +10,11 @@ import { Messenger } from '@estruyf/vscode/dist/client';
|
||||
import { EventData } from '@estruyf/vscode/dist/models';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import { PanelSettingsAtom } from '../state';
|
||||
import * as l10n from '@vscode/l10n';
|
||||
|
||||
export default function useMessages() {
|
||||
const [metadata, setMetadata] = useState<any>({});
|
||||
const [settings, setSettings] = useRecoilState(PanelSettingsAtom);
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const [localeReady, setLocaleReady] = useState<boolean>(false);
|
||||
const [focusElm, setFocus] = useState<TagType | null>(null);
|
||||
const [folderAndFiles, setFolderAndFiles] = useState<FolderInfo[] | undefined>(undefined);
|
||||
const [mediaSelecting, setMediaSelecting] = useState<DashboardData | undefined>(undefined);
|
||||
@@ -52,12 +50,6 @@ export default function useMessages() {
|
||||
case GeneralCommands.toWebview.setMode:
|
||||
setMode(message.payload);
|
||||
break;
|
||||
case GeneralCommands.toWebview.setLocalization:
|
||||
l10n.config({
|
||||
contents: message.payload
|
||||
})
|
||||
setLocaleReady(true)
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -99,7 +91,6 @@ export default function useMessages() {
|
||||
loading,
|
||||
mediaSelecting,
|
||||
mode,
|
||||
localeReady,
|
||||
unsetFocus
|
||||
};
|
||||
}
|
||||
|
||||
+11
-15
@@ -1,10 +1,10 @@
|
||||
import * as React from 'react';
|
||||
import * as Sentry from '@sentry/react';
|
||||
import { render } from 'react-dom';
|
||||
import { ViewPanel } from './ViewPanel';
|
||||
import * as Sentry from '@sentry/react';
|
||||
import { Integrations } from '@sentry/tracing';
|
||||
import { SENTRY_LINK, SentryIgnore } from '../constants';
|
||||
import { RecoilRoot } from 'recoil';
|
||||
import { I10nProvider } from '../dashboardWebView/providers/I10nProvider';
|
||||
import { SentryInit } from '../utils/sentryInit';
|
||||
|
||||
import './styles.css';
|
||||
|
||||
@@ -33,16 +33,10 @@ if (elm) {
|
||||
const version = elm?.getAttribute('data-version');
|
||||
const environment = elm?.getAttribute('data-environment');
|
||||
const isProd = elm?.getAttribute('data-isProd');
|
||||
const isCrashDisabled = elm?.getAttribute('data-is-crash-disabled');
|
||||
|
||||
if (isProd === 'true') {
|
||||
Sentry.init({
|
||||
dsn: SENTRY_LINK,
|
||||
integrations: [new Integrations.BrowserTracing()],
|
||||
tracesSampleRate: 0, // No performance tracing required
|
||||
release: version || '',
|
||||
environment: environment || '',
|
||||
ignoreErrors: SentryIgnore
|
||||
});
|
||||
if (isProd === 'true' && isCrashDisabled === 'false') {
|
||||
Sentry.init(SentryInit(version, environment));
|
||||
|
||||
Sentry.setTag("type", "panel");
|
||||
if (document.body.getAttribute(`data-vscode-theme-id`)) {
|
||||
@@ -51,9 +45,11 @@ if (elm) {
|
||||
}
|
||||
|
||||
render(
|
||||
<RecoilRoot>
|
||||
<ViewPanel />
|
||||
</RecoilRoot>,
|
||||
<I10nProvider>
|
||||
<RecoilRoot>
|
||||
<ViewPanel />
|
||||
</RecoilRoot>
|
||||
</I10nProvider>,
|
||||
elm
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
import { ExtensionState } from '../constants';
|
||||
import { Extension } from '../helpers';
|
||||
|
||||
export class Translations {
|
||||
/**
|
||||
* Translates an array of text from a source language to a target language.
|
||||
* @param text - The array of text to be translated.
|
||||
* @param source - The source language code.
|
||||
* @param target - The target language code.
|
||||
* @returns A Promise that resolves to an array of translated text, or undefined if translation is not possible.
|
||||
*/
|
||||
public static async translate(
|
||||
text: string[],
|
||||
source: string,
|
||||
target: string
|
||||
): Promise<string[] | undefined> {
|
||||
const deeplAuthKey = await Extension.getInstance().getSecret(
|
||||
ExtensionState.Secrets.Deepl.ApiKey
|
||||
);
|
||||
const azureAuthKey = await Extension.getInstance().getSecret(
|
||||
ExtensionState.Secrets.Azure.TranslatorKey
|
||||
);
|
||||
const azureRegion = await Extension.getInstance().getSecret(
|
||||
ExtensionState.Secrets.Azure.TranslatorRegion
|
||||
);
|
||||
|
||||
if (azureAuthKey && azureRegion) {
|
||||
return this.translateAzure(text, source, target, azureAuthKey, azureRegion);
|
||||
}
|
||||
|
||||
if (deeplAuthKey) {
|
||||
return this.translateDeepL(text, source, target, deeplAuthKey);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Translates an array of text using Azure Cognitive Services Translator API.
|
||||
* @param text - The array of text to be translated.
|
||||
* @param source - The source language code.
|
||||
* @param target - The target language code.
|
||||
* @param azureAuthKey - The Azure authentication key.
|
||||
* @param azureRegion - The Azure region for the translation service.
|
||||
* @returns A promise that resolves to an array of translated text.
|
||||
* @throws An error if the translation fails.
|
||||
*/
|
||||
private static async translateAzure(
|
||||
text: string[],
|
||||
source: string,
|
||||
target: string,
|
||||
azureAuthKey: string,
|
||||
azureRegion: string
|
||||
): Promise<string[]> {
|
||||
try {
|
||||
const body = JSON.stringify(text.map((t) => ({ Text: t })));
|
||||
|
||||
const response = await fetch(
|
||||
`https://api.cognitive.microsofttranslator.com/translate?api-version=3.0&to=${target}&from=${source}&textType=html`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Ocp-Apim-Subscription-Key': azureAuthKey,
|
||||
'Ocp-Apim-Subscription-Region': azureRegion,
|
||||
'Content-Type': 'application/json; charset=UTF-8'
|
||||
},
|
||||
body
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
return data.map((t: { translations: { text: string }[] }) => t.translations[0].text);
|
||||
} catch (error) {
|
||||
throw new Error(`Azure: ${(error as Error).message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Translates an array of text using the DeepL translation service.
|
||||
* @param text - The text to be translated.
|
||||
* @param source - The source language of the text.
|
||||
* @param target - The target language for the translation.
|
||||
* @param deeplAuthKey - The authentication key for accessing the DeepL API.
|
||||
* @returns A Promise that resolves to an array of translated text.
|
||||
* @throws If there is an error during the translation process.
|
||||
*/
|
||||
private static async translateDeepL(
|
||||
text: string[],
|
||||
source: string,
|
||||
target: string,
|
||||
deeplAuthKey: string
|
||||
): Promise<string[]> {
|
||||
try {
|
||||
const body = JSON.stringify({
|
||||
text,
|
||||
source_lang: source,
|
||||
target_lang: target
|
||||
});
|
||||
|
||||
let host = deeplAuthKey.endsWith(':fx') ? 'api-free.deepl.com' : 'api.deepl.com';
|
||||
|
||||
const response = await fetch(`https://${host}/v2/translate`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `DeepL-Auth-Key ${deeplAuthKey}`,
|
||||
'User-Agent': `FrontMatterCMS/${Extension.getInstance().version}`,
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json'
|
||||
},
|
||||
body
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
if (!data.translations || data.translations.length < 3) {
|
||||
throw new Error('Invalid response');
|
||||
}
|
||||
|
||||
return data.translations.map((t: { text: string }) => t.text);
|
||||
} catch (error) {
|
||||
throw new Error(`DeepL: ${(error as Error).message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import * as Sentry from '@sentry/react';
|
||||
import { Integrations } from '@sentry/tracing';
|
||||
import { SENTRY_LINK, SentryIgnore } from '../constants';
|
||||
|
||||
export const SentryInit = (
|
||||
version: string | null,
|
||||
environment: string | null
|
||||
): Sentry.BrowserOptions => ({
|
||||
dsn: SENTRY_LINK,
|
||||
integrations: [new Integrations.BrowserTracing()],
|
||||
tracesSampleRate: 0, // No performance tracing required
|
||||
release: version || '',
|
||||
environment: environment || '',
|
||||
ignoreErrors: SentryIgnore,
|
||||
beforeSend(event) {
|
||||
if (event.user) {
|
||||
delete event.user.ip_address;
|
||||
}
|
||||
return event;
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user