mirror of
https://github.com/estruyf/vscode-front-matter.git
synced 2026-08-04 16:02:50 +02:00
#176 - New collection field + uniform custom components
This commit is contained in:
Generated
+78
@@ -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
@@ -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>
|
||||
|
||||
@@ -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}`}`
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -31,7 +31,7 @@ const config = [{
|
||||
},
|
||||
{
|
||||
test: /\.css$/,
|
||||
use: ['style-loader', 'css-loader']
|
||||
use: ['style-loader', 'css-loader', 'postcss-loader']
|
||||
},
|
||||
{
|
||||
test: /\.m?js/,
|
||||
|
||||
Reference in New Issue
Block a user