#176 - New collection field + uniform custom components

This commit is contained in:
Elio Struyf
2022-02-10 20:18:13 +01:00
parent 946d84a7a9
commit f7f6f26997
41 changed files with 1436 additions and 8 deletions
+78
View File
@@ -22,8 +22,11 @@
"@sentry/tracing": "^6.13.3",
"@tailwindcss/forms": "^0.3.3",
"@types/glob": "7.1.3",
"@types/invariant": "^2.2.35",
"@types/js-yaml": "3.12.1",
"@types/lodash.omit": "^4.5.6",
"@types/lodash.uniqby": "4.7.6",
"@types/lodash.xor": "^4.5.6",
"@types/mocha": "^5.2.6",
"@types/node": "10.17.48",
"@types/node-fetch": "^2.5.12",
@@ -47,8 +50,11 @@
"html-loader": "1.3.2",
"html-webpack-plugin": "4.5.0",
"image-size": "^1.0.0",
"invariant": "^2.2.4",
"lodash-es": "^4.17.21",
"lodash.omit": "^4.5.0",
"lodash.uniqby": "4.7.0",
"lodash.xor": "^4.5.0",
"mdast-util-from-markdown": "1.0.0",
"node-json-db": "^1.3.0",
"npm-run-all": "^4.1.5",
@@ -712,6 +718,12 @@
"@types/node": "*"
}
},
"node_modules/@types/invariant": {
"version": "2.2.35",
"resolved": "https://registry.npmjs.org/@types/invariant/-/invariant-2.2.35.tgz",
"integrity": "sha512-DxX1V9P8zdJPYQat1gHyY0xj3efl8gnMVjiM9iCY6y27lj+PoQWkgjt8jDqmovPqULkKVpKRg8J36iQiA+EtEg==",
"dev": true
},
"node_modules/@types/js-yaml": {
"version": "3.12.1",
"resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-3.12.1.tgz",
@@ -730,6 +742,15 @@
"integrity": "sha512-/BHF5HAx3em7/KkzVKm3LrsD6HZAXuXO1AJZQ3cRRBZj4oHZDviWPYu0aEplAqDFNHZPW6d3G7KN+ONcCCC7pw==",
"dev": true
},
"node_modules/@types/lodash.omit": {
"version": "4.5.6",
"resolved": "https://registry.npmjs.org/@types/lodash.omit/-/lodash.omit-4.5.6.tgz",
"integrity": "sha512-KXPpOSNX2h0DAG2w7ajpk7TXvWF28ZHs5nJhOJyP0BQHkehgr948RVsToItMme6oi0XJkp19CbuNXkIX8FiBlQ==",
"dev": true,
"dependencies": {
"@types/lodash": "*"
}
},
"node_modules/@types/lodash.uniqby": {
"version": "4.7.6",
"resolved": "https://registry.npmjs.org/@types/lodash.uniqby/-/lodash.uniqby-4.7.6.tgz",
@@ -739,6 +760,15 @@
"@types/lodash": "*"
}
},
"node_modules/@types/lodash.xor": {
"version": "4.5.6",
"resolved": "https://registry.npmjs.org/@types/lodash.xor/-/lodash.xor-4.5.6.tgz",
"integrity": "sha512-rD3zHQuR04SMUWEn6CHLGYNRHEFgakV4K9QhFFP/Ly5xdN2MWsy+gWFA4MxhY+sKKSSKjktjPVyBTo/6rUdTjA==",
"dev": true,
"dependencies": {
"@types/lodash": "*"
}
},
"node_modules/@types/mdast": {
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.7.tgz",
@@ -5182,6 +5212,12 @@
"integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==",
"dev": true
},
"node_modules/lodash.omit": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.omit/-/lodash.omit-4.5.0.tgz",
"integrity": "sha1-brGa5aHuHdnfC5aeZs4Lf6MLXmA=",
"dev": true
},
"node_modules/lodash.topath": {
"version": "4.5.2",
"resolved": "https://registry.npmjs.org/lodash.topath/-/lodash.topath-4.5.2.tgz",
@@ -5194,6 +5230,12 @@
"integrity": "sha1-2ZwHpmnp5tJOE2Lf4mbGdhavEwI=",
"dev": true
},
"node_modules/lodash.xor": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.xor/-/lodash.xor-4.5.0.tgz",
"integrity": "sha1-TUjtfpgJWwYyWCunFNP/iuj7HbY=",
"dev": true
},
"node_modules/loose-envify": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
@@ -9755,6 +9797,12 @@
"@types/node": "*"
}
},
"@types/invariant": {
"version": "2.2.35",
"resolved": "https://registry.npmjs.org/@types/invariant/-/invariant-2.2.35.tgz",
"integrity": "sha512-DxX1V9P8zdJPYQat1gHyY0xj3efl8gnMVjiM9iCY6y27lj+PoQWkgjt8jDqmovPqULkKVpKRg8J36iQiA+EtEg==",
"dev": true
},
"@types/js-yaml": {
"version": "3.12.1",
"resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-3.12.1.tgz",
@@ -9773,6 +9821,15 @@
"integrity": "sha512-/BHF5HAx3em7/KkzVKm3LrsD6HZAXuXO1AJZQ3cRRBZj4oHZDviWPYu0aEplAqDFNHZPW6d3G7KN+ONcCCC7pw==",
"dev": true
},
"@types/lodash.omit": {
"version": "4.5.6",
"resolved": "https://registry.npmjs.org/@types/lodash.omit/-/lodash.omit-4.5.6.tgz",
"integrity": "sha512-KXPpOSNX2h0DAG2w7ajpk7TXvWF28ZHs5nJhOJyP0BQHkehgr948RVsToItMme6oi0XJkp19CbuNXkIX8FiBlQ==",
"dev": true,
"requires": {
"@types/lodash": "*"
}
},
"@types/lodash.uniqby": {
"version": "4.7.6",
"resolved": "https://registry.npmjs.org/@types/lodash.uniqby/-/lodash.uniqby-4.7.6.tgz",
@@ -9782,6 +9839,15 @@
"@types/lodash": "*"
}
},
"@types/lodash.xor": {
"version": "4.5.6",
"resolved": "https://registry.npmjs.org/@types/lodash.xor/-/lodash.xor-4.5.6.tgz",
"integrity": "sha512-rD3zHQuR04SMUWEn6CHLGYNRHEFgakV4K9QhFFP/Ly5xdN2MWsy+gWFA4MxhY+sKKSSKjktjPVyBTo/6rUdTjA==",
"dev": true,
"requires": {
"@types/lodash": "*"
}
},
"@types/mdast": {
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.7.tgz",
@@ -13263,6 +13329,12 @@
"integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==",
"dev": true
},
"lodash.omit": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.omit/-/lodash.omit-4.5.0.tgz",
"integrity": "sha1-brGa5aHuHdnfC5aeZs4Lf6MLXmA=",
"dev": true
},
"lodash.topath": {
"version": "4.5.2",
"resolved": "https://registry.npmjs.org/lodash.topath/-/lodash.topath-4.5.2.tgz",
@@ -13275,6 +13347,12 @@
"integrity": "sha1-2ZwHpmnp5tJOE2Lf4mbGdhavEwI=",
"dev": true
},
"lodash.xor": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.xor/-/lodash.xor-4.5.0.tgz",
"integrity": "sha1-TUjtfpgJWwYyWCunFNP/iuj7HbY=",
"dev": true
},
"loose-envify": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
+27 -1
View File
@@ -632,7 +632,8 @@
"tags",
"categories",
"draft",
"fields"
"fields",
"data-collection"
],
"description": "Define the type of field"
},
@@ -698,6 +699,11 @@
},
"fields": {
"$ref": "#contenttypefield"
},
"dataType": {
"type": "string",
"default": "",
"description": "The ID of your data type defined in the `frontMatter.data.types` setting"
}
},
"additionalProperties": false,
@@ -747,6 +753,20 @@
"fields"
]
}
},
{
"if": {
"properties": {
"type": {
"const": "data-collection"
}
}
},
"then": {
"required": [
"dataType"
]
}
}
]
}
@@ -1407,8 +1427,11 @@
"@sentry/tracing": "^6.13.3",
"@tailwindcss/forms": "^0.3.3",
"@types/glob": "7.1.3",
"@types/invariant": "^2.2.35",
"@types/js-yaml": "3.12.1",
"@types/lodash.omit": "^4.5.6",
"@types/lodash.uniqby": "4.7.6",
"@types/lodash.xor": "^4.5.6",
"@types/mocha": "^5.2.6",
"@types/node": "10.17.48",
"@types/node-fetch": "^2.5.12",
@@ -1432,8 +1455,11 @@
"html-loader": "1.3.2",
"html-webpack-plugin": "4.5.0",
"image-size": "^1.0.0",
"invariant": "^2.2.4",
"lodash-es": "^4.17.21",
"lodash.omit": "^4.5.0",
"lodash.uniqby": "4.7.0",
"lodash.xor": "^4.5.0",
"mdast-util-from-markdown": "1.0.0",
"node-json-db": "^1.3.0",
"npm-run-all": "^4.1.5",
@@ -0,0 +1,39 @@
import * as invariant from 'invariant';
import { createAutoField } from 'uniforms';
export { AutoFieldProps } from 'uniforms';
import BoolField from './BoolField';
import DateField from './DateField';
import ListField from './ListField';
import NestField from './NestField';
import NumField from './NumField';
import RadioField from './RadioField';
import SelectField from './SelectField';
import TextField from './TextField';
const AutoField = createAutoField(props => {
if (props.allowedValues) {
return props.checkboxes && props.fieldType !== Array
? RadioField
: SelectField;
}
switch (props.fieldType) {
case Array:
return ListField;
case Boolean:
return BoolField;
case Date:
return DateField;
case Number:
return NumField;
case Object:
return NestField;
case String:
return TextField;
}
return invariant(false, 'Unsupported field type: %s', props.fieldType);
});
export default AutoField;
@@ -0,0 +1,29 @@
import { ComponentType, createElement, Fragment } from 'react';
import { useForm } from 'uniforms';
import AutoField from './AutoField';
export type AutoFieldsProps = {
autoField?: ComponentType<{ name: string }>;
element?: ComponentType | string;
fields?: string[];
omitFields?: string[];
};
export default function AutoFields({
autoField = AutoField,
element = Fragment,
fields,
omitFields = [],
...props
}: AutoFieldsProps) {
const { schema } = useForm();
return createElement(
element,
props,
(fields ?? schema.getSubfields())
.filter(field => !omitFields.includes(field))
.map(field => createElement(autoField, { key: field, name: field })),
);
}
@@ -0,0 +1,13 @@
import { AutoForm } from 'uniforms';
import ValidatedQuickForm from './ValidatedQuickForm';
function Auto(parent: any) {
class _ extends AutoForm.Auto(parent) {
static Auto = Auto;
}
return _ as unknown as AutoForm;
}
export default Auto(ValidatedQuickForm);
@@ -0,0 +1,13 @@
import { BaseForm } from 'uniforms';
function Unstyled(parent: any) {
class _ extends parent {
static Unstyled = Unstyled;
static displayName = `Unstyled${parent.displayName}`;
}
return _ as unknown as typeof BaseForm;
}
export default Unstyled(BaseForm);
@@ -0,0 +1,52 @@
.field__toggle {
position: relative;
display: inline-block;
width: 50px;
height: 24px;
}
.field__toggle input {
opacity: 0;
width: 0;
height: 0;
}
.field__toggle__slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: var(--vscode-button-secondaryBackground);
-webkit-transition: .4s;
transition: .4s;
border-radius: 34px;
}
.field__toggle__slider:before {
position: absolute;
content: "";
height: 16px;
width: 16px;
left: 4px;
bottom: 4px;
background-color: white;
-webkit-transition: .4s;
transition: .4s;
border-radius: 50%;
}
input:checked + .field__toggle__slider {
background-color: var(--vscode-button-background);
}
input:focus + .field__toggle__slider {
box-shadow: 0 0 1px var(--vscode-button-background);
}
input:checked + .field__toggle__slider:before {
-webkit-transform: translateX(26px);
-ms-transform: translateX(26px);
transform: translateX(26px);
}
@@ -0,0 +1,44 @@
import * as React from 'react';
import { Ref } from 'react';
import { HTMLFieldProps, connectField, filterDOMProps } from 'uniforms';
import { LabelStyles } from './component-styles';
import './BoolField.css';
export type BoolFieldProps = HTMLFieldProps<
boolean,
HTMLDivElement,
{ inputRef?: Ref<HTMLInputElement> }
>;
function Bool({
disabled,
id,
inputRef,
label,
name,
onChange,
readOnly,
value,
...props
}: BoolFieldProps) {
return (
<div {...filterDOMProps(props)}>
{label && <label htmlFor={id} style={LabelStyles}>{label}</label>}
<label className="field__toggle">
<input
checked={value || false}
disabled={disabled}
id={id}
name={name}
onChange={() => !disabled && !readOnly && onChange(!value)}
ref={inputRef}
type="checkbox"
/>
<span className="field__toggle__slider"></span>
</label>
</div>
);
}
export default connectField<BoolFieldProps>(Bool, { kind: 'leaf' });
@@ -0,0 +1,57 @@
import * as React from 'react';
import { Ref } from 'react';
import { HTMLFieldProps, connectField, filterDOMProps } from 'uniforms';
/* istanbul ignore next */
const DateConstructor = (typeof global === 'object' ? global : window).Date;
const dateFormat = (value?: Date) => value?.toISOString().slice(0, -8);
export type DateFieldProps = HTMLFieldProps<
Date,
HTMLDivElement,
{ inputRef?: Ref<HTMLInputElement>; max?: Date; min?: Date }
>;
function Date({
disabled,
id,
inputRef,
label,
max,
min,
name,
onChange,
placeholder,
readOnly,
value,
...props
}: DateFieldProps) {
return (
<div {...filterDOMProps(props)}>
{label && <label htmlFor={id}>{label}</label>}
<input
disabled={disabled}
id={id}
max={dateFormat(max)}
min={dateFormat(min)}
name={name}
onChange={event => {
const date = new DateConstructor(event.target.valueAsNumber);
if (date.getFullYear() < 10000) {
onChange(date);
} else if (isNaN(event.target.valueAsNumber)) {
onChange(undefined);
}
}}
placeholder={placeholder}
readOnly={readOnly}
ref={inputRef}
type="datetime-local"
value={dateFormat(value) ?? ''}
/>
</div>
);
}
export default connectField<DateFieldProps>(Date, { kind: 'leaf' });
@@ -0,0 +1,19 @@
import * as React from 'react';
import { HTMLProps } from 'react';
import { Override, connectField, filterDOMProps } from 'uniforms';
export type ErrorFieldProps = Override<
Omit<HTMLProps<HTMLDivElement>, 'onChange'>,
{ error?: any; errorMessage?: string }
>;
function Error({ children, error, errorMessage, ...props }: ErrorFieldProps) {
return !error ? null : (
<div {...filterDOMProps(props)}>{children || errorMessage}</div>
);
}
export default connectField<ErrorFieldProps>(Error, {
initialValue: false,
kind: 'leaf',
});
@@ -0,0 +1,14 @@
.autoform-error {
background-color: var(--vscode-inputValidation-errorBackground);
border: 1px solid var(--vscode-inputValidation-errorBorder);
border-radius: 2px;
margin: 20px 0px;
padding: 10px;
color: var(--vscode-editor-foreground);
ul {
margin-bottom: 0;
}
}
@@ -0,0 +1,23 @@
import * as React from 'react';
import { HTMLProps } from 'react';
import { filterDOMProps, useForm } from 'uniforms';
import './ErrorsField.css';
export type ErrorsFieldProps = HTMLProps<HTMLDivElement>;
export default function ErrorsField(props: ErrorsFieldProps) {
const { error, schema } = useForm();
return !error && !props.children ? null : (
<div className='autoform-error'>
<div {...filterDOMProps(props)}>
{props.children}
<ul>
{schema.getErrorMessages(error).map((message, index) => (
<li key={index}>{message}</li>
))}
</ul>
</div>
</div>
);
}
@@ -0,0 +1,35 @@
import * as React from 'react';
import { HTMLProps, Ref, useEffect } from 'react';
import { Override, filterDOMProps, useField } from 'uniforms';
export type HiddenFieldProps = Override<
HTMLProps<HTMLInputElement>,
{
inputRef?: Ref<HTMLInputElement>;
name: string;
noDOM?: boolean;
value?: any;
}
>;
export default function HiddenField({ value, ...rawProps }: HiddenFieldProps) {
const props = useField(rawProps.name, rawProps, { initialValue: false })[0];
useEffect(() => {
if (value !== undefined && value !== props.value) {
props.onChange(value);
}
});
return props.noDOM ? null : (
<input
disabled={props.disabled}
name={props.name}
readOnly={props.readOnly}
ref={props.inputRef}
type="hidden"
value={value ?? props.value ?? ''}
{...filterDOMProps(props)}
/>
);
}
@@ -0,0 +1,21 @@
.autoform__list_add_field {
display: flex;
padding: 5px;
border: 1px dashed var(--vscode-editor-foreground);
width: 100%;
justify-content: center;
margin-top: .5rem;
&:hover {
border-color: var(--vscode-button-background);
color: var(--vscode-button-background);
cursor: pointer;
}
svg {
height: 1rem;
width: 1rem;
}
}
@@ -0,0 +1,63 @@
import { PlusIcon } from '@heroicons/react/outline';
import * as React from 'react';
import {
HTMLFieldProps,
connectField,
filterDOMProps,
joinName,
useField,
} from 'uniforms';
import './ListAddField.css';
export type ListAddFieldProps = HTMLFieldProps<
unknown,
HTMLSpanElement,
{ initialCount?: number }
>;
function ListAdd({
disabled,
initialCount,
name,
readOnly,
value,
...props
}: ListAddFieldProps) {
const nameParts = joinName(null, name);
const parentName = joinName(nameParts.slice(0, -1));
const parent = useField<
{ initialCount?: number; maxCount?: number },
unknown[]
>(parentName, { initialCount }, { absoluteName: true })[0];
const limitNotReached =
!disabled && !(parent.maxCount! <= parent.value!.length);
function onAction(event: React.KeyboardEvent | React.MouseEvent) {
if (
limitNotReached &&
!readOnly &&
(!('key' in event) || event.key === 'Enter')
) {
parent.onChange(parent.value!.concat([Object.assign({}, value)]));
}
}
return (
<span
className='autoform__list_add_field'
{...filterDOMProps(props as any)}
onClick={onAction}
onKeyDown={onAction}
role="button"
tabIndex={0}
>
<PlusIcon />
</span>
);
}
export default connectField<ListAddFieldProps>(ListAdd, {
initialValue: false,
kind: 'leaf',
});
@@ -0,0 +1,27 @@
.autoform__list_del_field {
display: flex;
width: 100%;
justify-content: center;
margin-top: .5rem;
&:hover {
border-color: var(--vscode-button-background);
color: var(--vscode-button-background);
cursor: pointer;
}
.line {
height: 1px;
background: var(--vscode-editor-foreground);
width: 100%;
margin-right: .5rem;
margin-top: .5rem;
}
svg {
height: 1.25rem;
width: 1.25rem;
}
}
@@ -0,0 +1,63 @@
import { TrashIcon } from '@heroicons/react/outline';
import * as React from 'react';
import {
HTMLFieldProps,
connectField,
filterDOMProps,
joinName,
useField,
} from 'uniforms';
import './ListDelField.css';
export type ListDelFieldProps = HTMLFieldProps<unknown, HTMLSpanElement>;
function ListDel({ disabled, name, readOnly, ...props }: ListDelFieldProps) {
const nameParts = joinName(null, name);
const nameIndex = +nameParts[nameParts.length - 1];
const parentName = joinName(nameParts.slice(0, -1));
const parent = useField<{ minCount?: number }, unknown[]>(
parentName,
{},
{ absoluteName: true },
)[0];
const limitNotReached =
!disabled && !(parent.minCount! >= parent.value!.length);
function onAction(
event:
| React.KeyboardEvent<HTMLSpanElement>
| React.MouseEvent<HTMLSpanElement, MouseEvent>,
) {
if (
limitNotReached &&
!readOnly &&
(!('key' in event) || event.key === 'Enter')
) {
const value = parent.value!.slice();
value.splice(nameIndex, 1);
parent.onChange(value);
}
}
return (
<span
className='autoform__list_del_field'
{...filterDOMProps(props)}
onClick={onAction}
onKeyDown={onAction}
role="button"
tabIndex={0}
>
<div className='line'></div>
<TrashIcon />
</span>
);
}
export default connectField<ListDelFieldProps>(ListDel, {
initialValue: false,
kind: 'leaf',
});
@@ -0,0 +1,7 @@
.autoform__list_field {
margin-bottom: 5px;
margin-top: 5px;
padding: 10px;
}
@@ -0,0 +1,50 @@
import * as React from 'react';
import { Children, cloneElement, isValidElement } from 'react';
import { HTMLFieldProps, connectField, filterDOMProps } from 'uniforms';
import { LabelStyles } from './component-styles';
import ListAddField from './ListAddField';
import ListItemField from './ListItemField';
import './ListField.css';
export type ListFieldProps = HTMLFieldProps<
unknown[],
HTMLDivElement,
{ initialCount?: number; itemProps?: object }
>;
function List({
children = <ListItemField name="$" />,
initialCount,
itemProps,
label,
value,
...props
}: ListFieldProps) {
return (
<div className="autoform__list_field" {...filterDOMProps(props)}>
{label && (
<div style={LabelStyles}>
{label}
</div>
)}
{value?.map((item, itemIndex) =>
Children.map(children, (child, childIndex) =>
isValidElement(child)
? cloneElement(child, {
key: `${itemIndex}-${childIndex}`,
name: child.props.name?.replace('$', '' + itemIndex),
...itemProps,
})
: child,
),
)}
<ListAddField initialCount={initialCount} name="$" />
</div>
);
}
export default connectField<ListFieldProps>(List);
@@ -0,0 +1,23 @@
import * as React from 'react';
import { ReactNode } from 'react';
import { connectField } from 'uniforms';
import AutoField from './AutoField';
import ListDelField from './ListDelField';
export type ListItemFieldProps = { children?: ReactNode; value?: unknown };
function ListItem({
children = <AutoField label={null} name="" />,
}: ListItemFieldProps) {
return (
<div>
<ListDelField name="" />
{children}
</div>
);
}
export default connectField<ListItemFieldProps>(ListItem, {
initialValue: false,
});
@@ -0,0 +1,41 @@
import * as React from 'react';
import { Ref } from 'react';
import { HTMLFieldProps, connectField, filterDOMProps } from 'uniforms';
export type LongTextFieldProps = HTMLFieldProps<
string,
HTMLDivElement,
{ inputRef?: Ref<HTMLTextAreaElement> }
>;
function LongText({
disabled,
id,
inputRef,
label,
name,
onChange,
placeholder,
readOnly,
value,
...props
}: LongTextFieldProps) {
return (
<div {...filterDOMProps(props)}>
{label && <label htmlFor={id}>{label}</label>}
<textarea
disabled={disabled}
id={id}
name={name}
onChange={event => onChange(event.target.value)}
placeholder={placeholder}
readOnly={readOnly}
ref={inputRef}
value={value ?? ''}
/>
</div>
);
}
export default connectField<LongTextFieldProps>(LongText, { kind: 'leaf' });
@@ -0,0 +1,31 @@
import * as React from 'react';
import { HTMLFieldProps, connectField, filterDOMProps } from 'uniforms';
import AutoField from './AutoField';
import { LabelStyles } from './component-styles';
export type NestFieldProps = HTMLFieldProps<
object,
HTMLDivElement,
{ itemProps?: object }
>;
function Nest({
children,
fields,
itemProps,
label,
...props
}: NestFieldProps) {
return (
<div {...filterDOMProps(props)}>
{label && <label style={LabelStyles}>{label}</label>}
{children ||
fields.map(field => (
<AutoField key={field} name={field} {...itemProps} />
))}
</div>
);
}
export default connectField<NestFieldProps>(Nest);
@@ -0,0 +1,54 @@
import * as React from 'react';
import { Ref } from 'react';
import { HTMLFieldProps, connectField, filterDOMProps } from 'uniforms';
import { LabelStyles } from './component-styles';
export type NumFieldProps = HTMLFieldProps<
number,
HTMLDivElement,
{ decimal?: boolean; inputRef?: Ref<HTMLInputElement> }
>;
function Num({
decimal,
disabled,
id,
inputRef,
label,
max,
min,
name,
onChange,
placeholder,
readOnly,
step,
value,
...props
}: NumFieldProps) {
return (
<div {...filterDOMProps(props)}>
{label && <label htmlFor={id} style={LabelStyles}>{label}</label>}
<input
disabled={disabled}
id={id}
max={max}
min={min}
name={name}
onChange={event => {
const parse = decimal ? parseFloat : parseInt;
const value = parse(event.target.value);
onChange(isNaN(value) ? undefined : value);
}}
placeholder={placeholder}
readOnly={readOnly}
ref={inputRef}
step={step || (decimal ? 0.01 : 1)}
type="number"
value={value ?? ''}
/>
</div>
);
}
export default connectField<NumFieldProps>(Num, { kind: 'leaf' });
@@ -0,0 +1,28 @@
import { QuickForm } from 'uniforms';
import AutoField from './AutoField';
import BaseForm from './BaseForm';
import ErrorsField from './ErrorsField';
import SubmitField from './SubmitField';
function Quick(parent: any) {
class _ extends QuickForm.Quick(parent) {
static Quick = Quick;
getAutoField() {
return AutoField;
}
getErrorsField() {
return ErrorsField;
}
getSubmitField() {
return SubmitField;
}
}
return _ as unknown as QuickForm;
}
export default Quick(BaseForm);
@@ -0,0 +1,62 @@
import omit = require('lodash.omit');
import * as React from 'react';
import { HTMLFieldProps, connectField, filterDOMProps } from 'uniforms';
import { LabelStyles } from './component-styles';
const base64: typeof btoa =
typeof btoa === 'undefined'
? /* istanbul ignore next */ x => Buffer.from(x).toString('base64')
: btoa;
const escape = (x: string) => base64(encodeURIComponent(x)).replace(/=+$/, '');
export type RadioFieldProps = HTMLFieldProps<
string,
HTMLDivElement,
{
allowedValues?: string[];
checkboxes?: boolean;
transform?: (value: string) => string;
}
>;
function Radio({
allowedValues,
disabled,
id,
label,
name,
onChange,
readOnly,
transform,
value,
...props
}: RadioFieldProps) {
return (
<div {...omit(filterDOMProps(props), ['checkboxes'])}>
{label && <label style={LabelStyles}>{label}</label>}
{allowedValues?.map(item => (
<div key={item}>
<input
checked={item === value}
disabled={disabled}
id={`${id}-${escape(item)}`}
name={name}
onChange={() => {
if (!readOnly) {
onChange(item);
}
}}
type="radio"
/>
<label htmlFor={`${id}-${escape(item)}`}>
{transform ? transform(item) : item}
</label>
</div>
))}
</div>
);
}
export default connectField<RadioFieldProps>(Radio, { kind: 'leaf' });
@@ -0,0 +1,109 @@
import xor = require('lodash.xor');
import * as React from 'react';
import { Ref } from 'react';
import { HTMLFieldProps, connectField, filterDOMProps } from 'uniforms';
import { LabelStyles } from './component-styles';
const base64: typeof btoa =
typeof btoa === 'undefined'
? /* istanbul ignore next */ x => Buffer.from(x).toString('base64')
: btoa;
const escape = (x: string) => base64(encodeURIComponent(x)).replace(/=+$/, '');
export type SelectFieldProps = HTMLFieldProps<
string | string[],
HTMLDivElement,
{
allowedValues?: string[];
checkboxes?: boolean;
disableItem?: (value: string) => boolean;
inputRef?: Ref<HTMLSelectElement>;
transform?: (value: string) => string;
}
>;
function Select({
allowedValues,
checkboxes,
disabled,
fieldType,
id,
inputRef,
label,
name,
onChange,
placeholder,
readOnly,
required,
disableItem,
transform,
value,
...props
}: SelectFieldProps) {
const multiple = fieldType === Array;
return (
<div {...filterDOMProps(props)}>
{label && <label htmlFor={id} style={LabelStyles}>{label}</label>}
{checkboxes ? (
allowedValues!.map(item => (
<div key={item}>
<input
checked={
fieldType === Array ? value!.includes(item) : value === item
}
disabled={disableItem?.(item) ?? disabled}
id={`${id}-${escape(item)}`}
name={name}
onChange={() => {
if (!readOnly) {
onChange(fieldType === Array ? xor([item], value) : item);
}
}}
type="checkbox"
/>
<label htmlFor={`${id}-${escape(item)}`}>
{transform ? transform(item) : item}
</label>
</div>
))
) : (
<select
disabled={disabled}
id={id}
multiple={multiple}
name={name}
onChange={event => {
if (!readOnly) {
const item = event.target.value;
if (multiple) {
const clear = event.target.selectedIndex === -1;
onChange(clear ? [] : xor([item], value));
} else {
onChange(item !== '' ? item : undefined);
}
}
}}
ref={inputRef}
value={value ?? ''}
style={{ width: "100%", padding: "0.5rem" }}
>
{(!!placeholder || !required || value === undefined) && !multiple && (
<option value="" disabled={required} hidden={required}>
{placeholder || label}
</option>
)}
{allowedValues?.map(value => (
<option disabled={disableItem?.(value)} key={value} value={value}>
{transform ? transform(value) : value}
</option>
))}
</select>
)}
</div>
);
}
export default connectField<SelectFieldProps>(Select, { kind: 'leaf' });
@@ -0,0 +1,29 @@
import { HTMLProps, Ref } from 'react';
import * as React from 'react';
import { Override, filterDOMProps, useForm } from 'uniforms';
export type SubmitFieldProps = Override<
HTMLProps<HTMLInputElement>,
{ inputRef?: Ref<HTMLInputElement>; value?: string }
>;
export default function SubmitField({
disabled,
inputRef,
readOnly,
value,
...props
}: SubmitFieldProps) {
const { error, state } = useForm();
return (
<input
disabled={disabled === undefined ? !!(error || state.disabled) : disabled}
readOnly={readOnly}
ref={inputRef}
type="submit"
{...(value ? { value } : {})}
{...filterDOMProps(props)}
/>
);
}
@@ -0,0 +1,48 @@
import { Ref } from 'react';
import * as React from 'react';
import { HTMLFieldProps, connectField, filterDOMProps } from 'uniforms';
import { LabelStyles } from './component-styles';
export type TextFieldProps = HTMLFieldProps<
string,
HTMLDivElement,
{ inputRef?: Ref<HTMLInputElement> }
>;
function Text({
autoComplete,
disabled,
id,
inputRef,
label,
name,
onChange,
placeholder,
readOnly,
type,
value,
...props
}: TextFieldProps) {
return (
<div {...filterDOMProps(props)}>
{label && <label htmlFor={id} style={LabelStyles}>{label}</label>}
<input
autoComplete={autoComplete}
disabled={disabled}
id={id}
name={name}
onChange={event => onChange(event.target.value)}
placeholder={placeholder}
readOnly={readOnly}
ref={inputRef}
type={type}
value={value ?? ''}
/>
</div>
);
}
Text.defaultProps = { type: 'text' };
export default connectField<TextFieldProps>(Text, { kind: 'leaf' });
@@ -0,0 +1,13 @@
import { ValidatedForm } from 'uniforms';
import BaseForm from './BaseForm';
function Validated(parent: any) {
class _ extends ValidatedForm.Validated(parent) {
static Validated = Validated;
}
return _ as unknown as ValidatedForm;
}
export default Validated(BaseForm);
@@ -0,0 +1,5 @@
import BaseForm from './BaseForm';
import QuickForm from './QuickForm';
import ValidatedForm from './ValidatedForm';
export default ValidatedForm.Validated(QuickForm.Quick(BaseForm));
@@ -0,0 +1,8 @@
export const LabelStyles: React.CSSProperties = {
display: "block",
marginBottom: ".5rem",
marginTop: ".5rem",
lineHeight: "16px",
fontWeight: "bold" };
@@ -0,0 +1,23 @@
export { default as AutoField, AutoFieldProps } from './AutoField';
export { default as AutoFields, AutoFieldsProps } from './AutoFields';
export { default as AutoForm } from './AutoForm';
export { default as BaseForm } from './BaseForm';
export { default as BoolField, BoolFieldProps } from './BoolField';
export { default as DateField, DateFieldProps } from './DateField';
export { default as ErrorField, ErrorFieldProps } from './ErrorField';
export { default as ErrorsField, ErrorsFieldProps } from './ErrorsField';
export { default as HiddenField, HiddenFieldProps } from './HiddenField';
export { default as ListAddField, ListAddFieldProps } from './ListAddField';
export { default as ListDelField, ListDelFieldProps } from './ListDelField';
export { default as ListField, ListFieldProps } from './ListField';
export { default as ListItemField, ListItemFieldProps } from './ListItemField';
export { default as LongTextField, LongTextFieldProps } from './LongTextField';
export { default as NestField, NestFieldProps } from './NestField';
export { default as NumField, NumFieldProps } from './NumField';
export { default as QuickForm } from './QuickForm';
export { default as RadioField, RadioFieldProps } from './RadioField';
export { default as SelectField, SelectFieldProps } from './SelectField';
export { default as SubmitField, SubmitFieldProps } from './SubmitField';
export { default as TextField, TextFieldProps } from './TextField';
export { default as ValidatedForm } from './ValidatedForm';
export { default as ValidatedQuickForm } from './ValidatedQuickForm';
@@ -2,7 +2,8 @@ import * as React from 'react';
import Ajv from 'ajv';
import { useEffect, useState } from 'react';
import { JSONSchemaBridge } from 'uniforms-bridge-json-schema';
import { AutoFields, AutoForm, ErrorsField } from 'uniforms-antd';
import { AutoFields, AutoForm, ErrorsField } from '../../../components/uniforms-frontmatter';
// import { AutoFields, AutoForm, ErrorsField } from 'uniforms-antd';
import { ErrorBoundary } from '@sentry/react';
import { DataFormControls } from './DataFormControls';
@@ -52,7 +53,7 @@ export const DataForm: React.FunctionComponent<IDataFormProps> = ({ schema, mode
schema={bridge}
model={model || {}}
onSubmit={onSubmit}
ref={form => form?.reset()}>
ref={(form: any) => form?.reset()}>
<div className={`fields`}>
<AutoFields />
</div>
+5 -3
View File
@@ -1,6 +1,6 @@
import { DashboardData } from '../models/DashboardData';
import { Template } from '../commands/Template';
import { DefaultFields, SETTINGS_CONTENT_FRONTMATTER_HIGHLIGHT, SETTING_AUTO_UPDATE_DATE, SETTING_CUSTOM_SCRIPTS, SETTING_SEO_CONTENT_MIN_LENGTH, SETTING_SEO_DESCRIPTION_FIELD, SETTING_SLUG_UPDATE_FILE_NAME, SETTING_PREVIEW_HOST, SETTING_DATE_FORMAT, SETTING_COMMA_SEPARATED_FIELDS, SETTING_TAXONOMY_CONTENT_TYPES, SETTING_PANEL_FREEFORM, SETTING_SEO_DESCRIPTION_LENGTH, SETTING_SEO_TITLE_LENGTH, SETTING_SLUG_PREFIX, SETTING_SLUG_SUFFIX, SETTING_TAXONOMY_CATEGORIES, SETTING_TAXONOMY_TAGS, SETTINGS_CONTENT_DRAFT_FIELD, SETTING_SEO_SLUG_LENGTH, SETTING_SITE_BASEURL, SETTING_TAXONOMY_CUSTOM, CONTEXT, SETTINGS_FRAMEWORK_ID, SETTINGS_FRAMEWORK_START, TelemetryEvent } from '../constants';
import { DefaultFields, SETTINGS_CONTENT_FRONTMATTER_HIGHLIGHT, SETTING_AUTO_UPDATE_DATE, SETTING_CUSTOM_SCRIPTS, SETTING_SEO_CONTENT_MIN_LENGTH, SETTING_SEO_DESCRIPTION_FIELD, SETTING_SLUG_UPDATE_FILE_NAME, SETTING_PREVIEW_HOST, SETTING_DATE_FORMAT, SETTING_COMMA_SEPARATED_FIELDS, SETTING_TAXONOMY_CONTENT_TYPES, SETTING_PANEL_FREEFORM, SETTING_SEO_DESCRIPTION_LENGTH, SETTING_SEO_TITLE_LENGTH, SETTING_SLUG_PREFIX, SETTING_SLUG_SUFFIX, SETTING_TAXONOMY_CATEGORIES, SETTING_TAXONOMY_TAGS, SETTINGS_CONTENT_DRAFT_FIELD, SETTING_SEO_SLUG_LENGTH, SETTING_SITE_BASEURL, SETTING_TAXONOMY_CUSTOM, CONTEXT, SETTINGS_FRAMEWORK_ID, SETTINGS_FRAMEWORK_START, TelemetryEvent, SETTINGS_DATA_TYPES } from '../constants';
import * as os from 'os';
import { PanelSettings, CustomScript as ICustomScript } from '../models/PanelSettings';
import { CancellationToken, Disposable, Uri, Webview, WebviewView, WebviewViewProvider, WebviewViewResolveContext, window, workspace, commands, env as vscodeEnv, ThemeIcon } from "vscode";
@@ -24,6 +24,7 @@ import { ImageHelper } from '../helpers/ImageHelper';
import { CustomScript } from '../helpers/CustomScript';
import { Link, Parent } from 'mdast-util-from-markdown/lib';
import { Telemetry } from '../helpers/Telemetry';
import { DataType } from '../models/DataType';
const FILE_LIMIT = 10;
@@ -440,7 +441,8 @@ export class ExplorerView implements WebviewViewProvider, Disposable {
framework: Settings.get<string>(SETTINGS_FRAMEWORK_ID),
commands: {
start: Settings.get<string>(SETTINGS_FRAMEWORK_START)
}
},
dataTypes: Settings.get<DataType[]>(SETTINGS_DATA_TYPES),
} as PanelSettings
});
}
@@ -787,7 +789,7 @@ export class ExplorerView implements WebviewViewProvider, Disposable {
const csp = [
`default-src 'none';`,
`img-src ${`vscode-file://vscode-app`} ${webView.cspSource} https://api.visitorbadge.io 'self' 'unsafe-inline'`,
`script-src ${isProd ? `'nonce-${nonce}'` : `http://${localServerUrl} http://0.0.0.0:${localPort}`}`,
`script-src 'unsafe-eval' ${isProd ? `'nonce-${nonce}'` : `http://${localServerUrl} http://0.0.0.0:${localPort}`}`,
`style-src ${webView.cspSource} 'self' 'unsafe-inline'`,
`font-src ${webView.cspSource}`,
`connect-src https://o1022172.ingest.sentry.io ${isProd ? `` : `ws://${localServerUrl} ws://0.0.0.0:${localPort} http://${localServerUrl} http://0.0.0.0:${localPort}`}`
+6 -1
View File
@@ -2,6 +2,7 @@ import { FileStat } from "vscode";
import { DraftField } from ".";
import { Choice } from "./Choice";
import { DashboardData } from "./DashboardData";
import { DataType } from "./DataType";
export interface PanelSettings {
seo: SEO;
@@ -23,6 +24,7 @@ export interface PanelSettings {
isBacker: boolean | undefined;
framework: string | undefined;
commands: FrameworkCommands;
dataTypes: DataType[] | undefined;
}
export interface FrameworkCommands {
@@ -38,10 +40,12 @@ export interface ContentType {
pageBundle?: boolean;
}
export type FieldType = "string" | "number" | "datetime" | "boolean" | "image" | "choice" | "tags" | "categories" | "draft" | "taxonomy" | "fields" | "data-collection";
export interface Field {
title?: string;
name: string;
type: "string" | "number" | "datetime" | "boolean" | "image" | "choice" | "tags" | "categories" | "draft" | "taxonomy" | "fields";
type: FieldType;
choices?: string[] | Choice[];
single?: boolean;
multiple?: boolean;
@@ -50,6 +54,7 @@ export interface Field {
taxonomyId?: string;
default?: string;
fields?: Field[];
dataType?: string;
}
export interface DateInfo {
@@ -0,0 +1,25 @@
import * as React from 'react';
import { useForm } from 'uniforms';
import { SubmitField } from 'uniforms-unstyled';
export interface IDataCollectionControlsProps {
model: any | null;
onClear: () => void;
}
export const DataCollectionControls: React.FunctionComponent<IDataCollectionControlsProps> = ({ model, onClear }: React.PropsWithChildren<IDataCollectionControlsProps>) => {
const { formRef } = useForm();
return (
<div className='data_controls__buttons'>
<SubmitField value={model ? `Update` : `Add`} />
<button className='ml-4' onClick={() => {
if (onClear) {
onClear();
}
formRef.reset();
}}>Cancel</button>
</div>
);
};
@@ -0,0 +1,131 @@
import Ajv from 'ajv';
import * as React from 'react';
import { useEffect, useState, useMemo, useCallback } from 'react';
import { AutoFields, AutoForm, ErrorsField } from '../../../components/uniforms-frontmatter';
import { JSONSchemaBridge } from 'uniforms-bridge-json-schema';
import { Field, PanelSettings } from '../../../models';
import { DataCollectionControls } from './DataCollectionControls';
import { PencilIcon, TrashIcon } from '@heroicons/react/outline';
export interface IDataCollectionFieldProps {
settings: PanelSettings;
field: Field;
value: any;
onSubmit: (data: any) => void;
}
export const DataCollectionField: React.FunctionComponent<IDataCollectionFieldProps> = ({ settings, field, value, onSubmit }: React.PropsWithChildren<IDataCollectionFieldProps>) => {
const [ schema, setSchema ] = useState<JSONSchemaBridge | null>(null);
const [ bridge, setBridge ] = useState<JSONSchemaBridge | null>(null);
const [ selectedIndex, setSelectedIndex ] = useState<number | null>(null);
const ajv = new Ajv({ allErrors: true, useDefaults: true });
const jsonValidator = (schema: object) => {
const validator = ajv.compile(schema);
return (crntModel: object) => {
validator(crntModel);
return validator.errors?.length ? { details: validator.errors } : null;
};
};
useEffect(() => {
if (schema) {
const schemaValidator = jsonValidator(schema);
const bridge = new JSONSchemaBridge(schema, schemaValidator);
setBridge(bridge);
}
}, [schema]);
useEffect(() => {
if (field?.dataType && settings.dataTypes) {
const crntSchema = settings.dataTypes.find(dataType => dataType.id === field.dataType);
if (crntSchema?.schema) {
setSchema(crntSchema.schema);
}
}
}, [settings?.dataTypes, field?.dataType]);
const model = useMemo(() => (value && selectedIndex !== null && selectedIndex !== undefined) ? value[selectedIndex] : null, [value, selectedIndex]);
const onUpdate = useCallback((data: any) => {
const dataClone: any[] = Object.assign([], value);
if (selectedIndex !== null && selectedIndex !== undefined) {
dataClone[selectedIndex] = data;
} else {
dataClone.push(data);
}
onSubmit(dataClone);
}, [value, selectedIndex, onSubmit]);
const deleteItem = useCallback((index: number) => {
const dataClone: any[] = Object.assign([], value);
if (!value) {
return;
}
dataClone.splice(index, 1);
onSubmit(dataClone);
}, [value, selectedIndex, onSubmit]);
if (!bridge) {
return null;
}
return (
<div className='data_collection__field'>
<div className='autoform'>
<AutoForm
schema={bridge}
model={model || {}}
onSubmit={onUpdate}
ref={form => form?.reset()}>
{
(model && selectedIndex !== null) && (
<h3>Editing: Record {selectedIndex + 1}</h3>
)
}
<div className={`fields`}>
<AutoFields />
</div>
<div className={`errors`}>
<ErrorsField />
</div>
<DataCollectionControls model={model} onClear={() => setSelectedIndex(null)} />
</AutoForm>
</div>
{
value && (
<div className='data_collection__list'>
<label>Data</label>
<ul>
{
value.map((v: any, i: number) => (
<li key={i}>
<span>Record {i+1}</span>
<button className='data_collection__list__button data_collection__list__button_edit' onClick={() => setSelectedIndex(i)}>
<PencilIcon className='data_collection__list__button_icon' />
<span className='sr-only'>Edit</span>
</button>
<button className='data_collection__list__button data_collection__list__button_delete' onClick={() => deleteItem(i)}>
<TrashIcon className='data_collection__list__button_icon' />
<span className='sr-only'>Delete</span>
</button>
</li>
))
}
</ul>
</div>
)
}
</div>
);
};
+22
View File
@@ -20,6 +20,7 @@ import { DateHelper } from '../../helpers/DateHelper';
import FieldBoundary from './ErrorBoundary/FieldBoundary';
import { DraftField } from './Fields/DraftField';
import { VsLabel } from './VscodeComponents';
import { DataCollectionField } from './DataCollection/DataCollectionField';
export interface IMetadata {
[prop: string]: string[] | string | null | IMetadata;
@@ -234,7 +235,28 @@ const Metadata: React.FunctionComponent<IMetadataProps> = ({settings, metadata,
}
return null;
} else if (field.type === 'data-collection') {
const collectionData = parent[field.name];
return (
<FieldBoundary key={field.name} fieldName={field.title || field.name}>
<div className={`metadata_field__box`}>
<VsLabel>
<div className={`metadata_field__label metadata_field__label_parent`}>
<span style={{ lineHeight: "16px"}}>{field.title || field.name}</span>
</div>
</VsLabel>
<DataCollectionField
settings={settings}
field={field}
value={collectionData}
onSubmit={(value) => sendUpdate(field.name, value, parentFields)} />
</div>
</FieldBoundary>
);
} else {
console.warn(`Unknown field type: ${field.type}`);
return null;
}
});
+1
View File
@@ -4,6 +4,7 @@ import { ViewPanel } from "./ViewPanel";
import * as Sentry from "@sentry/react";
import { Integrations } from "@sentry/tracing";
import { SENTRY_LINK } from "../constants";
import './styles.css';
// require('@vscode/codicons/dist/codicon.css');
import '@bendera/vscode-webview-elements/dist/vscode-table.js';
+94
View File
@@ -0,0 +1,94 @@
.data_collection__field {
.autoform {
background-color: transparent;
border: 2px dashed var(--vscode-button-background);
padding: 1.5rem;
filter: brightness(85%);
}
.fields > div {
margin-bottom: .5rem;
&:last-child {
margin-bottom: 0;
}
}
}
.data_controls__buttons {
display: flex;
justify-content: flex-end;
align-items: center;
margin-top:1rem;
button, input {
display: inline;
width: auto;
padding-left: 1rem;
padding-right: 1rem;
}
input {
background: var(--vscode-button-background);
color: var(--vscode-button-foreground);
margin-right: 1rem;
cursor: pointer;
&:hover {
background: var(--vscode-button-hoverBackground);
}
}
button {
background: var(--vscode-button-secondaryBackground);
color: var(--vscode-button-secondaryForeground);
&:hover {
background: var(--vscode-button-secondaryHoverBackground);
}
}
}
.data_collection__list {
margin-top: 1rem;
label {
display: block;
margin-bottom: .5rem;
font-weight: bold;
}
li {
margin-bottom: .5rem;
span {
margin-right: .5rem;
}
&:last-child {
margin-bottom: 0;
}
}
}
.data_collection__list__button {
width: auto;
background: none;
color: var(--vscode-editor-foreground);
&:hover {
color: var(--vscode-button-secondaryForeground);
background: var(--vscode-button-secondaryHoverBackground);
}
}
.data_collection__list__button_icon {
height: 1rem;
width: 1rem;
}
.sr-only {
display: none;
}
+1 -1
View File
@@ -31,7 +31,7 @@ const config = [{
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
use: ['style-loader', 'css-loader', 'postcss-loader']
},
{
test: /\.m?js/,