diff --git a/lib/js/liform-react/.gitignore b/lib/js/liform-react/.gitignore
new file mode 100644
index 000000000..62b82089f
--- /dev/null
+++ b/lib/js/liform-react/.gitignore
@@ -0,0 +1,12 @@
+node_modules
+npm-debug.log
+dist
+lib
+es
+.DS_Store
+yarn.lock
+.nyc_output/
+coverage/
+examples/bundle*
+package-lock.json
+built_docs/
diff --git a/lib/js/liform-react/package.json b/lib/js/liform-react/package.json
new file mode 100644
index 000000000..2fe35d4bd
--- /dev/null
+++ b/lib/js/liform-react/package.json
@@ -0,0 +1,37 @@
+{
+ "name": "@alchemy/liform-react",
+ "version": "1.0.0",
+ "description": "Generate forms from json-schema to use with React (and redux-form)",
+ "main": "./src/index.jsx",
+ "scripts": {},
+ "keywords": [
+ "react",
+ "json-schema",
+ "form",
+ "redux-form"
+ ],
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/limenius/liform-react.git"
+ },
+ "author": "Nacho Martin",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0"
+ },
+ "dependencies": {
+ "ajv": "^8.12.0",
+ "classnames": "^2.2.5",
+ "deepmerge": "^2.0.1",
+ "lodash": "^4.17.21",
+ "prop-types": "^15.5.10",
+ "react-redux": "^9.0.4",
+ "redux": "^4.2.1",
+ "redux-form": "^8.3.10"
+ },
+ "devDependencies": {
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0"
+ }
+}
diff --git a/lib/js/liform-react/src/Form.jsx b/lib/js/liform-react/src/Form.jsx
new file mode 100644
index 000000000..e69de29bb
diff --git a/lib/js/liform-react/src/buildSyncValidation.js b/lib/js/liform-react/src/buildSyncValidation.js
new file mode 100644
index 000000000..187d3f6d0
--- /dev/null
+++ b/lib/js/liform-react/src/buildSyncValidation.js
@@ -0,0 +1,84 @@
+import Ajv from "ajv";
+import merge from "deepmerge";
+import { set as _set } from "lodash";
+
+const setError = (error, schema) => {
+ // convert property accessor (.xxx[].xxx) notation to jsonPointers notation
+ if (error.instancePath.charAt(0) === ".") {
+ error.instancePath = error.instancePath.replace(/[.[]/gi, "/");
+ error.instancePath = error.instancePath.replace(/[\]]/gi, "");
+ }
+ const instancePathParts = error.instancePath.split("/").slice(1);
+ let instancePath = error.instancePath.slice(1).replace(/\//g, ".");
+ const type = findTypeInSchema(schema, instancePathParts);
+
+ let errorToSet;
+ if (type === "array" || type === "allOf" || type === "oneOf") {
+ errorToSet = { _error: error.message };
+ } else {
+ errorToSet = error.message;
+ }
+
+ let errors = {};
+ _set(errors, instancePath, errorToSet);
+ return errors;
+};
+
+const findTypeInSchema = (schema, instancePath) => {
+ if (!schema) {
+ return;
+ } else if (instancePath.length === 0 && schema.hasOwnProperty("type")) {
+ return schema.type;
+ } else {
+ if (schema.type === "array") {
+ return findTypeInSchema(schema.items, instancePath.slice(1));
+ } else if (schema.hasOwnProperty("allOf")) {
+ if (instancePath.length === 0) return "allOf";
+ schema = { ...schema, ...merge.all(schema.allOf) };
+ delete schema.allOf;
+ return findTypeInSchema(schema, instancePath);
+ } else if (schema.hasOwnProperty("oneOf")) {
+ if (instancePath.length === 0) return "oneOf";
+ schema.oneOf.forEach(item => {
+ let type = findTypeInSchema(item, instancePath);
+ if (type) {
+ return type;
+ }
+ });
+ } else {
+ return findTypeInSchema(
+ schema.properties[instancePath[0]],
+ instancePath.slice(1)
+ );
+ }
+ }
+};
+
+const buildSyncValidation = (schema, ajvParam = null) => {
+ let ajv = ajvParam;
+ if (ajv === null) {
+ ajv = new Ajv({
+ allErrors: true,
+ strict: false
+ });
+ }
+ return values => {
+ const valid = ajv.validate(schema, values);
+ if (valid) {
+ return {};
+ }
+ const ajvErrors = ajv.errors;
+
+ let errors = ajvErrors.map(error => {
+ return setError(error, schema);
+ });
+ // We need at least two elements
+ errors.push({});
+ errors.push({});
+ return merge.all(errors);
+ };
+};
+
+export default buildSyncValidation;
+
+export { setError };
diff --git a/lib/js/liform-react/src/compileSchema.js b/lib/js/liform-react/src/compileSchema.js
new file mode 100644
index 000000000..89becd4d1
--- /dev/null
+++ b/lib/js/liform-react/src/compileSchema.js
@@ -0,0 +1,44 @@
+function isObject(thing) {
+ return typeof thing === "object" && thing !== null && !Array.isArray(thing);
+}
+
+function compileSchema(schema, root) {
+ if (!root) {
+ root = schema;
+ }
+ let newSchema;
+
+ if (isObject(schema)) {
+ newSchema = {};
+ for (let i in schema) {
+ if (schema.hasOwnProperty(i)) {
+ if (i === "$ref") {
+ newSchema = compileSchema(resolveRef(schema[i], root), root);
+ } else {
+ newSchema[i] = compileSchema(schema[i], root);
+ }
+ }
+ }
+ return newSchema;
+ }
+
+ if (Array.isArray(schema)) {
+ newSchema = [];
+ for (let i = 0; i < schema.length; i += 1) {
+ newSchema[i] = compileSchema(schema[i], root);
+ }
+ return newSchema;
+ }
+
+ return schema;
+}
+
+function resolveRef(uri, schema) {
+ uri = uri.replace("#/", "");
+ const tokens = uri.split("/");
+ const tip = tokens.reduce((obj, token) => obj[token], schema);
+
+ return tip;
+}
+
+export default compileSchema;
diff --git a/lib/js/liform-react/src/index.jsx b/lib/js/liform-react/src/index.jsx
new file mode 100644
index 000000000..c05d4a7bd
--- /dev/null
+++ b/lib/js/liform-react/src/index.jsx
@@ -0,0 +1,68 @@
+import React from "react";
+import PropTypes from "prop-types";
+import DefaultTheme from "./themes/bootstrap3";
+import { reduxForm } from "redux-form";
+import renderFields from "./renderFields";
+import renderField from "./renderField";
+import processSubmitErrors from "./processSubmitErrors";
+import buildSyncValidation from "./buildSyncValidation";
+import { setError } from "./buildSyncValidation";
+import compileSchema from "./compileSchema";
+
+const BaseForm = props => {
+ const { schema, handleSubmit, theme, error, submitting, context } = props;
+ return (
+
+ );
+};
+
+const Liform = props => {
+ const schema = compileSchema(props.schema);
+ props.schema.showLabel = false;
+ const schemaWithOptions = compileSchema(props.schema);
+ const formName = props.formKey || props.schema.title || "form";
+
+ const FinalForm = reduxForm({
+ form: props.formKey || props.schema.title || "form",
+ validate: props.syncValidation || buildSyncValidation(schema, props.ajv),
+ initialValues: props.initialValues,
+ context: { ...props.context, formName }
+ })(props.baseForm || BaseForm);
+
+ return (
+
+ );
+};
+
+Liform.propTypes = {
+ schema: PropTypes.object,
+ onSubmit: PropTypes.func,
+ initialValues: PropTypes.object,
+ syncValidation: PropTypes.func,
+ formKey: PropTypes.string,
+ baseForm: PropTypes.func,
+ context: PropTypes.object,
+ ajv: PropTypes.object
+};
+
+export default Liform;
+
+export {
+ renderFields,
+ renderField,
+ processSubmitErrors,
+ DefaultTheme,
+ setError,
+ buildSyncValidation,
+ compileSchema,
+};
diff --git a/lib/js/liform-react/src/processSubmitErrors.js b/lib/js/liform-react/src/processSubmitErrors.js
new file mode 100644
index 000000000..74f0ab95d
--- /dev/null
+++ b/lib/js/liform-react/src/processSubmitErrors.js
@@ -0,0 +1,40 @@
+import { SubmissionError } from "redux-form";
+import { isEmpty as _isEmpty } from "lodash"; // added for empty check
+
+const convertToReduxFormErrors = obj => {
+ let objectWithoutChildrenAndFalseErrors = {};
+ Object.keys(obj).map(name => {
+ if (name === "children") {
+ objectWithoutChildrenAndFalseErrors = {
+ ...objectWithoutChildrenAndFalseErrors,
+ ...convertToReduxFormErrors(obj[name])
+ };
+ } else {
+ if (obj[name].hasOwnProperty("children")) {
+ // if children, take field from it and set them directly as own field
+ objectWithoutChildrenAndFalseErrors[name] = convertToReduxFormErrors(
+ obj[name]
+ );
+ } else {
+ if (
+ obj[name].hasOwnProperty("errors") &&
+ !_isEmpty(obj[name]["errors"])
+ ) {
+ // using lodash for empty error check, dont add them if empty
+ objectWithoutChildrenAndFalseErrors[name] = obj[name]["errors"];
+ }
+ }
+ }
+ return null;
+ });
+ return objectWithoutChildrenAndFalseErrors;
+};
+
+const processSubmitErrors = errors => {
+ if (errors.hasOwnProperty("errors")) {
+ errors = convertToReduxFormErrors(errors.errors);
+ throw new SubmissionError(errors);
+ }
+};
+
+export default processSubmitErrors;
diff --git a/lib/js/liform-react/src/renderField.js b/lib/js/liform-react/src/renderField.js
new file mode 100644
index 000000000..2e531301d
--- /dev/null
+++ b/lib/js/liform-react/src/renderField.js
@@ -0,0 +1,51 @@
+import React from "react";
+import deepmerge from "deepmerge";
+
+const guessWidget = (fieldSchema, theme) => {
+ if (fieldSchema.widget) {
+ return fieldSchema.widget;
+ } else if (fieldSchema.hasOwnProperty("enum")) {
+ return "choice";
+ } else if (fieldSchema.hasOwnProperty("oneOf")) {
+ return "oneOf";
+ } else if (theme[fieldSchema.format]) {
+ return fieldSchema.format;
+ }
+ return fieldSchema.type || "object";
+};
+
+const renderField = (
+ fieldSchema,
+ fieldName,
+ theme,
+ prefix = "",
+ context = {},
+ required = false
+) => {
+ if (fieldSchema.hasOwnProperty("allOf")) {
+ fieldSchema = { ...fieldSchema, ...deepmerge.all(fieldSchema.allOf) };
+ delete fieldSchema.allOf;
+ }
+
+ const widget = guessWidget(fieldSchema, theme);
+
+ if (!theme[widget]) {
+ throw new Error("liform: " + widget + " is not defined in the theme");
+ }
+
+ const newFieldName = prefix ? prefix + fieldName : fieldName;
+
+ return React.createElement(theme[widget], {
+ key: fieldName,
+ fieldName: widget === "oneOf" ? fieldName : newFieldName,
+ label:
+ fieldSchema.showLabel === false ? "" : fieldSchema.title || fieldName,
+ required: required,
+ schema: fieldSchema,
+ theme,
+ context,
+ prefix
+ });
+};
+
+export default renderField;
diff --git a/lib/js/liform-react/src/renderFields.js b/lib/js/liform-react/src/renderFields.js
new file mode 100644
index 000000000..ee25342fb
--- /dev/null
+++ b/lib/js/liform-react/src/renderFields.js
@@ -0,0 +1,38 @@
+import renderField from "./renderField";
+
+export const isRequired = (schema, fieldName) => {
+ if (!schema.required) {
+ return false;
+ }
+ return schema.required.indexOf(fieldName) !== -1;
+};
+
+const renderFields = (schema, theme, prefix = null, context = {}) => {
+ let props = [];
+ for (let i in schema.properties) {
+ props.push({ prop: i, propertyOrder: schema.properties[i].propertyOrder });
+ }
+ props = props.sort((a, b) => {
+ if (a.propertyOrder > b.propertyOrder) {
+ return 1;
+ } else if (a.propertyOrder < b.propertyOrder) {
+ return -1;
+ } else {
+ return 0;
+ }
+ });
+ return props.map(item => {
+ const name = item.prop;
+ const field = schema.properties[name];
+ return renderField(
+ field,
+ name,
+ theme,
+ prefix,
+ context,
+ isRequired(schema, name)
+ );
+ });
+};
+
+export default renderFields;
diff --git a/lib/js/liform-react/src/themes/bootstrap3/ArrayWidget.jsx b/lib/js/liform-react/src/themes/bootstrap3/ArrayWidget.jsx
new file mode 100644
index 000000000..6e2afd87a
--- /dev/null
+++ b/lib/js/liform-react/src/themes/bootstrap3/ArrayWidget.jsx
@@ -0,0 +1,152 @@
+import React from "react";
+import PropTypes from "prop-types";
+import renderField from "../../renderField";
+import { FieldArray } from "redux-form";
+import { times as _times} from "lodash";
+import ChoiceWidget from "./ChoiceWidget";
+import classNames from "classnames";
+
+const renderArrayFields = (
+ count,
+ schema,
+ theme,
+ fieldName,
+ remove,
+ context,
+ swap
+) => {
+ const prefix = fieldName + ".";
+ if (count) {
+ return _times(count, idx => {
+ return (
+
+
+ {idx !== count - 1 && count > 1 ? (
+
+ ) : (
+ ""
+ )}
+ {idx !== 0 && count > 1 ? (
+
+ ) : (
+ ""
+ )}
+
+
+
+ {renderField(
+ { ...schema, showLabel: false },
+ idx.toString(),
+ theme,
+ prefix,
+ context
+ )}
+
+ );
+ });
+ } else {
+ return null;
+ }
+};
+
+const renderInput = field => {
+ const className = classNames([
+ "arrayType",
+ { "has-error": field.meta.submitFailed && field.meta.error }
+ ]);
+
+ return (
+
+
+ {field.meta.submitFailed &&
+ field.meta.error && (
+
{field.meta.error}
+ )}
+ {renderArrayFields(
+ field.fields.length,
+ field.schema.items,
+ field.theme,
+ field.fieldName,
+ idx => field.fields.remove(idx),
+ field.context,
+ (a, b) => {
+ field.fields.swap(a, b);
+ }
+ )}
+
+
+
+ );
+};
+
+const CollectionWidget = props => {
+ return (
+
+ );
+};
+
+const ArrayWidget = props => {
+ // Arrays are tricky because they can be multiselects or collections
+ if (
+ props.schema.items.hasOwnProperty("enum") &&
+ props.schema.hasOwnProperty("uniqueItems") &&
+ props.schema.uniqueItems
+ ) {
+ return ChoiceWidget({
+ ...props,
+ schema: props.schema.items,
+ multiple: true
+ });
+ } else {
+ return CollectionWidget(props);
+ }
+};
+
+ArrayWidget.propTypes = {
+ schema: PropTypes.object.isRequired,
+ fieldName: PropTypes.string,
+ label: PropTypes.string,
+ theme: PropTypes.object,
+ context: PropTypes.object
+};
+
+export default ArrayWidget;
diff --git a/lib/js/liform-react/src/themes/bootstrap3/BaseInputWidget.jsx b/lib/js/liform-react/src/themes/bootstrap3/BaseInputWidget.jsx
new file mode 100644
index 000000000..fd7861409
--- /dev/null
+++ b/lib/js/liform-react/src/themes/bootstrap3/BaseInputWidget.jsx
@@ -0,0 +1,59 @@
+import React from "react";
+import PropTypes from "prop-types";
+import classNames from "classnames";
+import { Field } from "redux-form";
+
+const renderInput = field => {
+ const className = classNames([
+ "form-group",
+ { "has-error": field.meta.touched && field.meta.error }
+ ]);
+ return (
+
+
+
+ {field.meta.touched &&
+ field.meta.error && (
+ {field.meta.error}
+ )}
+ {field.description && (
+ {field.description}
+ )}
+
+ );
+};
+
+const BaseInputWidget = props => {
+ return (
+
+ );
+};
+
+BaseInputWidget.propTypes = {
+ schema: PropTypes.object.isRequired,
+ type: PropTypes.string.isRequired,
+ required: PropTypes.bool,
+ fieldName: PropTypes.string,
+ label: PropTypes.string,
+ normalizer: PropTypes.func
+};
+
+export default BaseInputWidget;
diff --git a/lib/js/liform-react/src/themes/bootstrap3/CheckboxWidget.jsx b/lib/js/liform-react/src/themes/bootstrap3/CheckboxWidget.jsx
new file mode 100644
index 000000000..aadcf10cc
--- /dev/null
+++ b/lib/js/liform-react/src/themes/bootstrap3/CheckboxWidget.jsx
@@ -0,0 +1,56 @@
+import React from "react";
+import PropTypes from "prop-types";
+import classNames from "classnames";
+import { Field } from "redux-form";
+
+const renderInput = field => {
+ const className = classNames([
+ "form-group",
+ { "has-error": field.meta.touched && field.meta.error }
+ ]);
+ return (
+
+
+
+
+ {field.meta.touched &&
+ field.meta.error && (
+
{field.meta.error}
+ )}
+ {field.description && (
+
{field.description}
+ )}
+
+ );
+};
+
+const CheckboxWidget = props => {
+ return (
+
+ );
+};
+
+CheckboxWidget.propTypes = {
+ schema: PropTypes.object.isRequired,
+ fieldName: PropTypes.string,
+ label: PropTypes.string,
+ theme: PropTypes.object
+};
+
+export default CheckboxWidget;
diff --git a/lib/js/liform-react/src/themes/bootstrap3/ChoiceExpandedWidget.jsx b/lib/js/liform-react/src/themes/bootstrap3/ChoiceExpandedWidget.jsx
new file mode 100644
index 000000000..71b776036
--- /dev/null
+++ b/lib/js/liform-react/src/themes/bootstrap3/ChoiceExpandedWidget.jsx
@@ -0,0 +1,67 @@
+import React from "react";
+import classNames from "classnames";
+import { Field } from "redux-form";
+
+const zipObject = (props, values) =>
+ props.reduce(
+ (prev, prop, i) => Object.assign(prev, { [prop]: values[i] }),
+ {}
+ );
+
+const renderChoice = field => {
+ const className = classNames([
+ "form-group",
+ { "has-error": field.meta.touched && field.meta.error }
+ ]);
+ const options = field.schema.enum;
+ const optionNames = field.schema.enum_titles || options;
+
+ const selectOptions = zipObject(options, optionNames);
+ return (
+
+
+ {Object.entries(selectOptions).map(([value, name]) => (
+
+
+
+ ))}
+
+ {field.meta.touched &&
+ field.meta.error && (
+
{field.meta.error}
+ )}
+ {field.description && (
+
{field.description}
+ )}
+
+ );
+};
+
+const ChoiceExpandedWidget = props => {
+ return (
+
+ );
+};
+
+export default ChoiceExpandedWidget;
diff --git a/lib/js/liform-react/src/themes/bootstrap3/ChoiceMultipleExpandedWidget.jsx b/lib/js/liform-react/src/themes/bootstrap3/ChoiceMultipleExpandedWidget.jsx
new file mode 100644
index 000000000..f4099150e
--- /dev/null
+++ b/lib/js/liform-react/src/themes/bootstrap3/ChoiceMultipleExpandedWidget.jsx
@@ -0,0 +1,84 @@
+import React from "react";
+import classNames from "classnames";
+import { Field } from "redux-form";
+
+const zipObject = (props, values) =>
+ props.reduce(
+ (prev, prop, i) => Object.assign(prev, { [prop]: values[i] }),
+ {}
+ );
+
+const changeValue = (checked, item, onChange, currentValue = []) => {
+ if (checked) {
+ if (currentValue.indexOf(checked) === -1) {
+ return onChange([...currentValue, item]);
+ }
+ } else {
+ return onChange(currentValue.filter(items => it === item));
+ }
+ return onChange(currentValue);
+};
+
+const renderChoice = field => {
+ const className = classNames([
+ "form-group",
+ { "has-error": field.meta.touched && field.meta.error }
+ ]);
+ const options = field.schema.items.enum;
+ const optionNames = field.schema.items.enum_titles || options;
+
+ const selectOptions = zipObject(options, optionNames);
+ return (
+
+
+ {Object.entries(selectOptions).map(([value, name]) => (
+
+
+
+ ))}
+
+ {field.meta.touched &&
+ field.meta.error && (
+
{field.meta.error}
+ )}
+ {field.description && (
+
{field.description}
+ )}
+
+ );
+};
+
+const ChoiceMultipleExpandedWidget = props => {
+ return (
+
+ );
+};
+
+export default ChoiceMultipleExpandedWidget;
diff --git a/lib/js/liform-react/src/themes/bootstrap3/ChoiceWidget.jsx b/lib/js/liform-react/src/themes/bootstrap3/ChoiceWidget.jsx
new file mode 100644
index 000000000..783c94f08
--- /dev/null
+++ b/lib/js/liform-react/src/themes/bootstrap3/ChoiceWidget.jsx
@@ -0,0 +1,78 @@
+import React from "react";
+import PropTypes from "prop-types";
+import classNames from "classnames";
+import { Field } from "redux-form";
+import { zipObject as _zipObject, map as _map } from "lodash";
+
+const renderSelect = field => {
+ const className = classNames([
+ "form-group",
+ { "has-error": field.meta.touched && field.meta.error }
+ ]);
+ const options = field.schema.enum;
+ const optionNames = field.schema.enum_titles || options;
+
+ const selectOptions = _zipObject(options, optionNames);
+ return (
+
+
+
+
+ {field.meta.touched &&
+ field.meta.error && (
+ {field.meta.error}
+ )}
+ {field.description && (
+ {field.description}
+ )}
+
+ );
+};
+
+const ChoiceWidget = props => {
+ return (
+
+ );
+};
+
+ChoiceWidget.propTypes = {
+ schema: PropTypes.object.isRequired,
+ fieldName: PropTypes.string,
+ label: PropTypes.string,
+ theme: PropTypes.object,
+ multiple: PropTypes.bool,
+ required: PropTypes.bool
+};
+
+export default ChoiceWidget;
diff --git a/lib/js/liform-react/src/themes/bootstrap3/ColorWidget.jsx b/lib/js/liform-react/src/themes/bootstrap3/ColorWidget.jsx
new file mode 100644
index 000000000..8a3b5ea71
--- /dev/null
+++ b/lib/js/liform-react/src/themes/bootstrap3/ColorWidget.jsx
@@ -0,0 +1,17 @@
+import React from "react";
+import PropTypes from "prop-types";
+import BaseInputWidget from "./BaseInputWidget";
+
+const ColorWidget = props => {
+ return ;
+};
+
+BaseInputWidget.propTypes = {
+ schema: PropTypes.object.isRequired,
+ type: PropTypes.string.isRequired,
+ required: PropTypes.bool,
+ fieldName: PropTypes.string,
+ label: PropTypes.string
+};
+
+export default ColorWidget;
diff --git a/lib/js/liform-react/src/themes/bootstrap3/CompatibleDateTimeWidget.jsx b/lib/js/liform-react/src/themes/bootstrap3/CompatibleDateTimeWidget.jsx
new file mode 100644
index 000000000..8fb85cf33
--- /dev/null
+++ b/lib/js/liform-react/src/themes/bootstrap3/CompatibleDateTimeWidget.jsx
@@ -0,0 +1,192 @@
+import React from "react";
+import classNames from "classnames";
+import { Field } from "redux-form";
+import DateSelector from "./DateSelector";
+
+// produces an array [start..end-1]
+const range = (start, end) =>
+ Array.from({ length: end - start }, (v, k) => k + start);
+
+// produces an array [start..end-1] padded with zeros, (two digits)
+const rangeZeroPad = (start, end) =>
+ Array.from({ length: end - start }, (v, k) => ("0" + (k + start)).slice(-2));
+
+const extractYear = value => {
+ return extractDateTimeToken(value, 0);
+};
+const extractMonth = value => {
+ return extractDateTimeToken(value, 1);
+};
+const extractDay = value => {
+ return extractDateTimeToken(value, 2);
+};
+const extractHour = value => {
+ return extractDateTimeToken(value, 3);
+};
+const extractMinute = value => {
+ return extractDateTimeToken(value, 4);
+};
+const extractSecond = value => {
+ return extractDateTimeToken(value, 5);
+};
+
+const extractDateTimeToken = (value, index) => {
+ if (!value) {
+ return "";
+ }
+ // Remove timezone Z
+ value = value.substring(0, value.length - 1);
+ const tokens = value.split(/[-T:]/);
+ if (tokens.length !== 6) {
+ return "";
+ }
+ return tokens[index];
+};
+
+class CompatibleDateTime extends React.Component {
+ constructor(props, context) {
+ super(props, context);
+ this.state = {
+ year: null,
+ month: null,
+ day: null,
+ hour: null,
+ minute: null,
+ second: null
+ };
+ this.onBlur = this.onBlur.bind(this);
+ }
+
+ // Produces a RFC 3339 full-date from the state
+ buildRfc3339Date() {
+ const year = this.state.year || "";
+ const month = this.state.month || "";
+ const day = this.state.day || "";
+ return year + "-" + month + "-" + day;
+ }
+
+ // Produces a RFC 3339 datetime from the state
+ buildRfc3339DateTime() {
+ const date = this.buildRfc3339Date();
+ const hour = this.state.hour || "";
+ const minute = this.state.minute || "";
+ const second = this.state.second || "";
+ return date + "T" + hour + ":" + minute + ":" + second + "Z";
+ }
+
+ onChangeField(field, e) {
+ const value = e.target.value;
+ let changeset = {};
+ changeset[field] = value;
+ this.setState(changeset, () => {
+ this.props.input.onChange(this.buildRfc3339DateTime());
+ });
+ }
+ onBlur() {
+ this.props.input.onBlur(this.buildRfc3339DateTime());
+ }
+ render() {
+ const field = this.props;
+ const className = classNames([
+ "form-group",
+ { "has-error": field.meta.touched && field.meta.error }
+ ]);
+ return (
+
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+
+ {field.meta.touched &&
+ field.meta.error && (
+
{field.meta.error}
+ )}
+ {field.description && (
+
{field.description}
+ )}
+
+ );
+ }
+}
+const CompatibleDateTimeWidget = props => {
+ return (
+
+ );
+};
+
+export default CompatibleDateTimeWidget;
+
+// Only for testing purposes
+export { extractDateTimeToken };
diff --git a/lib/js/liform-react/src/themes/bootstrap3/CompatibleDateWidget.jsx b/lib/js/liform-react/src/themes/bootstrap3/CompatibleDateWidget.jsx
new file mode 100644
index 000000000..ef329c0ac
--- /dev/null
+++ b/lib/js/liform-react/src/themes/bootstrap3/CompatibleDateWidget.jsx
@@ -0,0 +1,144 @@
+import React from "react";
+import classNames from "classnames";
+import { Field } from "redux-form";
+import DateSelector from "./DateSelector";
+
+// produces an array [start..end-1]
+const range = (start, end) =>
+ Array.from({ length: end - start }, (v, k) => k + start);
+
+// produces an array [start..end-1] padded with zeros, (two digits)
+const rangeZeroPad = (start, end) =>
+ Array.from({ length: end - start }, (v, k) => ("0" + (k + start)).slice(-2));
+
+const extractYear = value => {
+ return extractDateToken(value, 0);
+};
+const extractMonth = value => {
+ return extractDateToken(value, 1);
+};
+const extractDay = value => {
+ return extractDateToken(value, 2);
+};
+
+const extractDateToken = (value, index) => {
+ if (!value) {
+ return "";
+ }
+ const tokens = value.split(/-/);
+ if (tokens.length !== 3) {
+ return "";
+ }
+ return tokens[index];
+};
+
+class CompatibleDate extends React.Component {
+ constructor(props, context) {
+ super(props, context);
+ this.state = {
+ year: null,
+ month: null,
+ day: null,
+ hour: null,
+ minute: null,
+ second: null
+ };
+ this.onBlur = this.onBlur.bind(this);
+ }
+
+ // Produces a RFC 3339 full-date from the state
+ buildRfc3339Date() {
+ const year = this.state.year || "";
+ const month = this.state.month || "";
+ const day = this.state.day || "";
+ return year + "-" + month + "-" + day;
+ }
+
+ onChangeField(field, e) {
+ const value = e.target.value;
+ let changeset = {};
+ changeset[field] = value;
+ this.setState(changeset, () => {
+ this.props.input.onChange(this.buildRfc3339Date());
+ });
+ }
+
+ onBlur() {
+ this.props.input.onBlur(this.buildRfc3339Date());
+ }
+
+ render() {
+ const field = this.props;
+ const className = classNames([
+ "form-group",
+ { "has-error": field.meta.touched && field.meta.error }
+ ]);
+ return (
+
+
+
+ {field.meta.touched &&
+ field.meta.error && (
+
{field.meta.error}
+ )}
+ {field.description && (
+
{field.description}
+ )}
+
+ );
+ }
+}
+const CompatibleDateWidget = props => {
+ return (
+
+ );
+};
+
+export default CompatibleDateWidget;
+
+// Only for testing purposes
+export { extractDateToken };
diff --git a/lib/js/liform-react/src/themes/bootstrap3/DateSelector.jsx b/lib/js/liform-react/src/themes/bootstrap3/DateSelector.jsx
new file mode 100644
index 000000000..4b5227b6b
--- /dev/null
+++ b/lib/js/liform-react/src/themes/bootstrap3/DateSelector.jsx
@@ -0,0 +1,29 @@
+import React from "react";
+
+const DateSelector = props => {
+ return (
+
+ );
+};
+
+export default DateSelector;
diff --git a/lib/js/liform-react/src/themes/bootstrap3/DateTimeWidget.jsx b/lib/js/liform-react/src/themes/bootstrap3/DateTimeWidget.jsx
new file mode 100644
index 000000000..1833f8809
--- /dev/null
+++ b/lib/js/liform-react/src/themes/bootstrap3/DateTimeWidget.jsx
@@ -0,0 +1,8 @@
+import React from "react";
+import BaseInputWidget from "./BaseInputWidget";
+
+const DateTimeWidget = props => {
+ return ;
+};
+
+export default DateTimeWidget;
diff --git a/lib/js/liform-react/src/themes/bootstrap3/DateWidget.jsx b/lib/js/liform-react/src/themes/bootstrap3/DateWidget.jsx
new file mode 100644
index 000000000..bbc8bc69a
--- /dev/null
+++ b/lib/js/liform-react/src/themes/bootstrap3/DateWidget.jsx
@@ -0,0 +1,8 @@
+import React from "react";
+import BaseInputWidget from "./BaseInputWidget";
+
+const DateWidget = props => {
+ return ;
+};
+
+export default DateWidget;
diff --git a/lib/js/liform-react/src/themes/bootstrap3/EmailWidget.jsx b/lib/js/liform-react/src/themes/bootstrap3/EmailWidget.jsx
new file mode 100644
index 000000000..5c3aac95a
--- /dev/null
+++ b/lib/js/liform-react/src/themes/bootstrap3/EmailWidget.jsx
@@ -0,0 +1,18 @@
+import React from "react";
+import PropTypes from "prop-types";
+import BaseInputWidget from "./BaseInputWidget";
+
+const EmailWidget = props => {
+ return ;
+};
+
+EmailWidget.propTypes = {
+ schema: PropTypes.object.isRequired,
+ fieldName: PropTypes.string,
+ label: PropTypes.string,
+ theme: PropTypes.object,
+ multiple: PropTypes.bool,
+ required: PropTypes.bool
+};
+
+export default EmailWidget;
diff --git a/lib/js/liform-react/src/themes/bootstrap3/FileWidget.jsx b/lib/js/liform-react/src/themes/bootstrap3/FileWidget.jsx
new file mode 100644
index 000000000..28a4dd702
--- /dev/null
+++ b/lib/js/liform-react/src/themes/bootstrap3/FileWidget.jsx
@@ -0,0 +1,62 @@
+import React from "react";
+import { Field } from "redux-form";
+import classNames from "classnames";
+
+const processFile = (onChange, e) => {
+ const files = e.target.files;
+ return new Promise(() => {
+ let reader = new FileReader();
+ reader.addEventListener(
+ "load",
+ () => {
+ onChange(reader.result);
+ },
+ false
+ );
+ reader.readAsDataURL(files[0]);
+ });
+};
+
+const File = field => {
+ const className = classNames([
+ "form-group",
+ { "has-error": field.meta.touched && field.meta.error }
+ ]);
+ return (
+
+
+
+ {field.meta.touched &&
+ field.meta.error && (
+ {field.meta.error}
+ )}
+ {field.description && {field.description}}
+
+ );
+};
+
+const FileWidget = props => {
+ return (
+
+ );
+};
+
+export default FileWidget;
diff --git a/lib/js/liform-react/src/themes/bootstrap3/MoneyWidget.jsx b/lib/js/liform-react/src/themes/bootstrap3/MoneyWidget.jsx
new file mode 100644
index 000000000..722c866f5
--- /dev/null
+++ b/lib/js/liform-react/src/themes/bootstrap3/MoneyWidget.jsx
@@ -0,0 +1,61 @@
+import React from "react";
+import PropTypes from "prop-types";
+import classNames from "classnames";
+import { Field } from "redux-form";
+
+const renderInput = field => {
+ const className = classNames([
+ "form-group",
+ { "has-error": field.meta.touched && field.meta.error }
+ ]);
+ return (
+
+
+
+ €
+
+
+ {field.meta.touched &&
+ field.meta.error && (
+
{field.meta.error}
+ )}
+ {field.description && (
+
{field.description}
+ )}
+
+ );
+};
+
+const MoneyWidget = props => {
+ return (
+
+ );
+};
+
+MoneyWidget.propTypes = {
+ schema: PropTypes.object.isRequired,
+ fieldName: PropTypes.string,
+ label: PropTypes.string,
+ theme: PropTypes.object,
+ multiple: PropTypes.bool,
+ required: PropTypes.bool
+};
+
+export default MoneyWidget;
diff --git a/lib/js/liform-react/src/themes/bootstrap3/NumberWidget.jsx b/lib/js/liform-react/src/themes/bootstrap3/NumberWidget.jsx
new file mode 100644
index 000000000..00e02be95
--- /dev/null
+++ b/lib/js/liform-react/src/themes/bootstrap3/NumberWidget.jsx
@@ -0,0 +1,8 @@
+import React from "react";
+import BaseInputWidget from "./BaseInputWidget";
+
+const NumberWidget = props => {
+ return ;
+};
+
+export default NumberWidget;
diff --git a/lib/js/liform-react/src/themes/bootstrap3/ObjectWidget.jsx b/lib/js/liform-react/src/themes/bootstrap3/ObjectWidget.jsx
new file mode 100644
index 000000000..deacdbac4
--- /dev/null
+++ b/lib/js/liform-react/src/themes/bootstrap3/ObjectWidget.jsx
@@ -0,0 +1,27 @@
+import React from "react";
+import PropTypes from "prop-types";
+import renderFields from "../../renderFields";
+
+const Widget = props => {
+ return (
+
+ {props.label && }
+ {renderFields(
+ props.schema,
+ props.theme,
+ props.fieldName && props.fieldName + ".",
+ props.context
+ )}
+
+ );
+};
+
+Widget.propTypes = {
+ schema: PropTypes.object.isRequired,
+ fieldName: PropTypes.string,
+ label: PropTypes.string,
+ theme: PropTypes.object,
+ context: PropTypes.object
+};
+
+export default Widget;
diff --git a/lib/js/liform-react/src/themes/bootstrap3/PasswordWidget.jsx b/lib/js/liform-react/src/themes/bootstrap3/PasswordWidget.jsx
new file mode 100644
index 000000000..456f2793e
--- /dev/null
+++ b/lib/js/liform-react/src/themes/bootstrap3/PasswordWidget.jsx
@@ -0,0 +1,8 @@
+import React from "react";
+import BaseInputWidget from "./BaseInputWidget";
+
+const PasswordWidget = props => {
+ return ;
+};
+
+export default PasswordWidget;
diff --git a/lib/js/liform-react/src/themes/bootstrap3/PercentWidget.jsx b/lib/js/liform-react/src/themes/bootstrap3/PercentWidget.jsx
new file mode 100644
index 000000000..89707dd2a
--- /dev/null
+++ b/lib/js/liform-react/src/themes/bootstrap3/PercentWidget.jsx
@@ -0,0 +1,61 @@
+import React from "react";
+import PropTypes from "prop-types";
+import classNames from "classnames";
+import { Field } from "redux-form";
+
+const renderInput = field => {
+ const className = classNames([
+ "form-group",
+ { "has-error": field.meta.touched && field.meta.error }
+ ]);
+ return (
+
+
+
+
+ %
+
+ {field.meta.touched &&
+ field.meta.error && (
+
{field.meta.error}
+ )}
+ {field.description && (
+
{field.description}
+ )}
+
+ );
+};
+
+const Widget = props => {
+ return (
+
+ );
+};
+
+Widget.propTypes = {
+ schema: PropTypes.object.isRequired,
+ fieldName: PropTypes.string,
+ label: PropTypes.string,
+ theme: PropTypes.object,
+ multiple: PropTypes.bool,
+ required: PropTypes.bool
+};
+
+export default Widget;
diff --git a/lib/js/liform-react/src/themes/bootstrap3/SearchWidget.jsx b/lib/js/liform-react/src/themes/bootstrap3/SearchWidget.jsx
new file mode 100644
index 000000000..8a93e3efa
--- /dev/null
+++ b/lib/js/liform-react/src/themes/bootstrap3/SearchWidget.jsx
@@ -0,0 +1,8 @@
+import React from "react";
+import BaseInputWidget from "./BaseInputWidget";
+
+const SearchWidget = props => {
+ return ;
+};
+
+export default SearchWidget;
diff --git a/lib/js/liform-react/src/themes/bootstrap3/StringWidget.jsx b/lib/js/liform-react/src/themes/bootstrap3/StringWidget.jsx
new file mode 100644
index 000000000..ad84d401c
--- /dev/null
+++ b/lib/js/liform-react/src/themes/bootstrap3/StringWidget.jsx
@@ -0,0 +1,8 @@
+import React from "react";
+import BaseInputWidget from "./BaseInputWidget";
+
+const StringWidget = props => {
+ return ;
+};
+
+export default StringWidget;
diff --git a/lib/js/liform-react/src/themes/bootstrap3/TextareaWidget.jsx b/lib/js/liform-react/src/themes/bootstrap3/TextareaWidget.jsx
new file mode 100644
index 000000000..765b35243
--- /dev/null
+++ b/lib/js/liform-react/src/themes/bootstrap3/TextareaWidget.jsx
@@ -0,0 +1,57 @@
+import React from "react";
+import PropTypes from "prop-types";
+import classNames from "classnames";
+import { Field } from "redux-form";
+
+const renderInput = field => {
+ const className = classNames([
+ "form-group",
+ { "has-error": field.meta.touched && field.meta.error }
+ ]);
+ return (
+
+
+
+ {field.meta.touched &&
+ field.meta.error && (
+ {field.meta.error}
+ )}
+ {field.description && (
+ {field.description}
+ )}
+
+ );
+};
+
+const TextareaWidget = props => {
+ return (
+
+ );
+};
+
+TextareaWidget.propTypes = {
+ schema: PropTypes.object.isRequired,
+ fieldName: PropTypes.string,
+ label: PropTypes.string,
+ theme: PropTypes.object,
+ multiple: PropTypes.bool,
+ required: PropTypes.bool
+};
+
+export default TextareaWidget;
diff --git a/lib/js/liform-react/src/themes/bootstrap3/TimeWidget.jsx b/lib/js/liform-react/src/themes/bootstrap3/TimeWidget.jsx
new file mode 100644
index 000000000..f7bc9a3ad
--- /dev/null
+++ b/lib/js/liform-react/src/themes/bootstrap3/TimeWidget.jsx
@@ -0,0 +1,8 @@
+import React from "react";
+import BaseInputWidget from "./BaseInputWidget";
+
+const TimeWidget = props => {
+ return ;
+};
+
+export default TimeWidget;
diff --git a/lib/js/liform-react/src/themes/bootstrap3/UrlWidget.jsx b/lib/js/liform-react/src/themes/bootstrap3/UrlWidget.jsx
new file mode 100644
index 000000000..cfaf7da6b
--- /dev/null
+++ b/lib/js/liform-react/src/themes/bootstrap3/UrlWidget.jsx
@@ -0,0 +1,8 @@
+import React from "react";
+import BaseInputWidget from "./BaseInputWidget";
+
+const UrlWidget = props => {
+ return ;
+};
+
+export default UrlWidget;
diff --git a/lib/js/liform-react/src/themes/bootstrap3/index.jsx b/lib/js/liform-react/src/themes/bootstrap3/index.jsx
new file mode 100644
index 000000000..252829216
--- /dev/null
+++ b/lib/js/liform-react/src/themes/bootstrap3/index.jsx
@@ -0,0 +1,50 @@
+import StringWidget from "./StringWidget";
+import TextareaWidget from "./TextareaWidget";
+import EmailWidget from "./EmailWidget";
+import NumberWidget from "./NumberWidget";
+import MoneyWidget from "./MoneyWidget";
+import PercentWidget from "./PercentWidget";
+import ArrayWidget from "./ArrayWidget";
+import CheckboxWidget from "./CheckboxWidget";
+import ObjectWidget from "./ObjectWidget";
+import PasswordWidget from "./PasswordWidget";
+import SearchWidget from "./SearchWidget";
+import UrlWidget from "./UrlWidget";
+import ColorWidget from "./ColorWidget";
+import ChoiceWidget from "./ChoiceWidget";
+import ChoiceExpandedWidget from "./ChoiceExpandedWidget";
+import ChoiceMultipleExpandedWidget from "./ChoiceMultipleExpandedWidget";
+import DateWidget from "./DateWidget";
+import TimeWidget from "./TimeWidget";
+import DateTimeWidget from "./DateTimeWidget";
+import CompatibleDateWidget from "./CompatibleDateWidget";
+import CompatibleDateTimeWidget from "./CompatibleDateTimeWidget";
+import FileWidget from "./FileWidget";
+import OneOfChoiceWidget from "./oneOfChoiceWidget";
+
+export default {
+ object: ObjectWidget,
+ string: StringWidget,
+ textarea: TextareaWidget,
+ email: EmailWidget,
+ integer: NumberWidget,
+ number: NumberWidget,
+ money: MoneyWidget,
+ percent: PercentWidget,
+ array: ArrayWidget,
+ boolean: CheckboxWidget,
+ password: PasswordWidget,
+ search: SearchWidget,
+ url: UrlWidget,
+ color: ColorWidget,
+ choice: ChoiceWidget,
+ "choice-expanded": ChoiceExpandedWidget,
+ "choice-multiple-expanded": ChoiceMultipleExpandedWidget,
+ date: DateWidget,
+ datetime: DateTimeWidget,
+ time: TimeWidget,
+ "compatible-date": CompatibleDateWidget,
+ "compatible-datetime": CompatibleDateTimeWidget,
+ file: FileWidget,
+ oneOf: OneOfChoiceWidget
+};
diff --git a/lib/js/liform-react/src/themes/bootstrap3/oneOfChoiceWidget.jsx b/lib/js/liform-react/src/themes/bootstrap3/oneOfChoiceWidget.jsx
new file mode 100644
index 000000000..5d435d9b7
--- /dev/null
+++ b/lib/js/liform-react/src/themes/bootstrap3/oneOfChoiceWidget.jsx
@@ -0,0 +1,83 @@
+import React, { Component } from "react";
+import PropTypes from "prop-types";
+import classNames from "classnames";
+import { change } from "redux-form";
+import { connect } from "react-redux";
+import renderField from "../../renderField";
+import { map as _map} from "lodash";
+
+class OneOfChoiceWidget extends Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ choice: 0
+ };
+ this.renderOption = this.renderOption.bind(this);
+ this.selectItem = this.selectItem.bind(this);
+ }
+
+ render() {
+ const field = this.props;
+ const className = classNames(["form-group"]);
+ const schema = field.schema;
+ const options = schema.oneOf;
+
+ return (
+
+
+
+
{this.renderOption()}
+ {field.description && (
+
{field.description}
+ )}
+
+ );
+ }
+
+ renderOption() {
+ const field = this.props;
+ const schema = field.schema.oneOf[this.state.choice];
+ return renderField(
+ schema,
+ field.fieldName,
+ field.theme,
+ field.prefix,
+ field.context
+ );
+ }
+
+ selectItem(e) {
+ const { schema, context, dispatch } = this.props;
+ for (let property in schema.oneOf[this.state.choice].properties) {
+ dispatch(change(context.formName, property, null));
+ }
+ this.setState({ choice: e.target.value });
+ }
+}
+
+OneOfChoiceWidget.propTypes = {
+ schema: PropTypes.object.isRequired,
+ fieldName: PropTypes.string,
+ label: PropTypes.string,
+ theme: PropTypes.object,
+ multiple: PropTypes.bool,
+ required: PropTypes.bool
+};
+
+export default connect()(OneOfChoiceWidget);
diff --git a/lib/js/liform-react/src/themes/index.jsx b/lib/js/liform-react/src/themes/index.jsx
new file mode 100644
index 000000000..4ec0b6421
--- /dev/null
+++ b/lib/js/liform-react/src/themes/index.jsx
@@ -0,0 +1,42 @@
+import StringWidget from "./StringWidget";
+import TextareaWidget from "./TextareaWidget";
+import EmailWidget from "./EmailWidget";
+import NumberWidget from "./NumberWidget";
+import MoneyWidget from "./MoneyWidget";
+import PercentWidget from "./PercentWidget";
+import ArrayWidget from "./ArrayWidget";
+import CheckboxWidget from "./CheckboxWidget";
+import ObjectWidget from "./ObjectWidget";
+import PasswordWidget from "./PasswordWidget";
+import SearchWidget from "./SearchWidget";
+import UrlWidget from "./UrlWidget";
+import ColorWidget from "./ColorWidget";
+import ChoiceWidget from "./ChoiceWidget";
+import OneOfChoiceWidget from "./oneOfChoiceWidget";
+import DateWidget from "./DateWidget";
+import TimeWidget from "./TimeWidget";
+import DateTimeWidget from "./DateTimeWidget";
+import CompatibleDateWidget from "./CompatibleDateWidget";
+
+export default {
+ object: ObjectWidget,
+ string: StringWidget,
+ textarea: TextareaWidget,
+ email: EmailWidget,
+ integer: NumberWidget,
+ number: NumberWidget,
+ money: MoneyWidget,
+ percent: PercentWidget,
+ array: ArrayWidget,
+ boolean: CheckboxWidget,
+ password: PasswordWidget,
+ search: SearchWidget,
+ url: UrlWidget,
+ color: ColorWidget,
+ choice: ChoiceWidget,
+ date: DateWidget,
+ datetime: DateTimeWidget,
+ time: TimeWidget,
+ OneOfChoiceWidget: OneOfChoiceWidget,
+ "compatible-date": CompatibleDateWidget
+};
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 8e9b651a9..5372c3b01 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -621,6 +621,40 @@ importers:
specifier: ^18.2.37
version: 18.2.39
+ lib/js/liform-react:
+ dependencies:
+ ajv:
+ specifier: ^8.12.0
+ version: 8.12.0
+ classnames:
+ specifier: ^2.2.5
+ version: 2.3.2
+ deepmerge:
+ specifier: ^2.0.1
+ version: 2.2.1
+ lodash:
+ specifier: ^4.17.21
+ version: 4.17.21
+ prop-types:
+ specifier: ^15.5.10
+ version: 15.8.1
+ react-redux:
+ specifier: ^9.0.4
+ version: 9.0.4(@types/react@18.2.38)(react@18.2.0)(redux@4.2.1)
+ redux:
+ specifier: ^4.2.1
+ version: 4.2.1
+ redux-form:
+ specifier: ^8.3.10
+ version: 8.3.10(react-redux@9.0.4)(react@18.2.0)(redux@4.2.1)
+ devDependencies:
+ react:
+ specifier: ^18.2.0
+ version: 18.2.0
+ react-dom:
+ specifier: ^18.2.0
+ version: 18.2.0(react@18.2.0)
+
lib/js/navigation:
dependencies:
'@alchemy/core':
@@ -1004,9 +1038,6 @@ importers:
uploader/client:
dependencies:
- '@alchemy-fr/liform-react':
- specifier: ^0.9.3
- version: 0.9.3(react-redux@5.1.2)(redux-form@7.4.3)(redux@4.2.1)
'@alchemy/api':
specifier: workspace:*
version: link:../../lib/js/api
@@ -1016,6 +1047,9 @@ importers:
'@alchemy/core':
specifier: workspace:*
version: link:../../lib/js/core
+ '@alchemy/liform-react':
+ specifier: workspace:*
+ version: link:../../lib/js/liform-react
'@alchemy/navigation':
specifier: workspace:*
version: link:../../lib/js/navigation
@@ -1028,6 +1062,9 @@ importers:
'@alchemy/react-ps':
specifier: workspace:*
version: link:../../lib/js/react-ps
+ '@reduxjs/toolkit':
+ specifier: ^2.0.1
+ version: 2.0.1(react-redux@9.0.4)(react@18.2.0)
axios:
specifier: ^1.6.2
version: 1.6.2
@@ -1068,17 +1105,20 @@ importers:
specifier: ^3.1.14
version: 3.1.14(prop-types@15.8.1)(react-dom@18.2.0)(react@18.2.0)
react-redux:
- specifier: ^5.1
- version: 5.1.2(react@18.2.0)(redux@4.2.1)
+ specifier: ^9.0.4
+ version: 9.0.4(@types/react@18.2.38)(react@18.2.0)(redux@4.2.1)
react-toastify:
specifier: ^9.1.3
version: 9.1.3(react-dom@18.2.0)(react@18.2.0)
redux:
- specifier: ^4.0.1
+ specifier: ^4.2.1
version: 4.2.1
redux-form:
- specifier: ^7.4.2
- version: 7.4.3(react-redux@5.1.2)(react@18.2.0)(redux@4.2.1)
+ specifier: ^8.3.10
+ version: 8.3.10(react-redux@9.0.4)(react@18.2.0)(redux@4.2.1)
+ url:
+ specifier: ^0.11.3
+ version: 0.11.3
vite-plugin-svgr:
specifier: ^4.2.0
version: 4.2.0(typescript@5.3.2)(vite@5.0.2)
@@ -1148,22 +1188,6 @@ packages:
resolution: {integrity: sha512-/62yikz7NLScCGAAST5SHdnjaDJQBDq0M2muyRTpf2VQhw6StBg2ALiu73zSJQ4fMVLA+0uBhBHAle7Wg+2kSg==}
dev: true
- /@alchemy-fr/liform-react@0.9.3(react-redux@5.1.2)(redux-form@7.4.3)(redux@4.2.1):
- resolution: {integrity: sha512-WBlt8F3+CYnGKBdTPVk+MR0/R3OltVtcL4AGTJsixbAxmjGIA4zwQkeZr5FMc9HZ0nimIlNPdOhsBhTdSDaibQ==}
- peerDependencies:
- react-redux: ^5.0.6
- redux: ^3.5.2
- redux-form: ^7.2.0
- dependencies:
- ajv: 5.5.2
- classnames: 2.3.2
- deepmerge: 2.2.1
- prop-types: 15.8.1
- react-redux: 5.1.2(react@18.2.0)(redux@4.2.1)
- redux: 4.2.1
- redux-form: 7.4.3(react-redux@5.1.2)(react@18.2.0)(redux@4.2.1)
- dev: false
-
/@ampproject/remapping@2.2.1:
resolution: {integrity: sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==}
engines: {node: '>=6.0.0'}
@@ -4750,6 +4774,25 @@ packages:
- immer
dev: false
+ /@reduxjs/toolkit@2.0.1(react-redux@9.0.4)(react@18.2.0):
+ resolution: {integrity: sha512-fxIjrR9934cmS8YXIGd9e7s1XRsEU++aFc9DVNMFMRTM5Vtsg2DCRMj21eslGtDt43IUf9bJL3h5bwUlZleibA==}
+ peerDependencies:
+ react: ^16.9.0 || ^17.0.0 || ^18
+ react-redux: ^7.2.1 || ^8.1.3 || ^9.0.0
+ peerDependenciesMeta:
+ react:
+ optional: true
+ react-redux:
+ optional: true
+ dependencies:
+ immer: 10.0.3
+ react: 18.2.0
+ react-redux: 9.0.4(@types/react@18.2.38)(react@18.2.0)(redux@4.2.1)
+ redux: 5.0.0
+ redux-thunk: 3.1.0(redux@5.0.0)
+ reselect: 5.0.1
+ dev: false
+
/@remix-run/router@1.11.0:
resolution: {integrity: sha512-BHdhcWgeiudl91HvVa2wxqZjSHbheSgIiDvxrF1VjFzBzpTtuDPkOdOi3Iqvc08kXtFkLjhbS+ML9aM8mJS+wQ==}
engines: {node: '>=14.0.0'}
@@ -7164,6 +7207,10 @@ packages:
resolution: {integrity: sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA==}
dev: true
+ /@types/use-sync-external-store@0.0.3:
+ resolution: {integrity: sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==}
+ dev: false
+
/@types/uuid@9.0.7:
resolution: {integrity: sha512-WUtIVRUZ9i5dYXefDEAI7sh9/O7jGvHg7Df/5O/gtH3Yabe5odI3UWopVR1qbPXQtvOxWu3mM4XxlYeZtMWF4g==}
dev: true
@@ -7937,15 +7984,6 @@ packages:
dependencies:
ajv: 6.12.6
- /ajv@5.5.2:
- resolution: {integrity: sha512-Ajr4IcMXq/2QmMkEmSvxqfLN5zGmJ92gHXAeOXq1OekoH2rfDNsgdDoL2f7QaRCy7G/E6TpxBVdRuNraMztGHw==}
- dependencies:
- co: 4.6.0
- fast-deep-equal: 1.1.0
- fast-json-stable-stringify: 2.1.0
- json-schema-traverse: 0.3.1
- dev: false
-
/ajv@6.12.6:
resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==}
dependencies:
@@ -7954,6 +7992,15 @@ packages:
json-schema-traverse: 0.4.1
uri-js: 4.4.1
+ /ajv@8.12.0:
+ resolution: {integrity: sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==}
+ dependencies:
+ fast-deep-equal: 3.1.3
+ json-schema-traverse: 1.0.0
+ require-from-string: 2.0.2
+ uri-js: 4.4.1
+ dev: false
+
/amdefine@1.0.1:
resolution: {integrity: sha512-S2Hw0TtNkMJhIabBwIojKL9YHO5T0n5eNqWJ7Lrlel/zDbftQpxpapi8tZs3X1HWa+u+QeydGmzzNU0m09+Rcg==}
engines: {node: '>=0.4.2'}
@@ -9514,6 +9561,7 @@ packages:
/co@4.6.0:
resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==}
engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'}
+ dev: true
/code-point-at@1.1.0:
resolution: {integrity: sha512-RpAVKQA5T63xEj6/giIbUEtZwJ4UFIc3ZtvEkiaUERylqe8xb5IvqcgOurZLahv93CLKfxcw5YI+DZcUBRyLXA==}
@@ -10619,7 +10667,7 @@ packages:
/dom-helpers@5.2.1:
resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==}
dependencies:
- '@babel/runtime': 7.23.4
+ '@babel/runtime': 7.23.6
csstype: 3.1.2
/dom-serializer@0.2.2:
@@ -11548,10 +11596,6 @@ packages:
kind-of: 5.1.0
dev: true
- /fast-deep-equal@1.1.0:
- resolution: {integrity: sha512-fueX787WZKCV0Is4/T2cyAdM4+x1S3MXXOAhavE1ys/W42SHAPacLTQhucja22QBYrfGw50M2sRiXPtTGv9Ymw==}
- dev: false
-
/fast-deep-equal@3.1.3:
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
@@ -12814,10 +12858,6 @@ packages:
requiresBuild: true
dev: true
- /hoist-non-react-statics@2.5.5:
- resolution: {integrity: sha512-rqcy4pJo55FTTLWt+bU8ukscqHeE/e9KWvsOW2b/a3afxQZhwkQdT1rPPCJ0rYXdj4vNcasY8zHTH+jF/qStxw==}
- dev: false
-
/hoist-non-react-statics@3.3.2:
resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==}
dependencies:
@@ -13135,6 +13175,10 @@ packages:
engines: {node: '>= 4'}
dev: true
+ /immer@10.0.3:
+ resolution: {integrity: sha512-pwupu3eWfouuaowscykeckFmVTpqbzW+rXFCX8rQLkZzM9ftBmU/++Ra+o+L27mz03zJTlyV4UUr+fdKNffo4A==}
+ dev: false
+
/immutable@4.3.4:
resolution: {integrity: sha512-fsXeu4J4i6WNWSikpI88v/PcVflZz+6kMhUfIwc5SY+poQRPnaf5V7qds6SUyUN3cVxEzuCab7QIoLOQ+DQ1wA==}
@@ -14715,13 +14759,13 @@ packages:
/json-parse-even-better-errors@2.3.1:
resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==}
- /json-schema-traverse@0.3.1:
- resolution: {integrity: sha512-4JD/Ivzg7PoW8NzdrBSr3UFwC9mHgvI7Z6z3QGBsSHgKaRTUDmyZAAKJo2UbG1kUVfS9WS8bi36N49U1xw43DA==}
- dev: false
-
/json-schema-traverse@0.4.1:
resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
+ /json-schema-traverse@1.0.0:
+ resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==}
+ dev: false
+
/json-schema@0.4.0:
resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==}
@@ -17192,6 +17236,10 @@ packages:
pump: 2.0.1
dev: true
+ /punycode@1.4.1:
+ resolution: {integrity: sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==}
+ dev: false
+
/punycode@2.3.1:
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
engines: {node: '>=6'}
@@ -17232,7 +17280,6 @@ packages:
engines: {node: '>=0.6'}
dependencies:
side-channel: 1.0.4
- dev: true
/qs@6.5.3:
resolution: {integrity: sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==}
@@ -17839,7 +17886,7 @@ packages:
react: '>=16.3.0'
react-dom: '>=16.3.0'
dependencies:
- '@babel/runtime': 7.23.4
+ '@babel/runtime': 7.23.6
'@popperjs/core': 2.11.8
'@restart/hooks': 0.4.11(react@18.2.0)
'@types/warning': 3.0.3
@@ -17904,21 +17951,26 @@ packages:
resolution: {integrity: sha512-1tKOwxFn3dXVomH6pM9IkLkq2Y8oh+fh/lYW3MJ/B03URswUTqttgckOlbxY2XHF3vPG6uanSc4dVsLW/wk3wQ==}
dev: false
- /react-redux@5.1.2(react@18.2.0)(redux@4.2.1):
- resolution: {integrity: sha512-Ns1G0XXc8hDyH/OcBHOxNgQx9ayH3SPxBnFCOidGKSle8pKihysQw2rG/PmciUQRoclhVBO8HMhiRmGXnDja9Q==}
+ /react-redux@9.0.4(@types/react@18.2.38)(react@18.2.0)(redux@4.2.1):
+ resolution: {integrity: sha512-9J1xh8sWO0vYq2sCxK2My/QO7MzUMRi3rpiILP/+tDr8krBHixC6JMM17fMK88+Oh3e4Ae6/sHIhNBgkUivwFA==}
peerDependencies:
- react: ^0.14.0 || ^15.0.0-0 || ^16.0.0-0
- redux: ^2.0.0 || ^3.0.0 || ^4.0.0-0
+ '@types/react': ^18.2.25
+ react: ^18.0
+ react-native: '>=0.69'
+ redux: ^5.0.0
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ react-native:
+ optional: true
+ redux:
+ optional: true
dependencies:
- '@babel/runtime': 7.23.4
- hoist-non-react-statics: 3.3.2
- invariant: 2.2.4
- loose-envify: 1.4.0
- prop-types: 15.8.1
+ '@types/react': 18.2.38
+ '@types/use-sync-external-store': 0.0.3
react: 18.2.0
- react-is: 16.13.1
- react-lifecycles-compat: 3.0.4
redux: 4.2.1
+ use-sync-external-store: 1.2.0(react@18.2.0)
dev: false
/react-refresh@0.14.0:
@@ -18301,30 +18353,47 @@ packages:
esprima: 4.0.1
dev: true
- /redux-form@7.4.3(react-redux@5.1.2)(react@18.2.0)(redux@4.2.1):
- resolution: {integrity: sha512-h2LEGdEQ9XaX2wXZnf6zIUFSETk4jDqG4NUKwqkSfOJZDxT3H2hJmKVZI76j/YsE/8E3eY6yPoAENCxjLi1p+Q==}
+ /redux-form@8.3.10(react-redux@9.0.4)(react@18.2.0)(redux@4.2.1):
+ resolution: {integrity: sha512-Eeog8dJYUxCSZI/oBoy7VkprvMjj1lpUnHa3LwjVNZvYDNeiRZMoZoaAT+6nlK2YQ4aiBopKUMiLe4ihUOHCGg==}
+ engines: {node: '>=8.10'}
peerDependencies:
- react: ^15.0.0-0 || ^16.0.0-0
- react-redux: ^4.3.0 || ^5.0.0
- redux: ^3.0.0 || ^4.0.0
+ immutable: ^3.8.2 || ^4.0.0
+ react: ^16.4.2 || ^17.0.0 || ^18.0.0
+ react-redux: ^6.0.1 || ^7.0.0 || ^8.0.0
+ redux: ^3.7.2 || ^4.0.0
+ peerDependenciesMeta:
+ immutable:
+ optional: true
dependencies:
+ '@babel/runtime': 7.23.6
es6-error: 4.1.1
- hoist-non-react-statics: 2.5.5
+ hoist-non-react-statics: 3.3.2
invariant: 2.2.4
is-promise: 2.2.2
lodash: 4.17.21
- lodash-es: 4.17.21
prop-types: 15.8.1
react: 18.2.0
- react-lifecycles-compat: 3.0.4
- react-redux: 5.1.2(react@18.2.0)(redux@4.2.1)
+ react-is: 16.13.1
+ react-redux: 9.0.4(@types/react@18.2.38)(react@18.2.0)(redux@4.2.1)
redux: 4.2.1
dev: false
+ /redux-thunk@3.1.0(redux@5.0.0):
+ resolution: {integrity: sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==}
+ peerDependencies:
+ redux: ^5.0.0
+ dependencies:
+ redux: 5.0.0
+ dev: false
+
/redux@4.2.1:
resolution: {integrity: sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==}
dependencies:
- '@babel/runtime': 7.23.4
+ '@babel/runtime': 7.23.6
+ dev: false
+
+ /redux@5.0.0:
+ resolution: {integrity: sha512-blLIYmYetpZMET6Q6uCY7Jtl/Im5OBldy+vNPauA8vvsdqyt66oep4EUpAMWNHauTC6xa9JuRPhRB72rY82QGA==}
dev: false
/regenerate-unicode-properties@10.1.1:
@@ -18560,6 +18629,11 @@ packages:
engines: {node: '>=0.10.0'}
dev: true
+ /require-from-string@2.0.2:
+ resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
+ engines: {node: '>=0.10.0'}
+ dev: false
+
/require-main-filename@1.0.1:
resolution: {integrity: sha512-IqSUtOVP4ksd1C/ej5zeEh/BIP2ajqpn8c5x+q99gvcIG/Qf0cud5raVnE/Dwd0ua9TXYDoDc0RE5hBSdz22Ug==}
dev: true
@@ -18572,6 +18646,10 @@ packages:
resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==}
dev: false
+ /reselect@5.0.1:
+ resolution: {integrity: sha512-D72j2ubjgHpvuCiORWkOUxndHJrxDaSolheiz5CO+roz8ka97/4msh2E8F5qay4GawR5vzBt5MkbDHT+Rdy/Wg==}
+ dev: false
+
/resize-observer-polyfill@1.5.0:
resolution: {integrity: sha512-M2AelyJDVR/oLnToJLtuDJRBBWUGUvvGigj1411hXhAdyFWqMaqHp7TixW3FpiLuVaikIcR1QL+zqoJoZlOgpg==}
dev: false
@@ -20831,7 +20909,7 @@ packages:
peerDependencies:
react: '>=15.0.0'
dependencies:
- '@babel/runtime': 7.23.4
+ '@babel/runtime': 7.23.6
'@types/react': 18.2.39
invariant: 2.2.4
react: 18.2.0
@@ -21053,6 +21131,13 @@ packages:
resolution: {integrity: sha512-mtN6xk+Nac+oyJ/PrI7tzfmomRVNFIWKUbG8jdYFt52hxbiReFAXIjYskvu64/dvuW71IcB7lV8l0HvZMac6Jg==}
dev: false
+ /url@0.11.3:
+ resolution: {integrity: sha512-6hxOLGfZASQK/cijlZnZJTq8OXAkt/3YGfQX45vvMYXpZoo8NdWZcY73K108Jf759lS1Bv/8wXnHDTSz17dSRw==}
+ dependencies:
+ punycode: 1.4.1
+ qs: 6.11.2
+ dev: false
+
/use-callback-ref@1.3.0(@types/react@18.2.38)(react@18.2.0):
resolution: {integrity: sha512-3FT9PRuRdbB9HfXhEq35u4oZkvpJ5kuYbpqhCfmiZyReuRgpnhDlbr2ZEnnuS0RrJAPn6l23xjFg9kpDM+Ms7w==}
engines: {node: '>=10'}
diff --git a/uploader/client/package.json b/uploader/client/package.json
index d686ed5a3..41082af41 100644
--- a/uploader/client/package.json
+++ b/uploader/client/package.json
@@ -12,13 +12,14 @@
"translations-scan": "i18next-scanner --config i18next-scanner.config.js src/**/*.{js,jsx}"
},
"dependencies": {
- "@alchemy-fr/liform-react": "^0.9.3",
+ "@reduxjs/toolkit": "^2.0.1",
+ "@alchemy/liform-react": "workspace:*",
"@alchemy/api": "workspace:*",
- "@alchemy/core": "workspace:*",
"@alchemy/auth": "workspace:*",
- "@alchemy/react-auth": "workspace:*",
+ "@alchemy/core": "workspace:*",
"@alchemy/navigation": "workspace:*",
"@alchemy/phrasea-ui": "workspace:*",
+ "@alchemy/react-auth": "workspace:*",
"@alchemy/react-ps": "workspace:*",
"axios": "^1.6.2",
"bootstrap": "^4.3.1",
@@ -26,7 +27,6 @@
"i18next": "^21.8.0",
"i18next-browser-languagedetector": "^6.1.2",
"i18next-xhr-backend": "^3.1.2",
- "react-toastify": "^9.1.3",
"react": "^18.2.0",
"react-bootstrap": "^1.0.1",
"react-burger-menu": "^2.6.8",
@@ -34,9 +34,11 @@
"react-dropzone": "^14.2.1",
"react-i18next": "^11.15.1",
"react-loader-spinner": "^3.1.14",
- "react-redux": "^5.1",
- "redux": "^4.0.1",
- "redux-form": "^7.4.2",
+ "react-redux": "^9.0.4",
+ "react-toastify": "^9.1.3",
+ "redux": "^4.2.1",
+ "redux-form": "^8.3.10",
+ "url": "^0.11.3",
"vite-plugin-svgr": "^4.2.0"
},
"devDependencies": {
diff --git a/uploader/client/src/components/AssetForm.jsx b/uploader/client/src/components/AssetForm.jsx
index b3dbc5757..1fe5c40f6 100644
--- a/uploader/client/src/components/AssetForm.jsx
+++ b/uploader/client/src/components/AssetForm.jsx
@@ -86,7 +86,7 @@ export default class AssetForm extends Component {
}
if (r.errors && Object.keys(r.errors).length > 0) {
- const {errors} = r.body;
+ const {errors} = r;
const errs = {};
Object.keys(errors).forEach(i => {
diff --git a/uploader/client/src/components/AssetLiForm.jsx b/uploader/client/src/components/AssetLiForm.jsx
index f0311f396..9cdf1ace3 100644
--- a/uploader/client/src/components/AssetLiForm.jsx
+++ b/uploader/client/src/components/AssetLiForm.jsx
@@ -1,8 +1,9 @@
import React from 'react';
-import {createStore, combineReducers} from 'redux';
+import {combineReducers} from 'redux';
import {reducer as formReducer} from 'redux-form';
import {Provider} from 'react-redux';
-import Liform, {renderField, DefaultTheme} from '@alchemy-fr/liform-react';
+import Liform, {renderField, DefaultTheme} from '@alchemy/liform-react';
+import {configureStore} from '@reduxjs/toolkit';
const BaseForm = props => {
const {schema, handleSubmit, theme, error, submitting, onCancel} = props;
@@ -36,7 +37,7 @@ const BaseForm = props => {
const AssetLiForm = props => {
const reducer = combineReducers({form: formReducer});
- const store = createStore(reducer);
+ const store = configureStore({reducer});
const initialValues = {};
diff --git a/uploader/client/src/index.tsx b/uploader/client/src/index.tsx
index f29a85d64..7cb4a4907 100644
--- a/uploader/client/src/index.tsx
+++ b/uploader/client/src/index.tsx
@@ -2,7 +2,6 @@ import ReactDOM from 'react-dom/client';
import './scss/index.scss';
import './locales/i18n';
import Root from './Root.tsx';
-import React from 'react';
import {DashboardMenu} from '@alchemy/react-ps';
import config from './config';
import {initSentry} from '@alchemy/core';
@@ -10,10 +9,10 @@ import {initSentry} from '@alchemy/core';
initSentry(config);
ReactDOM.createRoot(document.getElementById('root')!).render(
-
+ <>
{config.displayServicesMenu && (
)}
-
+ >
);
diff --git a/uploader/client/vite.config.ts b/uploader/client/vite.config.ts
index e6dfbf1de..0ad0e531e 100644
--- a/uploader/client/vite.config.ts
+++ b/uploader/client/vite.config.ts
@@ -21,4 +21,9 @@ export default defineConfig({
port: 3000,
host: '0.0.0.0',
},
+ resolve: {
+ alias: {
+ path: "url",
+ },
+ },
});