@poirazis/supercomponents-shared 1.0.11 → 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.
@@ -1,14 +1,22 @@
1
1
  <script lang="ts">
2
- import { setContext, getContext } from "svelte";
2
+ import { setContext, getContext, createEventDispatcher } from "svelte";
3
3
  import type { Readable, Writable } from "svelte/store";
4
4
  import { derived, get, writable } from "svelte/store";
5
- const { createValidatorFromConstraints } = getContext("sdk");
6
- import type { FieldSchema, FieldType, UIFieldValidationRule } from "./types";
5
+ import type {
6
+ DataFetchDatasource,
7
+ FieldSchema,
8
+ FieldType,
9
+ Table,
10
+ TableSchema,
11
+ UIFieldValidationRule,
12
+ } from "@budibase/types";
13
+
14
+ const dispatch = createEventDispatcher();
7
15
 
8
16
  type FieldInfo<T = any> = {
9
17
  name: string;
10
18
  step: number;
11
- type: FieldType;
19
+ type: `${FieldType}`;
12
20
  fieldState: {
13
21
  fieldId: string;
14
22
  value: T;
@@ -27,28 +35,23 @@
27
35
  fieldSchema: FieldSchema | {};
28
36
  };
29
37
 
30
- export let dataSource: any | undefined = undefined;
38
+ export let dataSource: DataFetchDatasource | undefined = undefined;
31
39
  export let disabled: boolean = false;
32
40
  export let readonly: boolean = false;
33
41
  export let initialValues: Record<string, any> | undefined = undefined;
34
42
  export let size: "Medium" | "Large" | undefined = undefined;
35
- export let schema: Record<string, FieldSchema> | undefined = undefined;
36
- export let definition: any | undefined = undefined;
43
+ export let schema: TableSchema | undefined = undefined;
44
+ export let definition: Table | undefined = undefined;
37
45
  export let disableSchemaValidation: boolean = false;
38
46
  export let editAutoColumns: boolean = false;
39
-
40
- // For internal use only, to disable context when being used with standalone fields
41
47
  export let provideContext: boolean = true;
42
-
43
- // We export this store so that when we remount the inner form we can still
44
- // persist what step we're on
45
48
  export let currentStep: Writable<number>;
46
49
 
47
- const component = getContext("component");
48
- const { styleable, Provider, ActionTypes } = getContext("sdk");
50
+ const { Provider, ActionTypes, createValidatorFromConstraints } =
51
+ getContext("sdk");
49
52
 
50
53
  let fields: Writable<FieldInfo>[] = [];
51
- const formState = writable({
54
+ export const formState = writable({
52
55
  values: {},
53
56
  errors: {},
54
57
  valid: true,
@@ -56,23 +59,12 @@
56
59
  currentStep: get(currentStep),
57
60
  });
58
61
 
59
- // Reactive derived stores to derive form state from field array
60
62
  $: values = deriveFieldProperty(fields, (f) => f.fieldState.value);
61
63
  $: errors = deriveFieldProperty(fields, (f) => f.fieldState.error);
62
64
  $: enrichments = deriveBindingEnrichments(fields);
63
65
  $: valid = !Object.values($errors).some((error) => error != null);
64
- // Track if any field has been modified from its default value
65
- $: dirty = derived(
66
- fields,
67
- (fieldsValue) => fieldsValue.some((field) => {
68
- const { value, defaultValue } = field.fieldState;
69
- // Compare current value with default value to determine if field is dirty
70
- // Using JSON.stringify for deep comparison of objects and arrays
71
- return JSON.stringify(value) !== JSON.stringify(defaultValue);
72
- })
73
- );
66
+ $: dirty = deriveDirtyStatus(fields, initialValues);
74
67
 
75
- // Derive whether the current form step is valid
76
68
  $: currentStepValid = derived(
77
69
  [currentStep, ...fields],
78
70
  ([currentStepValue, ...fieldsValue]) => {
@@ -82,7 +74,14 @@
82
74
  }
83
75
  );
84
76
 
85
- // Update form state store from derived stores
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
+
86
85
  $: {
87
86
  formState.set({
88
87
  values: $values,
@@ -93,14 +92,10 @@
93
92
  });
94
93
  }
95
94
 
96
- // Derive value of whole form
97
95
  $: formValue = deriveFormValue(initialValues, $values, $enrichments);
98
96
 
99
- // Create data context to provide
100
97
  $: dataContext = {
101
98
  ...formValue,
102
-
103
- // These static values are prefixed to avoid clashes with actual columns
104
99
  __value: formValue,
105
100
  __valid: valid,
106
101
  __dirty: $dirty,
@@ -108,8 +103,6 @@
108
103
  __currentStepValid: $currentStepValid,
109
104
  };
110
105
 
111
- // Generates a derived store from an array of fields, comprised of a map of
112
- // extracted values from the field array
113
106
  const deriveFieldProperty = (
114
107
  fieldStores: Readable<FieldInfo>[],
115
108
  getProp: (_field: FieldInfo) => any
@@ -122,8 +115,19 @@
122
115
  });
123
116
  };
124
117
 
125
- // Derives any enrichments which need to be made so that bindings work for
126
- // special data types like attachments
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
+
127
131
  const deriveBindingEnrichments = (fieldStores: Readable<FieldInfo>[]) => {
128
132
  return derived(fieldStores, (fieldValues) => {
129
133
  const enrichments: Record<string, string> = {};
@@ -141,16 +145,12 @@
141
145
  });
142
146
  };
143
147
 
144
- // Derive the overall form value and deeply set all field paths so that we
145
- // can support things like JSON fields.
146
148
  const deriveFormValue = (
147
149
  initialValues: Record<string, any> | undefined,
148
150
  values: Record<string, any>,
149
151
  enrichments: Record<string, string>
150
152
  ) => {
151
153
  let formValue = cloneDeep(initialValues || {});
152
-
153
- // We need to sort the keys to avoid a JSON field overwriting a nested field
154
154
  const sortedFields = Object.entries(values || {})
155
155
  .map(([key, value]) => {
156
156
  const field = getField(key);
@@ -164,7 +164,6 @@
164
164
  return a.lastUpdate - b.lastUpdate;
165
165
  });
166
166
 
167
- // Merge all values and enrichments into a single value
168
167
  sortedFields.forEach(({ key, value }) => {
169
168
  deepSet(formValue, key, value);
170
169
  });
@@ -174,19 +173,15 @@
174
173
  return formValue;
175
174
  };
176
175
 
177
- // Searches the field array for a certain field
178
176
  const getField = (name: string) => {
179
177
  return fields.find((field) => get(field).name === name)!;
180
178
  };
181
179
 
182
- // Sanitises a value by ensuring it doesn't contain any invalid data
183
180
  const sanitiseValue = (
184
181
  value: any,
185
182
  schema: FieldSchema | undefined,
186
- type: FieldType
183
+ type: `${FieldType}`
187
184
  ) => {
188
- // Check arrays - remove any values not present in the field schema and
189
- // convert any values supplied to strings
190
185
  if (Array.isArray(value) && type === "array" && schema) {
191
186
  const options = schema?.constraints?.inclusion || [];
192
187
  return value
@@ -209,7 +204,6 @@
209
204
  if (!field) {
210
205
  return;
211
206
  }
212
- // Create validation function based on field schema
213
207
  const schemaConstraints = disableSchemaValidation
214
208
  ? null
215
209
  : schema?.[field]?.constraints;
@@ -220,10 +214,8 @@
220
214
  definition
221
215
  );
222
216
 
223
- // Sanitise the default value to ensure it doesn't contain invalid data
224
217
  defaultValue = sanitiseValue(defaultValue, schema?.[field], type);
225
218
 
226
- // If we've already registered this field then keep some existing state
227
219
  let initialValue = deepGet(initialValues, field) ?? defaultValue;
228
220
  let initialError = null;
229
221
  let fieldId = `id-${uuid()}`;
@@ -231,24 +223,16 @@
231
223
  if (existingField) {
232
224
  const { fieldState } = get(existingField);
233
225
  fieldId = fieldState.fieldId;
234
-
235
- // Determine the initial value for this field, reusing the current
236
- // value if one exists
237
226
  if (fieldState.value != null && fieldState.value !== "") {
238
227
  initialValue = fieldState.value;
239
228
  }
240
-
241
- // If this field has already been registered and we previously had an
242
- // error set, then re-run the validator to see if we can unset it
243
229
  if (fieldState.error) {
244
230
  initialError = validator?.(initialValue);
245
231
  }
246
232
  }
247
233
 
248
- // Auto columns are always disabled
249
234
  const isAutoColumn = !!schema?.[field]?.autocolumn;
250
235
 
251
- // Construct field info
252
236
  const fieldInfo = writable<FieldInfo>({
253
237
  name: field,
254
238
  type,
@@ -269,7 +253,6 @@
269
253
  fieldSchema: schema?.[field] ?? {},
270
254
  });
271
255
 
272
- // Add this field
273
256
  if (existingField) {
274
257
  const otherFields = fields.filter((info) => get(info).name !== field);
275
258
  fields = [...otherFields, fieldInfo];
@@ -283,8 +266,6 @@
283
266
  const stepFields = fields.filter(
284
267
  (field) => get(field).step === get(currentStep)
285
268
  );
286
- // We want to validate every field (even if validation fails early) to
287
- // ensure that all fields are populated with errors if invalid
288
269
  let valid = true;
289
270
  let hasScrolled = false;
290
271
  stepFields.forEach((field) => {
@@ -295,14 +276,13 @@
295
276
  hasScrolled = true;
296
277
  }
297
278
  });
298
-
299
279
  return valid;
300
280
  },
301
281
  reset: () => {
302
- // Reset the form by resetting each individual field
303
282
  fields.forEach((field) => {
304
283
  get(field).fieldApi.reset();
305
284
  });
285
+ dispatch("reset");
306
286
  },
307
287
  changeStep: ({
308
288
  type,
@@ -344,20 +324,16 @@
344
324
  },
345
325
  };
346
326
 
347
- // Creates an API for a specific field
348
327
  const makeFieldApi = (field: string) => {
349
- // Sets the value for a certain field and invokes validation
350
328
  const setValue = (value: any, skipCheck = false) => {
351
329
  const fieldInfo = getField(field);
352
330
  const { fieldState } = get(fieldInfo);
353
331
  const { validator } = fieldState;
354
332
 
355
- // Skip if the value is the same
356
333
  if (!skipCheck && fieldState.value === value) {
357
334
  return false;
358
335
  }
359
336
 
360
- // Update field state
361
337
  const error = validator?.(value);
362
338
  fieldInfo.update((state) => {
363
339
  state.fieldState.value = value;
@@ -365,17 +341,15 @@
365
341
  state.fieldState.lastUpdate = Date.now();
366
342
  return state;
367
343
  });
368
-
344
+ dispatch("change", { field, value });
369
345
  return true;
370
346
  };
371
347
 
372
- // Clears the value of a certain field back to the default value
373
348
  const reset = () => {
374
349
  const fieldInfo = getField(field);
375
350
  const { fieldState } = get(fieldInfo);
376
351
  const newValue = fieldState.defaultValue;
377
352
 
378
- // Update field state
379
353
  fieldInfo.update((state) => {
380
354
  state.fieldState.value = newValue;
381
355
  state.fieldState.error = null;
@@ -384,8 +358,6 @@
384
358
  });
385
359
  };
386
360
 
387
- // We don't want to actually remove the field state when deregistering, just
388
- // remove any errors and validation
389
361
  const deregister = () => {
390
362
  const fieldInfo = getField(field);
391
363
  fieldInfo.update((state) => {
@@ -395,14 +367,9 @@
395
367
  });
396
368
  };
397
369
 
398
- // Updates the disabled state of a certain field
399
370
  const setDisabled = (fieldDisabled: boolean) => {
400
371
  const fieldInfo = getField(field);
401
-
402
- // Auto columns are always disabled
403
372
  const isAutoColumn = !!schema?.[field]?.autocolumn;
404
-
405
- // Update disabled state
406
373
  fieldInfo.update((state) => {
407
374
  state.fieldState.disabled = disabled || fieldDisabled || isAutoColumn;
408
375
  return state;
@@ -415,7 +382,6 @@
415
382
  setDisabled,
416
383
  deregister,
417
384
  validate: () => {
418
- // Validate the field by force setting the same value again
419
385
  const fieldInfo = getField(field);
420
386
  setValue(get(fieldInfo).fieldState.value, true);
421
387
  return !get(fieldInfo).fieldState.error;
@@ -423,18 +389,12 @@
423
389
  };
424
390
  };
425
391
 
426
- // Provide form state and api for full control by children
427
392
  setContext("form", {
428
393
  formState,
429
394
  formApi,
430
-
431
- // Datasource is needed by attachment fields to be able to upload files
432
- // to the correct table ID
433
395
  dataSource,
434
396
  });
435
397
 
436
- // Provide form step context so that forms without any step components
437
- // register their fields to step 1
438
398
  setContext("form-step", writable(1));
439
399
 
440
400
  const handleUpdateFieldValue = ({
@@ -474,7 +434,6 @@
474
434
  }
475
435
  };
476
436
 
477
- // Action context to pass to children
478
437
  const actions = [
479
438
  { type: ActionTypes.ValidateForm, callback: formApi.validate },
480
439
  { type: ActionTypes.ClearForm, callback: formApi.reset },
@@ -483,27 +442,23 @@
483
442
  { type: ActionTypes.ScrollTo, callback: handleScrollToField },
484
443
  ];
485
444
 
486
- // Helper functions
487
- function uuid() {
488
- return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(
489
- /[xy]/g,
490
- function (c) {
491
- var r = (Math.random() * 16) | 0,
492
- v = c == "x" ? r : (r & 0x3) | 0x8;
493
- return v.toString(16);
494
- }
495
- );
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
+ });
496
452
  }
497
453
 
498
- function cloneDeep(obj: any) {
499
- if (obj === null || typeof obj !== "object") {
454
+ // Deep clone an object
455
+ function cloneDeep<T>(obj: T): T {
456
+ if (obj == null || typeof obj !== "object") {
500
457
  return obj;
501
458
  }
502
-
503
459
  if (Array.isArray(obj)) {
504
- return obj.map((item) => cloneDeep(item));
460
+ return obj.map(cloneDeep) as any;
505
461
  }
506
-
507
462
  const cloned: any = {};
508
463
  for (const key in obj) {
509
464
  if (Object.prototype.hasOwnProperty.call(obj, key)) {
@@ -513,46 +468,44 @@
513
468
  return cloned;
514
469
  }
515
470
 
516
- function deepGet(obj: any, path: string) {
517
- if (!obj) return undefined;
518
- const parts = path.split(".");
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(".");
519
477
  let current = obj;
520
-
521
- for (let i = 0; i < parts.length; i++) {
522
- if (current === null || current === undefined) {
478
+ for (const key of pathArray) {
479
+ if (current == null) {
523
480
  return undefined;
524
481
  }
525
- current = current[parts[i]];
482
+ current = current[key];
526
483
  }
527
-
528
484
  return current;
529
485
  }
530
486
 
531
- function deepSet(obj: any, path: string, value: any) {
532
- if (!obj) return;
533
- const parts = path.split(".");
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(".");
534
493
  let current = obj;
535
-
536
- for (let i = 0; i < parts.length - 1; i++) {
537
- const part = parts[i];
538
- if (!current[part] || typeof current[part] !== "object") {
539
- current[part] = {};
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] = {};
540
498
  }
541
- current = current[part];
499
+ current = current[key];
542
500
  }
543
-
544
- current[parts[parts.length - 1]] = value;
501
+ current[pathArray[pathArray.length - 1]] = value;
545
502
  }
546
503
  </script>
547
504
 
548
505
  {#if provideContext}
549
506
  <Provider {actions} data={dataContext}>
550
- <div class={size}>
551
- <slot />
552
- </div>
507
+ <slot />
553
508
  </Provider>
554
509
  {:else}
555
- <div class={size}>
556
- <slot />
557
- </div>
510
+ <slot />
558
511
  {/if}
@@ -2,23 +2,37 @@
2
2
  import { getContext } from "svelte";
3
3
  import InnerForm from "./InnerForm.svelte";
4
4
  import { writable } from "svelte/store";
5
- import type { FieldSchema } from "@budibase/types";
5
+ import type {
6
+ DataFetchDatasource,
7
+ Table,
8
+ TableSchema,
9
+ } from "@budibase/types";
6
10
 
7
- export let dataSource: any;
8
- export let size: "Medium" | "Large" = "Medium";
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";
9
24
  export let disabled: boolean = false;
10
25
  export let readonly: boolean = false;
11
- export let actionType: "Create" | "Update" | "View" = "Create";
26
+ export let actionType: "Create" = "Create";
12
27
  export let initialFormStep: string | number = 1;
13
-
14
- // Not exposed as a builder setting. Used internally to disable validation
15
- // for fields rendered in things like search blocks.
16
28
  export let disableSchemaValidation: boolean = false;
17
-
18
- // Not exposed as a builder setting. Used internally to allow searching on
19
- // auto columns.
20
29
  export let editAutoColumns: boolean = false;
21
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
+
22
36
  const context = getContext("context");
23
37
  const component = getContext("component");
24
38
  const { fetchDatasourceSchema, fetchDatasourceDefinition } =
@@ -32,8 +46,8 @@
32
46
  return parsedFormStep;
33
47
  };
34
48
 
35
- let definition: any | undefined;
36
- let schema: Record<string, FieldSchema> | undefined;
49
+ let definition: Table | undefined;
50
+ let schema: TableSchema | undefined;
37
51
  let loaded = false;
38
52
  let currentStep =
39
53
  getContext("current-step") || writable(getInitialFormStep());
@@ -50,34 +64,30 @@
50
64
  schemaKey + JSON.stringify(initialValues) + disabled + readonly
51
65
  );
52
66
 
53
- // Returns the closes data context which isn't a built in context
54
67
  const getInitialValues = (
55
68
  type: string,
56
- dataSource: any,
69
+ dataSource: DataFetchDatasource,
57
70
  path: string[],
58
71
  context: Record<string, any>
59
72
  ) => {
60
- // Only inherit values for update forms
61
73
  if (type !== "Update") {
62
74
  return {};
63
75
  }
64
- // Only inherit values for forms targeting internal tables
65
76
  const dsType = dataSource?.type;
66
77
  if (dsType !== "table" && dsType !== "viewV2") {
67
78
  return {};
68
79
  }
69
- // Look up the component tree and find something that is provided by an
70
- // ancestor that matches our datasource. This is for backwards compatibility
71
- // as previously we could use the "closest" context.
72
- for (let id of [...path].reverse().slice(1)) {
73
- // Check for matching view datasource
80
+
81
+ if (row && dsType === "table" && row?.tableId === dataSource.tableId) {
82
+ return row;
83
+ }
84
+ for (let id of path.toReversed().slice(1)) {
74
85
  if (
75
86
  dataSource.type === "viewV2" &&
76
87
  context[id]?._viewId === dataSource.id
77
88
  ) {
78
89
  return context[id];
79
90
  }
80
- // Check for matching table datasource
81
91
  if (
82
92
  dataSource.type === "table" &&
83
93
  context[id]?.tableId === dataSource.tableId
@@ -88,8 +98,7 @@
88
98
  return {};
89
99
  };
90
100
 
91
- // Fetches the form schema from this form's dataSource
92
- const fetchSchema = async (dataSource: any) => {
101
+ const fetchSchema = async (dataSource: DataFetchDatasource) => {
93
102
  try {
94
103
  definition = await fetchDatasourceDefinition(dataSource);
95
104
  } catch (error) {
@@ -102,12 +111,7 @@
102
111
  }
103
112
  };
104
113
 
105
- // Generates a predictable string that uniquely identifies a schema. We can't
106
- // simply stringify the whole schema as there are array fields which have
107
- // random order.
108
- const generateSchemaKey = (
109
- schema: Record<string, FieldSchema> | undefined
110
- ) => {
114
+ const generateSchemaKey = (schema: TableSchema | undefined) => {
111
115
  if (!schema) {
112
116
  return null;
113
117
  }
@@ -115,23 +119,13 @@
115
119
  fields.sort();
116
120
  return fields.map((field) => `${field}:${schema[field].type}`).join("-");
117
121
  };
118
-
119
- // Helper function to generate a hash string from input
120
- function hashString(str: string) {
121
- let hash = 0;
122
- if (str.length === 0) return hash.toString();
123
- for (let i = 0; i < str.length; i++) {
124
- const char = str.charCodeAt(i);
125
- hash = (hash << 5) - hash + char;
126
- hash = hash & hash; // Convert to 32bit integer
127
- }
128
- return hash.toString();
129
- }
130
122
  </script>
131
123
 
132
124
  {#if loaded}
133
125
  {#key resetKey}
134
126
  <InnerForm
127
+ bind:form
128
+ bind:formState
135
129
  {dataSource}
136
130
  {size}
137
131
  {disabled}
@@ -142,6 +136,8 @@
142
136
  {disableSchemaValidation}
143
137
  {editAutoColumns}
144
138
  {currentStep}
139
+ on:change
140
+ on:reset
145
141
  >
146
142
  <slot />
147
143
  </InnerForm>