@poirazis/supercomponents-shared 1.0.9 → 1.0.18

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.
@@ -0,0 +1,511 @@
1
+ <script lang="ts">
2
+ import { setContext, getContext, createEventDispatcher } from "svelte";
3
+ import type { Readable, Writable } from "svelte/store";
4
+ import { derived, get, writable } from "svelte/store";
5
+ import type {
6
+ DataFetchDatasource,
7
+ FieldSchema,
8
+ FieldType,
9
+ Table,
10
+ TableSchema,
11
+ UIFieldValidationRule,
12
+ } from "@budibase/types";
13
+
14
+ const dispatch = createEventDispatcher();
15
+
16
+ type FieldInfo<T = any> = {
17
+ name: string;
18
+ step: number;
19
+ type: `${FieldType}`;
20
+ fieldState: {
21
+ fieldId: string;
22
+ value: T;
23
+ defaultValue: T;
24
+ disabled: boolean;
25
+ readonly: boolean;
26
+ validator: ((_value: T) => string | null) | null;
27
+ error: string | null | undefined;
28
+ lastUpdate: number;
29
+ };
30
+ fieldApi: {
31
+ setValue(_value: T): void;
32
+ validate(): boolean;
33
+ reset(): void;
34
+ };
35
+ fieldSchema: FieldSchema | {};
36
+ };
37
+
38
+ export let dataSource: DataFetchDatasource | undefined = undefined;
39
+ export let disabled: boolean = false;
40
+ export let readonly: boolean = false;
41
+ export let initialValues: Record<string, any> | undefined = undefined;
42
+ export let size: "Medium" | "Large" | undefined = undefined;
43
+ export let schema: TableSchema | undefined = undefined;
44
+ export let definition: Table | undefined = undefined;
45
+ export let disableSchemaValidation: boolean = false;
46
+ export let editAutoColumns: boolean = false;
47
+ export let provideContext: boolean = true;
48
+ export let currentStep: Writable<number>;
49
+
50
+ const { Provider, ActionTypes, createValidatorFromConstraints } =
51
+ getContext("sdk");
52
+
53
+ let fields: Writable<FieldInfo>[] = [];
54
+ export const formState = writable({
55
+ values: {},
56
+ errors: {},
57
+ valid: true,
58
+ dirty: false,
59
+ currentStep: get(currentStep),
60
+ });
61
+
62
+ $: values = deriveFieldProperty(fields, (f) => f.fieldState.value);
63
+ $: errors = deriveFieldProperty(fields, (f) => f.fieldState.error);
64
+ $: enrichments = deriveBindingEnrichments(fields);
65
+ $: valid = !Object.values($errors).some((error) => error != null);
66
+ $: dirty = deriveDirtyStatus(fields, initialValues);
67
+
68
+ $: currentStepValid = derived(
69
+ [currentStep, ...fields],
70
+ ([currentStepValue, ...fieldsValue]) => {
71
+ return !fieldsValue
72
+ .filter((f) => f.step === currentStepValue)
73
+ .some((f) => f.fieldState.error != null);
74
+ }
75
+ );
76
+
77
+ // Offer the form as a bindable property so it can be puppeteered by parent components
78
+ export let form;
79
+ $: form = {
80
+ formState,
81
+ formApi,
82
+ dataSource,
83
+ };
84
+
85
+ $: {
86
+ formState.set({
87
+ values: $values,
88
+ errors: $errors,
89
+ valid,
90
+ dirty: $dirty,
91
+ currentStep: $currentStep,
92
+ });
93
+ }
94
+
95
+ $: formValue = deriveFormValue(initialValues, $values, $enrichments);
96
+
97
+ $: dataContext = {
98
+ ...formValue,
99
+ __value: formValue,
100
+ __valid: valid,
101
+ __dirty: $dirty,
102
+ __currentStep: $currentStep,
103
+ __currentStepValid: $currentStepValid,
104
+ };
105
+
106
+ const deriveFieldProperty = (
107
+ fieldStores: Readable<FieldInfo>[],
108
+ getProp: (_field: FieldInfo) => any
109
+ ) => {
110
+ return derived(fieldStores, (fieldValues) => {
111
+ return fieldValues.reduce(
112
+ (map, field) => ({ ...map, [field.name]: getProp(field) }),
113
+ {}
114
+ );
115
+ });
116
+ };
117
+
118
+ const deriveDirtyStatus = (
119
+ fieldStores: Readable<FieldInfo>[],
120
+ initialValues: Record<string, any> | undefined
121
+ ) => {
122
+ return derived(fieldStores, (fieldValues) => {
123
+ return fieldValues.some((field) => {
124
+ const initial =
125
+ deepGet(initialValues, field.name) ?? field.fieldState.defaultValue;
126
+ return field.fieldState.value !== initial;
127
+ });
128
+ });
129
+ };
130
+
131
+ const deriveBindingEnrichments = (fieldStores: Readable<FieldInfo>[]) => {
132
+ return derived(fieldStores, (fieldValues) => {
133
+ const enrichments: Record<string, string> = {};
134
+ fieldValues.forEach((field) => {
135
+ if (field.type === "attachment") {
136
+ const value = field.fieldState.value;
137
+ let url = null;
138
+ if (Array.isArray(value) && value[0] != null) {
139
+ url = value[0].url;
140
+ }
141
+ enrichments[`${field.name}_first`] = url;
142
+ }
143
+ });
144
+ return enrichments;
145
+ });
146
+ };
147
+
148
+ const deriveFormValue = (
149
+ initialValues: Record<string, any> | undefined,
150
+ values: Record<string, any>,
151
+ enrichments: Record<string, string>
152
+ ) => {
153
+ let formValue = cloneDeep(initialValues || {});
154
+ const sortedFields = Object.entries(values || {})
155
+ .map(([key, value]) => {
156
+ const field = getField(key);
157
+ return {
158
+ key,
159
+ value,
160
+ lastUpdate: get(field).fieldState?.lastUpdate || 0,
161
+ };
162
+ })
163
+ .sort((a, b) => {
164
+ return a.lastUpdate - b.lastUpdate;
165
+ });
166
+
167
+ sortedFields.forEach(({ key, value }) => {
168
+ deepSet(formValue, key, value);
169
+ });
170
+ Object.entries(enrichments || {}).forEach(([key, value]) => {
171
+ deepSet(formValue, key, value);
172
+ });
173
+ return formValue;
174
+ };
175
+
176
+ const getField = (name: string) => {
177
+ return fields.find((field) => get(field).name === name)!;
178
+ };
179
+
180
+ const sanitiseValue = (
181
+ value: any,
182
+ schema: FieldSchema | undefined,
183
+ type: `${FieldType}`
184
+ ) => {
185
+ if (Array.isArray(value) && type === "array" && schema) {
186
+ const options = schema?.constraints?.inclusion || [];
187
+ return value
188
+ .map((opt) => String(opt))
189
+ .filter((opt) => options.includes(opt));
190
+ }
191
+ return value;
192
+ };
193
+
194
+ const formApi = {
195
+ registerField: (
196
+ field: string,
197
+ type: FieldType,
198
+ defaultValue: string | null = null,
199
+ fieldDisabled: boolean = false,
200
+ fieldReadOnly: boolean = false,
201
+ validationRules: UIFieldValidationRule[],
202
+ step: number = 1
203
+ ) => {
204
+ if (!field) {
205
+ return;
206
+ }
207
+ const schemaConstraints = disableSchemaValidation
208
+ ? null
209
+ : schema?.[field]?.constraints;
210
+ const validator = createValidatorFromConstraints(
211
+ schemaConstraints,
212
+ validationRules,
213
+ field,
214
+ definition
215
+ );
216
+
217
+ defaultValue = sanitiseValue(defaultValue, schema?.[field], type);
218
+
219
+ let initialValue = deepGet(initialValues, field) ?? defaultValue;
220
+ let initialError = null;
221
+ let fieldId = `id-${uuid()}`;
222
+ const existingField = getField(field);
223
+ if (existingField) {
224
+ const { fieldState } = get(existingField);
225
+ fieldId = fieldState.fieldId;
226
+ if (fieldState.value != null && fieldState.value !== "") {
227
+ initialValue = fieldState.value;
228
+ }
229
+ if (fieldState.error) {
230
+ initialError = validator?.(initialValue);
231
+ }
232
+ }
233
+
234
+ const isAutoColumn = !!schema?.[field]?.autocolumn;
235
+
236
+ const fieldInfo = writable<FieldInfo>({
237
+ name: field,
238
+ type,
239
+ step: step || 1,
240
+ fieldState: {
241
+ fieldId,
242
+ value: initialValue,
243
+ error: initialError,
244
+ disabled:
245
+ disabled || fieldDisabled || (isAutoColumn && !editAutoColumns),
246
+ readonly:
247
+ readonly || fieldReadOnly || (schema?.[field] as any)?.readonly,
248
+ defaultValue,
249
+ validator,
250
+ lastUpdate: Date.now(),
251
+ },
252
+ fieldApi: makeFieldApi(field),
253
+ fieldSchema: schema?.[field] ?? {},
254
+ });
255
+
256
+ if (existingField) {
257
+ const otherFields = fields.filter((info) => get(info).name !== field);
258
+ fields = [...otherFields, fieldInfo];
259
+ } else {
260
+ fields = [...fields, fieldInfo];
261
+ }
262
+
263
+ return fieldInfo;
264
+ },
265
+ validate: () => {
266
+ const stepFields = fields.filter(
267
+ (field) => get(field).step === get(currentStep)
268
+ );
269
+ let valid = true;
270
+ let hasScrolled = false;
271
+ stepFields.forEach((field) => {
272
+ const fieldValid = get(field).fieldApi.validate();
273
+ valid = valid && fieldValid;
274
+ if (!valid && !hasScrolled) {
275
+ handleScrollToField({ field: get(field) });
276
+ hasScrolled = true;
277
+ }
278
+ });
279
+ return valid;
280
+ },
281
+ reset: () => {
282
+ fields.forEach((field) => {
283
+ get(field).fieldApi.reset();
284
+ });
285
+ dispatch("reset");
286
+ },
287
+ changeStep: ({
288
+ type,
289
+ number,
290
+ }: {
291
+ type: "next" | "prev" | "first" | "specific";
292
+ number: any;
293
+ }) => {
294
+ if (type === "next") {
295
+ currentStep.update((step) => step + 1);
296
+ } else if (type === "prev") {
297
+ currentStep.update((step) => Math.max(1, step - 1));
298
+ } else if (type === "first") {
299
+ currentStep.set(1);
300
+ } else if (type === "specific" && number && !isNaN(number)) {
301
+ currentStep.set(parseInt(number));
302
+ }
303
+ },
304
+ setStep: (step: number) => {
305
+ if (step) {
306
+ currentStep.set(step);
307
+ }
308
+ },
309
+ setFieldValue: (fieldName: string, value: any) => {
310
+ const field = getField(fieldName);
311
+ if (!field) {
312
+ return;
313
+ }
314
+ const { fieldApi } = get(field);
315
+ fieldApi.setValue(value);
316
+ },
317
+ resetField: (fieldName: string) => {
318
+ const field = getField(fieldName);
319
+ if (!field) {
320
+ return;
321
+ }
322
+ const { fieldApi } = get(field);
323
+ fieldApi.reset();
324
+ },
325
+ };
326
+
327
+ const makeFieldApi = (field: string) => {
328
+ const setValue = (value: any, skipCheck = false) => {
329
+ const fieldInfo = getField(field);
330
+ const { fieldState } = get(fieldInfo);
331
+ const { validator } = fieldState;
332
+
333
+ if (!skipCheck && fieldState.value === value) {
334
+ return false;
335
+ }
336
+
337
+ const error = validator?.(value);
338
+ fieldInfo.update((state) => {
339
+ state.fieldState.value = value;
340
+ state.fieldState.error = error;
341
+ state.fieldState.lastUpdate = Date.now();
342
+ return state;
343
+ });
344
+ dispatch("change", { field, value });
345
+ return true;
346
+ };
347
+
348
+ const reset = () => {
349
+ const fieldInfo = getField(field);
350
+ const { fieldState } = get(fieldInfo);
351
+ const newValue = fieldState.defaultValue;
352
+
353
+ fieldInfo.update((state) => {
354
+ state.fieldState.value = newValue;
355
+ state.fieldState.error = null;
356
+ state.fieldState.lastUpdate = Date.now();
357
+ return state;
358
+ });
359
+ };
360
+
361
+ const deregister = () => {
362
+ const fieldInfo = getField(field);
363
+ fieldInfo.update((state) => {
364
+ state.fieldState.validator = null;
365
+ state.fieldState.error = null;
366
+ return state;
367
+ });
368
+ };
369
+
370
+ const setDisabled = (fieldDisabled: boolean) => {
371
+ const fieldInfo = getField(field);
372
+ const isAutoColumn = !!schema?.[field]?.autocolumn;
373
+ fieldInfo.update((state) => {
374
+ state.fieldState.disabled = disabled || fieldDisabled || isAutoColumn;
375
+ return state;
376
+ });
377
+ };
378
+
379
+ return {
380
+ setValue,
381
+ reset,
382
+ setDisabled,
383
+ deregister,
384
+ validate: () => {
385
+ const fieldInfo = getField(field);
386
+ setValue(get(fieldInfo).fieldState.value, true);
387
+ return !get(fieldInfo).fieldState.error;
388
+ },
389
+ };
390
+ };
391
+
392
+ setContext("form", {
393
+ formState,
394
+ formApi,
395
+ dataSource,
396
+ });
397
+
398
+ setContext("form-step", writable(1));
399
+
400
+ const handleUpdateFieldValue = ({
401
+ type,
402
+ field,
403
+ value,
404
+ }: {
405
+ type: "set" | "reset";
406
+ field: string;
407
+ value: any;
408
+ }) => {
409
+ if (type === "set") {
410
+ formApi.setFieldValue(field, value);
411
+ } else {
412
+ formApi.resetField(field);
413
+ }
414
+ };
415
+
416
+ const handleScrollToField = (props: { field: FieldInfo | string }) => {
417
+ let field;
418
+ if (typeof props.field === "string") {
419
+ field = get(getField(props.field));
420
+ } else {
421
+ field = props.field;
422
+ }
423
+ const fieldId = field.fieldState.fieldId;
424
+ const fieldElement = document.getElementById(fieldId);
425
+ if (fieldElement) {
426
+ fieldElement.focus({ preventScroll: true });
427
+ }
428
+ const label = document.querySelector<HTMLElement>(
429
+ `label[for="${fieldId}"]`
430
+ );
431
+ if (label) {
432
+ label.style.scrollMargin = "100px";
433
+ label.scrollIntoView({ behavior: "smooth", block: "nearest" });
434
+ }
435
+ };
436
+
437
+ const actions = [
438
+ { type: ActionTypes.ValidateForm, callback: formApi.validate },
439
+ { type: ActionTypes.ClearForm, callback: formApi.reset },
440
+ { type: ActionTypes.ChangeFormStep, callback: formApi.changeStep },
441
+ { type: ActionTypes.UpdateFieldValue, callback: handleUpdateFieldValue },
442
+ { type: ActionTypes.ScrollTo, callback: handleScrollToField },
443
+ ];
444
+
445
+ // Generate a UUID (simplified version for brevity)
446
+ function uuid(): string {
447
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
448
+ const r = (Math.random() * 16) | 0;
449
+ const v = c === "x" ? r : (r & 0x3) | 0x8;
450
+ return v.toString(16);
451
+ });
452
+ }
453
+
454
+ // Deep clone an object
455
+ function cloneDeep<T>(obj: T): T {
456
+ if (obj == null || typeof obj !== "object") {
457
+ return obj;
458
+ }
459
+ if (Array.isArray(obj)) {
460
+ return obj.map(cloneDeep) as any;
461
+ }
462
+ const cloned: any = {};
463
+ for (const key in obj) {
464
+ if (Object.prototype.hasOwnProperty.call(obj, key)) {
465
+ cloned[key] = cloneDeep(obj[key]);
466
+ }
467
+ }
468
+ return cloned;
469
+ }
470
+
471
+ // Get a value from an object using a dot-notation path
472
+ function deepGet(obj: any, path: string | string[]): any {
473
+ if (!obj || !path) {
474
+ return undefined;
475
+ }
476
+ const pathArray = Array.isArray(path) ? path : path.split(".");
477
+ let current = obj;
478
+ for (const key of pathArray) {
479
+ if (current == null) {
480
+ return undefined;
481
+ }
482
+ current = current[key];
483
+ }
484
+ return current;
485
+ }
486
+
487
+ // Set a value in an object using a dot-notation path
488
+ function deepSet(obj: any, path: string | string[], value: any): void {
489
+ if (!obj || !path) {
490
+ return;
491
+ }
492
+ const pathArray = Array.isArray(path) ? path : path.split(".");
493
+ let current = obj;
494
+ for (let i = 0; i < pathArray.length - 1; i++) {
495
+ const key = pathArray[i];
496
+ if (!current[key] || typeof current[key] !== "object") {
497
+ current[key] = {};
498
+ }
499
+ current = current[key];
500
+ }
501
+ current[pathArray[pathArray.length - 1]] = value;
502
+ }
503
+ </script>
504
+
505
+ {#if provideContext}
506
+ <Provider {actions} data={dataContext}>
507
+ <slot />
508
+ </Provider>
509
+ {:else}
510
+ <slot />
511
+ {/if}
@@ -0,0 +1,145 @@
1
+ <script lang="ts">
2
+ import { getContext } from "svelte";
3
+ import InnerForm from "./InnerForm.svelte";
4
+ import { writable } from "svelte/store";
5
+ import type {
6
+ DataFetchDatasource,
7
+ Table,
8
+ TableSchema,
9
+ } from "@budibase/types";
10
+
11
+ // Local utility function
12
+ const hashString = (str: string): string => {
13
+ let hash = 0;
14
+ for (let i = 0; i < str.length; i++) {
15
+ const char = str.charCodeAt(i);
16
+ hash = (hash << 5) - hash + char;
17
+ hash = hash & hash; // Convert to 32-bit integer
18
+ }
19
+ return hash.toString(16);
20
+ };
21
+
22
+ export let dataSource: DataFetchDatasource;
23
+ export let size: "Medium" | "Large";
24
+ export let disabled: boolean = false;
25
+ export let readonly: boolean = false;
26
+ export let actionType: "Create" = "Create";
27
+ export let initialFormStep: string | number = 1;
28
+ export let disableSchemaValidation: boolean = false;
29
+ export let editAutoColumns: boolean = false;
30
+
31
+ // Export the full form API to be used by parents
32
+ export let form;
33
+ export let formState;
34
+ export let row;
35
+
36
+ const context = getContext("context");
37
+ const component = getContext("component");
38
+ const { fetchDatasourceSchema, fetchDatasourceDefinition } =
39
+ getContext("sdk");
40
+
41
+ const getInitialFormStep = () => {
42
+ const parsedFormStep = parseInt(initialFormStep.toString());
43
+ if (isNaN(parsedFormStep)) {
44
+ return 1;
45
+ }
46
+ return parsedFormStep;
47
+ };
48
+
49
+ let definition: Table | undefined;
50
+ let schema: TableSchema | undefined;
51
+ let loaded = false;
52
+ let currentStep =
53
+ getContext("current-step") || writable(getInitialFormStep());
54
+
55
+ $: fetchSchema(dataSource);
56
+ $: schemaKey = generateSchemaKey(schema);
57
+ $: initialValues = getInitialValues(
58
+ actionType,
59
+ dataSource,
60
+ $component.path,
61
+ $context
62
+ );
63
+ $: resetKey = hashString(
64
+ schemaKey + JSON.stringify(initialValues) + disabled + readonly
65
+ );
66
+
67
+ const getInitialValues = (
68
+ type: string,
69
+ dataSource: DataFetchDatasource,
70
+ path: string[],
71
+ context: Record<string, any>
72
+ ) => {
73
+ if (type !== "Update") {
74
+ return {};
75
+ }
76
+ const dsType = dataSource?.type;
77
+ if (dsType !== "table" && dsType !== "viewV2") {
78
+ return {};
79
+ }
80
+
81
+ if (row && dsType === "table" && row?.tableId === dataSource.tableId) {
82
+ return row;
83
+ }
84
+ for (let id of path.toReversed().slice(1)) {
85
+ if (
86
+ dataSource.type === "viewV2" &&
87
+ context[id]?._viewId === dataSource.id
88
+ ) {
89
+ return context[id];
90
+ }
91
+ if (
92
+ dataSource.type === "table" &&
93
+ context[id]?.tableId === dataSource.tableId
94
+ ) {
95
+ return context[id];
96
+ }
97
+ }
98
+ return {};
99
+ };
100
+
101
+ const fetchSchema = async (dataSource: DataFetchDatasource) => {
102
+ try {
103
+ definition = await fetchDatasourceDefinition(dataSource);
104
+ } catch (error) {
105
+ definition = undefined;
106
+ }
107
+ const res = await fetchDatasourceSchema(dataSource);
108
+ schema = res || {};
109
+ if (!loaded) {
110
+ loaded = true;
111
+ }
112
+ };
113
+
114
+ const generateSchemaKey = (schema: TableSchema | undefined) => {
115
+ if (!schema) {
116
+ return null;
117
+ }
118
+ const fields = Object.keys(schema);
119
+ fields.sort();
120
+ return fields.map((field) => `${field}:${schema[field].type}`).join("-");
121
+ };
122
+ </script>
123
+
124
+ {#if loaded}
125
+ {#key resetKey}
126
+ <InnerForm
127
+ bind:form
128
+ bind:formState
129
+ {dataSource}
130
+ {size}
131
+ {disabled}
132
+ {readonly}
133
+ {schema}
134
+ {definition}
135
+ {initialValues}
136
+ {disableSchemaValidation}
137
+ {editAutoColumns}
138
+ {currentStep}
139
+ on:change
140
+ on:reset
141
+ >
142
+ <slot />
143
+ </InnerForm>
144
+ {/key}
145
+ {/if}