quasar-ui-danx 0.4.68 → 0.4.71

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.68",
3
+ "version": "0.4.71",
4
4
  "author": "Dan <dan@flytedesk.com>",
5
5
  "description": "DanX Vue / Quasar component library",
6
6
  "license": "MIT",
@@ -1,7 +1,7 @@
1
1
  import { useDebounceFn } from "@vueuse/core";
2
2
  import { FaSolidCopy as CopyIcon, FaSolidPencil as EditIcon, FaSolidTrash as DeleteIcon } from "danx-icon";
3
3
  import { uid } from "quasar";
4
- import { h, isReactive, Ref, shallowReactive, shallowRef } from "vue";
4
+ import { h, Ref, shallowRef } from "vue";
5
5
  import { ConfirmActionDialog, CreateNewWithNameDialog } from "../components";
6
6
  import type {
7
7
  ActionGlobalOptions,
@@ -55,39 +55,27 @@ export function useActions(actions: ActionOptions[], globalOptions: ActionGlobal
55
55
  */
56
56
  function getAction(actionName: string, actionOptions?: Partial<ActionOptions>): ResourceAction {
57
57
  /// Resolve the action options or resource action based on the provided input
58
- let resourceAction: Partial<ResourceAction> = actions.find(a => a.name === actionName) || { name: actionName };
59
-
60
- if (actionOptions) {
61
- Object.assign(resourceAction, actionOptions);
62
- }
58
+ const baseOptions: Partial<ResourceAction> = actions.find(a => a.name === actionName) || { name: actionName };
63
59
 
64
60
  // If the action is already reactive, return it
65
- if (!isReactive(resourceAction) || !("__type" in resourceAction)) {
66
- resourceAction = storeObject({
67
- onAction: globalOptions?.routes?.applyAction,
68
- onBatchAction: globalOptions?.routes?.batchAction,
69
- onBatchSuccess: globalOptions?.controls?.clearSelectedRows,
70
- ...globalOptions,
71
- ...resourceAction,
72
- isApplying: false,
73
- __type: "__Action:" + namespace
74
- });
75
-
76
- // Splice the resourceAction in place of the action in the actions list
77
- actions.splice(actions.findIndex(a => a.name === actionName), 1, resourceAction as ResourceAction);
78
- }
79
-
80
- // Return a clone of the action so it can be modified without affecting the original
81
- const clonedAction = shallowReactive({ ...resourceAction }) as ResourceAction;
61
+ const resourceAction = storeObject({
62
+ onAction: globalOptions?.routes?.applyAction,
63
+ onBatchAction: globalOptions?.routes?.batchAction,
64
+ onBatchSuccess: globalOptions?.controls?.clearSelectedRows,
65
+ ...baseOptions,
66
+ ...actionOptions,
67
+ isApplying: false,
68
+ __type: "__Action:" + namespace
69
+ }) as ResourceAction;
82
70
 
83
71
  // Assign Trigger function if it doesn't exist
84
- if (clonedAction.debounce) {
85
- clonedAction.trigger = useDebounceFn((target, input) => performAction(clonedAction, target, input), clonedAction.debounce);
72
+ if (resourceAction.debounce) {
73
+ resourceAction.trigger = useDebounceFn((target, input) => performAction(resourceAction, target, input), resourceAction.debounce);
86
74
  } else {
87
- clonedAction.trigger = (target, input) => performAction(clonedAction, target, input);
75
+ resourceAction.trigger = (target, input) => performAction(resourceAction, target, input);
88
76
  }
89
77
 
90
- return clonedAction;
78
+ return resourceAction;
91
79
  }
92
80
 
93
81
  /**
@@ -1,6 +1,6 @@
1
1
  import { Ref } from "vue";
2
2
  import { danxOptions } from "../config";
3
- import { HttpResponse, RequestApi } from "../types";
3
+ import { ActiveRequest, HttpResponse, RequestApi } from "../types";
4
4
  import { sleep } from "./utils";
5
5
 
6
6
  /**
@@ -8,7 +8,7 @@ import { sleep } from "./utils";
8
8
  * to make GET and POST requests easier w/ JSON payloads
9
9
  */
10
10
  export const request: RequestApi = {
11
- abortControllers: {},
11
+ activeRequests: {},
12
12
 
13
13
  url(url) {
14
14
  if (url.startsWith("http")) {
@@ -19,20 +19,29 @@ export const request: RequestApi = {
19
19
 
20
20
  async call(url, options) {
21
21
  options = options || {};
22
- const abortKey = options?.abortOn !== undefined ? options.abortOn : url + JSON.stringify(options.params || "");
22
+ const requestKey = options?.requestKey || url + JSON.stringify(options.params || "");
23
+ const waitOnPrevious = !!options?.waitOnPrevious;
24
+ const useMostRecentResponse = !!options?.useMostRecentResponse;
25
+ const shouldAbortPrevious = !waitOnPrevious;
23
26
  const timestamp = Date.now();
24
27
 
25
- if (abortKey) {
26
- const abort = new AbortController();
27
- const previousAbort = request.abortControllers[abortKey];
28
+ // If there was a request with the same key made that is still active, track that here
29
+ const previousRequest = request.activeRequests[requestKey];
30
+
31
+ // Set the current active request to this one
32
+ const currentRequest: ActiveRequest = { timestamp };
33
+ request.activeRequests[requestKey] = currentRequest;
34
+
35
+ if (shouldAbortPrevious) {
28
36
  // If there is already an abort controller set for this key, abort it
29
- if (previousAbort) {
30
- previousAbort.abort.abort("Request was aborted due to a newer request being made");
37
+ if (previousRequest) {
38
+ previousRequest.abortController?.abort("Request was aborted due to a newer request being made");
31
39
  }
32
40
 
41
+ const abortController = new AbortController();
33
42
  // Set the new abort controller for this key
34
- request.abortControllers[abortKey] = { abort, timestamp };
35
- options.signal = abort.signal;
43
+ currentRequest.abortController = abortController;
44
+ options.signal = abortController.signal;
36
45
  }
37
46
 
38
47
  if (options.params) {
@@ -47,49 +56,85 @@ export const request: RequestApi = {
47
56
  delete options.params;
48
57
  }
49
58
 
50
- let response = null;
59
+ let resolvePromise!: (value: any) => any;
60
+ let rejectPromise!: (reason?: any) => any;
61
+ currentRequest.requestPromise = new Promise((resolve, reject) => {
62
+ resolvePromise = resolve;
63
+ rejectPromise = reject;
64
+ });
65
+
66
+ // If there is a previous request still active, wait for it to finish before proceeding (if the waitForPrevious flag is set)
67
+ if (waitOnPrevious && previousRequest?.requestPromise) {
68
+ try {
69
+ await previousRequest.requestPromise;
70
+ } catch (e) {
71
+ // We don't care if it fails, we just need to wait for it to complete
72
+ }
73
+ }
74
+
75
+ // Wait to finish the request before proceeding
76
+ let response: Response;
51
77
  try {
52
78
  response = await fetch(request.url(url), options);
53
79
  } catch (e) {
54
80
  if (options.ignoreAbort && (e + "").match(/Request was aborted/)) {
55
- return { abort: true };
81
+ const abortResponse = { abort: true };
82
+ resolvePromise(abortResponse);
83
+ return abortResponse;
56
84
  }
85
+ rejectPromise(e);
57
86
  throw e;
58
87
  }
59
88
 
60
89
  // Verify the app version of the client and server are matching
61
90
  checkAppVersion(response);
62
91
 
63
- // handle the case where the request was aborted too late, and we need to abort the response via timestamp check
64
- if (abortKey) {
65
- // If the request was aborted too late, but there was still another request that was made after the current,
66
- // then abort the current request with an abort flag
67
- if (timestamp < request.abortControllers[abortKey].timestamp) {
68
- return { abort: true };
92
+ // Track the most recent request (maybe another request with the same key was made after this request started)
93
+ let mostRecentRequest = request.activeRequests[requestKey];
94
+
95
+ // Always fetch the result
96
+ let responseJson = await response.json();
97
+
98
+ // Send the real JSON response to the promise in case other requests are waiting on this request result
99
+ resolvePromise(responseJson);
100
+
101
+ // If this request is not the most recent request...
102
+ if (mostRecentRequest.timestamp !== timestamp) {
103
+ // and it should be aborted but was aborted too late, return an aborted response
104
+ if (shouldAbortPrevious) {
105
+ responseJson = { abort: true };
106
+ } else if (useMostRecentResponse) {
107
+ // or if there is a more recent request, and the useMoreRecentResponse flag is set, update this response to the more recent one
108
+ do {
109
+ // Always update on each iteration to make sure we're checking the current most recent
110
+ // (maybe additional requests will be made before the current most recent finishes)
111
+ mostRecentRequest = request.activeRequests[requestKey];
112
+ responseJson = await mostRecentRequest.requestPromise;
113
+
114
+ // If the most recent request is the same as this one, break out of the loop
115
+ if (request.activeRequests[requestKey].timestamp === mostRecentRequest.timestamp) {
116
+ break;
117
+ }
118
+ } while (mostRecentRequest.timestamp !== request.activeRequests[requestKey].timestamp);
69
119
  }
70
-
71
- // Otherwise, the current is the most recent request, so we can delete the abort controller
72
- delete request.abortControllers[abortKey];
73
120
  }
74
121
 
75
- const result = await response.json();
76
-
77
122
  if (response.status === 401) {
78
123
  const onUnauthorized = danxOptions.value.request?.onUnauthorized;
79
- return onUnauthorized ? onUnauthorized(result, response) : {
124
+ return onUnauthorized ? onUnauthorized(responseJson, response) : {
80
125
  error: true,
81
126
  message: "Unauthorized",
82
- ...result
127
+ ...responseJson
83
128
  };
84
129
  }
85
130
 
86
131
  if (response.status > 400) {
87
- if (result.exception && !result.error) {
88
- result.error = true;
132
+ if (responseJson.exception && !responseJson.error) {
133
+ responseJson.error = true;
89
134
  }
90
135
  }
91
136
 
92
- return result;
137
+ return responseJson;
93
138
  },
94
139
 
95
140
  async poll(url: string, options, interval, fnUntil) {
@@ -56,7 +56,8 @@ export function useActionRoutes(baseUrl: string, extend?: object): ListControlsR
56
56
  async applyAction(action, target, data, options?) {
57
57
  options = {
58
58
  ...options,
59
- ignoreAbort: true,
59
+ waitOnPrevious: true,
60
+ useMostRecentResponse: true,
60
61
  headers: {
61
62
  ...options?.headers,
62
63
  "X-Timestamp": Date.now().toString()
@@ -86,7 +87,7 @@ export function useActionRoutes(baseUrl: string, extend?: object): ListControlsR
86
87
  batchAction(action, targets, data, options?) {
87
88
  options = {
88
89
  ...options,
89
- ignoreAbort: true
90
+ waitOnPrevious: true
90
91
  };
91
92
  return request.post(`${baseUrl}/batch-action`, { action, filter: { id: targets.map(r => r.id) }, data }, options);
92
93
  },
@@ -1,7 +1,15 @@
1
1
  import { AnyObject } from "./shared";
2
2
 
3
+ export interface ActiveRequest {
4
+ requestPromise?: Promise<any>,
5
+ abortController?: AbortController,
6
+ timestamp: number
7
+ }
8
+
3
9
  export interface RequestApi {
4
- abortControllers: { [key: string]: { abort: AbortController, timestamp: number } };
10
+ activeRequests: {
11
+ [key: string]: ActiveRequest
12
+ };
5
13
 
6
14
  url(url: string): string;
7
15
 
@@ -30,7 +38,9 @@ export interface RequestOptions {
30
38
  }
31
39
 
32
40
  export interface RequestCallOptions extends RequestInit {
33
- abortOn?: string;
41
+ requestKey?: string;
42
+ waitOnPrevious?: boolean;
43
+ useMostRecentResponse?: boolean;
34
44
  ignoreAbort?: boolean;
35
45
  params?: AnyObject;
36
46
  }