quasar-ui-danx 0.2.31 → 0.3.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.
@@ -34,22 +34,12 @@
34
34
  />
35
35
  </template>
36
36
  <template #header-cell="rowProps">
37
- <QTh
38
- :key="rowProps.key"
39
- :props="rowProps"
40
- :data-drop-zone="`resize-column-` + rowProps.col.name"
41
- :class="cls['handle-drop-zone']"
42
- >
43
- {{ rowProps.col.label }}
44
- <HandleDraggable
45
- v-if="rowProps.col.resizeable"
46
- :drop-zone="`resize-column-` + rowProps.col.name"
47
- :class="cls['resize-handle']"
48
- @resize="onResizeColumn(rowProps.col, $event)"
49
- >
50
- <RowResizeIcon class="w-4 text-gray-600" />
51
- </HandleDraggable>
52
- </QTh>
37
+ <ActionTableHeaderColumn
38
+ v-model="columnSettings"
39
+ :row-props="rowProps"
40
+ :name="name"
41
+ @update:model-value="onUpdateColumnSettings"
42
+ />
53
43
  </template>
54
44
  <template #body-cell="rowProps">
55
45
  <ActionTableColumn
@@ -65,13 +55,12 @@
65
55
  </template>
66
56
 
67
57
  <script setup>
68
- import { QTable, QTh } from "quasar";
58
+ import { QTable } from "quasar";
69
59
  import { ref } from "vue";
70
60
  import { getItem, setItem } from "../../helpers";
71
- import { DragHandleIcon as RowResizeIcon } from "../../svg";
72
- import { HandleDraggable } from "../DragAndDrop";
73
61
  import { ActionVnode } from "../Utility";
74
62
  import ActionTableColumn from "./ActionTableColumn.vue";
63
+ import ActionTableHeaderColumn from "./ActionTableHeaderColumn";
75
64
  import EmptyTableState from "./EmptyTableState.vue";
76
65
  import { mapSortBy, registerStickyScrolling } from "./listHelpers";
77
66
  import TableSummaryRow from "./TableSummaryRow.vue";
@@ -118,32 +107,7 @@ registerStickyScrolling(actionTable);
118
107
 
119
108
  const COLUMN_SETTINGS_KEY = `column-settings-${props.name}`;
120
109
  const columnSettings = ref(getItem(COLUMN_SETTINGS_KEY) || {});
121
- function onResizeColumn(column, val) {
122
- columnSettings.value = {
123
- ...columnSettings.value,
124
- [column.name]: {
125
- width: Math.max(Math.min(val.distance + val.startDropZoneSize, column.maxWidth || 500), column.minWidth || 80)
126
- }
127
- };
110
+ function onUpdateColumnSettings() {
128
111
  setItem(COLUMN_SETTINGS_KEY, columnSettings.value);
129
112
  }
130
113
  </script>
131
-
132
- <style lang="scss" module="cls">
133
- .handle-drop-zone {
134
- .resize-handle {
135
- position: absolute;
136
- top: 0;
137
- right: -.45em;
138
- width: .9em;
139
- opacity: 0;
140
- transition: all .3s;
141
- }
142
-
143
- &:hover {
144
- .resize-handle {
145
- opacity: 1;
146
- }
147
- }
148
- }
149
- </style>
@@ -52,11 +52,14 @@ const props = defineProps({
52
52
  const row = computed(() => props.rowProps.row);
53
53
  const column = computed(() => props.rowProps.col);
54
54
  const value = computed(() => props.rowProps.value);
55
- const isSaving = computed(() => column.value.isSaving && column.value.isSaving(row.value));
55
+ const isSaving = computed(() => row.value.isSaving?.value);
56
56
 
57
57
  const columnStyle = computed(() => {
58
58
  const width = props.settings?.width || column.value.width;
59
- return width ? { width: `${width}px` } : null;
59
+ return {
60
+ width: width ? `${width}px` : undefined,
61
+ minWidth: column.value.minWidth ? `${column.value.minWidth}px` : undefined
62
+ };
60
63
  });
61
64
 
62
65
  const columnClass = computed(() => ({
@@ -0,0 +1,83 @@
1
+ <template>
2
+ <QTh
3
+ :key="rowProps.key"
4
+ :props="rowProps"
5
+ :data-drop-zone="isResizeable && `resize-column-` + column.name"
6
+ :class="isResizeable && cls['handle-drop-zone']"
7
+ :style="columnStyle"
8
+ >
9
+ {{ column.label }}
10
+ <HandleDraggable
11
+ v-if="isResizeable"
12
+ :drop-zone="`resize-column-` + column.name"
13
+ :class="cls['resize-handle']"
14
+ @resize="onResizeColumn"
15
+ >
16
+ <RowResizeIcon class="w-4 text-gray-600" />
17
+ </HandleDraggable>
18
+ </QTh>
19
+ </template>
20
+ <script setup>
21
+ import { QTh } from "quasar";
22
+ import { computed } from "vue";
23
+ import { DragHandleIcon as RowResizeIcon } from "../../svg";
24
+ import { HandleDraggable } from "../DragAndDrop";
25
+
26
+ const emit = defineEmits(["update:model-value"]);
27
+ const props = defineProps({
28
+ modelValue: {
29
+ type: Object,
30
+ required: true
31
+ },
32
+ name: {
33
+ type: String,
34
+ required: true
35
+ },
36
+ rowProps: {
37
+ type: Object,
38
+ required: true
39
+ }
40
+ });
41
+
42
+ const column = computed(() => props.rowProps.col);
43
+ const isResizeable = computed(() => column.value.resizeable);
44
+
45
+ const columnStyle = computed(() => {
46
+ const width = props.settings?.width || column.value.width;
47
+ return {
48
+ width: width ? `${width}px` : undefined,
49
+ minWidth: column.value.minWidth ? `${column.value.minWidth}px` : undefined,
50
+ ...(column.value.headerStyle || {})
51
+ };
52
+ });
53
+
54
+
55
+ function onResizeColumn(val) {
56
+ const settings = {
57
+ ...props.modelValue,
58
+ [column.value.name]: {
59
+ width: Math.max(Math.min(val.distance + val.startDropZoneSize, column.value.maxWidth || 500), column.value.minWidth || 80)
60
+ }
61
+ };
62
+ emit("update:model-value", settings);
63
+ }
64
+ </script>
65
+
66
+ <style lang="scss" module="cls">
67
+ .handle-drop-zone {
68
+ .resize-handle {
69
+ position: absolute;
70
+ top: 0;
71
+ right: -.45em;
72
+ width: .9em;
73
+ opacity: 0;
74
+ transition: all .3s;
75
+ }
76
+
77
+ &:hover {
78
+ .resize-handle {
79
+ opacity: 1;
80
+ }
81
+ }
82
+ }
83
+ </style>
@@ -2,7 +2,7 @@
2
2
  <div class="flex items-center transition-all" :class="{'w-72': showFilters, 'w-[6.5rem]': !showFilters}">
3
3
  <div class="flex-grow">
4
4
  <QBtn
5
- class="toggle-button border-blue-700"
5
+ class="btn-blue-highlight border-blue-700"
6
6
  :class="{'highlighted': showFilters}"
7
7
  @click="$emit('update:show-filters', !showFilters)"
8
8
  >
@@ -38,7 +38,6 @@ const props = defineProps({
38
38
  const activeCount = computed(() => Object.keys(props.filter).filter(key => props.filter[key] !== undefined).length);
39
39
  </script>
40
40
  <style lang="scss" scoped>
41
-
42
41
  .btn-blue-highlight {
43
42
  @apply rounded-lg border border-solid;
44
43
 
@@ -2,12 +2,13 @@
2
2
  <QToggle
3
3
  :data-testid="'boolean-field-' + field.id"
4
4
  :model-value="modelValue"
5
+ :disable="disable || readonly"
5
6
  :toggle-indeterminate="toggleIndeterminate"
6
7
  :indeterminate-value="undefined"
7
8
  @update:model-value="$emit('update:model-value', $event)"
8
9
  >
9
10
  <FieldLabel
10
- :field="field"
11
+ :field="{...field, label}"
11
12
  :show-name="showName"
12
13
  :class="labelClass"
13
14
  />
@@ -19,6 +20,10 @@ import FieldLabel from "./FieldLabel";
19
20
 
20
21
  defineEmits(["update:model-value"]);
21
22
  defineProps({
23
+ label: {
24
+ type: String,
25
+ default: null
26
+ },
22
27
  modelValue: {
23
28
  type: [Boolean],
24
29
  default: undefined
@@ -32,6 +37,8 @@ defineProps({
32
37
  default: "text-sm"
33
38
  },
34
39
  showName: Boolean,
35
- toggleIndeterminate: Boolean
40
+ toggleIndeterminate: Boolean,
41
+ disable: Boolean,
42
+ readonly: Boolean
36
43
  });
37
44
  </script>
@@ -1,5 +1,46 @@
1
1
  <template>
2
2
  <div class="rendered-form">
3
+ <div v-if="form.variations > 1" class="mb-4">
4
+ <QTabs v-model="currentVariation" class="text-xs">
5
+ <QTab
6
+ v-for="(name, index) in variationNames"
7
+ :key="name"
8
+ :name="name"
9
+ class="p-0"
10
+ >
11
+ <div class="flex flex-nowrap items-center text-sm">
12
+ <div>{{ name }}</div>
13
+ <template v-if="!disable && !readonly">
14
+ <a
15
+ @click="() => (variationToEdit = name) && (newVariationName = name)"
16
+ class="ml-1 p-1 hover:opacity-100 opacity-20 hover:bg-blue-200 rounded"
17
+ >
18
+ <EditIcon class="w-3 text-blue-900" />
19
+ </a>
20
+ <a
21
+ v-if="index > 0"
22
+ @click="variationToDelete = name"
23
+ class="ml-1 p-1 hover:opacity-100 opacity-20 hover:bg-red-200 rounded"
24
+ >
25
+ <RemoveIcon class="w-3 text-red-900" />
26
+ </a>
27
+ </template>
28
+ </div>
29
+ </QTab>
30
+ <QTab
31
+ v-if="canAddVariation"
32
+ name="add"
33
+ key="add-new-variation"
34
+ @click="onAddVariation"
35
+ class="bg-blue-600 rounded-t-lg !text-white"
36
+ >
37
+ <template v-if="saving">
38
+ <QSpinnerBall class="w-4" />
39
+ </template>
40
+ <template v-else>+ Add Variation</template>
41
+ </QTab>
42
+ </QTabs>
43
+ </div>
3
44
  <div
4
45
  v-for="(field, index) in mappedFields"
5
46
  :key="field.id"
@@ -7,7 +48,8 @@
7
48
  >
8
49
  <Component
9
50
  :is="field.component"
10
- v-model="fieldValues[field.name]"
51
+ :key="field.name + '-' + currentVariation"
52
+ :model-value="getFieldValue(field.name)"
11
53
  :field="field"
12
54
  :label="field.label || undefined"
13
55
  :no-label="noLabel"
@@ -17,10 +59,36 @@
17
59
  @update:model-value="onInput(field.name, $event)"
18
60
  />
19
61
  </div>
62
+ <ConfirmDialog
63
+ v-if="variationToEdit"
64
+ title="Change variation name"
65
+ @confirm="onChangeVariationName"
66
+ @close="variationToEdit = null"
67
+ >
68
+ <TextField
69
+ v-model="newVariationName"
70
+ label="Enter name"
71
+ placeholder="Variation Name"
72
+ input-class="bg-white"
73
+ />
74
+ </ConfirmDialog>
75
+ <ConfirmDialog
76
+ v-if="variationToDelete"
77
+ :title="`Remove variation ${variationToDelete}?`"
78
+ content="You cannot undo this action. If there was any analytics collected for this variation, it will still be attributed to the ad."
79
+ confirm-class="bg-red-900 text-white"
80
+ content-class="w-96"
81
+ @confirm="onRemoveVariation(variationToDelete)"
82
+ @close="variationToDelete = ''"
83
+ />
20
84
  </div>
21
85
  </template>
22
86
  <script setup>
23
- import { reactive } from "vue";
87
+ import { PencilIcon as EditIcon } from "@heroicons/vue/solid";
88
+ import { computed, ref } from "vue";
89
+ import { FlashMessages, incrementName, replace } from "../../../helpers";
90
+ import { TrashIcon as RemoveIcon } from "../../../svg";
91
+ import { ConfirmDialog } from "../../Utility";
24
92
  import {
25
93
  BooleanField,
26
94
  DateField,
@@ -36,17 +104,18 @@ import {
36
104
  const emit = defineEmits(["update:values"]);
37
105
  const props = defineProps({
38
106
  values: {
39
- type: Object,
107
+ type: Array,
40
108
  default: null
41
109
  },
42
- fields: {
43
- type: Array,
110
+ form: {
111
+ type: Object,
44
112
  required: true
45
113
  },
46
114
  noLabel: Boolean,
47
115
  showName: Boolean,
48
116
  disable: Boolean,
49
- readonly: Boolean
117
+ readonly: Boolean,
118
+ saving: Boolean
50
119
  });
51
120
 
52
121
  const FORM_FIELD_MAP = {
@@ -61,16 +130,83 @@ const FORM_FIELD_MAP = {
61
130
  WYSIWYG: WysiwygField
62
131
  };
63
132
 
64
- const mappedFields = props.fields.map((field) => ({
133
+ const mappedFields = props.form.fields.map((field) => ({
65
134
  placeholder: `Enter ${field.label}`,
66
135
  ...field,
67
136
  component: FORM_FIELD_MAP[field.type],
68
137
  default: field.type === "BOOLEAN" ? false : ""
69
138
  }));
70
139
 
71
- const fieldValues = reactive(props.values || {});
140
+ const variationNames = computed(() => {
141
+ return [...new Set(props.values.map(v => v.variation))].sort();
142
+ });
143
+
144
+ const currentVariation = ref(variationNames.value[0] || "default");
145
+ const newVariationName = ref("");
146
+ const variationToEdit = ref("");
147
+ const variationToDelete = ref("");
148
+ const canAddVariation = computed(() => variationNames.value.length < props.form.variations && !props.readonly && !props.disable);
149
+
150
+ function getFieldResponse(name) {
151
+ if (!props.values) return undefined;
152
+ return props.values.find((v) => v.variation === currentVariation.value && v.name === name);
153
+ }
154
+ function getFieldValue(name) {
155
+ return getFieldResponse(name)?.value;
156
+ }
157
+ function onInput(name, value) {
158
+ const fieldResponse = getFieldResponse(name);
159
+ const newFieldResponse = {
160
+ name,
161
+ variation: currentVariation.value,
162
+ value
163
+ };
164
+ const newValues = replace(props.values, fieldResponse, newFieldResponse, true);
165
+ emit("update:values", newValues);
166
+ }
167
+
168
+ function onAddVariation() {
169
+ if (props.saving) return;
170
+
171
+ const previousName = variationNames.value[variationNames.value.length - 1];
172
+ const newName = incrementName(previousName === "default" ? "Variation" : previousName);
173
+
174
+ const newVariation = props.form.fields.map((field) => ({
175
+ variation: newName,
176
+ name: field.name,
177
+ value: field.type === "BOOLEAN" ? false : null
178
+ }));
179
+ const newValues = [...props.values, ...newVariation];
180
+ emit("update:values", newValues);
181
+ currentVariation.value = newName;
182
+ }
183
+
184
+ function onChangeVariationName() {
185
+ if (!newVariationName.value) return;
186
+ if (variationNames.value.includes(newVariationName.value)) {
187
+ FlashMessages.error("Variation name already exists");
188
+ return;
189
+ }
190
+ const newValues = props.values.map((v) => {
191
+ if (v.variation === variationToEdit.value) {
192
+ return { ...v, variation: newVariationName.value };
193
+ }
194
+ return v;
195
+ });
196
+ emit("update:values", newValues);
197
+
198
+ currentVariation.value = newVariationName.value;
199
+ variationToEdit.value = "";
200
+ newVariationName.value = "";
201
+ }
202
+
203
+ function onRemoveVariation(name) {
204
+ const newValues = props.values.filter((v) => v.variation !== name);
205
+ emit("update:values", newValues);
72
206
 
73
- function onInput(key, value) {
74
- emit("update:values", { ...fieldValues, [key]: value });
207
+ if (currentVariation.value === name) {
208
+ currentVariation.value = variationNames.value[0];
209
+ }
210
+ variationToDelete.value = "";
75
211
  }
76
212
  </script>
@@ -8,5 +8,6 @@ export * from "./tableColumns";
8
8
  export { default as ActionMenu } from "./ActionMenu.vue";
9
9
  export { default as ActionTable } from "./ActionTable.vue";
10
10
  export { default as ActionTableColumn } from "./ActionTableColumn.vue";
11
+ export { default as ActionTableHeaderColumn } from "./ActionTableHeaderColumn.vue";
11
12
  export { default as EmptyTableState } from "./EmptyTableState.vue";
12
13
  export { default as TableSummaryRow } from "./TableSummaryRow.vue";
@@ -1,8 +1,8 @@
1
- import { computed, ref, watch } from "vue";
2
- import { getItem, setItem, waitForRef } from "../../helpers";
1
+ import { computed, Ref, ref, ShallowRef, shallowRef, watch } from "vue";
2
+ import { ActionTargetItem, getItem, setItem, waitForRef } from "../../helpers";
3
3
  import { getFilterFromUrl } from "./listHelpers";
4
4
 
5
- interface ListActionsOptions {
5
+ export interface ListActionsOptions {
6
6
  listRoute: Function;
7
7
  summaryRoute?: Function | null;
8
8
  filterFieldOptionsRoute?: Function | null;
@@ -13,6 +13,14 @@ interface ListActionsOptions {
13
13
  refreshFilters?: boolean;
14
14
  }
15
15
 
16
+ export interface PagedItems {
17
+ data: any[] | undefined;
18
+ meta: {
19
+ total: number;
20
+ last_page?: number;
21
+ } | undefined;
22
+ }
23
+
16
24
  export function useListControls(name: string, {
17
25
  listRoute,
18
26
  summaryRoute = null,
@@ -25,14 +33,19 @@ export function useListControls(name: string, {
25
33
  }: ListActionsOptions) {
26
34
  let isInitialized = false;
27
35
  const PAGE_SETTINGS_KEY = `${name}-pagination-settings`;
28
- const pagedItems = ref(null);
29
- const filter = ref({});
36
+ const pagedItems: Ref<PagedItems | null> = shallowRef(null);
37
+ const filter: Ref<object | any> = ref({});
30
38
  const globalFilter = ref({});
31
39
  const showFilters = ref(false);
32
- const selectedRows = ref([]);
40
+ const selectedRows = shallowRef([]);
33
41
  const isLoadingList = ref(false);
34
42
  const isLoadingSummary = ref(false);
35
- const summary = ref(null);
43
+ const summary = shallowRef(null);
44
+
45
+ // The active ad for viewing / editing
46
+ const activeItem: ShallowRef<ActionTargetItem | null> = shallowRef(null);
47
+ // Controls the active panel (ie: tab) if rendering a panels drawer or similar
48
+ const activePanel = shallowRef(null);
36
49
 
37
50
  // Filter fields are the field values available for the currently applied filter on Creative Groups
38
51
  // (ie: all states available under the current filter)
@@ -86,7 +99,7 @@ export function useListControls(name: string, {
86
99
  isLoadingSummary.value = true;
87
100
  const summaryFilter = { id: null, ...filter.value, ...globalFilter.value };
88
101
  if (selectedRows.value.length) {
89
- summaryFilter.id = selectedRows.value.map((row) => row.id);
102
+ summaryFilter.id = selectedRows.value.map((row: { id: string }) => row.id);
90
103
  }
91
104
  summary.value = await summaryRoute(summaryFilter);
92
105
  isLoadingSummary.value = false;
@@ -108,7 +121,7 @@ export function useListControls(name: string, {
108
121
  * Watches for a filter URL parameter and applies the filter if it is set.
109
122
  */
110
123
  function applyFilterFromUrl(url: string, filterFields = null) {
111
- if (url.match(urlPattern)) {
124
+ if (urlPattern && url.match(urlPattern)) {
112
125
  // A flat list of valid filterable field names
113
126
  const validFilterKeys = filterFields?.value?.map(group => group.fields.map(field => field.name)).flat();
114
127
 
@@ -125,15 +138,32 @@ export function useListControls(name: string, {
125
138
 
126
139
  // Set the reactive pager to map from the Laravel pagination to Quasar pagination
127
140
  // and automatically update the list of ads
128
- function setPagedItems(items) {
141
+ function setPagedItems(items: any[] | PagedItems) {
142
+ let data, meta;
143
+
129
144
  if (Array.isArray(items)) {
130
- pagedItems.value = { data: items, meta: { total: items.length } };
145
+ data = items;
146
+ meta = { total: items.length };
147
+
131
148
  } else {
132
- pagedItems.value = items;
133
- if (items?.meta && items.meta.total !== quasarPagination.value.rowsNumber) {
134
- quasarPagination.value.rowsNumber = items.meta.total;
135
- }
149
+ data = items.data;
150
+ meta = items.meta;
136
151
  }
152
+
153
+ // Update the Quasar pagination rows number if it is different from the total
154
+ if (meta && meta.total !== quasarPagination.value.rowsNumber) {
155
+ quasarPagination.value.rowsNumber = meta.total;
156
+ }
157
+
158
+ // Add a reactive isSaving property to each item (for performance reasons in checking saving state)
159
+ data = data.map((item: any) => {
160
+ // We want to keep the isSaving state if it is already set, as optimizations prevent reloading the
161
+ // components, and therefore reactivity is not responding to the new isSaving state
162
+ const oldItem = pagedItems.value?.data?.find(i => i.id === item.id);
163
+ return { ...item, isSaving: oldItem?.isSaving || ref(false) };
164
+ });
165
+
166
+ pagedItems.value = { data, meta };
137
167
  }
138
168
 
139
169
  /**
@@ -148,9 +178,12 @@ export function useListControls(name: string, {
148
178
  *
149
179
  * @param updatedItem
150
180
  */
151
- function setItemInPagedList(updatedItem) {
181
+ function setItemInList(updatedItem: any) {
152
182
  const data = pagedItems.value?.data?.map(item => (item.id === updatedItem.id && (item.updated_at === null || item.updated_at <= updatedItem.updated_at)) ? updatedItem : item);
153
- pagedItems.value = { ...pagedItems.value, data };
183
+ setPagedItems({
184
+ data,
185
+ meta: { total: pagedItems.value.meta.total }
186
+ });
154
187
 
155
188
  // Update the active item as well if it is set
156
189
  if (activeItem.value?.id === updatedItem.id) {
@@ -160,10 +193,10 @@ export function useListControls(name: string, {
160
193
 
161
194
  /**
162
195
  * Loads more items into the list.
163
- * @param index
164
- * @param perPage
165
196
  */
166
- async function loadMore(index, perPage = undefined) {
197
+ async function loadMore(index: number, perPage = undefined) {
198
+ if (!moreRoute) return;
199
+
167
200
  const newItems = await moreRoute({
168
201
  page: index + 1,
169
202
  perPage,
@@ -171,7 +204,10 @@ export function useListControls(name: string, {
171
204
  });
172
205
 
173
206
  if (newItems && newItems.length > 0) {
174
- pagedItems.value.data = [...pagedItems.value.data, ...newItems];
207
+ setPagedItems({
208
+ data: [...pagedItems.value.data, ...newItems],
209
+ meta: { total: pagedItems.value.meta.total }
210
+ });
175
211
  return true;
176
212
  }
177
213
 
@@ -180,7 +216,6 @@ export function useListControls(name: string, {
180
216
 
181
217
  /**
182
218
  * Refreshes the list, summary, and filter field options.
183
- * @returns {Promise<Awaited<void>[]>}
184
219
  */
185
220
  async function refreshAll() {
186
221
  return Promise.all([loadList(), loadSummary(), loadFilterFieldOptions(), getActiveItemDetails()]);
@@ -188,10 +223,8 @@ export function useListControls(name: string, {
188
223
 
189
224
  /**
190
225
  * Updates the settings in local storage
191
- * @param key
192
- * @param value
193
226
  */
194
- function updateSettings(key, value) {
227
+ function updateSettings(key: string, value: any) {
195
228
  const settings = getItem(PAGE_SETTINGS_KEY) || {};
196
229
  settings[key] = value;
197
230
  setItem(PAGE_SETTINGS_KEY, settings);
@@ -242,11 +275,6 @@ export function useListControls(name: string, {
242
275
  setItem(PAGE_SETTINGS_KEY, settings);
243
276
  }
244
277
 
245
- // The active ad for viewing / editing
246
- const activeItem = ref(null);
247
- // Controls the active panel (ie: tab) if rendering a panels drawer or similar
248
- const activePanel = ref(null);
249
-
250
278
  /**
251
279
  * Gets the additional details for the currently active item.
252
280
  * (ie: data that is not normally loaded in the list because it is not needed for the list view)
@@ -261,7 +289,8 @@ export function useListControls(name: string, {
261
289
  // NOTE: race conditions might allow the finished loading item to be different to the currently
262
290
  // requested item
263
291
  if (result?.id === activeItem.value?.id) {
264
- activeItem.value = result;
292
+ const loadedItem = pagedItems.value?.data.find((i: { id: string }) => i.id === result.id);
293
+ activeItem.value = { ...result, isSaving: loadedItem.isSaving || ref(false) };
265
294
  }
266
295
  }
267
296
 
@@ -289,12 +318,13 @@ export function useListControls(name: string, {
289
318
  /**
290
319
  * Gets the next item in the list at the given offset (ie: 1 or -1) from the current position in the list of the
291
320
  * selected item. If the next item is on a previous or next page, it will load the page first then select the item
292
- * @param offset
293
- * @returns {Promise<void>}
294
321
  */
295
- async function getNextItem(offset) {
296
- const index = pagedItems.value?.data.findIndex(i => i.id === activeItem.value.id);
297
- if (index === undefined) return;
322
+ async function getNextItem(offset: number) {
323
+ if (!pagedItems.value) return;
324
+
325
+ const index = pagedItems.value.data.findIndex((i: { id: string }) => i.id === activeItem.value?.id);
326
+ if (index === undefined || index === null) return;
327
+
298
328
  let nextIndex = index + offset;
299
329
 
300
330
  // Load the previous page if the offset is before index 0
@@ -321,7 +351,7 @@ export function useListControls(name: string, {
321
351
  }
322
352
  }
323
353
 
324
- activeItem.value = pagedItems.value.data[nextIndex];
354
+ activeItem.value = pagedItems.value?.data[nextIndex];
325
355
  }
326
356
 
327
357
  // Initialize the list actions and load settings, lists, summaries, filter fields, etc.
@@ -358,6 +388,6 @@ export function useListControls(name: string, {
358
388
  getNextItem,
359
389
  activatePanel,
360
390
  applyFilterFromUrl,
361
- setItemInPagedList
391
+ setItemInList
362
392
  };
363
393
  }
@@ -9,9 +9,9 @@ export interface TableColumn {
9
9
  field: string,
10
10
  format?: Function,
11
11
  innerClass?: string | object,
12
- style?: string,
13
- headerStyle?: string,
14
- isSaving?: boolean | Function,
12
+ style?: string | object,
13
+ headerStyle?: string | object,
14
+ isSavingRow?: boolean | Function,
15
15
  label: string,
16
16
  maxWidth?: number,
17
17
  minWidth?: number,