Merge branch 'issue/865' into beta

This commit is contained in:
Elio Struyf
2024-10-10 09:19:59 +02:00
10 changed files with 5765 additions and 222 deletions

5675
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1366,7 +1366,14 @@
"description": "%setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.single.description%"
},
"wysiwyg": {
"type": "boolean",
"type": [
"boolean",
"string"
],
"enum": [
"html",
"markdown"
],
"default": false,
"description": "%setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.wysiwyg.description%"
},
@@ -2887,7 +2894,14 @@
"react-router-dom": "^6.8.0",
"react-sortable-hoc": "^2.0.0",
"recoil": "^0.7.7",
"remark-gfm": "^3.0.1",
"rehype-parse": "^9.0.1",
"rehype-remark": "^10.0.0",
"rehype-stringify": "^10.0.1",
"remark": "^15.0.1",
"remark-gfm": "^4.0.0",
"remark-parse": "^11.0.0",
"remark-rehype": "^11.1.1",
"remark-stringify": "^11.0.0",
"rimraf": "^3.0.2",
"semver": "^7.3.8",
"simple-git": "^3.16.0",
@@ -2897,6 +2911,7 @@
"tailwindcss-animate": "^1.0.7",
"ts-loader": "^9.4.2",
"typescript": "^4.9.5",
"unified": "^11.0.5",
"uniforms": "^3.10.2",
"uniforms-antd": "^3.10.2",
"uniforms-bridge-json-schema": "^3.10.2",

View File

@@ -204,7 +204,7 @@
"setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.choices.items.properties.id.description": "The choice ID",
"setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.choices.items.properties.title.description": "The choice title",
"setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.single.description": "Is a single line field",
"setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.wysiwyg.description": "Is a WYSIWYG field (HTML output)",
"setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.wysiwyg.description": "Is a WYSIWYG field. You can set it to markdown or HTML.",
"setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.multiple.description": "Do you allow to select multiple values?",
"setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.isPreviewImage.description": "Specify if the image field can be used as preview. Be aware, you can only have one preview image per content type.",
"setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.hidden.description": "Do you want to hide the field from the metadata section?",

View File

@@ -103,7 +103,7 @@ export interface Field {
type: FieldType;
choices?: string[] | Choice[];
single?: boolean;
wysiwyg?: boolean;
wysiwyg?: boolean | string;
multiple?: boolean;
isPreviewImage?: boolean;
hidden?: boolean;

View File

@@ -16,7 +16,6 @@ const DEBOUNCE_TIME = 300;
export interface ITextFieldProps extends BaseFieldProps<string> {
singleLine: boolean | undefined;
wysiwyg: boolean | undefined;
limit: number | undefined;
rows?: number;
name: string;
@@ -26,12 +25,9 @@ export interface ITextFieldProps extends BaseFieldProps<string> {
onChange: (txtValue: string) => void;
}
const WysiwygField = React.lazy(() => import('./WysiwygField'));
export const TextField: React.FunctionComponent<ITextFieldProps> = ({
placeholder,
singleLine,
wysiwyg,
limit,
label,
description,
@@ -199,13 +195,7 @@ export const TextField: React.FunctionComponent<ITextFieldProps> = ({
</div>
)}
{wysiwyg ? (
<React.Suspense
fallback={<div>{localize(LocalizationKey.panelFieldsTextFieldLoading)}</div>}
>
<WysiwygField text={text || ''} onChange={onTextChange} />
</React.Suspense>
) : singleLine ? (
{singleLine ? (
<input
className={`metadata_field__input`}
value={text || ''}

View File

@@ -27,12 +27,14 @@ import {
NumberField,
CustomField,
FieldCollection,
TagPicker
TagPicker,
WYSIWYGType
} from '.';
import { fieldWhenClause } from '../../../utils/fieldWhenClause';
import { ContentTypeRelationshipField } from './ContentTypeRelationshipField';
import * as l10n from '@vscode/l10n';
import { LocalizationKey } from '../../../localization';
import { LocalizationKey, localize } from '../../../localization';
import { GeneralCommands } from '../../../constants';
const WysiwygField = React.lazy(() => import('./WysiwygField'));
export interface IWrapperFieldProps {
field: Field;
@@ -210,24 +212,43 @@ export const WrapperField: React.FunctionComponent<IWrapperFieldProps> = ({
limit = settings?.seo.description;
}
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}
limit={limit}
wysiwyg={field.wysiwyg}
rows={4}
onChange={onFieldChange}
value={(fieldValue as string) || null}
required={!!field.required}
settings={settings}
actions={field.actions}
/>
</FieldBoundary>
);
if (field.wysiwyg) {
return (
<FieldBoundary key={field.name} fieldName={field.title || field.name}>
<React.Suspense
fallback={<div>{localize(LocalizationKey.panelFieldsTextFieldLoading)}</div>}
>
<WysiwygField
name={field.name}
label={field.title || field.name}
description={field.description}
limit={limit}
type={typeof field.wysiwyg === 'boolean' ? 'html' : field.wysiwyg.toLowerCase() as WYSIWYGType}
onChange={onFieldChange}
value={(fieldValue as string) || null}
required={!!field.required} />
</React.Suspense>
</FieldBoundary>
);
} else {
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}
limit={limit}
rows={4}
onChange={onFieldChange}
value={(fieldValue as string) || null}
required={!!field.required}
settings={settings}
actions={field.actions}
/>
</FieldBoundary>
);
}
} else if (field.type === 'number') {
let nrValue: number | null = field.numberOptions?.isDecimal ? parseFloat(fieldValue as string) : parseInt(fieldValue as string);
if (isNaN(nrValue)) {
@@ -556,7 +577,10 @@ export const WrapperField: React.FunctionComponent<IWrapperFieldProps> = ({
</FieldBoundary>
);
} else {
console.warn(l10n.t(LocalizationKey.panelFieldsWrapperFieldUnknown, field.type));
messageHandler.send(GeneralCommands.toVSCode.logging.verbose, {
message: localize(LocalizationKey.panelFieldsWrapperFieldUnknown, field.type),
location: 'PANEL'
});
return null;
}
};

View File

@@ -3,25 +3,186 @@ import * as React from 'react';
const ReactQuill = require('react-quill');
import 'react-quill/dist/quill.snow.css';
export interface IWysiwygFieldProps {
text: string;
import { PencilIcon } from '@heroicons/react/24/outline';
import { unified } from "unified";
import remarkParse from 'remark-parse'
import remarkGfm from 'remark-gfm'
import remarkRehype from 'remark-rehype'
import remarkStringify from "remark-stringify";
import rehypeParse from "rehype-parse";
import rehypeRemark from "rehype-remark";
import rehypeStringify from 'rehype-stringify'
import { differenceInSeconds } from "date-fns";
import { BaseFieldProps } from '../../../models';
import { FieldMessage, FieldTitle } from '.';
import { LocalizationKey, localize } from '../../../localization';
import { useRecoilState } from 'recoil';
import { RequiredFieldsAtom } from '../../state';
import { useDebounce } from '../../../hooks/useDebounce';
const DEBOUNCE_TIME = 500;
function markdownToHtml(markdownText: string) {
const file = unified()
.use(remarkParse)
.use(remarkRehype)
.use(remarkGfm)
.use(rehypeStringify)
.processSync(markdownText);
const htmlContents = String(file);
return htmlContents.replace(/<del>/g, '<s>').replace(/<\/del>/g, '</s>');
}
function htmlToMarkdown(htmlText: string) {
const file = unified()
.use(rehypeParse, { emitParseErrors: true, duplicateAttribute: false })
.use(rehypeRemark)
.use(remarkGfm)
.use(remarkStringify)
.processSync(htmlText);
return String(file);
}
export type WYSIWYGType = "html" | "markdown";
export interface IWysiwygFieldProps extends BaseFieldProps<string> {
name: string;
limit?: number;
type: WYSIWYGType;
onChange: (txtValue: string) => void;
}
const WysiwygField: React.FunctionComponent<IWysiwygFieldProps> = ({
text,
onChange
label,
description,
value,
type = "html",
onChange,
limit,
required
}: React.PropsWithChildren<IWysiwygFieldProps>) => {
const modules = {
toolbar: [
[{ header: [1, 2, 3, false] }],
['bold', 'italic', 'underline', 'strike'],
[{ list: 'ordered' }, { list: 'bullet' }],
['clean']
]
};
const [, setRequiredFields] = useRecoilState(RequiredFieldsAtom);
const [lastUpdated, setLastUpdated] = React.useState<number | null>(null);
const [text, setText] = React.useState<string | null | undefined>(type === "html" ? value : markdownToHtml(value || ""));
const debouncedText = useDebounce<string | null | undefined>(text, DEBOUNCE_TIME);
return <ReactQuill modules={modules} value={text || ''} onChange={onChange} />;
const onTextChange = (newValue: string) => {
setText(newValue);
setLastUpdated(Date.now());
}
const modules = React.useMemo(() => {
const styles = ['bold', 'italic', 'strike'];
if (type === "html") {
styles.push('underline');
}
return {
toolbar: [
[{ header: [1, 2, 3, false] }],
styles,
[{ list: 'ordered' }, { list: 'bullet' }],
['clean']
]
};
}, [type]);
const isValid = React.useMemo(() => {
let temp = true;
if (limit && limit !== -1) {
temp = (text || '').length <= limit;
}
return temp;
}, [limit, text]);
const updateRequired = React.useCallback(
(isValid: boolean) => {
setRequiredFields((prev) => {
let clone = Object.assign([], prev);
if (isValid) {
clone = clone.filter((item) => item !== label);
} else {
clone.push(label);
}
return clone;
});
},
[setRequiredFields]
);
const showRequiredState = React.useMemo(() => {
return required && !text;
}, [required, text]);
const border = React.useMemo(() => {
if (showRequiredState) {
updateRequired(false);
return '1px solid var(--vscode-inputValidation-errorBorder)';
} else if (!isValid) {
updateRequired(true);
return '1px solid var(--vscode-inputValidation-warningBorder)';
} else {
updateRequired(true);
return '1px solid var(--vscode-inputValidation-infoBorder)';
}
}, [showRequiredState, isValid]);
/**
* Update the text value when the value changes
*/
React.useEffect(() => {
if (text !== value && (lastUpdated === null || differenceInSeconds(Date.now(), lastUpdated) > 2)) {
setText(type === "html" ? value : markdownToHtml(value || ""));
}
setLastUpdated(null);
}, [value]);
/**
* Update the value when the debounced text changes
*/
React.useEffect(() => {
if (debouncedText !== undefined && value !== debouncedText && lastUpdated !== null) {
const valueToUpdate = type === "html" ? debouncedText : htmlToMarkdown(debouncedText || "");
onChange(valueToUpdate || "");
}
}, [debouncedText]);
return (
<div className={`metadata_field`}>
<FieldTitle
label={label}
icon={<PencilIcon />}
required={required}
/>
<ReactQuill
modules={modules}
value={text || ''}
onChange={onTextChange}
style={{ border }}
/>
{limit && limit > 0 && (text || '').length > limit && (
<div className={`metadata_field__limit`}>
{localize(LocalizationKey.panelFieldsTextFieldLimit, `${(text || '').length}/${limit}`)}
</div>
)}
<FieldMessage
description={description}
name={label.toLowerCase()}
showRequired={showRequiredState}
/>
</div>
);
};
export default WysiwygField;

View File

@@ -991,7 +991,11 @@ vscode-divider {
/* Quill changes */
.ql-toolbar.ql-snow,
.ql-container.ql-snow {
border-color: var(--vscode-inputValidation-infoBorder) !important;
border: 0;
}
.ql-toolbar.ql-snow {
border-bottom: 1px solid var(--vscode-inputValidation-infoBorder);
}
.ql-toolbar.ql-snow {
@@ -1023,6 +1027,7 @@ vscode-divider {
.ql-editor {
background-color: var(--vscode-input-background) !important;
padding: var(--input-padding-vertical) var(--input-padding-horizontal) !important;
min-height: 100px;
}
.ql-snow.ql-toolbar button:hover,

View File

@@ -2,9 +2,7 @@
/* eslint-disable */
const path = require('path');
const {
ProvidePlugin
} = require('webpack');
const { ProvidePlugin } = require('webpack');
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
const WebpackManifestPlugin = require('webpack-manifest-plugin').WebpackManifestPlugin;
const ESLintPlugin = require('eslint-webpack-plugin');

View File

@@ -2,6 +2,7 @@
/* eslint-disable */
const path = require('path');
const { ProvidePlugin } = require('webpack');
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
const WebpackManifestPlugin = require('webpack-manifest-plugin').WebpackManifestPlugin;
const ESLintPlugin = require('eslint-webpack-plugin');
@@ -19,7 +20,7 @@ const config = [{
resolve: {
extensions: ['.ts', '.js', '.tsx', '.jsx'],
fallback: {
"path": require.resolve("path-browserify")
"path": require.resolve("path-browserify"),
}
},
module: {
@@ -69,7 +70,13 @@ const config = [{
extensions: ['ts', 'tsx'],
exclude: ['node_modules', 'dist'],
emitWarning: false,
})
}),
new ProvidePlugin({
// Make a global `process` variable that points to the `process` package,
// because the `util` package expects there to be a global variable named `process`.
// Thanks to https://stackoverflow.com/a/65018686/14239942
process: 'process/browser'
}),
],
devServer: {
compress: true,