Merge branch 'dev' of github.com:estruyf/vscode-front-matter into dev

This commit is contained in:
Elio Struyf
2024-02-27 09:43:51 +01:00
55 changed files with 867 additions and 388 deletions
+17 -1
View File
@@ -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)) {
+1 -1
View 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
View File
@@ -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
};
}
}
+3 -3
View File
@@ -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
View File
@@ -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');
+7 -1
View File
@@ -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`
}
}
};
+3
View File
@@ -21,6 +21,9 @@ export const GeneralCommands = {
get: 'getSecret',
set: 'setSecret'
},
content: {
locales: 'getContentLocales'
},
runCommand: 'runCommand',
getLocalization: 'getLocalization',
openOnWebsite: 'openOnWebsite'
+2 -1
View File
@@ -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.`
];
+1 -1
View File
@@ -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',
-10
View File
@@ -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';
+2 -2
View File
@@ -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>
+1 -11
View File
@@ -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
};
}
+8 -12
View File
@@ -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(() => {
+24 -24
View File
@@ -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
+2 -1
View File
@@ -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 };
+1 -6
View File
@@ -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;
}
/**
+12 -1
View File
@@ -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;
}
+3 -1
View File
@@ -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) || [],
+1 -86
View File
@@ -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(
+32 -4
View File
@@ -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),
+4 -2
View File
@@ -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;
+3
View File
@@ -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));
}
+20 -2
View File
@@ -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);
}
}
+1 -15
View File
@@ -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
+7 -4
View File
@@ -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);
}
}
+36 -16
View File
@@ -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.
*/
+4
View File
@@ -12,6 +12,10 @@ export interface ContentFolder {
originalPath?: string;
$schema?: string;
extended?: boolean;
locale?: string;
localeTitle?: string;
localeSourcePath?: string;
defaultLocale?: string;
locales: I18nConfig[];
}
+1
View File
@@ -0,0 +1 @@
export type FilterType = 'contentFolders' | 'tags' | 'categories';
+2
View File
@@ -191,6 +191,8 @@ export interface FolderInfo {
title: string;
files: number;
lastModified: FileInfo[];
locale?: string;
localeTitle?: string;
}
export interface FileInfo extends FileStat {
+1
View File
@@ -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';
+3 -1
View File
@@ -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) => {
+1 -2
View File
@@ -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}
/>
-9
View File
@@ -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
View File
@@ -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
);
}
+132
View File
@@ -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}`);
}
}
}
+21
View File
@@ -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;
}
});