quasar-ui-danx 0.2.31 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,