Skip to content

Commit 305c95f

Browse files
committed
Merge branch 'issue/865' into beta
2 parents 8660f5f + 3b7671a commit 305c95f

File tree

10 files changed

+9086
-3543
lines changed

10 files changed

+9086
-3543
lines changed

package-lock.json

Lines changed: 8829 additions & 3486 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1366,7 +1366,14 @@
13661366
"description": "%setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.single.description%"
13671367
},
13681368
"wysiwyg": {
1369-
"type": "boolean",
1369+
"type": [
1370+
"boolean",
1371+
"string"
1372+
],
1373+
"enum": [
1374+
"html",
1375+
"markdown"
1376+
],
13701377
"default": false,
13711378
"description": "%setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.wysiwyg.description%"
13721379
},
@@ -2887,7 +2894,14 @@
28872894
"react-router-dom": "^6.8.0",
28882895
"react-sortable-hoc": "^2.0.0",
28892896
"recoil": "^0.7.7",
2890-
"remark-gfm": "^3.0.1",
2897+
"rehype-parse": "^9.0.1",
2898+
"rehype-remark": "^10.0.0",
2899+
"rehype-stringify": "^10.0.1",
2900+
"remark": "^15.0.1",
2901+
"remark-gfm": "^4.0.0",
2902+
"remark-parse": "^11.0.0",
2903+
"remark-rehype": "^11.1.1",
2904+
"remark-stringify": "^11.0.0",
28912905
"rimraf": "^3.0.2",
28922906
"semver": "^7.3.8",
28932907
"simple-git": "^3.16.0",
@@ -2897,6 +2911,7 @@
28972911
"tailwindcss-animate": "^1.0.7",
28982912
"ts-loader": "^9.4.2",
28992913
"typescript": "^4.9.5",
2914+
"unified": "^11.0.5",
29002915
"uniforms": "^3.10.2",
29012916
"uniforms-antd": "^3.10.2",
29022917
"uniforms-bridge-json-schema": "^3.10.2",

package.nls.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,7 @@
204204
"setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.choices.items.properties.id.description": "The choice ID",
205205
"setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.choices.items.properties.title.description": "The choice title",
206206
"setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.single.description": "Is a single line field",
207-
"setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.wysiwyg.description": "Is a WYSIWYG field (HTML output)",
207+
"setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.wysiwyg.description": "Is a WYSIWYG field. You can set it to markdown or HTML.",
208208
"setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.multiple.description": "Do you allow to select multiple values?",
209209
"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.",
210210
"setting.frontMatter.taxonomy.contentTypes.items.properties.fields.items.properties.hidden.description": "Do you want to hide the field from the metadata section?",

src/models/PanelSettings.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ export interface Field {
103103
type: FieldType;
104104
choices?: string[] | Choice[];
105105
single?: boolean;
106-
wysiwyg?: boolean;
106+
wysiwyg?: boolean | string;
107107
multiple?: boolean;
108108
isPreviewImage?: boolean;
109109
hidden?: boolean;

src/panelWebView/components/Fields/TextField.tsx

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ const DEBOUNCE_TIME = 300;
1616

1717
export interface ITextFieldProps extends BaseFieldProps<string> {
1818
singleLine: boolean | undefined;
19-
wysiwyg: boolean | undefined;
2019
limit: number | undefined;
2120
rows?: number;
2221
name: string;
@@ -26,12 +25,9 @@ export interface ITextFieldProps extends BaseFieldProps<string> {
2625
onChange: (txtValue: string) => void;
2726
}
2827

29-
const WysiwygField = React.lazy(() => import('./WysiwygField'));
30-
3128
export const TextField: React.FunctionComponent<ITextFieldProps> = ({
3229
placeholder,
3330
singleLine,
34-
wysiwyg,
3531
limit,
3632
label,
3733
description,
@@ -199,13 +195,7 @@ export const TextField: React.FunctionComponent<ITextFieldProps> = ({
199195
</div>
200196
)}
201197

202-
{wysiwyg ? (
203-
<React.Suspense
204-
fallback={<div>{localize(LocalizationKey.panelFieldsTextFieldLoading)}</div>}
205-
>
206-
<WysiwygField text={text || ''} onChange={onTextChange} />
207-
</React.Suspense>
208-
) : singleLine ? (
198+
{singleLine ? (
209199
<input
210200
className={`metadata_field__input`}
211201
value={text || ''}

src/panelWebView/components/Fields/WrapperField.tsx

Lines changed: 46 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,14 @@ import {
2727
NumberField,
2828
CustomField,
2929
FieldCollection,
30-
TagPicker
30+
TagPicker,
31+
WYSIWYGType
3132
} from '.';
3233
import { fieldWhenClause } from '../../../utils/fieldWhenClause';
3334
import { ContentTypeRelationshipField } from './ContentTypeRelationshipField';
34-
import * as l10n from '@vscode/l10n';
35-
import { LocalizationKey } from '../../../localization';
35+
import { LocalizationKey, localize } from '../../../localization';
36+
import { GeneralCommands } from '../../../constants';
37+
const WysiwygField = React.lazy(() => import('./WysiwygField'));
3638

3739
export interface IWrapperFieldProps {
3840
field: Field;
@@ -210,24 +212,43 @@ export const WrapperField: React.FunctionComponent<IWrapperFieldProps> = ({
210212
limit = settings?.seo.description;
211213
}
212214

213-
return (
214-
<FieldBoundary key={field.name} fieldName={field.title || field.name}>
215-
<TextField
216-
name={field.name}
217-
label={field.title || field.name}
218-
description={field.description}
219-
singleLine={field.single}
220-
limit={limit}
221-
wysiwyg={field.wysiwyg}
222-
rows={4}
223-
onChange={onFieldChange}
224-
value={(fieldValue as string) || null}
225-
required={!!field.required}
226-
settings={settings}
227-
actions={field.actions}
228-
/>
229-
</FieldBoundary>
230-
);
215+
if (field.wysiwyg) {
216+
return (
217+
<FieldBoundary key={field.name} fieldName={field.title || field.name}>
218+
<React.Suspense
219+
fallback={<div>{localize(LocalizationKey.panelFieldsTextFieldLoading)}</div>}
220+
>
221+
<WysiwygField
222+
name={field.name}
223+
label={field.title || field.name}
224+
description={field.description}
225+
limit={limit}
226+
type={typeof field.wysiwyg === 'boolean' ? 'html' : field.wysiwyg.toLowerCase() as WYSIWYGType}
227+
onChange={onFieldChange}
228+
value={(fieldValue as string) || null}
229+
required={!!field.required} />
230+
</React.Suspense>
231+
</FieldBoundary>
232+
);
233+
} else {
234+
return (
235+
<FieldBoundary key={field.name} fieldName={field.title || field.name}>
236+
<TextField
237+
name={field.name}
238+
label={field.title || field.name}
239+
description={field.description}
240+
singleLine={field.single}
241+
limit={limit}
242+
rows={4}
243+
onChange={onFieldChange}
244+
value={(fieldValue as string) || null}
245+
required={!!field.required}
246+
settings={settings}
247+
actions={field.actions}
248+
/>
249+
</FieldBoundary>
250+
);
251+
}
231252
} else if (field.type === 'number') {
232253
let nrValue: number | null = field.numberOptions?.isDecimal ? parseFloat(fieldValue as string) : parseInt(fieldValue as string);
233254
if (isNaN(nrValue)) {
@@ -556,7 +577,10 @@ export const WrapperField: React.FunctionComponent<IWrapperFieldProps> = ({
556577
</FieldBoundary>
557578
);
558579
} else {
559-
console.warn(l10n.t(LocalizationKey.panelFieldsWrapperFieldUnknown, field.type));
580+
messageHandler.send(GeneralCommands.toVSCode.logging.verbose, {
581+
message: localize(LocalizationKey.panelFieldsWrapperFieldUnknown, field.type),
582+
location: 'PANEL'
583+
});
560584
return null;
561585
}
562586
};

src/panelWebView/components/Fields/WysiwygField.tsx

Lines changed: 175 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,25 +3,186 @@ import * as React from 'react';
33
const ReactQuill = require('react-quill');
44
import 'react-quill/dist/quill.snow.css';
55

6-
export interface IWysiwygFieldProps {
7-
text: string;
6+
import { PencilIcon } from '@heroicons/react/24/outline';
7+
8+
import { unified } from "unified";
9+
import remarkParse from 'remark-parse'
10+
import remarkGfm from 'remark-gfm'
11+
import remarkRehype from 'remark-rehype'
12+
import remarkStringify from "remark-stringify";
13+
14+
import rehypeParse from "rehype-parse";
15+
import rehypeRemark from "rehype-remark";
16+
import rehypeStringify from 'rehype-stringify'
17+
18+
import { differenceInSeconds } from "date-fns";
19+
import { BaseFieldProps } from '../../../models';
20+
import { FieldMessage, FieldTitle } from '.';
21+
import { LocalizationKey, localize } from '../../../localization';
22+
import { useRecoilState } from 'recoil';
23+
import { RequiredFieldsAtom } from '../../state';
24+
import { useDebounce } from '../../../hooks/useDebounce';
25+
26+
const DEBOUNCE_TIME = 500;
27+
28+
function markdownToHtml(markdownText: string) {
29+
const file = unified()
30+
.use(remarkParse)
31+
.use(remarkRehype)
32+
.use(remarkGfm)
33+
.use(rehypeStringify)
34+
.processSync(markdownText);
35+
36+
const htmlContents = String(file);
37+
return htmlContents.replace(/<del>/g, '<s>').replace(/<\/del>/g, '</s>');
38+
}
39+
40+
function htmlToMarkdown(htmlText: string) {
41+
const file = unified()
42+
.use(rehypeParse, { emitParseErrors: true, duplicateAttribute: false })
43+
.use(rehypeRemark)
44+
.use(remarkGfm)
45+
.use(remarkStringify)
46+
.processSync(htmlText);
47+
return String(file);
48+
}
49+
50+
export type WYSIWYGType = "html" | "markdown";
51+
52+
export interface IWysiwygFieldProps extends BaseFieldProps<string> {
53+
name: string;
54+
limit?: number;
55+
type: WYSIWYGType;
856
onChange: (txtValue: string) => void;
957
}
1058

1159
const WysiwygField: React.FunctionComponent<IWysiwygFieldProps> = ({
12-
text,
13-
onChange
60+
label,
61+
description,
62+
value,
63+
type = "html",
64+
onChange,
65+
limit,
66+
required
1467
}: React.PropsWithChildren<IWysiwygFieldProps>) => {
15-
const modules = {
16-
toolbar: [
17-
[{ header: [1, 2, 3, false] }],
18-
['bold', 'italic', 'underline', 'strike'],
19-
[{ list: 'ordered' }, { list: 'bullet' }],
20-
['clean']
21-
]
22-
};
23-
24-
return <ReactQuill modules={modules} value={text || ''} onChange={onChange} />;
68+
const [, setRequiredFields] = useRecoilState(RequiredFieldsAtom);
69+
const [lastUpdated, setLastUpdated] = React.useState<number | null>(null);
70+
const [text, setText] = React.useState<string | null | undefined>(type === "html" ? value : markdownToHtml(value || ""));
71+
const debouncedText = useDebounce<string | null | undefined>(text, DEBOUNCE_TIME);
72+
73+
const onTextChange = (newValue: string) => {
74+
setText(newValue);
75+
setLastUpdated(Date.now());
76+
}
77+
78+
const modules = React.useMemo(() => {
79+
const styles = ['bold', 'italic', 'strike'];
80+
81+
if (type === "html") {
82+
styles.push('underline');
83+
}
84+
85+
return {
86+
toolbar: [
87+
[{ header: [1, 2, 3, false] }],
88+
styles,
89+
[{ list: 'ordered' }, { list: 'bullet' }],
90+
['clean']
91+
]
92+
};
93+
}, [type]);
94+
95+
const isValid = React.useMemo(() => {
96+
let temp = true;
97+
if (limit && limit !== -1) {
98+
temp = (text || '').length <= limit;
99+
}
100+
return temp;
101+
}, [limit, text]);
102+
103+
const updateRequired = React.useCallback(
104+
(isValid: boolean) => {
105+
setRequiredFields((prev) => {
106+
let clone = Object.assign([], prev);
107+
108+
if (isValid) {
109+
clone = clone.filter((item) => item !== label);
110+
} else {
111+
clone.push(label);
112+
}
113+
114+
return clone;
115+
});
116+
},
117+
[setRequiredFields]
118+
);
119+
120+
const showRequiredState = React.useMemo(() => {
121+
return required && !text;
122+
}, [required, text]);
123+
124+
const border = React.useMemo(() => {
125+
if (showRequiredState) {
126+
updateRequired(false);
127+
return '1px solid var(--vscode-inputValidation-errorBorder)';
128+
} else if (!isValid) {
129+
updateRequired(true);
130+
return '1px solid var(--vscode-inputValidation-warningBorder)';
131+
} else {
132+
updateRequired(true);
133+
return '1px solid var(--vscode-inputValidation-infoBorder)';
134+
}
135+
}, [showRequiredState, isValid]);
136+
137+
/**
138+
* Update the text value when the value changes
139+
*/
140+
React.useEffect(() => {
141+
if (text !== value && (lastUpdated === null || differenceInSeconds(Date.now(), lastUpdated) > 2)) {
142+
setText(type === "html" ? value : markdownToHtml(value || ""));
143+
}
144+
setLastUpdated(null);
145+
}, [value]);
146+
147+
/**
148+
* Update the value when the debounced text changes
149+
*/
150+
React.useEffect(() => {
151+
if (debouncedText !== undefined && value !== debouncedText && lastUpdated !== null) {
152+
const valueToUpdate = type === "html" ? debouncedText : htmlToMarkdown(debouncedText || "");
153+
onChange(valueToUpdate || "");
154+
}
155+
}, [debouncedText]);
156+
157+
return (
158+
<div className={`metadata_field`}>
159+
<FieldTitle
160+
label={label}
161+
icon={<PencilIcon />}
162+
required={required}
163+
/>
164+
165+
<ReactQuill
166+
modules={modules}
167+
value={text || ''}
168+
onChange={onTextChange}
169+
style={{ border }}
170+
/>
171+
172+
{limit && limit > 0 && (text || '').length > limit && (
173+
<div className={`metadata_field__limit`}>
174+
{localize(LocalizationKey.panelFieldsTextFieldLimit, `${(text || '').length}/${limit}`)}
175+
</div>
176+
)}
177+
178+
<FieldMessage
179+
description={description}
180+
name={label.toLowerCase()}
181+
showRequired={showRequiredState}
182+
/>
183+
184+
</div>
185+
);
25186
};
26187

27188
export default WysiwygField;

0 commit comments

Comments
 (0)