quasar-ui-danx 0.4.5 → 0.4.7

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "quasar-ui-danx",
3
- "version": "0.4.5",
3
+ "version": "0.4.7",
4
4
  "author": "Dan <dan@flytedesk.com>",
5
5
  "description": "DanX Vue / Quasar component library",
6
6
  "license": "MIT",
@@ -43,7 +43,7 @@
43
43
  "vite-svg-loader": "^5.1.0",
44
44
  "vue": "^3.4.21",
45
45
  "vue-eslint-parser": "^9.4.2",
46
- "vue-router": "^4.0.0"
46
+ "vue-router": "^4.3.2"
47
47
  },
48
48
  "dependencies": {
49
49
  "@heroicons/vue": "v1",
@@ -1,7 +1,7 @@
1
1
  <template>
2
2
  <div
3
3
  class="dx-action-table overflow-hidden"
4
- :class="{'dx-no-data': !hasData}"
4
+ :class="{'dx-no-data': !hasData, 'dx-is-loading': loadingList || loadingSummary, 'dx-is-loading-list': loadingList}"
5
5
  >
6
6
  <ActionVnode />
7
7
  <QTable
@@ -1,5 +1,10 @@
1
1
  <template>
2
2
  <div>
3
+ <FieldLabel
4
+ v-if="label"
5
+ :label="label"
6
+ class="mb-1 block"
7
+ />
3
8
  <QSelect
4
9
  ref="selectField"
5
10
  v-bind="$props"
@@ -15,6 +20,7 @@
15
20
  option-label="label"
16
21
  option-value="value"
17
22
  placeholder=""
23
+ label=""
18
24
  :input-class="{'is-hidden': !isShowing, [inputClass]: true}"
19
25
  class="max-w-full dx-select-field"
20
26
  @filter="onFilter"
@@ -68,6 +74,7 @@
68
74
  import { ChevronDownIcon as DropDownIcon } from "@heroicons/vue/outline";
69
75
  import { QSelect, QSelectProps } from "quasar";
70
76
  import { computed, isRef, nextTick, ref } from "vue";
77
+ import FieldLabel from "./FieldLabel";
71
78
 
72
79
  export interface Props extends QSelectProps {
73
80
  modelValue?: any;
@@ -1,12 +1,14 @@
1
1
  <template>
2
2
  <div>
3
- <div v-if="readonly">
4
- <LabelValueBlock
5
- :label="label || field?.label || 'Text'"
6
- :value="modelValue"
7
- />
8
- </div>
9
- <template v-else>
3
+ <FieldLabel
4
+ :field="field"
5
+ :label="label"
6
+ :show-name="showName"
7
+ :class="labelClass"
8
+ :value="readonly ? modelValue : ''"
9
+ class="mb-1 block"
10
+ />
11
+ <template v-if="!readonly">
10
12
  <QInput
11
13
  :placeholder="field?.placeholder"
12
14
  outlined
@@ -24,16 +26,7 @@
24
26
  :debounce="debounce"
25
27
  @keydown.enter="$emit('submit')"
26
28
  @update:model-value="$emit('update:model-value', $event)"
27
- >
28
- <template #label>
29
- <FieldLabel
30
- :field="field"
31
- :label="label"
32
- :show-name="showName"
33
- :class="labelClass"
34
- />
35
- </template>
36
- </QInput>
29
+ />
37
30
  <MaxLengthCounter
38
31
  :length="modelValue?.length || 0"
39
32
  :max-length="field?.maxLength"
@@ -46,7 +39,6 @@
46
39
  import { TextFieldProps } from "../../../../types";
47
40
  import MaxLengthCounter from "../Utilities/MaxLengthCounter";
48
41
  import FieldLabel from "./FieldLabel";
49
- import LabelValueBlock from "./LabelValueBlock";
50
42
 
51
43
  defineEmits(["update:model-value", "submit"]);
52
44
  withDefaults(defineProps<TextFieldProps>(), {
@@ -9,24 +9,28 @@
9
9
  :class="{'has-selection': selectedCount}"
10
10
  >
11
11
  <div class="flex flex-nowrap items-center">
12
- <div
13
- v-if="selectedCount"
14
- class="flex items-center"
15
- >
16
- <ClearIcon
17
- class="w-6 mr-3"
18
- @click="$emit('clear')"
12
+ <div class="relative">
13
+ <QSpinner
14
+ v-if="loading"
15
+ class="absolute top-0 left-0"
16
+ size="18"
19
17
  />
20
- {{ fNumber(selectedCount) }} {{ selectedLabel }}
18
+ <div
19
+ :class="{'opacity-0': loading}"
20
+ class="flex items-center nowrap"
21
+ >
22
+ <ClearIcon
23
+ v-if="selectedCount"
24
+ class="w-6 mr-3 cursor-pointer"
25
+ @click="$emit('clear')"
26
+ />
27
+
28
+ {{ fNumber(selectedCount || itemCount) }}
29
+ </div>
21
30
  </div>
22
- <div v-else-if="itemCount">
23
- {{ fNumber(itemCount) }} {{ label }}
31
+ <div class="ml-2">
32
+ {{ selectedCount ? selectedLabel : label }}
24
33
  </div>
25
- <QSpinner
26
- v-if="loading"
27
- class="ml-3"
28
- size="18"
29
- />
30
34
  </div>
31
35
  </QTd>
32
36
  <QTd
@@ -48,49 +52,49 @@ import { fNumber } from "../../helpers";
48
52
 
49
53
  defineEmits(["clear"]);
50
54
  const props = defineProps({
51
- loading: Boolean,
52
- label: {
53
- type: String,
54
- default: "Rows"
55
- },
56
- selectedLabel: {
57
- type: String,
58
- default: "Selected"
59
- },
60
- selectedCount: {
61
- type: Number,
62
- default: 0
63
- },
64
- itemCount: {
65
- type: Number,
66
- default: 0
67
- },
68
- summary: {
69
- type: Object,
70
- default: null
71
- },
72
- columns: {
73
- type: Array,
74
- required: true
75
- },
76
- stickyColspan: {
77
- type: Number,
78
- default: 2
79
- }
55
+ loading: Boolean,
56
+ label: {
57
+ type: String,
58
+ default: "Rows"
59
+ },
60
+ selectedLabel: {
61
+ type: String,
62
+ default: "Selected"
63
+ },
64
+ selectedCount: {
65
+ type: Number,
66
+ default: 0
67
+ },
68
+ itemCount: {
69
+ type: Number,
70
+ default: 0
71
+ },
72
+ summary: {
73
+ type: Object,
74
+ default: null
75
+ },
76
+ columns: {
77
+ type: Array,
78
+ required: true
79
+ },
80
+ stickyColspan: {
81
+ type: Number,
82
+ default: 2
83
+ }
80
84
  });
81
85
 
82
86
  const summaryColumns = computed(() => {
83
- // The sticky columns are where we display the selection count and should not be included in the summary columns
84
- return props.columns.slice(props.stickyColspan - 1);
87
+ // The sticky columns are where we display the selection count and should not be included in the summary columns
88
+ return props.columns.slice(props.stickyColspan - 1);
85
89
  });
86
90
 
87
91
  function formatValue(column) {
88
- const value = props.summary[column.name];
89
- if (value === undefined) return "";
92
+ const value = props.summary[column.name];
93
+ if (value === undefined) return "";
90
94
 
91
- if (column.format) {
92
- return column.format(value);
93
- }
94
- return value;
95
+ if (column.format) {
96
+ return column.format(value);
97
+ }
98
+ return value;
95
99
  }
96
100
  </script>
@@ -16,8 +16,8 @@ import { getFilterFromUrl } from "./listHelpers";
16
16
 
17
17
  export function useListControls(name: string, options: ListControlsOptions): ActionController {
18
18
  let isInitialized = false;
19
- let vueRoute: RouteLocationNormalizedLoaded | null = null;
20
- let vueRouter: Router | null = null;
19
+ let vueRoute: RouteLocationNormalizedLoaded | null | undefined;
20
+ let vueRouter: Router | null | undefined;
21
21
  const PAGE_SETTINGS_KEY = `dx-${name}-pager`;
22
22
  const pagedItems = shallowRef<PagedItems | null>(null);
23
23
  const activeFilter = ref<ListControlsFilter>({});
@@ -75,8 +75,12 @@ export function useListControls(name: string, options: ListControlsOptions): Act
75
75
  async function loadList() {
76
76
  if (!isInitialized) return;
77
77
  isLoadingList.value = true;
78
- setPagedItems(await options.routes.list(pager.value));
79
- isLoadingList.value = false;
78
+ try {
79
+ setPagedItems(await options.routes.list(pager.value));
80
+ isLoadingList.value = false;
81
+ } catch (e) {
82
+ // Fail silently
83
+ }
80
84
  }
81
85
 
82
86
  async function loadSummary() {
@@ -87,8 +91,12 @@ export function useListControls(name: string, options: ListControlsOptions): Act
87
91
  if (selectedRows.value.length) {
88
92
  summaryFilter.id = selectedRows.value.map((row) => row.id);
89
93
  }
90
- summary.value = await options.routes.summary(summaryFilter);
91
- isLoadingSummary.value = false;
94
+ try {
95
+ summary.value = await options.routes.summary(summaryFilter);
96
+ isLoadingSummary.value = false;
97
+ } catch (e) {
98
+ // Fail silently
99
+ }
92
100
  }
93
101
 
94
102
  async function loadListAndSummary() {
@@ -110,8 +118,12 @@ export function useListControls(name: string, options: ListControlsOptions): Act
110
118
  async function loadFieldOptions() {
111
119
  if (!options.routes.fieldOptions || !isInitialized) return;
112
120
  isLoadingFilters.value = true;
113
- fieldOptions.value = await options.routes.fieldOptions(activeFilter.value) || {};
114
- isLoadingFilters.value = false;
121
+ try {
122
+ fieldOptions.value = await options.routes.fieldOptions(activeFilter.value) || {};
123
+ isLoadingFilters.value = false;
124
+ } catch (e) {
125
+ // Fail silently
126
+ }
115
127
  }
116
128
 
117
129
  /**
@@ -197,18 +209,22 @@ export function useListControls(name: string, options: ListControlsOptions): Act
197
209
  async function loadMore(index: number, perPage: number | undefined = undefined) {
198
210
  if (!options.routes.more) return false;
199
211
 
200
- const newItems = await options.routes.more({
201
- page: index + 1,
202
- perPage,
203
- filter: { ...activeFilter.value, ...globalFilter.value }
204
- });
205
-
206
- if (newItems && newItems.length > 0) {
207
- setPagedItems({
208
- data: [...(pagedItems.value?.data || []), ...newItems],
209
- meta: { total: pagedItems.value?.meta?.total || 0 }
212
+ try {
213
+ const newItems = await options.routes.more({
214
+ page: index + 1,
215
+ perPage,
216
+ filter: { ...activeFilter.value, ...globalFilter.value }
210
217
  });
211
- return true;
218
+
219
+ if (newItems && newItems.length > 0) {
220
+ setPagedItems({
221
+ data: [...(pagedItems.value?.data || []), ...newItems],
222
+ meta: { total: pagedItems.value?.meta?.total || 0 }
223
+ });
224
+ return true;
225
+ }
226
+ } catch (e) {
227
+ // Fail silently
212
228
  }
213
229
 
214
230
  return false;
@@ -283,14 +299,18 @@ export function useListControls(name: string, options: ListControlsOptions): Act
283
299
  async function getActiveItemDetails() {
284
300
  if (!activeItem.value || !options.routes.details) return;
285
301
 
286
- const result = await options.routes.details(activeItem.value);
302
+ try {
303
+ const result = await options.routes.details(activeItem.value);
287
304
 
288
- if (!result || !result.__type || !result.id) {
289
- return console.error("Invalid response from details route: All responses must include a __type and id field. result =", result);
290
- }
305
+ if (!result || !result.__type || !result.id) {
306
+ return console.error("Invalid response from details route: All responses must include a __type and id field. result =", result);
307
+ }
291
308
 
292
- // Reassign the active item to the store object to ensure reactivity
293
- activeItem.value = storeObject(result);
309
+ // Reassign the active item to the store object to ensure reactivity
310
+ activeItem.value = storeObject(result);
311
+ } catch (e) {
312
+ // Fail silently
313
+ }
294
314
  }
295
315
 
296
316
  // Whenever the active item changes, fill the additional item details
@@ -314,12 +334,8 @@ export function useListControls(name: string, options: ListControlsOptions): Act
314
334
  activePanel.value = panel;
315
335
 
316
336
  // Push vue router change /:id/:panel
317
- if (vueRoute && vueRouter && item?.id) {
318
- vueRouter.push({
319
- name: Array.isArray(vueRoute.name) ? vueRoute.name[0] : vueRoute.name,
320
- params: { id: item.id, panel },
321
- replace: true
322
- });
337
+ if (item?.id) {
338
+ updateRouteParams({ id: item.id, panel });
323
339
  }
324
340
  }
325
341
 
@@ -330,7 +346,7 @@ export function useListControls(name: string, options: ListControlsOptions): Act
330
346
  activeItem.value = item ? storeObject(item) : item;
331
347
 
332
348
  if (!item?.id) {
333
- vueRouter?.push({ name: vueRoute?.name || "home" });
349
+ updateRouteParams({});
334
350
  }
335
351
  }
336
352
 
@@ -385,17 +401,16 @@ export function useListControls(name: string, options: ListControlsOptions): Act
385
401
  }
386
402
 
387
403
  // Initialize the list actions and load settings, lists, summaries, filter fields, etc.
388
- function initialize(initOptions: ListControlsInitializeOptions) {
389
- vueRouter = initOptions.vueRouter;
390
- vueRoute = initOptions.vueRoute;
404
+ function initialize(initOptions?: ListControlsInitializeOptions) {
405
+ vueRouter = initOptions?.vueRouter;
406
+ vueRoute = initOptions?.vueRouter?.currentRoute.value;
391
407
  isInitialized = true;
392
408
  loadSettings();
393
409
 
394
- console.log("initialize() called", name, options, "initOptions", initOptions);
395
410
  /**
396
411
  * Watch the id params in the route and set the active item to the item with the given id.
397
412
  */
398
- if (options.routes.details && vueRoute) {
413
+ if (options.routes.details && vueRoute && vueRouter) {
399
414
  const { params, meta } = vueRoute;
400
415
 
401
416
  const controlRouteName = vueRoute.name;
@@ -409,6 +424,18 @@ export function useListControls(name: string, options: ListControlsOptions): Act
409
424
  }
410
425
  }
411
426
 
427
+ /**
428
+ * Updates the URL bar and route to the given params.
429
+ */
430
+ function updateRouteParams(params: AnyObject) {
431
+ if (vueRouter && vueRoute) {
432
+ vueRouter.push({
433
+ name: (Array.isArray(vueRoute.name) ? vueRoute.name[0] : vueRoute.name) || "home",
434
+ params
435
+ });
436
+ }
437
+ }
438
+
412
439
  function setPanelFromRoute(params: RouteParams, meta: AnyObject) {
413
440
  const id = Array.isArray(params?.id) ? params.id[0] : params?.id;
414
441
  if (id && meta.type) {
@@ -0,0 +1,20 @@
1
+ let appRefreshed = false;
2
+
3
+ export function refreshApplication(callback: () => void) {
4
+ // Only allow refreshing the application once
5
+ if (appRefreshed) return;
6
+ appRefreshed = true;
7
+
8
+ // Create a hidden iframe
9
+ const iframe = document.createElement("iframe");
10
+ iframe.style.display = "none";
11
+ iframe.src = window.location.href;
12
+ document.body.appendChild(iframe);
13
+
14
+ // Listen for the iframe to finish loading
15
+ iframe.onload = () => {
16
+ // Remove the iframe
17
+ document.body.removeChild(iframe);
18
+ callback();
19
+ };
20
+ }
@@ -1,4 +1,5 @@
1
1
  export * from "./actions";
2
+ export * from "./app";
2
3
  export * from "./array";
3
4
  export * from "./compatibility";
4
5
  export * from "./date";
@@ -1,6 +1,6 @@
1
1
  import { Ref } from "vue";
2
2
  import { danxOptions } from "../config";
3
- import { RequestApi } from "../types";
3
+ import { HttpResponse, RequestApi } from "../types";
4
4
 
5
5
  /**
6
6
  * A simple request helper that wraps the fetch API
@@ -17,59 +17,58 @@ export const request: RequestApi = {
17
17
  },
18
18
 
19
19
  async call(url, options) {
20
- try {
20
+ options = options || {};
21
+ const abortKey = options?.abortOn !== undefined ? options.abortOn : url;
22
+ const timestamp = new Date().getTime();
23
+
24
+ if (abortKey) {
21
25
  const abort = new AbortController();
22
- const timestamp = new Date().getTime();
23
-
24
- console.log("call", url, options);
25
- if (options?.abortOn) {
26
- console.log("setting up abort", options.abortOn);
27
- const previousAbort = options.abortOn && request.abortControllers[options.abortOn];
28
- // If there is already an abort controller set for this key, abort it
29
- if (previousAbort) {
30
- previousAbort.abort.abort();
31
- }
32
-
33
- // Set the new abort controller for this key
34
- request.abortControllers[options.abortOn] = { abort, timestamp };
35
- options.signal = abort.signal;
26
+ const previousAbort = request.abortControllers[abortKey];
27
+ // If there is already an abort controller set for this key, abort it
28
+ if (previousAbort) {
29
+ previousAbort.abort.abort("Request was aborted due to a newer request being made");
36
30
  }
37
31
 
38
- console.log("current timestamp:", timestamp);
39
- const response = await fetch(request.url(url), options);
32
+ // Set the new abort controller for this key
33
+ request.abortControllers[abortKey] = { abort, timestamp };
34
+ options.signal = abort.signal;
35
+ }
40
36
 
41
- // handle the case where the request was aborted too late, and we need to abort the response via timestamp check
42
- if (options?.abortOn) {
43
- if (request.abortControllers[options.abortOn].timestamp !== timestamp) {
44
- console.log("aborting request", options.abortOn, timestamp, "!==", request.abortControllers[options.abortOn].timestamp);
45
- return { abort: true };
46
- }
37
+ const response = await fetch(request.url(url), options);
47
38
 
48
- delete request.abortControllers[options.abortOn];
49
- }
39
+ // Verify the app version of the client and server are matching
40
+ checkAppVersion(response);
50
41
 
51
- const result = await response.json();
52
-
53
- if (response.status === 401) {
54
- const onUnauthorized = danxOptions.value.request?.onUnauthorized;
55
- return onUnauthorized ? onUnauthorized(response) : {
56
- error: true,
57
- message: "Unauthorized"
58
- };
42
+ // handle the case where the request was aborted too late, and we need to abort the response via timestamp check
43
+ if (abortKey) {
44
+ // If the request was aborted too late, but there was still another request that was made after the current,
45
+ // then abort the current request with an abort flag
46
+ if (timestamp < request.abortControllers[abortKey].timestamp) {
47
+ throw new Error("Request was aborted due to a newer request being made: " + timestamp + " < " + request.abortControllers[abortKey].timestamp);
59
48
  }
60
49
 
61
- if (response.status > 400) {
62
- if (result.exception && !result.error) {
63
- result.error = true;
64
- }
65
- }
50
+ // Otherwise, the current is the most recent request, so we can delete the abort controller
51
+ delete request.abortControllers[abortKey];
52
+ }
53
+
54
+ const result = await response.json();
66
55
 
67
- return result;
68
- } catch (error: any) {
69
- return {
70
- error: error.message || "An error occurred fetching the data"
56
+ if (response.status === 401) {
57
+ const onUnauthorized = danxOptions.value.request?.onUnauthorized;
58
+ return onUnauthorized ? onUnauthorized(result, response) : {
59
+ error: true,
60
+ message: "Unauthorized",
61
+ ...result
71
62
  };
72
63
  }
64
+
65
+ if (response.status > 400) {
66
+ if (result.exception && !result.error) {
67
+ result.error = true;
68
+ }
69
+ }
70
+
71
+ return result;
73
72
  },
74
73
  async get(url, options) {
75
74
  return await request.call(url, {
@@ -97,6 +96,23 @@ export const request: RequestApi = {
97
96
  }
98
97
  };
99
98
 
99
+ /**
100
+ * Checks the app version of the client and server to see if they match.
101
+ * If they do not match, the onAppVersionMismatch callback is called
102
+ */
103
+ function checkAppVersion(response: HttpResponse) {
104
+ const requestOptions = danxOptions.value.request;
105
+ if (!requestOptions || !requestOptions.headers || !requestOptions.onAppVersionMismatch) {
106
+ return;
107
+ }
108
+
109
+ const clientAppVersion = requestOptions.headers["X-App-Version"] || "";
110
+ const serverAppVersion = response.headers.get("X-App-Version");
111
+ if (clientAppVersion && clientAppVersion !== serverAppVersion) {
112
+ requestOptions.onAppVersionMismatch(serverAppVersion);
113
+ }
114
+ }
115
+
100
116
  /**
101
117
  * Fetches a resource list applying the filter. If there is a selected resource,
102
118
  * stores that resource from the already populated list. If that resource does not exist
@@ -5,8 +5,7 @@ import { request } from "./request";
5
5
  export function useActionRoutes(baseUrl: string): ListControlsRoutes {
6
6
  return {
7
7
  list(pager?) {
8
- console.log("here");
9
- return request.post(`${baseUrl}/list`, pager, { abortOn: "list" });
8
+ return request.post(`${baseUrl}/list`, pager);
10
9
  },
11
10
  summary(filter) {
12
11
  return request.post(`${baseUrl}/summary`, { filter });
@@ -45,7 +45,7 @@
45
45
 
46
46
  &.q-field--labeled {
47
47
  .q-field__control-container {
48
- padding-top: 1.1rem;
48
+ padding-top: 0;
49
49
  }
50
50
 
51
51
  &.q-textarea {
@@ -1,5 +1,5 @@
1
1
  import { ComputedRef, Ref, ShallowRef } from "vue";
2
- import { RouteLocationNormalizedLoaded, Router } from "vue-router";
2
+ import { Router } from "vue-router";
3
3
  import { ActionTargetItem } from "./actions";
4
4
  import { AnyObject, LabelValueItem } from "./shared";
5
5
 
@@ -68,8 +68,7 @@ export interface PagedItems {
68
68
  }
69
69
 
70
70
  export interface ListControlsInitializeOptions {
71
- vueRoute: RouteLocationNormalizedLoaded;
72
- vueRouter: Router;
71
+ vueRouter?: Router;
73
72
  }
74
73
 
75
74
  export interface ActionController {
@@ -1,3 +1,5 @@
1
+ import { AnyObject } from "src/types/shared";
2
+
1
3
  export interface RequestApi {
2
4
  abortControllers: { [key: string]: { abort: AbortController, timestamp: number } };
3
5
 
@@ -20,8 +22,9 @@ export interface HttpResponse {
20
22
 
21
23
  export interface RequestOptions {
22
24
  baseUrl?: string;
23
- headers?: object;
24
- onUnauthorized?: (response) => object;
25
+ headers?: AnyObject;
26
+ onUnauthorized?: (result: any, response: Response) => object;
27
+ onAppVersionMismatch?: (version) => void;
25
28
  }
26
29
 
27
30
  export interface RequestCallOptions extends RequestInit {
package/tsconfig.json CHANGED
@@ -42,7 +42,6 @@
42
42
  "types/**/*.d.ts"
43
43
  ],
44
44
  "exclude": [
45
- "node_modules",
46
45
  "dist",
47
46
  "**/*.spec.ts"
48
47
  ]