@7365admin1/layer-common 3.2.8-staging.212 → 3.2.8-staging.214
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.
|
@@ -130,6 +130,7 @@
|
|
|
130
130
|
|
|
131
131
|
<script setup lang="ts">
|
|
132
132
|
import useSiteSettings from "../composables/useSiteSettings";
|
|
133
|
+
import { cameraErrorConverter } from "../utils/data";
|
|
133
134
|
|
|
134
135
|
const prop = defineProps({
|
|
135
136
|
title: {
|
|
@@ -298,11 +299,7 @@ async function submit() {
|
|
|
298
299
|
|
|
299
300
|
emit("success", `${prop.title} added successfully!`);
|
|
300
301
|
} catch (error: any) {
|
|
301
|
-
|
|
302
|
-
"error",
|
|
303
|
-
error.response?._data?.message || "Failed to add camera"
|
|
304
|
-
);
|
|
305
|
-
emit("error", error.response?._data?.message || "Failed to add camera");
|
|
302
|
+
emit("error", cameraErrorConverter(error, prop.type));
|
|
306
303
|
} finally {
|
|
307
304
|
disable.value = false;
|
|
308
305
|
}
|
|
@@ -248,6 +248,7 @@
|
|
|
248
248
|
<script setup lang="ts">
|
|
249
249
|
import type { PropType } from "vue";
|
|
250
250
|
import useSiteSettings from "../composables/useSiteSettings";
|
|
251
|
+
import { cameraErrorConverter } from "../utils/data";
|
|
251
252
|
|
|
252
253
|
const events = defineEmits(["update:value", "row-click"]);
|
|
253
254
|
|
|
@@ -363,8 +364,7 @@ function openDialogDelete() {
|
|
|
363
364
|
}
|
|
364
365
|
|
|
365
366
|
function handleError(msg: string) {
|
|
366
|
-
|
|
367
|
-
showMessage(text, "error");
|
|
367
|
+
showMessage(msg, "error");
|
|
368
368
|
}
|
|
369
369
|
|
|
370
370
|
function handleSuccess() {
|
|
@@ -416,10 +416,7 @@ async function submitDelete() {
|
|
|
416
416
|
await getCameraRefresh();
|
|
417
417
|
dialogDelete.value = false;
|
|
418
418
|
} catch (error: any) {
|
|
419
|
-
showMessage(
|
|
420
|
-
error?.response?._data?.message || "Failed to delete camera.",
|
|
421
|
-
"error"
|
|
422
|
-
);
|
|
419
|
+
showMessage(cameraErrorConverter(error, prop.type), "error");
|
|
423
420
|
}
|
|
424
421
|
}
|
|
425
422
|
</script>
|
|
@@ -348,7 +348,8 @@ const { getRoleById: _getRoleById, deleteRole } = useRole();
|
|
|
348
348
|
|
|
349
349
|
const { data: role, refresh: getRoleById } = useLazyAsyncData(
|
|
350
350
|
"role-permissions-get-by-id",
|
|
351
|
-
() => _getRoleById(roleId.value)
|
|
351
|
+
() => _getRoleById(roleId.value),
|
|
352
|
+
{ immediate: false }
|
|
352
353
|
);
|
|
353
354
|
|
|
354
355
|
watchEffect(() => {
|
package/composables/useMember.ts
CHANGED
|
@@ -28,10 +28,6 @@ export default function useMember() {
|
|
|
28
28
|
function getAllByUserId(user: string) {
|
|
29
29
|
return useNuxtApp().$api<TMember>(`/api/members/users/${user}`);
|
|
30
30
|
}
|
|
31
|
-
function getByMemberId(user: string) {
|
|
32
|
-
return useNuxtApp().$api<TMember>(`/api//user/${user}`);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
31
|
function getByUserIdType(user: string, type: string) {
|
|
36
32
|
return useNuxtApp().$api<TMember>(`/api/members/user/${user}/app/${type}`);
|
|
37
33
|
}
|
|
@@ -165,7 +161,6 @@ export default function useMember() {
|
|
|
165
161
|
createUserByVerification,
|
|
166
162
|
createMemberInvite,
|
|
167
163
|
getByUserIdType,
|
|
168
|
-
getByMemberId,
|
|
169
164
|
updateMemberStatus,
|
|
170
165
|
updateMemberRole,
|
|
171
166
|
createMemberDirect,
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@7365admin1/layer-common",
|
|
3
3
|
"license": "MIT",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"version": "3.2.8-staging.
|
|
5
|
+
"version": "3.2.8-staging.214",
|
|
6
6
|
"author": "7365admin1",
|
|
7
7
|
"main": "./nuxt.config.ts",
|
|
8
8
|
"//files": "What a consumer extending this layer actually loads. Without this npm ships the whole working tree - the changesets, the CI workflows, the render harness in tools/ and any scratch directory that happened to exist at publish time. Nuxt resolves a layer by directory, so every runtime directory below has to stay listed; adding a new top-level runtime directory means adding it here too.",
|
package/utils/data.test.ts
CHANGED
|
@@ -93,3 +93,87 @@ test("a non-array levels value is counted as zero, not as its own length", () =>
|
|
|
93
93
|
assert.equal(levelCount({ levels: 7 as unknown as [] }), 0);
|
|
94
94
|
assert.equal(levelCount({ levels: "12" as unknown as [] }), 0);
|
|
95
95
|
});
|
|
96
|
+
|
|
97
|
+
import { cameraErrorConverter } from "./data.ts";
|
|
98
|
+
|
|
99
|
+
/** Shape of an ofetch failure: the status and body live under `response`. */
|
|
100
|
+
const apiError = (status: number, message?: string) => ({
|
|
101
|
+
response: { status, _data: message === undefined ? {} : { message } },
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("a duplicate reads as a duplicate", () => {
|
|
105
|
+
assert.match(
|
|
106
|
+
cameraErrorConverter(apiError(400, "ANPR already exist."), "ip"),
|
|
107
|
+
/already exists on this site/
|
|
108
|
+
);
|
|
109
|
+
assert.match(
|
|
110
|
+
cameraErrorConverter(apiError(409), "ip"),
|
|
111
|
+
/already exists on this site/
|
|
112
|
+
);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("a failure that is NOT a duplicate never says duplicate", () => {
|
|
116
|
+
const notDuplicate = [
|
|
117
|
+
cameraErrorConverter(apiError(401), "ip"),
|
|
118
|
+
cameraErrorConverter(apiError(403), "ip"),
|
|
119
|
+
cameraErrorConverter(apiError(404), "ip"),
|
|
120
|
+
cameraErrorConverter(apiError(429), "ip"),
|
|
121
|
+
cameraErrorConverter(apiError(500, "Failed to create ANPR."), "ip"),
|
|
122
|
+
cameraErrorConverter(new Error("Network request failed"), "ip"),
|
|
123
|
+
];
|
|
124
|
+
|
|
125
|
+
for (const message of notDuplicate) {
|
|
126
|
+
assert.doesNotMatch(message, /already exist/i, message);
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("each cause gets its own message", () => {
|
|
131
|
+
assert.match(cameraErrorConverter(apiError(401), "ip"), /session has expired/);
|
|
132
|
+
assert.match(
|
|
133
|
+
cameraErrorConverter(apiError(403), "ip"),
|
|
134
|
+
/do not have permission/
|
|
135
|
+
);
|
|
136
|
+
assert.match(cameraErrorConverter(apiError(404), "ip"), /no longer exists/);
|
|
137
|
+
assert.match(cameraErrorConverter(apiError(429), "ip"), /Too many attempts/);
|
|
138
|
+
assert.match(
|
|
139
|
+
cameraErrorConverter(apiError(500, "Failed to create ANPR."), "ip"),
|
|
140
|
+
/could not save this CCTV camera/
|
|
141
|
+
);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test("no response at all reads as a connection problem, not a server refusal", () => {
|
|
145
|
+
assert.match(
|
|
146
|
+
cameraErrorConverter(new Error("Failed to fetch"), "ip"),
|
|
147
|
+
/Could not reach the server/
|
|
148
|
+
);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test("a rejected field is named the way the form names it", () => {
|
|
152
|
+
assert.equal(
|
|
153
|
+
cameraErrorConverter(apiError(400, '"host" is required'), "ip"),
|
|
154
|
+
"URL is required."
|
|
155
|
+
);
|
|
156
|
+
assert.equal(
|
|
157
|
+
cameraErrorConverter(apiError(400, '"name" is not allowed to be empty'), "ip"),
|
|
158
|
+
"Camera Name cannot be empty."
|
|
159
|
+
);
|
|
160
|
+
// A field with no friendly label still reads as a sentence.
|
|
161
|
+
assert.equal(
|
|
162
|
+
cameraErrorConverter(apiError(400, '"guardPost" must be a number'), "ip"),
|
|
163
|
+
"guardPost must be a number."
|
|
164
|
+
);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test("a plain 400 sentence from the API is passed through unchanged", () => {
|
|
168
|
+
assert.equal(
|
|
169
|
+
cameraErrorConverter(apiError(400, "Invalid _id format"), "ip"),
|
|
170
|
+
"Invalid _id format"
|
|
171
|
+
);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test("the camera type decides the wording", () => {
|
|
175
|
+
assert.match(cameraErrorConverter(apiError(404), "anpr"), /ANPR camera/);
|
|
176
|
+
assert.match(cameraErrorConverter(apiError(404), "ip"), /CCTV camera/);
|
|
177
|
+
// Unknown/absent type falls back to CCTV, which is what the panel defaults to.
|
|
178
|
+
assert.match(cameraErrorConverter(apiError(404)), /CCTV camera/);
|
|
179
|
+
});
|
package/utils/data.ts
CHANGED
|
@@ -30,6 +30,98 @@ export const errorConverter = (data: any): string => {
|
|
|
30
30
|
return error;
|
|
31
31
|
};
|
|
32
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Field names the camera API validates, in the wording the camera form uses,
|
|
35
|
+
* so a rejection reads as the label the person is looking at.
|
|
36
|
+
*/
|
|
37
|
+
const CAMERA_FIELD_LABELS: Record<string, string> = {
|
|
38
|
+
host: "URL",
|
|
39
|
+
name: "Camera Name",
|
|
40
|
+
username: "User",
|
|
41
|
+
password: "Password",
|
|
42
|
+
direction: "Type",
|
|
43
|
+
category: "Category",
|
|
44
|
+
site: "Site",
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/** Joi phrasing -> everyday phrasing. Anything unmapped is passed through. */
|
|
48
|
+
const CAMERA_VALIDATION_PHRASES: Array<[RegExp, string]> = [
|
|
49
|
+
[/^is required$/, "is required."],
|
|
50
|
+
[/^is not allowed to be empty$/, "cannot be empty."],
|
|
51
|
+
[/^must be a string$/, "is not valid."],
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Turns a save/delete failure on a site camera into a sentence the person
|
|
56
|
+
* setting up the camera can act on.
|
|
57
|
+
*
|
|
58
|
+
* The camera panel used to report every `type: "ip"` failure as "CCTV camera
|
|
59
|
+
* already exist", whatever actually went wrong - a signed-out session, a
|
|
60
|
+
* missing permission, a rejected field and a server outage all read as a
|
|
61
|
+
* duplicate. This maps the cases the API really returns instead.
|
|
62
|
+
*/
|
|
63
|
+
export const cameraErrorConverter = (error: any, type?: string): string => {
|
|
64
|
+
const camera = type === "anpr" ? "ANPR camera" : "CCTV camera";
|
|
65
|
+
|
|
66
|
+
const status =
|
|
67
|
+
error?.response?.status ?? error?.statusCode ?? error?.status ?? null;
|
|
68
|
+
|
|
69
|
+
const serverMessage = String(
|
|
70
|
+
error?.response?._data?.message ?? error?.data?.message ?? ""
|
|
71
|
+
).trim();
|
|
72
|
+
|
|
73
|
+
// No response at all: the request never reached the API.
|
|
74
|
+
if (!status) {
|
|
75
|
+
return `Could not reach the server, so this ${camera} was not saved. Check your internet connection and try again.`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (status === 401) {
|
|
79
|
+
return `Your session has expired. Sign in again, then save this ${camera}.`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (status === 403) {
|
|
83
|
+
return `You do not have permission to change cameras on this site. Ask your iService365 administrator for access.`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (status === 404) {
|
|
87
|
+
return `This ${camera} no longer exists. Refresh the list and try again.`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (status === 429) {
|
|
91
|
+
return "Too many attempts in a short time. Wait a moment and try again.";
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// The API reports a clash from the unique index as "ANPR already exist.",
|
|
95
|
+
// which is the same message for a CCTV record.
|
|
96
|
+
if (status === 409 || /already exist|duplicate/i.test(serverMessage)) {
|
|
97
|
+
return `A ${camera} with this URL already exists on this site. Check the list before adding it again.`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (status >= 500) {
|
|
101
|
+
return `The server could not save this ${camera}. Try again, and contact support if it keeps happening.`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (status === 400 && serverMessage) {
|
|
105
|
+
// Joi rejections arrive as `"host" is required`.
|
|
106
|
+
const field = serverMessage.match(/^"(\w+)"\s+(.+?)\.?$/);
|
|
107
|
+
|
|
108
|
+
if (field) {
|
|
109
|
+
const label = CAMERA_FIELD_LABELS[field[1]] ?? field[1];
|
|
110
|
+
const phrase =
|
|
111
|
+
CAMERA_VALIDATION_PHRASES.find(([pattern]) =>
|
|
112
|
+
pattern.test(field[2])
|
|
113
|
+
)?.[1] ?? `${field[2]}.`;
|
|
114
|
+
|
|
115
|
+
return `${label} ${phrase}`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return serverMessage;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return errorConverter(error);
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
|
|
33
125
|
/**
|
|
34
126
|
* A service-provider account can only be shown ITS OWN work orders, feedbacks
|
|
35
127
|
* and key logs, so every one of those screens scopes its request by the
|