@omnitend/dashboard-for-laravel 0.22.0 → 0.24.0

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.
@@ -61,8 +61,8 @@
61
61
  <DFormCheckbox
62
62
  v-else
63
63
  v-model="fieldValue"
64
- :disabled="isDisabled || isReadonly"
65
- v-bind="field.inputProps"
64
+ :disabled="isDisabled || isReadonly || isPlaintext"
65
+ v-bind="controlPropsWithGuards"
66
66
  >
67
67
  <DXFieldLabel :label="resolvedLabel" class="visually-hidden" />
68
68
  </DFormCheckbox>
@@ -97,8 +97,8 @@
97
97
  <DFormCheckbox
98
98
  v-else
99
99
  v-model="fieldValue"
100
- :disabled="isDisabled || isReadonly"
101
- v-bind="field.inputProps"
100
+ :disabled="isDisabled || isReadonly || isPlaintext"
101
+ v-bind="controlPropsWithGuards"
102
102
  >
103
103
  <DXFieldLabel :label="resolvedLabel" :info="resolvedInfo" />
104
104
  </DFormCheckbox>
@@ -159,8 +159,8 @@
159
159
  <DXSwitch
160
160
  v-else
161
161
  v-model="switchModel"
162
- :disabled="isDisabled || isReadonly"
163
- v-bind="field.inputProps"
162
+ :disabled="isDisabled || isReadonly || isPlaintext"
163
+ v-bind="controlPropsWithGuards"
164
164
  >
165
165
  <DXFieldLabel
166
166
  :label="switchText"
@@ -201,8 +201,8 @@
201
201
  <DXSwitch
202
202
  v-else
203
203
  v-model="switchModel"
204
- :disabled="isDisabled || isReadonly"
205
- v-bind="field.inputProps"
204
+ :disabled="isDisabled || isReadonly || isPlaintext"
205
+ v-bind="controlPropsWithGuards"
206
206
  >
207
207
  <DXFieldLabel :label="switchText" :info="resolvedInfo" />
208
208
  </DXSwitch>
@@ -306,8 +306,8 @@
306
306
  v-model="fieldValue"
307
307
  :field="field"
308
308
  :model="model"
309
- :disabled="isDisabled || isReadonly"
310
- v-bind="field.inputProps"
309
+ :disabled="isDisabled || isReadonly || isPlaintext"
310
+ v-bind="controlPropsWithGuards"
311
311
  />
312
312
 
313
313
  <!-- Textarea. Error clearing happens in the v-model setter. -->
@@ -319,10 +319,35 @@
319
319
  :rows="field.rows || 3"
320
320
  :state="fieldState"
321
321
  :disabled="isDisabled"
322
- :readonly="isReadonly"
323
- v-bind="field.inputProps"
322
+ :readonly="isReadonly || isPlaintext"
323
+ :plaintext="isPlaintext"
324
+ v-bind="inputPropsWithGuards"
324
325
  />
325
326
 
327
+ <!-- Searchable select: type to filter a long option list, while the
328
+ model keeps the option's VALUE (an id), not the typed text. The
329
+ `autocomplete` type below models the text, so it can't back a
330
+ foreign key. -->
331
+ <template v-else-if="field.type === 'select' && field.searchable">
332
+ <DAutocomplete
333
+ v-if="searchableOptionsReady"
334
+ v-model="fieldValue"
335
+ :options="resolvedOptions"
336
+ :placeholder="field.placeholder"
337
+ :required="field.required"
338
+ :state="fieldState"
339
+ :disabled="isDisabled || isReadonly || isPlaintext"
340
+ open-on-focus
341
+ v-bind="controlPropsWithGuards"
342
+ />
343
+ <!-- BAutocomplete resolves the input's display text from the model
344
+ AT MOUNT and doesn't re-derive it when `options` arrive later,
345
+ so an async list would leave the raw id showing ("51" instead
346
+ of "Waitrose") — and a long list is exactly the case that loads
347
+ async. Hold the control until the options are in. -->
348
+ <DFormInput v-else placeholder="Loading…" disabled />
349
+ </template>
350
+
326
351
  <!-- Select (sync or async options) -->
327
352
  <DFormSelect
328
353
  v-else-if="field.type === 'select'"
@@ -330,8 +355,8 @@
330
355
  :required="field.required"
331
356
  :options="resolvedOptions"
332
357
  :state="fieldState"
333
- :disabled="isDisabled || isReadonly"
334
- v-bind="field.inputProps"
358
+ :disabled="isDisabled || isReadonly || isPlaintext"
359
+ v-bind="controlPropsWithGuards"
335
360
  />
336
361
 
337
362
  <!-- Radio group -->
@@ -341,8 +366,8 @@
341
366
  :options="resolvedOptions"
342
367
  :required="field.required"
343
368
  :state="fieldState"
344
- :disabled="isDisabled || isReadonly"
345
- v-bind="field.inputProps"
369
+ :disabled="isDisabled || isReadonly || isPlaintext"
370
+ v-bind="controlPropsWithGuards"
346
371
  />
347
372
 
348
373
  <!-- File / image upload -->
@@ -352,8 +377,8 @@
352
377
  :accept="field.accept || (field.type === 'image' ? 'image/*' : undefined)"
353
378
  :required="field.required"
354
379
  :state="fieldState"
355
- :disabled="isDisabled || isReadonly"
356
- v-bind="field.inputProps"
380
+ :disabled="isDisabled || isReadonly || isPlaintext"
381
+ v-bind="controlPropsWithGuards"
357
382
  @change="handleFileChange"
358
383
  />
359
384
  <img
@@ -384,8 +409,9 @@
384
409
  :max="field.max"
385
410
  :state="fieldState"
386
411
  :disabled="isDisabled"
387
- :readonly="isReadonly"
388
- v-bind="field.inputProps"
412
+ :readonly="isReadonly || isPlaintext"
413
+ :plaintext="isPlaintext"
414
+ v-bind="inputPropsWithGuards"
389
415
  @input="handleCurrencyInput"
390
416
  @focus="handleCurrencyFocus"
391
417
  @blur="handleCurrencyBlur"
@@ -401,8 +427,9 @@
401
427
  :max="field.max"
402
428
  :state="fieldState"
403
429
  :disabled="isDisabled"
404
- :readonly="isReadonly"
405
- v-bind="field.inputProps"
430
+ :readonly="isReadonly || isPlaintext"
431
+ :plaintext="isPlaintext"
432
+ v-bind="inputPropsWithGuards"
406
433
  />
407
434
 
408
435
  <template v-if="field.type === 'percentage'" #append>
@@ -422,9 +449,10 @@
422
449
  :placeholder="field.placeholder"
423
450
  :state="fieldState"
424
451
  :disabled="isDisabled"
425
- :readonly="isReadonly"
452
+ :readonly="isReadonly || isPlaintext"
453
+ :plaintext="isPlaintext"
426
454
  autocomplete="off"
427
- v-bind="field.inputProps"
455
+ v-bind="inputPropsWithGuards"
428
456
  />
429
457
  <datalist :id="datalistId">
430
458
  <option
@@ -437,6 +465,31 @@
437
465
  </datalist>
438
466
  </template>
439
467
 
468
+ <!-- Password with a reveal toggle (the default for `type: "password"`).
469
+ Typing a long password blind is the one place the eye really earns
470
+ its keep, so it's opt-out (`revealable: false`) rather than opt-in. -->
471
+ <DInputGroup v-else-if="isRevealablePassword">
472
+ <DFormInput
473
+ v-model="textFieldModel"
474
+ :required="field.required"
475
+ :placeholder="field.placeholder"
476
+ :state="fieldState"
477
+ :disabled="isDisabled"
478
+ :readonly="isReadonly"
479
+ v-bind="passwordInputProps"
480
+ />
481
+ <template #append>
482
+ <DButton
483
+ variant="outline-secondary"
484
+ :icon="passwordRevealed ? 'eye-slash' : 'eye'"
485
+ :aria-label="passwordRevealed ? 'Hide password' : 'Show password'"
486
+ :aria-pressed="passwordRevealed"
487
+ :disabled="isDisabled"
488
+ @click="passwordRevealed = !passwordRevealed"
489
+ />
490
+ </template>
491
+ </DInputGroup>
492
+
440
493
  <!-- Text-based inputs (text/email/password/number/url/tel/date/time/datetime) -->
441
494
  <DFormInput
442
495
  v-else
@@ -449,8 +502,9 @@
449
502
  :max="field.max"
450
503
  :state="fieldState"
451
504
  :disabled="isDisabled"
452
- :readonly="isReadonly"
453
- v-bind="field.inputProps"
505
+ :readonly="isReadonly || isPlaintext"
506
+ :plaintext="isPlaintext"
507
+ v-bind="inputPropsWithGuards"
454
508
  />
455
509
 
456
510
  <!-- Validation error -->
@@ -505,6 +559,8 @@ import DXSwitch from "./DXSwitch.vue";
505
559
  import DFormInvalidFeedback from "../base/DFormInvalidFeedback.vue";
506
560
  import DFormText from "../base/DFormText.vue";
507
561
  import DInputGroup from "../base/DInputGroup.vue";
562
+ import DAutocomplete from "../base/DAutocomplete.vue";
563
+ import DButton from "../base/DButton.vue";
508
564
  import DXFieldLabel from "./DXFieldLabel.vue";
509
565
  import type { UseFormReturn } from "../../composables/useForm";
510
566
  import type { FieldDefinition, FieldOption, FieldType, LabelCols } from "../../types";
@@ -880,6 +936,77 @@ const isDisabled = computed(() => {
880
936
 
881
937
  const isReadonly = computed(() => resolveMaybe(props.field.readonly) ?? false);
882
938
 
939
+ // Display-only: the value as static text rather than a control. bvn's
940
+ // `plaintext` only swaps the class, so pair it with `readonly` at every call
941
+ // site — otherwise the "static text" is still editable.
942
+ const isPlaintext = computed(() => resolveMaybe(props.field.plaintext) ?? false);
943
+
944
+ const passwordRevealed = ref(false);
945
+
946
+ // See the template: the searchable select can't render before its options
947
+ // resolve, or it shows the raw id instead of the option's label.
948
+ const searchableOptionsReady = computed(
949
+ () => !props.field.optionsLoader || loadedOptions.value !== null,
950
+ );
951
+
952
+ const isRevealablePassword = computed(
953
+ () =>
954
+ props.field.type === "password" &&
955
+ (props.field.revealable ?? true) &&
956
+ !isPlaintext.value,
957
+ );
958
+
959
+ // DXTable reuses ONE DXField instance per field key across modal opens (the
960
+ // v-for is keyed on `field.key`, which is stable across rows) and swaps in a
961
+ // fresh form per record. Without this, revealing row A's password leaves row
962
+ // B's — and the create form's — showing in clear text.
963
+ watch(
964
+ () => [props.form, props.field.key],
965
+ () => {
966
+ passwordRevealed.value = false;
967
+ },
968
+ );
969
+
970
+ /*
971
+ * `inputProps` is an escape hatch that deliberately wins over the field's own
972
+ * bindings — it is spread last. But it must not win over the guarantees the
973
+ * field makes, or those guarantees are only documentation: a `plaintext` field
974
+ * that `inputProps: { readonly: false }` re-opens for editing isn't read-only,
975
+ * and a password whose `type` `inputProps` sets to "text" is unmasked under a
976
+ * toggle still reading "Show password". So the guards are re-applied ON TOP of
977
+ * inputProps. Everything else in inputProps still passes through, and a field
978
+ * that isn't plaintext/readonly is untouched — `inputProps: { readonly: true }`
979
+ * still works as before.
980
+ */
981
+
982
+ // For controls with a native readonly state (the text family + textarea).
983
+ const inputPropsWithGuards = computed(() => {
984
+ const merged: Record<string, any> = { ...props.field.inputProps };
985
+ if (isPlaintext.value) {
986
+ merged.plaintext = true;
987
+ merged.readonly = true;
988
+ } else if (isReadonly.value) {
989
+ merged.readonly = true;
990
+ }
991
+ return merged;
992
+ });
993
+
994
+ // For controls with NO native readonly state (select/radio/checkbox/switch/
995
+ // file/component), where read-only is expressed as `disabled`.
996
+ const controlPropsWithGuards = computed(() => {
997
+ const merged: Record<string, any> = { ...props.field.inputProps };
998
+ if (isDisabled.value || isReadonly.value || isPlaintext.value) {
999
+ merged.disabled = true;
1000
+ }
1001
+ return merged;
1002
+ });
1003
+
1004
+ // The reveal toggle owns the password input's `type`.
1005
+ const passwordInputProps = computed(() => ({
1006
+ ...inputPropsWithGuards.value,
1007
+ type: passwordRevealed.value ? "text" : "password",
1008
+ }));
1009
+
883
1010
  const inputType = computed<string>(() => {
884
1011
  const type: FieldType = props.field.type;
885
1012
  if (type === "datetime") return "datetime-local";