Merge branch 'dev' into issue/598

This commit is contained in:
Elio Struyf
2023-07-19 17:23:41 -04:00
13 changed files with 214 additions and 28 deletions
+9
View File
@@ -2,8 +2,15 @@
## [8.5.0] - 2023-xx-xx
### 🧪 Experimental features
- External UI script support for dashboards
- Front matter AI 🤖
### ✨ New features
- Added description AI suggestion for GitHub sponsors
- The Visual Studio Code theme support is now released in the stable version
- [#424](https://github.com/estruyf/vscode-front-matter/issues/424): Snippet wrapping to allow easier updates or changes to previously set snippets in the content
- [#585](https://github.com/estruyf/vscode-front-matter/issues/585): New content relationship field type (`contentRelationship`)
@@ -18,6 +25,7 @@
- [#591](https://github.com/estruyf/vscode-front-matter/issues/591): Support for date format in the `datetime` field
- [#593](https://github.com/estruyf/vscode-front-matter/issues/593): Add support for date formatting in the preview path
- [#599](https://github.com/estruyf/vscode-front-matter/issues/599): Add a placeholder when the base panel view is empty
- [#602](https://github.com/estruyf/vscode-front-matter/issues/602): Find content outside the Front Matter workspace folder
### ⚡️ Optimizations
@@ -32,6 +40,7 @@
- [#590](https://github.com/estruyf/vscode-front-matter/issues/590): Fix for image fields inside a sub-block
- [#595](https://github.com/estruyf/vscode-front-matter/issues/595): Fix for media metadata now showing up
- [#596](https://github.com/estruyf/vscode-front-matter/issues/596): Fix for number field in block data
- [#603](https://github.com/estruyf/vscode-front-matter/issues/603): Fix problem with page bundles and path placeholders
## [8.4.0] - 2023-04-03 - [Release notes](https://beta.frontmatter.codes/updates/v8.4.0)
+26 -9
View File
@@ -182,7 +182,7 @@ export class Folders {
) {
staticFolder =
staticFolder === '/' || staticFolder === './'
? Folders.getAbsFilePath('[[workspace]]')
? Folders.getAbsFilePath(WORKSPACE_PLACEHOLDER)
: Folders.getAbsFilePath(staticFolder);
const wsFolder = Folders.getWorkspaceFolder();
if (wsFolder) {
@@ -272,26 +272,23 @@ export class Folders {
for (const folder of folders) {
try {
const folderPath = parseWinPath(folder.path);
let projectStart = parseWinPath(folder.path).replace(wsFolder, '');
if (typeof projectStart === 'string') {
projectStart = projectStart.replace(/\\/g, '/');
projectStart = projectStart.startsWith('/') ? projectStart.substring(1) : projectStart;
if (typeof folderPath === 'string') {
let files: Uri[] = [];
for (const fileType of supportedFiles || DEFAULT_FILE_TYPES) {
let filePath = join(
projectStart,
folderPath,
folder.excludeSubdir ? '/' : '**',
`*${fileType.startsWith('.') ? '' : '.'}${fileType}`
);
if (projectStart === '' && folder.excludeSubdir) {
if (folderPath === '' && folder.excludeSubdir) {
filePath = `*${fileType.startsWith('.') ? '' : '.'}${fileType}`;
}
let foundFiles = await workspace.findFiles(filePath, '**/node_modules/**');
let foundFiles = await Folders.findFiles(filePath);
// Make sure these file are coming from the folder path (this could be an issue in multi-root workspaces)
foundFiles = foundFiles.filter((f) => parseWinPath(f.fsPath).startsWith(folderPath));
@@ -460,7 +457,13 @@ export class Folders {
private static absWsFolder(folder: ContentFolder, wsFolder?: Uri) {
const isWindows = process.platform === 'win32';
let absPath = folder.path.replace(WORKSPACE_PLACEHOLDER, parseWinPath(wsFolder?.fsPath || ''));
if (absPath.includes('../')) {
absPath = join(absPath);
}
absPath = isWindows ? absPath.split('/').join('\\') : absPath;
return parseWinPath(absPath);
}
@@ -577,4 +580,18 @@ export class Folders {
});
});
}
/**
* Find all files
* @param pattern
* @returns
*/
private static async findFiles(pattern: string): Promise<Uri[]> {
return new Promise((resolve) => {
glob(pattern, { ignore: '**/node_modules/**' }, (err, files) => {
const allFiles = files.map((file) => Uri.file(file));
resolve(allFiles);
});
});
}
}
+6 -1
View File
@@ -11,7 +11,7 @@ import {
SETTING_DATE_FORMAT
} from './../constants';
import { ArticleHelper } from './../helpers/ArticleHelper';
import { join } from 'path';
import { join, parse } from 'path';
import { commands, env, Uri, ViewColumn, window, WebviewPanel } from 'vscode';
import { Extension, parseWinPath, processKnownPlaceholders, Settings } from '../helpers';
import { ContentFolder, ContentType, PreviewSettings } from '../models';
@@ -290,6 +290,11 @@ export class Preview {
const folderPath = wsFolder ? parseWinPath(wsFolder.fsPath) : '';
const relativePath = filePath.replace(folderPath, '');
pathname = processPathPlaceholders(pathname, relativePath, filePath, selectedFolder);
const file = parse(filePath);
if (file.name.toLowerCase() === 'index' && pathname.endsWith(slug)) {
slug = '';
}
}
// Support front matter placeholders - {{fm.<field>}}
@@ -7,11 +7,8 @@ export default function useThemeColors() {
const { experimental } = useSettingsContext();
const getColors = useCallback((defaultColors: string, themeColors: string) => {
if (experimental) {
return themeColors;
}
return defaultColors;
// The feature is now enabled by default
return themeColors;
}, [experimental]);
return {
+3 -7
View File
@@ -107,10 +107,8 @@ if (elm) {
const url = elm?.getAttribute('data-url');
const experimental = elm?.getAttribute('data-experimental');
if (experimental) {
updateCssVariables();
mutationObserver.observe(document.body, { childList: false, attributes: true });
}
updateCssVariables();
mutationObserver.observe(document.body, { childList: false, attributes: true });
if (isProd === 'true') {
Sentry.init({
@@ -127,9 +125,7 @@ if (elm) {
});
}
if (experimental) {
elm.setAttribute("class", "experimental bg-[var(--vscode-editor-background)] text-[var(--vscode-editor-foreground)]");
}
elm.setAttribute("class", `${experimental ? "experimental" : ""} bg-[var(--vscode-editor-background)] text-[var(--vscode-editor-foreground)]`);
if (type === 'preview') {
render(
+68 -2
View File
@@ -5,13 +5,14 @@ import { Folders } from '../../commands/Folders';
import { Command } from '../../panelWebView/Command';
import { CommandToCode } from '../../panelWebView/CommandToCode';
import { BaseListener } from './BaseListener';
import { commands, ThemeIcon, window } from 'vscode';
import { ArticleHelper, ContentType, Logger, Settings } from '../../helpers';
import { authentication, commands, ThemeIcon, window } from 'vscode';
import { ArticleHelper, ContentType, Extension, Logger, Settings } from '../../helpers';
import {
COMMAND_NAME,
DefaultFields,
SETTING_COMMA_SEPARATED_FIELDS,
SETTING_DATE_FORMAT,
SETTING_SEO_TITLE_FIELD,
SETTING_TAXONOMY_CONTENT_TYPES
} from '../../constants';
import { Article, Preview } from '../../commands';
@@ -19,6 +20,9 @@ import { ParsedFrontMatter } from '../../parsers';
import { processKnownPlaceholders } from '../../helpers/PlaceholderHelper';
import { Field, PostMessageData } from '../../models';
import { encodeEmoji } from '../../utils';
import { ExplorerView } from '../../explorerView/ExplorerView';
import { MessageHandlerData } from '@estruyf/vscode';
import { SponsorAi } from '../../services/SponsorAI';
const FILE_LIMIT = 10;
@@ -68,9 +72,71 @@ export class DataListener extends BaseListener {
case CommandToCode.getDataEntries:
this.getDataFileEntries(msg.command, msg.requestId || '', msg.payload);
break;
case CommandToCode.aiSuggestDescription:
this.aiSuggestTaxonomy(msg.command, msg.requestId);
break;
}
}
private static async aiSuggestTaxonomy(command: string, requestId?: string) {
if (!command || !requestId) {
return;
}
const extPath = Extension.getInstance().extensionPath;
const panel = ExplorerView.getInstance(extPath);
const editor = window.activeTextEditor;
if (!editor) {
panel.getWebview()?.postMessage({
command,
requestId,
error: 'No active editor'
} as MessageHandlerData<string>);
return;
}
const article = ArticleHelper.getFrontMatter(editor);
if (!article || !article.data) {
panel.getWebview()?.postMessage({
command,
requestId,
error: 'No article data'
} as MessageHandlerData<string>);
return;
}
const githubAuth = await authentication.getSession('github', ['read:user'], { silent: true });
if (!githubAuth || !githubAuth.accessToken) {
return;
}
const titleField = (Settings.get(SETTING_SEO_TITLE_FIELD) as string) || DefaultFields.Title;
const suggestion = await SponsorAi.getDescription(
githubAuth.accessToken,
article.data[titleField] || '',
article.content || ''
);
console.log(suggestion);
if (!suggestion) {
panel.getWebview()?.postMessage({
command,
requestId,
error: 'No article data'
} as MessageHandlerData<string>);
return;
}
panel.getWebview()?.postMessage({
command,
requestId,
payload: suggestion || []
} as MessageHandlerData<string>);
}
/**
* Retrieve the information about the registered folders and its files
*/
+1
View File
@@ -115,6 +115,7 @@ export class TaxonomyListener extends BaseListener {
requestId,
error: 'No article data'
} as MessageHandlerData<string>);
return;
}
panel.getWebview()?.postMessage({
+1
View File
@@ -41,6 +41,7 @@ export enum CommandToCode {
generateSlug = 'generate-slug',
stopServer = 'stop-server',
aiSuggestTaxonomy = 'ai-suggest-taxonomy',
aiSuggestDescription = 'ai-suggest-description',
searchByType = 'search-by-type',
processMediaData = 'process-media-data'
}
@@ -1,17 +1,21 @@
import { PencilIcon } from '@heroicons/react/outline';
import { PencilIcon, SparklesIcon } from '@heroicons/react/outline';
import * as React from 'react';
import { useCallback, useEffect, useMemo } from 'react';
import { useRecoilState } from 'recoil';
import { BaseFieldProps } from '../../../models';
import { BaseFieldProps, PanelSettings } from '../../../models';
import { RequiredFieldsAtom } from '../../state';
import { FieldTitle } from './FieldTitle';
import { FieldMessage } from './FieldMessage';
import { messageHandler } from '@estruyf/vscode/dist/client';
import { CommandToCode } from '../../CommandToCode';
export interface ITextFieldProps extends BaseFieldProps<string> {
singleLine: boolean | undefined;
wysiwyg: boolean | undefined;
limit: number | undefined;
rows?: number;
name: string;
settings: PanelSettings;
onChange: (txtValue: string) => void;
}
@@ -25,11 +29,14 @@ export const TextField: React.FunctionComponent<ITextFieldProps> = ({
description,
value,
rows,
name,
settings,
onChange,
required
}: React.PropsWithChildren<ITextFieldProps>) => {
const [, setRequiredFields] = useRecoilState(RequiredFieldsAtom);
const [text, setText] = React.useState<string | null>(value);
const [loading, setLoading] = React.useState<boolean>(false);
const onTextChange = (txtValue: string) => {
setText(txtValue);
@@ -75,6 +82,37 @@ export const TextField: React.FunctionComponent<ITextFieldProps> = ({
}
}, [showRequiredState, isValid]);
const suggestDescription = () => {
setLoading(true);
messageHandler.request<string>(CommandToCode.aiSuggestDescription).then((suggestion) => {
setLoading(false);
if (suggestion) {
setText(suggestion);
onChange(suggestion);
}
}).catch(() => {
setLoading(false);
});
};
const actionElement = useMemo(() => {
if (!settings?.aiEnabled || settings.seo.descriptionField !== name) {
return;
}
return (
<button
className='metadata_field__title__action'
title={`Use Front Matter AI to suggest ${label?.toLowerCase()}`}
type='button'
onClick={() => suggestDescription()}
disabled={loading}>
<SparklesIcon />
</button>
);
}, [settings?.aiEnabled, name]);
useEffect(() => {
if (text !== value) {
setText(value);
@@ -83,7 +121,15 @@ export const TextField: React.FunctionComponent<ITextFieldProps> = ({
return (
<div className={`metadata_field`}>
<FieldTitle label={label} icon={<PencilIcon />} required={required} />
{
loading && (
<div className='metadata_field__loading'>
Generating suggestion...
</div>
)
}
<FieldTitle label={label} actionElement={actionElement} icon={<PencilIcon />} required={required} />
{wysiwyg ? (
<React.Suspense fallback={<div>Loading field</div>}>
@@ -209,6 +209,7 @@ export const WrapperField: React.FunctionComponent<IWrapperFieldProps> = ({
return (
<FieldBoundary key={field.name} fieldName={field.title || field.name}>
<TextField
name={field.name}
label={field.title || field.name}
description={field.description}
singleLine={field.single}
@@ -218,6 +219,7 @@ export const WrapperField: React.FunctionComponent<IWrapperFieldProps> = ({
onChange={(value) => onSendUpdate(field.name, value, parentFields)}
value={(fieldValue as string) || null}
required={!!field.required}
settings={settings}
/>
</FieldBoundary>
);
@@ -226,6 +226,8 @@ const TagPicker: React.FunctionComponent<ITagPickerProps> = ({
sendUpdate(uniqValues);
setInputValue('');
}
}).catch(() => {
setLoading(false);
});
}, [selected]);
+5
View File
@@ -286,6 +286,7 @@ button {
/* Metadata section - Content type */
.metadata_field {
margin-bottom: 1rem;
position: relative;
}
.metadata_field__label {
@@ -294,6 +295,10 @@ button {
justify-content: space-between;
margin-bottom: 0.5rem;
&.metadata_field__alert {
justify-content: flex-start;
}
div {
display: flex;
align-items: center;
+40 -1
View File
@@ -1,4 +1,4 @@
import { SETTING_SEO_TITLE_LENGTH } from '../constants';
import { SETTING_SEO_DESCRIPTION_LENGTH, SETTING_SEO_TITLE_LENGTH } from '../constants';
import { Logger, Notifications, Settings, TaxonomyHelper } from '../helpers';
import fetch from 'node-fetch';
import { TagType } from '../panelWebView/TagType';
@@ -47,6 +47,45 @@ export class SponsorAi {
}
}
public static async getDescription(token: string, title: string, content: string) {
try {
const controller = new AbortController();
const timeout = setTimeout(() => {
Notifications.warning(`The AI title generation took too long. Please try again later.`);
controller.abort();
}, 10000);
const signal = controller.signal;
let articleContent = content;
if (articleContent.length > 2000) {
articleContent = articleContent.substring(0, 2000);
}
const response = await fetch(`${AI_URL}/description`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
accept: 'application/json'
},
body: JSON.stringify({
title: title,
content: articleContent,
token: token,
nrOfCharacters: Settings.get<number>(SETTING_SEO_DESCRIPTION_LENGTH) || 160
}),
signal: signal as any
});
clearTimeout(timeout);
const data: string = await response.text();
return data || '';
} catch (e) {
Logger.error(`Sponsor AI: ${(e as Error).message}`);
return undefined;
}
}
/**
* Get taxonomy suggestions from the AI
* @param token