@merkaly/nuxt 0.12.3 → 0.12.4

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/README.md CHANGED
@@ -113,6 +113,39 @@ The path is `/phog` because ad blockers drop requests to paths such as
113
113
  `/posthog`, `/analytics`, `/tracking` or `/telemetry`; a first-party, neutral
114
114
  path keeps events flowing.
115
115
 
116
+ ## Notifications and modals
117
+
118
+ `useNotify()` is the standard for feedback and confirmations in every app:
119
+
120
+ | Need | Use |
121
+ |---|---|
122
+ | Short feedback | `$notify.toast({ title, variant })` |
123
+ | Confirm an action (text and two buttons) | `$notify.confirm({ title, message, confirmText, cancelText, variant, onConfirm })` |
124
+ | Modal with its own content (form, list) | a component with `<BModal>`, opened with `useModal().create({ component: markRaw(component), ...props }).show()` |
125
+
126
+ ```ts
127
+ const $notify = useNotify();
128
+
129
+ $notify.confirm({
130
+ cancelText: $t('actions.back'),
131
+ confirmText: $t('order.cancel.submit'),
132
+ message: $t('order.cancel.confirm'),
133
+ onConfirm: () => cancel({ id }).then(() => !error.value), // false keeps it open (the error is shown by the app)
134
+ title: $t('order.cancel.modalTitle', { number }),
135
+ variant: 'danger',
136
+ });
137
+ ```
138
+
139
+ - `onConfirm` returning a promise keeps the modal open with both buttons disabled until it settles; it
140
+ closes afterwards unless the promise resolves `false`. Adapters' `execute` resolves on api errors too,
141
+ so check `error` before returning.
142
+ - Each call creates its own modal and destroys it when hidden.
143
+ - No inline "click again to confirm" and no `<BModal v-model>` in a page: open modals through `create`.
144
+ - A component opened by `create` is mounted outside the page tree: pass data as props and answer
145
+ through emits (`onSaved`), never `inject`. Do not use `modelValue` for data: the orchestrator shows the modal through it
146
+ (use a named model such as `v-model:page`). Destroy it on `onHidden` when created from a click.
147
+ - Props already set in the app `bootstrap.config.ts` are defaults: do not repeat them.
148
+
116
149
  ## Money formatting
117
150
 
118
151
  Money in Merkaly is stored as **integer cents**. `MKFormatMoney`, `MKInputMoney`,
package/dist/module.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "compatibility": {
5
5
  "nuxt": ">=3.14.0"
6
6
  },
7
- "version": "0.12.3",
7
+ "version": "0.12.4",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "1.0.2",
10
10
  "unbuild": "3.6.1"
@@ -106,8 +106,8 @@ function getRowAttrs(item, idx) {
106
106
  </script>
107
107
 
108
108
  <template>
109
- <BCard no-body>
110
- <BCardHeader v-if="!props.hideHeader" class="align-items-center position-relative">
109
+ <BCard class="card-flush" no-body>
110
+ <BCardHeader v-if="!props.hideHeader" class="align-items-center position-relative py-5 gap-2 gap-md-5">
111
111
  <BCardTitle>
112
112
  <div v-if="hasBulkSlot && !props.hideSelect && selectionState.selectedCount" class="w-35px">
113
113
  <DropdownIcon icon="caret-down" toggle-class="p-2">
@@ -181,99 +181,100 @@ function getRowAttrs(item, idx) {
181
181
  style="bottom: -6px; z-index: 10" />
182
182
  </BCardHeader>
183
183
 
184
- <BTableSimple
185
- :class="{ 'h-100': !visibleItems.length || $datagrid.loading }"
186
- class="mb-0 overflow-auto"
187
- hover
188
- responsive="lg"
189
- small
190
- table-class="align-middle table-row-dashed h-100">
191
- <BThead class="sticky-top z-index-1 align-middle">
192
- <BTr class="text-start text-body-secondary fw-bold text-uppercase">
193
- <BTh v-if="hasDetailsSlot" class="w-25px" style="padding: unset !important;" />
184
+ <BCardBody class="pt-0">
185
+ <BTableSimple
186
+ :class="{ 'h-100': !visibleItems.length || $datagrid.loading }"
187
+ class="mb-0 overflow-auto"
188
+ responsive="lg"
189
+ table-class="align-middle table-row-dashed fs-6 gy-5 h-100">
190
+ <BThead class="sticky-top z-index-1 align-middle">
191
+ <BTr class="text-start text-gray-500 fw-bold fs-7 text-uppercase gs-0">
192
+ <BTh v-if="hasDetailsSlot" class="w-25px" style="padding: unset !important;" />
194
193
 
195
- <BTh v-if="!props.hideSelect" class="w-40px px-0">
196
- <BFormCheckbox
197
- :disabled="$datagrid.loading"
198
- :indeterminate="selectionState.checkboxAllAttrs.indeterminate"
199
- :model-value="selectionState.allChecked"
200
- class="form-check-sm form-check-custom cell-checkbox"
201
- @update:model-value="toggleCheckAll()" />
202
- </BTh>
203
-
204
- <template v-for="[key, column] in visibleColumns" :key="key">
205
- <BTh :class="[column.class, column.thClass, 'text-nowrap']">
206
- <slot :name="`head[${key}]`" v-bind="{ column, key }">
207
- <span v-text="column.title ?? sentenceCase(key)" />
208
- </slot>
209
- </BTh>
210
- </template>
211
-
212
- <BTh v-if="hasActionsSlot" class="text-end px-3" />
213
- </BTr>
214
- </BThead>
215
-
216
- <BTbody v-if="!visibleItems.length">
217
- <BTd :colspan="tableColspan">
218
- <slot name="empty">
219
- <div class="text-center text-muted" v-text="props.emptyText ?? t('empty')" />
220
- </slot>
221
- </BTd>
222
- </BTbody>
223
-
224
- <BTbody v-else class="fw-semibold text-nowrap">
225
- <template v-for="(item, idx) in visibleItems" :key="getRowKey(item, idx)">
226
- <BTr v-bind="getRowAttrs(item, idx)">
227
- <BTd v-if="hasDetailsSlot" class="w-25px" style="padding: unset !important;">
228
- <BButton
229
- class="w-25px h-100 rounded-0 p-0 border-end border-dashed"
230
- size="sm"
231
- variant="none"
232
- @click="toggleDetails(item)">
233
- <FormatIcon :name="item._showDetails ? 'chevron-down' : 'chevron-right'" class="p-0" size="sm"
234
- variant="primary" />
235
- </BButton>
236
- </BTd>
237
-
238
- <BTd v-if="!props.hideSelect" class="w-40px px-0">
194
+ <BTh v-if="!props.hideSelect" class="w-40px px-0">
239
195
  <BFormCheckbox
240
- v-model="item._checked"
241
196
  :disabled="$datagrid.loading"
242
- class="form-check-sm form-check-custom cell-checkbox" />
243
- </BTd>
197
+ :indeterminate="selectionState.checkboxAllAttrs.indeterminate"
198
+ :model-value="selectionState.allChecked"
199
+ class="form-check-sm form-check-custom cell-checkbox"
200
+ @update:model-value="toggleCheckAll()" />
201
+ </BTh>
244
202
 
245
203
  <template v-for="[key, column] in visibleColumns" :key="key">
246
- <BTd :class="[column.class, column.tdClass]">
247
- <slot :name="`row[${key}]`" v-bind="{ column, idx, item, key }">
248
- <span v-text="column.getter?.(item) ?? getItemValue(item, key)" />
204
+ <BTh :class="[column.class, column.thClass, 'text-nowrap']">
205
+ <slot :name="`head[${key}]`" v-bind="{ column, key }">
206
+ <span v-text="column.title ?? sentenceCase(key)" />
249
207
  </slot>
250
- </BTd>
208
+ </BTh>
251
209
  </template>
252
210
 
253
- <BTd v-if="hasActionsSlot" class="text-end px-3">
254
- <DropdownIcon :text="t('actions')" icon="caret-down" toggle-class="py-1 px-3" variant="light-dark">
255
- <slot :index="idx" :item="item" name="actions" />
256
- </DropdownIcon>
257
- </BTd>
211
+ <BTh v-if="hasActionsSlot" class="text-end px-3" />
258
212
  </BTr>
213
+ </BThead>
259
214
 
260
- <BTr v-if="hasDetailsSlot && item._showDetails">
261
- <BTd :colspan="tableColspan" class="p-0">
262
- <slot
263
- :index="idx"
264
- :item="item"
265
- :toggle-details="() => toggleDetails(item)"
266
- name="details" />
267
- </BTd>
268
- </BTr>
269
- </template>
270
- </BTbody>
215
+ <BTbody v-if="!visibleItems.length">
216
+ <BTd :colspan="tableColspan">
217
+ <slot name="empty">
218
+ <div class="text-center text-muted" v-text="props.emptyText ?? t('empty')" />
219
+ </slot>
220
+ </BTd>
221
+ </BTbody>
222
+
223
+ <BTbody v-else class="fw-semibold text-gray-600 text-nowrap">
224
+ <template v-for="(item, idx) in visibleItems" :key="getRowKey(item, idx)">
225
+ <BTr v-bind="getRowAttrs(item, idx)">
226
+ <BTd v-if="hasDetailsSlot" class="w-25px" style="padding: unset !important;">
227
+ <BButton
228
+ class="w-25px h-100 rounded-0 p-0 border-end border-dashed"
229
+ size="sm"
230
+ variant="none"
231
+ @click="toggleDetails(item)">
232
+ <FormatIcon
233
+ :name="item._showDetails ? 'chevron-down' : 'chevron-right'" class="p-0" size="sm"
234
+ variant="primary" />
235
+ </BButton>
236
+ </BTd>
237
+
238
+ <BTd v-if="!props.hideSelect" class="w-40px px-0">
239
+ <BFormCheckbox
240
+ v-model="item._checked"
241
+ :disabled="$datagrid.loading"
242
+ class="form-check-sm form-check-custom cell-checkbox" />
243
+ </BTd>
244
+
245
+ <template v-for="[key, column] in visibleColumns" :key="key">
246
+ <BTd :class="[column.class, column.tdClass]">
247
+ <slot :name="`row[${key}]`" v-bind="{ column, idx, item, key }">
248
+ <span v-text="column.getter?.(item) ?? getItemValue(item, key)" />
249
+ </slot>
250
+ </BTd>
251
+ </template>
252
+
253
+ <BTd v-if="hasActionsSlot" class="text-end px-3">
254
+ <DropdownIcon :text="t('actions')" icon="caret-down" toggle-class="py-1 px-3" variant="light-dark">
255
+ <slot :index="idx" :item="item" name="actions" />
256
+ </DropdownIcon>
257
+ </BTd>
258
+ </BTr>
259
+
260
+ <BTr v-if="hasDetailsSlot && item._showDetails">
261
+ <BTd :colspan="tableColspan" class="p-0">
262
+ <slot
263
+ :index="idx"
264
+ :item="item"
265
+ :toggle-details="() => toggleDetails(item)"
266
+ name="details" />
267
+ </BTd>
268
+ </BTr>
269
+ </template>
270
+ </BTbody>
271
271
 
272
- <BTfoot>
273
- <slot name="tfoot" />
274
- </BTfoot>
272
+ <BTfoot>
273
+ <slot name="tfoot" />
274
+ </BTfoot>
275
275
 
276
- </BTableSimple>
276
+ </BTableSimple>
277
+ </BCardBody>
277
278
 
278
279
  <BCardFooter v-if="!props.hideFooter">
279
280
  <slot name="footer" />
@@ -317,24 +318,6 @@ function getRowAttrs(item, idx) {
317
318
  .cell-checkbox input {
318
319
  cursor: pointer;
319
320
  }
320
-
321
- table thead {
322
- font-size: 0.8rem;
323
- background: var(--bs-body-bg);
324
- }
325
- table thead:before {
326
- content: "";
327
- position: absolute;
328
- inset: 0;
329
- z-index: -1;
330
- background: rgb(var(--bs-primary-rgb), 0.05);
331
- }
332
- table thead tr th:is(:first-child, :last-child) {
333
- padding: 0 0.75rem !important;
334
- }
335
- table tbody tr td:is(:first-child, :last-child) {
336
- padding: 0 0.75rem !important;
337
- }
338
321
  </style>
339
322
 
340
323
  <i18n lang="json">
@@ -6,7 +6,7 @@ export interface NotifyOptions {
6
6
  export interface NotifyConfirmOptions extends NotifyOptions {
7
7
  cancelText?: string;
8
8
  confirmText?: string;
9
- onConfirm?: () => void;
9
+ onConfirm?: () => Promise<unknown> | unknown;
10
10
  }
11
11
  export interface NotifyAlertOptions extends NotifyOptions {
12
12
  okText?: string;
@@ -1,4 +1,5 @@
1
1
  import { push } from "notivue";
2
+ import { computed, ref } from "vue";
2
3
  import { useModal } from "bootstrap-vue-next";
3
4
  export function useNotify() {
4
5
  const $modal = useModal();
@@ -25,15 +26,27 @@ export function useNotify() {
25
26
  instance.show();
26
27
  }
27
28
  function confirm(options) {
28
- const instance = $modal.create({
29
+ const busy = ref(false);
30
+ const instance = $modal.create(computed(() => ({
29
31
  body: options.message,
32
+ busy: busy.value,
33
+ // disables both buttons
30
34
  cancelTitle: options.cancelText || "Cancel",
31
35
  cancelVariant: "secondary",
36
+ noCloseOnEsc: busy.value,
32
37
  okTitle: options.confirmText || "OK",
33
38
  okVariant: options.variant || "primary",
34
- onOk: options.onConfirm,
39
+ onHidden: () => void instance.destroy(),
40
+ // one modal per call: never pile up hidden ones
41
+ onOk: (event) => {
42
+ const result = options.onConfirm?.();
43
+ if (!(result instanceof Promise)) return;
44
+ event.preventDefault();
45
+ busy.value = true;
46
+ result.then((value) => value !== false && instance.hide("ok")).finally(() => void (busy.value = false));
47
+ },
35
48
  title: options.title || "Confirm"
36
- });
49
+ })));
37
50
  instance.show();
38
51
  }
39
52
  return { toast, alert, confirm };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@merkaly/nuxt",
3
- "version": "0.12.3",
3
+ "version": "0.12.4",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/merkaly-io/nuxt.git"