quasar-ui-danx 0.4.5 → 0.4.6

Sign up to get free protection for your applications and to get access to all the features.
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.6",
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,57 @@ 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(response) : {
59
+ error: true,
60
+ message: "Unauthorized"
71
61
  };
72
62
  }
63
+
64
+ if (response.status > 400) {
65
+ if (result.exception && !result.error) {
66
+ result.error = true;
67
+ }
68
+ }
69
+
70
+ return result;
73
71
  },
74
72
  async get(url, options) {
75
73
  return await request.call(url, {
@@ -97,6 +95,23 @@ export const request: RequestApi = {
97
95
  }
98
96
  };
99
97
 
98
+ /**
99
+ * Checks the app version of the client and server to see if they match.
100
+ * If they do not match, the onAppVersionMismatch callback is called
101
+ */
102
+ function checkAppVersion(response: HttpResponse) {
103
+ const requestOptions = danxOptions.value.request;
104
+ if (!requestOptions || !requestOptions.headers || !requestOptions.onAppVersionMismatch) {
105
+ return;
106
+ }
107
+
108
+ const clientAppVersion = requestOptions.headers["X-App-Version"] || "";
109
+ const serverAppVersion = response.headers.get("X-App-Version");
110
+ if (clientAppVersion && clientAppVersion !== serverAppVersion) {
111
+ requestOptions.onAppVersionMismatch(serverAppVersion);
112
+ }
113
+ }
114
+
100
115
  /**
101
116
  * Fetches a resource list applying the filter. If there is a selected resource,
102
117
  * 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;
25
+ headers?: AnyObject;
24
26
  onUnauthorized?: (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
  ]