quasar-ui-danx 0.2.32 → 0.3.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -46,7 +46,8 @@ const columnStyle = computed(() => {
46
46
  const width = props.settings?.width || column.value.width;
47
47
  return {
48
48
  width: width ? `${width}px` : undefined,
49
- minWidth: column.value.minWidth ? `${column.value.minWidth}px` : undefined
49
+ minWidth: column.value.minWidth ? `${column.value.minWidth}px` : undefined,
50
+ ...(column.value.headerStyle || {})
50
51
  };
51
52
  });
52
53
 
@@ -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>
@@ -21,7 +21,7 @@
21
21
  <a
22
22
  v-if="uploadedFiles.length > 0"
23
23
  class="ml-3 text-red-900"
24
- @click="onClear"
24
+ @click="clearUploadedFiles"
25
25
  >Clear</a>
26
26
  <input
27
27
  ref="file"
@@ -81,7 +81,7 @@ const props = defineProps({
81
81
  readonly: Boolean
82
82
  });
83
83
 
84
- const { onComplete, onDrop, onFilesSelected, uploadedFiles, onClear, onRemove } = useMultiFileUpload();
84
+ const { onComplete, onDrop, onFilesSelected, uploadedFiles, clearUploadedFiles, onRemove } = useMultiFileUpload();
85
85
  onMounted(() => {
86
86
  if (props.modelValue) {
87
87
  uploadedFiles.value = props.modelValue;
@@ -234,7 +234,7 @@ function resolveSelectionLabel(option) {
234
234
  * @returns {string|*|string}
235
235
  */
236
236
  function resolveValue(option) {
237
- if (typeof option === "string") {
237
+ if (!option || typeof option === "string") {
238
238
  return option;
239
239
  }
240
240
  let value = option.value;
@@ -21,7 +21,7 @@
21
21
  <a
22
22
  v-if="uploadedFile"
23
23
  class="ml-3 text-red-900"
24
- @click="onClear"
24
+ @click="clearUploadedFile"
25
25
  >Clear</a>
26
26
  <input
27
27
  ref="file"
@@ -67,7 +67,7 @@ const props = defineProps({
67
67
  disable: Boolean,
68
68
  readonly: Boolean
69
69
  });
70
- const { onComplete, onDrop, onFileSelected, uploadedFile, onClear } = useSingleFileUpload();
70
+ const { onComplete, onDrop, onFileSelected, uploadedFile, clearUploadedFile } = useSingleFileUpload();
71
71
  onComplete(() => emit("update:model-value", uploadedFile.value));
72
72
 
73
73
  onMounted(() => {
@@ -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 || "(Default)" }}</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 !== false"
64
+ title="Change variation name"
65
+ @confirm="onChangeVariationName"
66
+ @close="variationToEdit = false"
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,85 @@ 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] || "");
145
+ const newVariationName = ref("");
146
+ const variationToEdit = ref(false);
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 ? "Variation 1" : 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 = false;
200
+ newVariationName.value = "";
201
+ }
202
+
203
+ function onRemoveVariation(name) {
204
+ if (!name) return;
205
+
206
+ const newValues = props.values.filter((v) => v.variation !== name);
207
+ emit("update:values", newValues);
72
208
 
73
- function onInput(key, value) {
74
- emit("update:values", { ...fieldValues, [key]: value });
209
+ if (currentVariation.value === name) {
210
+ currentVariation.value = variationNames.value[0];
211
+ }
212
+ variationToDelete.value = "";
75
213
  }
76
214
  </script>
@@ -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,
@@ -6,7 +6,7 @@
6
6
  <QTabPanel v-for="panel in panels" :key="panel.name" :name="panel.name">
7
7
  <RenderVnode
8
8
  v-if="panel.vnode"
9
- :vnode="panel.vnode()"
9
+ :vnode="panel.vnode"
10
10
  />
11
11
  </QTabPanel>
12
12
  </QTabPanels>
@@ -1,5 +1,21 @@
1
1
  <script>
2
- const RenderVnode = (props) => props.vnode;
3
- RenderVnode.props = { vnode: { type: Object, required: true } };
2
+ import { isRef, isVNode } from "vue";
3
+
4
+ const RenderVnode = (props) => {
5
+ if (isVNode(props.vnode)) {
6
+ return props.vnode;
7
+ }
8
+
9
+ if (isRef(props.vnode)) {
10
+ return props.vnode.value;
11
+ }
12
+
13
+ if (typeof props.vnode === "function") {
14
+ return props.vnode();
15
+ }
16
+
17
+ return null;
18
+ };
19
+ RenderVnode.props = { vnode: { type: [Function, Object], required: true } };
4
20
  export default RenderVnode;
5
21
  </script>
@@ -1,13 +1,8 @@
1
1
  import { QNotifyCreateOptions } from "quasar";
2
-
3
- export interface DanxFileUploadOptions {
4
- directory?: string;
5
- presignedUploadUrl?: Function | null;
6
- uploadCompletedUrl?: Function | null;
7
- }
2
+ import { FileUploadOptions } from "../helpers";
8
3
 
9
4
  export interface DanxOptions {
10
- fileUpload: DanxFileUploadOptions;
5
+ fileUpload: FileUploadOptions;
11
6
  flashMessages: {
12
7
  default: QNotifyCreateOptions;
13
8
  success: QNotifyCreateOptions;