@stubber/form-fields 1.6.1 → 1.6.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.
@@ -15,7 +15,8 @@
15
15
  </script>
16
16
 
17
17
  <script>import { Button } from "@stubber/ui/button";
18
- import SelectField, {} from "./select-field.svelte";
18
+ import SelectField, {
19
+ } from "./select-field.svelte";
19
20
  export let fieldStore;
20
21
  const params = $fieldStore.params || {};
21
22
  const dependencies = $fieldStore.formDependencies;
@@ -25,54 +26,51 @@ const show_create_contact_modal = () => {
25
26
  const orguuid = dependencies?.stubber?.orguuid;
26
27
  createWantToModal("_create_new_contact", stubref, orguuid);
27
28
  };
28
- const field = $fieldStore;
29
- if (field.params) {
30
- field.params.load_options = async (search_term) => {
31
- const socket = dependencies?.clienthub?.socket;
32
- const orguuid = dependencies?.stubber?.orguuid;
33
- const stubref = dependencies?.stubber?.stubref;
34
- if (!socket || !orguuid || !stubref) {
35
- console.error("ClientHub socket or stubber details not available.");
36
- return [];
37
- }
38
- const details = {
39
- resource_name: "contacts"
40
- };
41
- details.params = $fieldStore.params || {};
42
- details.params.orguuid = orguuid;
43
- details.params.stubref = stubref;
44
- details.params.input = search_term;
45
- if (details.params.limit === void 0) {
46
- details.params.limit = 50;
47
- }
48
- console.log("Loading contacts with details:", details);
49
- return new Promise((resolve, reject) => {
50
- socket.emit("request", { type: "resource", details }, (res) => {
51
- if (res.success) {
52
- console.log("Contacts loaded:", res);
53
- const contacts = res.payload?.contacts || [];
54
- const result = contacts.map((contact) => {
55
- let label = contact._default_label;
56
- if (params.label && contact[params.label]) {
57
- label = contact[params.label];
58
- }
59
- return {
60
- value: contact,
61
- label
62
- };
63
- });
64
- resolve(result);
65
- } else {
66
- console.error("Failed to load contacts:", res);
67
- resolve([]);
68
- }
69
- });
70
- });
29
+ const load_options = async (search_term) => {
30
+ const socket = dependencies?.clienthub?.socket;
31
+ const orguuid = dependencies?.stubber?.orguuid;
32
+ const stubref = dependencies?.stubber?.stubref;
33
+ if (!socket || !orguuid || !stubref) {
34
+ console.error("ClientHub socket or stubber details not available.");
35
+ return [];
36
+ }
37
+ const details = {
38
+ resource_name: "contacts"
71
39
  };
72
- }
40
+ details.params = $fieldStore.params || {};
41
+ details.params.orguuid = orguuid;
42
+ details.params.stubref = stubref;
43
+ details.params.input = search_term;
44
+ if (details.params.limit === void 0) {
45
+ details.params.limit = 50;
46
+ }
47
+ console.log("Loading contacts with details:", details);
48
+ return new Promise((resolve, reject) => {
49
+ socket.emit("request", { type: "resource", details }, (res) => {
50
+ if (res.success) {
51
+ console.log("Contacts loaded:", res);
52
+ const contacts = res.payload?.contacts || [];
53
+ const result = contacts.map((contact) => {
54
+ let label = contact._default_label;
55
+ if (params.label && contact[params.label]) {
56
+ label = contact[params.label];
57
+ }
58
+ return {
59
+ value: contact,
60
+ label
61
+ };
62
+ });
63
+ resolve(result);
64
+ } else {
65
+ console.error("Failed to load contacts:", res);
66
+ resolve([]);
67
+ }
68
+ });
69
+ });
70
+ };
73
71
  </script>
74
72
 
75
- <SelectField {fieldStore}>
73
+ <SelectField {fieldStore} {load_options}>
76
74
  <Button
77
75
  on:click={show_create_contact_modal}
78
76
  variant="ghost"
@@ -1,16 +1,15 @@
1
1
  import { SvelteComponent } from "svelte";
2
2
  import type { Writable } from "svelte/store";
3
3
  import type { IBaseField, IBuiltField, IInitialForm } from "../interfaces";
4
- export interface IContactSelectorFieldParams {
4
+ export interface IContactSelectorFieldParams extends ISelectFieldParams {
5
5
  label?: string;
6
6
  limit?: number;
7
- load_options?: (search_term: string) => Promise<ISelectFieldOption[]>;
8
7
  }
9
8
  export declare const contact_selector_field_param_spec: IInitialForm;
10
9
  export interface IContactSelectorField extends IBaseField<IContactSelectorFieldParams> {
11
10
  fieldtype: "contactselector";
12
11
  }
13
- import { type ISelectFieldOption } from "./select-field.svelte";
12
+ import { type ISelectFieldParams } from "./select-field.svelte";
14
13
  declare const __propDef: {
15
14
  props: {
16
15
  fieldStore: Writable<IBuiltField<IContactSelectorFieldParams>>;
@@ -130,7 +130,7 @@ $: selectedValue = currency_list.find((c) => c.value === current_details?.curren
130
130
 
131
131
  <FieldLabel {fieldStore} />
132
132
  <div
133
- class="flex-row items-center gap-2 border-input bg-background selection:bg-primary dark:bg-input/30 selection:text-primary-foreground ring-offset-background placeholder:text-muted-foreground shadow-xs flex h-9 w-full min-w-0 rounded-md border px-3 py-1 text-base outline-none transition-[color,box-shadow] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-[invalid]:border-destructive aria-[invalid]:ring-destructive/50 aria-[invalid]:focus-visible:ring-destructive/50"
133
+ class="flex h-9 w-full min-w-0 flex-row items-center gap-2 rounded-md border border-input bg-background px-3 py-1 text-base outline-none ring-offset-background transition-[color,box-shadow] selection:bg-primary selection:text-primary-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-[invalid]:border-destructive aria-[invalid]:ring-destructive/50 aria-[invalid]:focus-visible:ring-destructive/50 md:text-sm dark:bg-input/30"
134
134
  >
135
135
  <!-- currency indicator front left -->
136
136
  <div class="pointer-events-none text-gray-500">
@@ -143,7 +143,7 @@ $: selectedValue = currency_list.find((c) => c.value === current_details?.curren
143
143
  {/if}
144
144
  </div>
145
145
  <input
146
- class="w-full focus-within:outline-none bg-transparent"
146
+ class="w-full bg-transparent focus-within:outline-none"
147
147
  use:inputRegexMask={currencyRegex}
148
148
  value={initial_ui_value}
149
149
  inputmode="decimal"
@@ -160,13 +160,13 @@ $: selectedValue = currency_list.find((c) => c.value === current_details?.curren
160
160
  variant="outline"
161
161
  role="combobox"
162
162
  aria-expanded={open}
163
- class="w-28 border-none bg-transparent px-1 justify-between"
163
+ class="w-28 justify-between border-none bg-transparent px-1"
164
164
  >
165
165
  {selectedValue}
166
166
  <i class="fas fa-sort ml-2 h-4 w-4 shrink-0 opacity-50" />
167
167
  </Button>
168
168
  </Popover.Trigger>
169
- <Popover.Content class="p-0 max-h-[50vh] overflow-y-scroll">
169
+ <Popover.Content class="max-h-[50vh] overflow-y-scroll p-0">
170
170
  <Command.Root>
171
171
  <Command.Input placeholder="Search currencies..." />
172
172
  <Command.Empty>No currency found.</Command.Empty>
@@ -33,6 +33,7 @@ import FieldLabel from "../FieldLabel.svelte";
33
33
  import FieldMessage from "../FieldMessage.svelte";
34
34
  import { set_value_details } from "../utils";
35
35
  export let fieldStore;
36
+ export let load_options = void 0;
36
37
  let open = false;
37
38
  let loading = false;
38
39
  let items = [];
@@ -70,19 +71,18 @@ function closeAndFocusTrigger(triggerId) {
70
71
  let last_search = void 0;
71
72
  const handle_search = async (search) => {
72
73
  last_search = search;
73
- if (params.load_options) {
74
+ if (load_options) {
74
75
  loading = true;
75
- const remote_results = await params.load_options($state.search);
76
+ const remote_results = await load_options($state.search);
76
77
  loading = false;
77
78
  items = map_field_options(remote_results);
78
79
  }
79
80
  };
80
81
  let debounced_handle_search = null;
81
- if (params.load_options) {
82
+ if (load_options) {
82
83
  debounced_handle_search = debounce(handle_search, 300);
83
84
  }
84
- $: if (params.load_options && $state.search !== void 0 && $state.search !== last_search) {
85
- console.log("doing debounced search", !!debounced_handle_search);
85
+ $: if (load_options && $state.search !== void 0 && $state.search !== last_search) {
86
86
  if (debounced_handle_search) debounced_handle_search($state.search);
87
87
  }
88
88
  const handle_select = (item_key, trigger) => {
@@ -110,7 +110,7 @@ const handle_select = (item_key, trigger) => {
110
110
  </Button>
111
111
  </Popover.Trigger>
112
112
  <Popover.Content sameWidth class="p-0 z-[100]">
113
- <Command.Root shouldFilter={!params.load_options} {state}>
113
+ <Command.Root shouldFilter={!load_options} {state}>
114
114
  <Command.Input class="border-none outline-none focus:ring-0" placeholder="Search..." />
115
115
  <Command.List>
116
116
  {#if loading}
@@ -7,7 +7,6 @@ export interface ISelectFieldOption {
7
7
  }
8
8
  export interface ISelectFieldParams {
9
9
  options?: ISelectFieldOption[];
10
- load_options?: (search_term: string) => Promise<ISelectFieldOption[]>;
11
10
  }
12
11
  export declare const select_field_param_spec: IInitialForm;
13
12
  export interface ISelectField extends IBaseField<ISelectFieldParams> {
@@ -16,6 +15,7 @@ export interface ISelectField extends IBaseField<ISelectFieldParams> {
16
15
  declare const __propDef: {
17
16
  props: {
18
17
  fieldStore: Writable<IBuiltField<ISelectFieldParams>>;
18
+ load_options?: ((search_term: string) => Promise<ISelectFieldOption[]>) | undefined;
19
19
  };
20
20
  events: {
21
21
  [evt: string]: CustomEvent<any>;
@@ -61,9 +61,6 @@ const load_options = async (search_term) => {
61
61
  });
62
62
  });
63
63
  };
64
- if ($fieldStore.params) {
65
- $fieldStore.params.load_options = load_options;
66
- }
67
64
  </script>
68
65
 
69
- <SelectField {fieldStore} />
66
+ <SelectField {fieldStore} {load_options} />
@@ -121,12 +121,30 @@ const contentField = StateField.define({
121
121
  },
122
122
  update(value, transaction) {
123
123
  if (transaction.docChanged) {
124
- $fieldStore.value = parse_string ? parse_string_value(transaction.newDoc.toString()) : transaction.newDoc.toString();
125
- return transaction.newDoc.toString();
124
+ let new_value = transaction.newDoc.toString();
125
+ if (new_value !== $fieldStore.value) {
126
+ $fieldStore.value = parse_string ? parse_string_value(new_value) : new_value;
127
+ }
128
+ return new_value;
126
129
  }
127
130
  return value;
128
131
  }
129
132
  });
133
+ $: update_editor_content($fieldStore.value);
134
+ function update_editor_content(new_value) {
135
+ if (editor_view) {
136
+ const current_value = editor_view.state.doc.toString();
137
+ if (new_value !== current_value && typeof new_value === "string") {
138
+ editor_view.dispatch({
139
+ changes: {
140
+ from: 0,
141
+ to: current_value.length,
142
+ insert: new_value
143
+ }
144
+ });
145
+ }
146
+ }
147
+ }
130
148
  function fake_handlebars_lang() {
131
149
  const curly_decorator = new MatchDecorator({
132
150
  regexp: /(\{\{[^}]*\}\})|(~~[^\s]*)/g,
@@ -180,43 +198,63 @@ function parse_string_value(value) {
180
198
 
181
199
  <FieldLabel {fieldStore} />
182
200
  {#if !is_object}
183
- <div
184
- use:setup_editor
185
- class=" stubber-cm text-sm {isValid ? 'stubber-valid' : 'stubber-invalid'}"
186
- />
201
+ <div use:setup_editor class="stubber-cm" aria-invalid={!isValid} />
187
202
  {:else}
188
203
  <p class="text-surface-400">The value of this field is not a string.</p>
189
204
  {/if}
190
205
  <FieldMessage {fieldStore} />
191
206
 
207
+ <!-- below styles are basically this tailwind string:
208
+ flex h-9 w-full min-w-0 flex-row items-center gap-2 rounded-md border border-input bg-background px-3 py-1 text-base outline-none ring-offset-background transition-[color,box-shadow] selection:bg-primary selection:text-primary-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-[invalid]:border-destructive aria-[invalid]:ring-destructive/50 aria-[invalid]:focus-visible:ring-destructive/50 md:text-sm dark:bg-input/30 -->
209
+
192
210
  <style>
193
211
  .stubber-cm :global(.cm-editor) {
194
- /* Base styles */
195
- background-color: rgba(255, 255, 255, 0.85);
212
+ width: 100%;
213
+ min-width: 0;
196
214
  border-radius: 0.375rem;
215
+ border-width: 1px;
197
216
  --tw-border-opacity: 1;
198
217
  border-color: hsl(var(--input) / var(--tw-border-opacity, 1));
199
- border-width: 1px;
200
- border-style: solid;
201
- width: 100%;
202
- color: rgb(31 41 51);
218
+ --tw-bg-opacity: 1;
219
+ background-color: hsl(var(--background) / var(--tw-bg-opacity, 1));
203
220
 
204
- /* Top inset shadow only */
205
- box-shadow: inset 0px 16px 16px -16px rgba(0, 0, 0, 0.1333);
221
+ font-size: 0.875rem;
222
+ line-height: 1.25rem;
223
+ outline: 2px solid transparent;
224
+ outline-offset: 2px;
225
+ --tw-ring-offset-color: hsl(var(--background) / 1);
226
+ transition-property: color, box-shadow;
227
+ transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
228
+ transition-duration: 150ms;
206
229
  }
207
230
 
208
- .stubber-cm :global(.cm-editor.cm-focused) {
209
- outline: none;
210
- border-width: 1px;
211
- border-color: hsl(var(--input) / var(--tw-border-opacity, 1));
231
+ @media (prefers-color-scheme: dark) {
232
+ .stubber-cm :global(.cm-editor) {
233
+ background-color: hsl(var(--input) / 0.3);
234
+ }
235
+ }
236
+
237
+ .stubber-cm[aria-invalid="true"] :global(.cm-editor) {
238
+ --tw-border-opacity: 1;
239
+ border-color: hsl(var(--destructive) / var(--tw-border-opacity, 1));
240
+ --tw-ring-color: hsl(var(--destructive) / 0.5);
212
241
  }
213
242
 
214
- .stubber-cm.stubber-valid :global(.cm-editor.cm-focused) {
215
- border-color: rgb(170, 170, 170, 0.5);
243
+ .stubber-cm :global(.cm-editor.cm-focused) {
244
+ --tw-border-opacity: 1;
245
+ border-color: hsl(var(--ring) / var(--tw-border-opacity, 1));
246
+ --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width)
247
+ var(--tw-ring-offset-color);
248
+ --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(3px + var(--tw-ring-offset-width))
249
+ var(--tw-ring-color);
250
+ box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
251
+ --tw-ring-color: hsl(var(--ring) / 0.5);
216
252
  }
217
253
 
218
- .stubber-cm.stubber-invalid :global(.cm-editor) {
219
- border-color: rgb(225 45 57);
254
+ .stubber-cm[aria-invalid="true"] :global(.cm-editor.cm-focused) {
255
+ --tw-ring-color: hsl(var(--destructive) / 0.5);
256
+ --tw-border-opacity: 1;
257
+ border-color: hsl(var(--destructive) / var(--tw-border-opacity, 1));
220
258
  }
221
259
 
222
260
  .stubber-cm :global(.cm-editor .cm-scroller) {
@@ -236,6 +274,7 @@ function parse_string_value(value) {
236
274
  }
237
275
 
238
276
  .stubber-cm :global(.cm-editor .cm-placeholder) {
277
+ --tw-text-opacity: 1;
239
278
  color: hsl(var(--muted-foreground) / var(--tw-text-opacity, 1));
240
279
  }
241
280
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stubber/form-fields",
3
- "version": "1.6.1",
3
+ "version": "1.6.3",
4
4
  "description": "An automatic form builder based on field specifications",
5
5
  "keywords": [
6
6
  "components",
@@ -50,6 +50,7 @@
50
50
  "postcss": "^8.4.28",
51
51
  "prettier": "^2.8.0",
52
52
  "prettier-plugin-svelte": "^2.10.1",
53
+ "prettier-plugin-tailwindcss": "^0.4.1",
53
54
  "publint": "^0.2.2",
54
55
  "socket.io-client": "^4.8.1",
55
56
  "svelte-check": "^3.4.3",
@@ -65,7 +66,7 @@
65
66
  "@codemirror/lang-javascript": "^6.2.4",
66
67
  "@codemirror/state": "^6.5.2",
67
68
  "@codemirror/view": "^6.38.4",
68
- "@stubber/ui": "^1.13.5",
69
+ "@stubber/ui": "^1.13.6",
69
70
  "ag-grid-community": "^31.0.2",
70
71
  "ag-grid-enterprise": "^31.0.2",
71
72
  "codemirror": "^6.0.2",