@firecms/collection_editor 3.0.0-beta.2-pre.2 → 3.0.0-beta.2-pre.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/form/Field.d.ts +53 -0
- package/dist/form/Formex.d.ts +4 -0
- package/dist/form/index.d.ts +5 -0
- package/dist/form/types.d.ts +25 -0
- package/dist/form/useCreateFormex.d.ts +9 -0
- package/dist/form/utils.d.ts +44 -0
- package/dist/index.es.js +2612 -2328
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +2 -2
- package/dist/index.umd.js.map +1 -1
- package/dist/types/collection_editor_controller.d.ts +3 -2
- package/dist/types/config_controller.d.ts +3 -3
- package/dist/ui/collection_editor/CollectionEditorDialog.d.ts +3 -5
- package/dist/ui/collection_editor/CollectionEditorWelcomeView.d.ts +2 -2
- package/dist/ui/collection_editor/CollectionPropertiesEditorForm.d.ts +1 -2
- package/dist/ui/collection_editor/EnumForm.d.ts +1 -2
- package/dist/ui/collection_editor/PropertyEditView.d.ts +5 -5
- package/dist/ui/collection_editor/PropertyTree.d.ts +14 -13
- package/dist/ui/collection_editor/SwitchControl.d.ts +8 -0
- package/dist/ui/collection_editor/properties/CommonPropertyFields.d.ts +0 -1
- package/dist/ui/collection_editor/util.d.ts +1 -0
- package/package.json +5 -5
- package/src/ConfigControllerProvider.tsx +23 -21
- package/src/form/Field.tsx +162 -0
- package/src/form/Formex.tsx +8 -0
- package/src/form/README.md +165 -0
- package/src/form/index.ts +5 -0
- package/src/form/types.ts +27 -0
- package/src/form/useCreateFormex.tsx +137 -0
- package/src/form/utils.ts +169 -0
- package/src/types/collection_editor_controller.tsx +4 -3
- package/src/types/config_controller.tsx +3 -3
- package/src/ui/CollectionViewHeaderAction.tsx +1 -1
- package/src/ui/EditorCollectionAction.tsx +3 -3
- package/src/ui/HomePageEditorCollectionAction.tsx +2 -2
- package/src/ui/MissingReferenceWidget.tsx +2 -1
- package/src/ui/NewCollectionButton.tsx +3 -3
- package/src/ui/NewCollectionCard.tsx +2 -1
- package/src/ui/PropertyAddColumnComponent.tsx +1 -1
- package/src/ui/RootCollectionSuggestions.tsx +2 -1
- package/src/ui/collection_editor/CollectionDetailsForm.tsx +2 -2
- package/src/ui/collection_editor/CollectionEditorDialog.tsx +422 -374
- package/src/ui/collection_editor/CollectionEditorWelcomeView.tsx +19 -12
- package/src/ui/collection_editor/CollectionPropertiesEditorForm.tsx +26 -18
- package/src/ui/collection_editor/EnumForm.tsx +118 -114
- package/src/ui/collection_editor/GetCodeDialog.tsx +1 -1
- package/src/ui/collection_editor/PropertyEditView.tsx +198 -142
- package/src/ui/collection_editor/PropertyFieldPreview.tsx +5 -1
- package/src/ui/collection_editor/PropertyTree.tsx +132 -113
- package/src/ui/collection_editor/SubcollectionsEditTab.tsx +18 -11
- package/src/ui/collection_editor/SwitchControl.tsx +39 -0
- package/src/ui/collection_editor/import/CollectionEditorImportMapping.tsx +10 -2
- package/src/ui/collection_editor/properties/BlockPropertyField.tsx +2 -2
- package/src/ui/collection_editor/properties/BooleanPropertyField.tsx +13 -9
- package/src/ui/collection_editor/properties/CommonPropertyFields.tsx +11 -37
- package/src/ui/collection_editor/properties/DateTimePropertyField.tsx +2 -2
- package/src/ui/collection_editor/properties/EnumPropertyField.tsx +3 -6
- package/src/ui/collection_editor/properties/MapPropertyField.tsx +2 -2
- package/src/ui/collection_editor/properties/NumberPropertyField.tsx +2 -2
- package/src/ui/collection_editor/properties/ReferencePropertyField.tsx +11 -14
- package/src/ui/collection_editor/properties/RepeatPropertyField.tsx +10 -9
- package/src/ui/collection_editor/properties/StoragePropertyField.tsx +15 -9
- package/src/ui/collection_editor/properties/StringPropertyField.tsx +2 -2
- package/src/ui/collection_editor/properties/UrlPropertyField.tsx +2 -2
- package/src/ui/collection_editor/properties/advanced/AdvancedPropertyValidation.tsx +27 -18
- package/src/ui/collection_editor/properties/validation/ArrayPropertyValidation.tsx +2 -2
- package/src/ui/collection_editor/properties/validation/GeneralPropertyValidation.tsx +27 -16
- package/src/ui/collection_editor/properties/validation/NumberPropertyValidation.tsx +33 -18
- package/src/ui/collection_editor/properties/validation/StringPropertyValidation.tsx +99 -80
- package/src/ui/collection_editor/util.ts +7 -0
- package/src/ui/collection_editor/utils/strings.ts +2 -1
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# Formex - React Form Library
|
|
2
|
+
|
|
3
|
+
Formex is a lightweight, flexible library designed to simplify form handling within React applications. By leveraging React's powerful context and hooks features, Formex allows for efficient form state management with minimal boilerplate code.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- Lightweight and easy to integrate
|
|
8
|
+
- Supports custom field components
|
|
9
|
+
- Built-in validation handling
|
|
10
|
+
- Provides both field-level and form-level state management
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
To install Formex, you can use either npm or yarn:
|
|
15
|
+
|
|
16
|
+
```sh
|
|
17
|
+
npm install your-formex-package-name
|
|
18
|
+
|
|
19
|
+
# or if you're using yarn
|
|
20
|
+
|
|
21
|
+
yarn add your-formex-package-name
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Quick Start
|
|
25
|
+
|
|
26
|
+
To get started with Formex, you first need to create your form context and form controller using the `useCreateFormex` hook. Then, you can structure your form using the `<Field />` components provided by Formex.
|
|
27
|
+
|
|
28
|
+
### Step 1: Create your form controller
|
|
29
|
+
|
|
30
|
+
```jsx
|
|
31
|
+
import React from 'react';
|
|
32
|
+
import { useCreateFormex } from 'formex-library';
|
|
33
|
+
|
|
34
|
+
const MyForm = () => {
|
|
35
|
+
const formController = useCreateFormex({
|
|
36
|
+
initialValues: {
|
|
37
|
+
name: '',
|
|
38
|
+
email: '',
|
|
39
|
+
},
|
|
40
|
+
// Optionally add a validation function
|
|
41
|
+
// validation: values => {
|
|
42
|
+
// const errors = {};
|
|
43
|
+
// if (!values.name) errors.name = 'Name is required';
|
|
44
|
+
// return errors;
|
|
45
|
+
// },
|
|
46
|
+
onSubmit: (values) => {
|
|
47
|
+
console.log('Form Submitted:', values);
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<form onSubmit={formController.submitForm}>
|
|
53
|
+
{/* Field components go here */}
|
|
54
|
+
</form>
|
|
55
|
+
);
|
|
56
|
+
};
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Step 2: Use the `<Field />` component
|
|
60
|
+
|
|
61
|
+
```jsx
|
|
62
|
+
import { Field } from 'formex-library';
|
|
63
|
+
|
|
64
|
+
// Inside your form component
|
|
65
|
+
<Field name="name">
|
|
66
|
+
{({ field }) => (
|
|
67
|
+
<input
|
|
68
|
+
{...field}
|
|
69
|
+
placeholder="Your name"
|
|
70
|
+
/>
|
|
71
|
+
)}
|
|
72
|
+
</Field>
|
|
73
|
+
|
|
74
|
+
<Field name="email">
|
|
75
|
+
{({ field }) => (
|
|
76
|
+
<input
|
|
77
|
+
{...field}
|
|
78
|
+
type="email"
|
|
79
|
+
placeholder="Your email"
|
|
80
|
+
/>
|
|
81
|
+
)}
|
|
82
|
+
</Field>
|
|
83
|
+
|
|
84
|
+
<button type="submit">Submit</button>
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Handling Submissions
|
|
88
|
+
|
|
89
|
+
Wrap your form inputs and submit button within a form element and pass the `submitForm` method from your form controller to the form's `onSubmit` event:
|
|
90
|
+
|
|
91
|
+
```jsx
|
|
92
|
+
<form onSubmit={formController.submitForm}>
|
|
93
|
+
{/* Fields and submit button */}
|
|
94
|
+
</form>
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## API Reference
|
|
98
|
+
|
|
99
|
+
### `useCreateFormex`
|
|
100
|
+
|
|
101
|
+
Hook to create a form controller.
|
|
102
|
+
|
|
103
|
+
**Parameters**
|
|
104
|
+
|
|
105
|
+
- `initialValues`: An object with your form's initial values.
|
|
106
|
+
- `initialErrors` (optional): An object for any initial validation errors.
|
|
107
|
+
- `validation` (optional): A function for validating form data.
|
|
108
|
+
- `validateOnChange` (optional): If `true`, validates fields whenever they change.
|
|
109
|
+
- `onSubmit`: A function that fires when the form is submitted.
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
### `<Field />`
|
|
113
|
+
|
|
114
|
+
A component used to render individual form fields.
|
|
115
|
+
|
|
116
|
+
**Props**
|
|
117
|
+
|
|
118
|
+
- `name`: The name of the form field.
|
|
119
|
+
- `as` (optional): The component or HTML tag that should be rendered. Defaults to `"input"`.
|
|
120
|
+
- `children`: A function that returns the field input component. Receives field props as its parameter.
|
|
121
|
+
|
|
122
|
+
**Example**
|
|
123
|
+
|
|
124
|
+
```jsx
|
|
125
|
+
<Field name="username">
|
|
126
|
+
{({ field }) => <input {...field} />}
|
|
127
|
+
</Field>
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Customization
|
|
131
|
+
|
|
132
|
+
Formex is designed to be flexible. You can create custom field components, use any validation library, or integrate with UI component libraries.
|
|
133
|
+
|
|
134
|
+
### Using with UI Libraries
|
|
135
|
+
|
|
136
|
+
```jsx
|
|
137
|
+
import { Field } from 'formex-library';
|
|
138
|
+
import { TextField } from 'some-ui-library';
|
|
139
|
+
|
|
140
|
+
<Field name="username">
|
|
141
|
+
{({ field }) => (
|
|
142
|
+
<TextField {...field} label="Username" />
|
|
143
|
+
)}
|
|
144
|
+
</Field>
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### Custom Validation
|
|
148
|
+
|
|
149
|
+
Leverage the `validation` function in `useCreateFormex` to integrate any validation logic or library.
|
|
150
|
+
|
|
151
|
+
```jsx
|
|
152
|
+
const validate = values => {
|
|
153
|
+
const errors = {};
|
|
154
|
+
if (!values.email.includes('@')) {
|
|
155
|
+
errors.email = 'Invalid email';
|
|
156
|
+
}
|
|
157
|
+
return errors;
|
|
158
|
+
};
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## Conclusion
|
|
162
|
+
|
|
163
|
+
Formex provides a simple yet powerful way to manage forms in React applications. It reduces the amount of boilerplate code needed and offers flexibility to work with custom components and validation strategies. Whether you are building simple or complex forms, Formex can help streamline your form management process.
|
|
164
|
+
|
|
165
|
+
For further examples and advanced usage, refer to the Formex documentation or source code.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import React, { FormEvent } from "react";
|
|
2
|
+
|
|
3
|
+
export type FormexController<T extends object> = {
|
|
4
|
+
values: T;
|
|
5
|
+
setValues: (values: T) => void;
|
|
6
|
+
setFieldValue: (key: string, value: any, shouldValidate?: boolean) => void;
|
|
7
|
+
touched: Record<string, boolean>;
|
|
8
|
+
setFieldTouched: (key: string, touched: boolean, shouldValidate?: boolean) => void;
|
|
9
|
+
dirty: boolean;
|
|
10
|
+
setDirty: (dirty: boolean) => void;
|
|
11
|
+
setSubmitCount: (submitCount: number) => void;
|
|
12
|
+
errors: Record<string, string>;
|
|
13
|
+
setFieldError: (key: string, error?: string) => void;
|
|
14
|
+
handleChange: (event: React.SyntheticEvent) => void,
|
|
15
|
+
handleBlur: (event: React.FocusEvent) => void,
|
|
16
|
+
submitForm: (event?: FormEvent<HTMLFormElement>) => void;
|
|
17
|
+
validate: () => void;
|
|
18
|
+
resetForm: (props?: FormexResetProps<T>) => void;
|
|
19
|
+
submitCount: number;
|
|
20
|
+
isSubmitting: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type FormexResetProps<T extends object> = {
|
|
24
|
+
values?: T;
|
|
25
|
+
errors?: Record<string, string>;
|
|
26
|
+
touched?: Record<string, boolean>;
|
|
27
|
+
};
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import React, { FormEvent, useEffect, useState } from "react";
|
|
2
|
+
import { setIn } from "./utils";
|
|
3
|
+
import { FormexController, FormexResetProps } from "./types";
|
|
4
|
+
|
|
5
|
+
export function useCreateFormex<T extends object>({ initialValues, initialErrors, validation, validateOnChange = false, onSubmit, validateOnInitialRender = false }: {
|
|
6
|
+
initialValues: T,
|
|
7
|
+
initialErrors?: Record<string, string>,
|
|
8
|
+
validateOnChange?: boolean,
|
|
9
|
+
validateOnInitialRender?: boolean,
|
|
10
|
+
validation?: (values: T) => Record<string, string>,
|
|
11
|
+
onSubmit?: (values: T, controller: FormexController<T>) => void | Promise<void>
|
|
12
|
+
}): FormexController<T> {
|
|
13
|
+
|
|
14
|
+
const valuesRef = React.useRef<T>(initialValues);
|
|
15
|
+
|
|
16
|
+
const [values, setValuesInner] = useState<T>(initialValues);
|
|
17
|
+
const [touchedState, setTouchedState] = useState<Record<string, boolean>>({});
|
|
18
|
+
const [errors, setErrors] = useState<Record<string, string>>(initialErrors ?? {});
|
|
19
|
+
const [dirty, setDirty] = useState(false);
|
|
20
|
+
const [submitCount, setSubmitCount] = useState(0);
|
|
21
|
+
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
22
|
+
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
if (validateOnInitialRender) {
|
|
25
|
+
validate();
|
|
26
|
+
}
|
|
27
|
+
}, []);
|
|
28
|
+
|
|
29
|
+
const setValues = (newValues: T) => {
|
|
30
|
+
valuesRef.current = newValues;
|
|
31
|
+
setValuesInner(newValues);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const validate = () => {
|
|
35
|
+
const values = valuesRef.current;
|
|
36
|
+
const validationErrors = validation?.(values);
|
|
37
|
+
setErrors(validationErrors ?? {});
|
|
38
|
+
return validationErrors;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const setFieldValue = (key: string, value: any, shouldValidate?: boolean) => {
|
|
42
|
+
const newValues = setIn(valuesRef.current, key, value);
|
|
43
|
+
valuesRef.current = newValues;
|
|
44
|
+
setValues(newValues);
|
|
45
|
+
if (shouldValidate) {
|
|
46
|
+
validate();
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const setFieldError = (key: string, error: string | undefined) => {
|
|
51
|
+
console.log("setFieldError", {key, error, errors })
|
|
52
|
+
const newErrors = { ...errors };
|
|
53
|
+
if (error) {
|
|
54
|
+
newErrors[key] = error;
|
|
55
|
+
} else {
|
|
56
|
+
delete newErrors[key];
|
|
57
|
+
}
|
|
58
|
+
setErrors(newErrors);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const setFieldTouched = (key: string, touched: boolean, shouldValidate?: boolean | undefined) => {
|
|
62
|
+
const newTouched = { ...touchedState };
|
|
63
|
+
newTouched[key] = touched;
|
|
64
|
+
setTouchedState(newTouched);
|
|
65
|
+
if (shouldValidate) {
|
|
66
|
+
validate();
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const handleChange = (event: React.SyntheticEvent) => {
|
|
71
|
+
const target = event.target as HTMLInputElement;
|
|
72
|
+
const value = target.type === "checkbox" ? target.checked : target.value;
|
|
73
|
+
const name = target.name;
|
|
74
|
+
setFieldValue(name, value, validateOnChange);
|
|
75
|
+
setFieldTouched(name, true);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const handleBlur = (event: React.FocusEvent) => {
|
|
79
|
+
console.log("handleBlur")
|
|
80
|
+
const target = event.target as HTMLInputElement;
|
|
81
|
+
const name = target.name;
|
|
82
|
+
setFieldTouched(name, true);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const submit = async (e?: FormEvent<HTMLFormElement>) => {
|
|
86
|
+
e?.preventDefault();
|
|
87
|
+
e?.stopPropagation();
|
|
88
|
+
setIsSubmitting(true);
|
|
89
|
+
setSubmitCount(submitCount + 1);
|
|
90
|
+
const validationErrors = validation?.(valuesRef.current);
|
|
91
|
+
if (validationErrors && Object.keys(validationErrors).length > 0) {
|
|
92
|
+
setErrors(validationErrors);
|
|
93
|
+
} else {
|
|
94
|
+
setErrors({});
|
|
95
|
+
await onSubmit?.(valuesRef.current, controllerRef.current);
|
|
96
|
+
}
|
|
97
|
+
setIsSubmitting(false);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const resetForm = (props?: FormexResetProps<T>) => {
|
|
101
|
+
const {
|
|
102
|
+
values: valuesProp,
|
|
103
|
+
errors: errorsProp,
|
|
104
|
+
touched: touchedProp
|
|
105
|
+
} = props ?? {};
|
|
106
|
+
valuesRef.current = valuesProp ?? initialValues;
|
|
107
|
+
setValues(valuesProp ?? initialValues);
|
|
108
|
+
setErrors(errorsProp ?? {});
|
|
109
|
+
setTouchedState(touchedProp ?? {});
|
|
110
|
+
setDirty(false);
|
|
111
|
+
setSubmitCount(0);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const controller: FormexController<T> = {
|
|
115
|
+
values,
|
|
116
|
+
handleChange,
|
|
117
|
+
isSubmitting,
|
|
118
|
+
setValues,
|
|
119
|
+
setFieldValue,
|
|
120
|
+
errors,
|
|
121
|
+
setFieldError,
|
|
122
|
+
touched: touchedState,
|
|
123
|
+
setFieldTouched,
|
|
124
|
+
dirty,
|
|
125
|
+
setDirty,
|
|
126
|
+
submitForm: submit,
|
|
127
|
+
submitCount,
|
|
128
|
+
setSubmitCount,
|
|
129
|
+
handleBlur,
|
|
130
|
+
validate,
|
|
131
|
+
resetForm
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const controllerRef = React.useRef<FormexController<T>>(controller);
|
|
135
|
+
controllerRef.current = controller;
|
|
136
|
+
return controller
|
|
137
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
/** @private is the value an empty array? */
|
|
4
|
+
export const isEmptyArray = (value?: any) =>
|
|
5
|
+
Array.isArray(value) && value.length === 0;
|
|
6
|
+
|
|
7
|
+
/** @private is the given object a Function? */
|
|
8
|
+
export const isFunction = (obj: any): obj is Function =>
|
|
9
|
+
typeof obj === "function";
|
|
10
|
+
|
|
11
|
+
/** @private is the given object an Object? */
|
|
12
|
+
export const isObject = (obj: any): obj is Object =>
|
|
13
|
+
obj !== null && typeof obj === "object";
|
|
14
|
+
|
|
15
|
+
/** @private is the given object an integer? */
|
|
16
|
+
export const isInteger = (obj: any): boolean =>
|
|
17
|
+
String(Math.floor(Number(obj))) === obj;
|
|
18
|
+
|
|
19
|
+
/** @private is the given object a string? */
|
|
20
|
+
export const isString = (obj: any): obj is string =>
|
|
21
|
+
Object.prototype.toString.call(obj) === "[object String]";
|
|
22
|
+
|
|
23
|
+
/** @private is the given object a NaN? */
|
|
24
|
+
// eslint-disable-next-line no-self-compare
|
|
25
|
+
export const isNaN = (obj: any): boolean => obj !== obj;
|
|
26
|
+
|
|
27
|
+
/** @private Does a React component have exactly 0 children? */
|
|
28
|
+
export const isEmptyChildren = (children: any): boolean =>
|
|
29
|
+
React.Children.count(children) === 0;
|
|
30
|
+
|
|
31
|
+
/** @private is the given object/value a promise? */
|
|
32
|
+
export const isPromise = (value: any): value is PromiseLike<any> =>
|
|
33
|
+
isObject(value) && isFunction(value.then);
|
|
34
|
+
|
|
35
|
+
/** @private is the given object/value a type of synthetic event? */
|
|
36
|
+
export const isInputEvent = (value: any): value is React.SyntheticEvent<any> =>
|
|
37
|
+
value && isObject(value) && isObject(value.target);
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Same as document.activeElement but wraps in a try-catch block. In IE it is
|
|
41
|
+
* not safe to call document.activeElement if there is nothing focused.
|
|
42
|
+
*
|
|
43
|
+
* The activeElement will be null only if the document or document body is not
|
|
44
|
+
* yet defined.
|
|
45
|
+
*
|
|
46
|
+
* @param {?Document} doc Defaults to current document.
|
|
47
|
+
* @return {Element | null}
|
|
48
|
+
* @see https://github.com/facebook/fbjs/blob/master/packages/fbjs/src/core/dom/getActiveElement.js
|
|
49
|
+
*/
|
|
50
|
+
export function getActiveElement(doc?: Document): Element | null {
|
|
51
|
+
doc = doc || (typeof document !== "undefined" ? document : undefined);
|
|
52
|
+
if (typeof doc === "undefined") {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
return doc.activeElement || doc.body;
|
|
57
|
+
} catch (e) {
|
|
58
|
+
return doc.body;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Deeply get a value from an object via its path.
|
|
64
|
+
*/
|
|
65
|
+
export function getIn(
|
|
66
|
+
obj: any,
|
|
67
|
+
key: string | string[],
|
|
68
|
+
def?: any,
|
|
69
|
+
p = 0
|
|
70
|
+
) {
|
|
71
|
+
const path = toPath(key);
|
|
72
|
+
while (obj && p < path.length) {
|
|
73
|
+
obj = obj[path[p++]];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// check if path is not in the end
|
|
77
|
+
if (p !== path.length && !obj) {
|
|
78
|
+
return def;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return obj === undefined ? def : obj;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function setIn(obj: any, path: string, value: any): any {
|
|
85
|
+
const res: any = clone(obj); // this keeps inheritance when obj is a class
|
|
86
|
+
let resVal: any = res;
|
|
87
|
+
let i = 0;
|
|
88
|
+
const pathArray = toPath(path);
|
|
89
|
+
|
|
90
|
+
for (; i < pathArray.length - 1; i++) {
|
|
91
|
+
const currentPath: string = pathArray[i];
|
|
92
|
+
const currentObj: any = getIn(obj, pathArray.slice(0, i + 1));
|
|
93
|
+
|
|
94
|
+
if (currentObj && (isObject(currentObj) || Array.isArray(currentObj))) {
|
|
95
|
+
resVal = resVal[currentPath] = clone(currentObj);
|
|
96
|
+
} else {
|
|
97
|
+
const nextPath: string = pathArray[i + 1];
|
|
98
|
+
resVal = resVal[currentPath] =
|
|
99
|
+
isInteger(nextPath) && Number(nextPath) >= 0 ? [] : {};
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Return original object if new value is the same as current
|
|
104
|
+
if ((i === 0 ? obj : resVal)[pathArray[i]] === value) {
|
|
105
|
+
return obj;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (value === undefined) {
|
|
109
|
+
delete resVal[pathArray[i]];
|
|
110
|
+
} else {
|
|
111
|
+
resVal[pathArray[i]] = value;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// If the path array has a single element, the loop did not run.
|
|
115
|
+
// Deleting on `resVal` had no effect in this scenario, so we delete on the result instead.
|
|
116
|
+
if (i === 0 && value === undefined) {
|
|
117
|
+
delete res[pathArray[i]];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return res;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Recursively a set the same value for all keys and arrays nested object, cloning
|
|
125
|
+
* @param object
|
|
126
|
+
* @param value
|
|
127
|
+
* @param visited
|
|
128
|
+
* @param response
|
|
129
|
+
*/
|
|
130
|
+
export function setNestedObjectValues<T>(
|
|
131
|
+
object: any,
|
|
132
|
+
value: any,
|
|
133
|
+
visited: any = new WeakMap(),
|
|
134
|
+
response: any = {}
|
|
135
|
+
): T {
|
|
136
|
+
for (const k of Object.keys(object)) {
|
|
137
|
+
const val = object[k];
|
|
138
|
+
if (isObject(val)) {
|
|
139
|
+
if (!visited.get(val)) {
|
|
140
|
+
visited.set(val, true);
|
|
141
|
+
// In order to keep array values consistent for both dot path and
|
|
142
|
+
// bracket syntax, we need to check if this is an array so that
|
|
143
|
+
// this will output { friends: [true] } and not { friends: { "0": true } }
|
|
144
|
+
response[k] = Array.isArray(val) ? [] : {};
|
|
145
|
+
setNestedObjectValues(val, value, visited, response[k]);
|
|
146
|
+
}
|
|
147
|
+
} else {
|
|
148
|
+
response[k] = value;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return response;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function clone(value: any) {
|
|
156
|
+
if (Array.isArray(value)) {
|
|
157
|
+
return [...value];
|
|
158
|
+
} else if (typeof value === "object" && value !== null) {
|
|
159
|
+
return { ...value };
|
|
160
|
+
} else {
|
|
161
|
+
return value; // This is for primitive types which do not need cloning.
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function toPath(value: string | string[]) {
|
|
166
|
+
if (Array.isArray(value)) return value; // Already in path array form.
|
|
167
|
+
// Replace brackets with dots, remove leading/trailing dots, then split by dot.
|
|
168
|
+
return value.replace(/\[(\d+)]/g, ".$1").replace(/^\./, "").replace(/\.$/, "").split(".");
|
|
169
|
+
}
|
|
@@ -9,7 +9,7 @@ import { PersistedCollection } from "./persisted_collection";
|
|
|
9
9
|
export interface CollectionEditorController {
|
|
10
10
|
|
|
11
11
|
editCollection: (props: {
|
|
12
|
-
|
|
12
|
+
id?: string,
|
|
13
13
|
fullPath?: string,
|
|
14
14
|
parentCollectionIds: string[],
|
|
15
15
|
parentCollection?: PersistedCollection
|
|
@@ -23,14 +23,15 @@ export interface CollectionEditorController {
|
|
|
23
23
|
},
|
|
24
24
|
parentCollectionIds: string[],
|
|
25
25
|
parentCollection?: PersistedCollection,
|
|
26
|
-
redirect: boolean
|
|
26
|
+
redirect: boolean,
|
|
27
|
+
sourceClick?: string
|
|
27
28
|
}) => void;
|
|
28
29
|
|
|
29
30
|
editProperty: (props: {
|
|
30
31
|
propertyKey?: string,
|
|
31
32
|
property?: Property,
|
|
32
33
|
currentPropertiesOrder?: string[],
|
|
33
|
-
|
|
34
|
+
editedCollectionId: string,
|
|
34
35
|
parentCollectionIds: string[],
|
|
35
36
|
collection: PersistedCollection
|
|
36
37
|
}) => void;
|
|
@@ -26,14 +26,14 @@ export interface CollectionsConfigController {
|
|
|
26
26
|
export type UpdateCollectionParams<M extends Record<string, any>> = {
|
|
27
27
|
id: string,
|
|
28
28
|
collectionData: Partial<PersistedCollection<M>>,
|
|
29
|
-
|
|
29
|
+
previousId?: string,
|
|
30
30
|
parentCollectionIds?: string[]
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
export type SaveCollectionParams<M extends Record<string, any>> = {
|
|
34
34
|
id: string,
|
|
35
35
|
collectionData: PersistedCollection<M>,
|
|
36
|
-
|
|
36
|
+
previousId?: string,
|
|
37
37
|
parentCollectionIds?: string[]
|
|
38
38
|
}
|
|
39
39
|
|
|
@@ -55,6 +55,6 @@ export type DeletePropertyParams = {
|
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
export type DeleteCollectionParams = {
|
|
58
|
-
|
|
58
|
+
id: string,
|
|
59
59
|
parentCollectionIds?: string[]
|
|
60
60
|
}
|
|
@@ -39,7 +39,7 @@ export function EditorCollectionAction({
|
|
|
39
39
|
if (!equal(getObjectOrNull(tableController.filterValues), getObjectOrNull(collection.initialFilter)) ||
|
|
40
40
|
!equal(getObjectOrNull(tableController.sortBy), getObjectOrNull(collection.initialSort))) {
|
|
41
41
|
saveDefaultFilterButton = <>
|
|
42
|
-
{collection.initialFilter || collection.initialSort && <Tooltip
|
|
42
|
+
{(collection.initialFilter || collection.initialSort) && <Tooltip
|
|
43
43
|
title={"Reset to default filter and sort"}>
|
|
44
44
|
<Button
|
|
45
45
|
color={"primary"}
|
|
@@ -64,7 +64,7 @@ export function EditorCollectionAction({
|
|
|
64
64
|
variant={"outlined"}
|
|
65
65
|
onClick={() => configController
|
|
66
66
|
?.saveCollection({
|
|
67
|
-
id: collection.
|
|
67
|
+
id: collection.id,
|
|
68
68
|
parentCollectionIds,
|
|
69
69
|
collectionData: mergeDeep(collection as PersistedCollection,
|
|
70
70
|
{
|
|
@@ -89,7 +89,7 @@ export function EditorCollectionAction({
|
|
|
89
89
|
color={"primary"}
|
|
90
90
|
disabled={!canEditCollection}
|
|
91
91
|
onClick={canEditCollection
|
|
92
|
-
? () => collectionEditorController?.editCollection({
|
|
92
|
+
? () => collectionEditorController?.editCollection({ id: collection.id, fullPath, parentCollectionIds, parentCollection: parentCollection as PersistedCollection })
|
|
93
93
|
: undefined}>
|
|
94
94
|
<SettingsIcon/>
|
|
95
95
|
</IconButton>
|
|
@@ -25,13 +25,13 @@ export function HomePageEditorCollectionAction({
|
|
|
25
25
|
});
|
|
26
26
|
|
|
27
27
|
const onEditCollectionClicked = useCallback(() => {
|
|
28
|
-
collectionEditorController?.editCollection({
|
|
28
|
+
collectionEditorController?.editCollection({ id: collection.id, parentCollectionIds: [] });
|
|
29
29
|
}, [collectionEditorController, path]);
|
|
30
30
|
|
|
31
31
|
const [deleteRequested, setDeleteRequested] = useState(false);
|
|
32
32
|
|
|
33
33
|
const deleteCollection = useCallback(() => {
|
|
34
|
-
configController?.deleteCollection({
|
|
34
|
+
configController?.deleteCollection({ id: collection.id }).then(() => {
|
|
35
35
|
setDeleteRequested(false);
|
|
36
36
|
snackbarController.open({
|
|
37
37
|
message: "Collection deleted",
|
|
@@ -16,7 +16,8 @@ export function MissingReferenceWidget({ path: pathProp }: {
|
|
|
16
16
|
collectionEditor.createCollection({
|
|
17
17
|
initialValues: { path, name: unslugify(path) },
|
|
18
18
|
parentCollectionIds,
|
|
19
|
-
redirect: false
|
|
19
|
+
redirect: false,
|
|
20
|
+
sourceClick: "missing_reference"
|
|
20
21
|
});
|
|
21
22
|
}}>
|
|
22
23
|
Create
|
|
@@ -7,9 +7,9 @@ export function NewCollectionButton() {
|
|
|
7
7
|
variant={"outlined"}
|
|
8
8
|
onClick={() => collectionEditorController.createCollection({
|
|
9
9
|
parentCollectionIds: [],
|
|
10
|
-
redirect: true
|
|
11
|
-
|
|
12
|
-
}>
|
|
10
|
+
redirect: true,
|
|
11
|
+
sourceClick: "new_collection_button"
|
|
12
|
+
})}>
|
|
13
13
|
<AddIcon/>
|
|
14
14
|
New collection
|
|
15
15
|
</Button>
|
|
@@ -25,7 +25,8 @@ export function NewCollectionCard({
|
|
|
25
25
|
? () => collectionEditorController.createCollection({
|
|
26
26
|
initialValues: group ? { group } : undefined,
|
|
27
27
|
parentCollectionIds: [],
|
|
28
|
-
redirect: true
|
|
28
|
+
redirect: true,
|
|
29
|
+
sourceClick: "new_collection_card"
|
|
29
30
|
})
|
|
30
31
|
: undefined}>
|
|
31
32
|
|
|
@@ -29,7 +29,7 @@ export function PropertyAddColumnComponent({
|
|
|
29
29
|
// className={onHover ? "bg-white dark:bg-gray-950" : undefined}
|
|
30
30
|
onClick={() => {
|
|
31
31
|
collectionEditorController.editProperty({
|
|
32
|
-
|
|
32
|
+
editedCollectionId: collection.id,
|
|
33
33
|
parentCollectionIds,
|
|
34
34
|
currentPropertiesOrder: getDefaultPropertiesOrder(collection),
|
|
35
35
|
collection
|
|
@@ -40,7 +40,8 @@ export function RootCollectionSuggestions() {
|
|
|
40
40
|
? () => collectionEditorController.createCollection({
|
|
41
41
|
initialValues: { path, name: unslugify(path) },
|
|
42
42
|
parentCollectionIds: [],
|
|
43
|
-
redirect: true
|
|
43
|
+
redirect: true,
|
|
44
|
+
sourceClick: "root_collection_suggestion"
|
|
44
45
|
})
|
|
45
46
|
: undefined}
|
|
46
47
|
size="small">
|