@7365admin1/layer-common 4.97.1-staging.598 → 4.97.1-staging.600
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/components/AnprCameraBadge.vue +15 -2
- package/components/CameraForm.vue +73 -17
- package/components/CameraMain.vue +12 -3
- package/components/Chat/Message.vue +8 -3
- package/components/Snackbar.vue +36 -0
- package/components/TableMain.vue +12 -1
- package/components/VehicleAddSelection.vue +11 -4
- package/components/VehicleManagement.vue +83 -30
- package/components/VisitorDataFromScannedQRCodeForm.vue +6 -1
- package/components/VisitorForm.vue +43 -4
- package/components/VisitorManagement.vue +145 -8
- package/composables/useVisitorCameraChoices.ts +61 -0
- package/package.json +1 -1
- package/types/camera.d.ts +2 -0
- package/types/comment.d.ts +2 -0
- package/utils/anpr-direction.ts +7 -6
- package/utils/chat-sender.ts +17 -0
- package/utils/vehicle-actions.ts +23 -5
- package/utils/visitor-barrier.ts +42 -0
- package/utils/visitor-camera.ts +80 -0
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
class="anpr-badge anpr-badge--sync"
|
|
19
19
|
:disabled="syncing"
|
|
20
20
|
:aria-label="`Sync ${plateNumber} to all active ANPR cameras`"
|
|
21
|
-
v-tooltip:top="`On ${
|
|
21
|
+
v-tooltip:top="`On ${onItsList} of ${expected} active camera${expected === 1 ? '' : 's'} - sync to all`"
|
|
22
22
|
@click.stop="sync"
|
|
23
23
|
>
|
|
24
24
|
<v-progress-circular v-if="syncing" indeterminate size="11" width="2" />
|
|
@@ -77,6 +77,8 @@ const props = defineProps({
|
|
|
77
77
|
type: { type: String, default: "" },
|
|
78
78
|
/** The pass's expiry: once passed, "Allowed" reads "Expired" in red. */
|
|
79
79
|
end: { type: [String, Date] as PropType<string | Date | null>, default: null },
|
|
80
|
+
/** False hides Sync (the host's right to write this plate to the cameras). */
|
|
81
|
+
canSync: { type: Boolean, default: true },
|
|
80
82
|
/** Active cameras per list at the site; null hides Sync. */
|
|
81
83
|
activeCounts: {
|
|
82
84
|
type: Object as PropType<{ allowed: number; blocked: number } | null>,
|
|
@@ -120,10 +122,11 @@ function listTone(list: string) {
|
|
|
120
122
|
const showSync = computed(
|
|
121
123
|
() =>
|
|
122
124
|
!!props.plateId &&
|
|
125
|
+
props.canSync &&
|
|
123
126
|
!expired.value &&
|
|
124
127
|
LIVE.includes(props.status) &&
|
|
125
128
|
expected.value > 0 &&
|
|
126
|
-
|
|
129
|
+
onItsList.value < expected.value
|
|
127
130
|
);
|
|
128
131
|
|
|
129
132
|
async function sync() {
|
|
@@ -166,6 +169,16 @@ const uniqueCameras = computed(() => {
|
|
|
166
169
|
});
|
|
167
170
|
|
|
168
171
|
const count = computed<number>(() => uniqueCameras.value.length);
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Cameras holding the plate on ITS list - what Sync compares with. A resident
|
|
175
|
+
* blocked while its camera was off is still on that camera's allowed list,
|
|
176
|
+
* which is not a block (owner, 2026-10-23). An entry with no list still counts.
|
|
177
|
+
*/
|
|
178
|
+
const onItsList = computed<number>(() => {
|
|
179
|
+
const wanted = props.type === "blocklist" ? "blocked" : "allowed";
|
|
180
|
+
return uniqueCameras.value.filter((cam) => !cam.list || cam.list === wanted).length;
|
|
181
|
+
});
|
|
169
182
|
const isBlocked = computed(() => props.cameras?.some((c) => c.list === "blocked"));
|
|
170
183
|
</script>
|
|
171
184
|
|
|
@@ -139,15 +139,22 @@
|
|
|
139
139
|
/>
|
|
140
140
|
</v-col>
|
|
141
141
|
|
|
142
|
-
<!--
|
|
143
|
-
|
|
142
|
+
<!-- The gate this ANPR camera watches (owner, 2026-10-23), in place
|
|
143
|
+
of Guard House: 1 to 20, one camera per gate at a site - a gate
|
|
144
|
+
another camera uses cannot be picked. Messages name the camera
|
|
145
|
+
by it ("Entry Gate 1"). -->
|
|
146
|
+
<v-col v-if="isANPR" cols="12">
|
|
147
|
+
<InputLabel class="text-capitalize" title="Gate" required />
|
|
144
148
|
<v-select
|
|
145
|
-
v-model="camera.
|
|
146
|
-
:items="
|
|
149
|
+
v-model="camera.gate"
|
|
150
|
+
:items="gateOptions"
|
|
151
|
+
item-props
|
|
147
152
|
class="filter-field"
|
|
148
153
|
density="comfortable"
|
|
154
|
+
:rules="[gateRule]"
|
|
155
|
+
data-test="camera-gate"
|
|
149
156
|
/>
|
|
150
|
-
</v-col>
|
|
157
|
+
</v-col>
|
|
151
158
|
|
|
152
159
|
<v-col v-if="isANPR" cols="12">
|
|
153
160
|
<InputLabel class="text-capitalize" title="User" required />
|
|
@@ -224,9 +231,8 @@
|
|
|
224
231
|
</v-row>
|
|
225
232
|
</v-card-actions>
|
|
226
233
|
|
|
227
|
-
<!--
|
|
228
|
-
|
|
229
|
-
allowed list - on the camera itself. Ask before saving; the API refuses
|
|
234
|
+
<!-- An Entry / Both / Residents camera changed to Visitors or Exit
|
|
235
|
+
empties its allowed list - on the camera itself; its blocklist stays. Ask before saving; the API refuses
|
|
230
236
|
the change without this confirmation. -->
|
|
231
237
|
<v-dialog v-model="confirmClear" persistent width="540">
|
|
232
238
|
<DialogReusablePrompt
|
|
@@ -242,7 +248,7 @@
|
|
|
242
248
|
|
|
243
249
|
<!-- A new Entry / Both / Residents / Visitors camera, or a type change
|
|
244
250
|
that means the camera should now hold plates it did not (Visitors or
|
|
245
|
-
Exit to Entry / Both / Residents
|
|
251
|
+
Exit to Entry / Both / Residents; a new Exit camera too): ask first whether
|
|
246
252
|
to add the site's vehicles to it now. "Not now" saves the camera
|
|
247
253
|
only; its vehicles can be added later with Sync vehicles in the
|
|
248
254
|
camera list. -->
|
|
@@ -264,13 +270,15 @@
|
|
|
264
270
|
>
|
|
265
271
|
Cancel
|
|
266
272
|
</v-btn>
|
|
273
|
+
<!-- A short label: the long "Not now, save changes only" ran out of
|
|
274
|
+
the button. The message above says what it does. -->
|
|
267
275
|
<v-btn
|
|
268
276
|
class="text-none"
|
|
269
277
|
variant="outlined"
|
|
270
278
|
:loading="disable"
|
|
271
279
|
@click="saveWithoutVehicles"
|
|
272
280
|
>
|
|
273
|
-
{{ prop.mode === "edit" ? "
|
|
281
|
+
{{ prop.mode === "edit" ? "Save changes only" : "Add camera only" }}
|
|
274
282
|
</v-btn>
|
|
275
283
|
<v-btn
|
|
276
284
|
class="text-none screen-btn-primary"
|
|
@@ -421,6 +429,7 @@ const camera = ref<TSiteCamera>({
|
|
|
421
429
|
direction: "both",
|
|
422
430
|
name: "",
|
|
423
431
|
guardPost: null,
|
|
432
|
+
gate: null,
|
|
424
433
|
status: "active",
|
|
425
434
|
});
|
|
426
435
|
|
|
@@ -456,7 +465,7 @@ const message = ref("");
|
|
|
456
465
|
const isANPR = computed(() => prop.type === "anpr");
|
|
457
466
|
const isCCTV = computed(() => prop.type === "ip");
|
|
458
467
|
|
|
459
|
-
const { addCamera, updateSiteCamera, testSiteCamera } = useSiteSettings();
|
|
468
|
+
const { addCamera, updateSiteCamera, testSiteCamera, getAllSiteCameras } = useSiteSettings();
|
|
460
469
|
|
|
461
470
|
/**
|
|
462
471
|
* Two ways to say where a camera is, and the guided one is the default.
|
|
@@ -592,6 +601,7 @@ const hasChanges = computed(() => {
|
|
|
592
601
|
camera.value.status !== prop.camera.status ||
|
|
593
602
|
camera.value.category !== prop.camera.category ||
|
|
594
603
|
camera.value.guardPost !== prop.camera.guardPost ||
|
|
604
|
+
(camera.value.gate ?? null) !== (prop.camera.gate ?? null) ||
|
|
595
605
|
camera.value.direction !== prop.camera.direction ||
|
|
596
606
|
camera.value.name !== prop.camera.name ||
|
|
597
607
|
// Switching an existing camera over to the recorder fields is a change
|
|
@@ -640,8 +650,9 @@ const clearPromptMessage = computed(() =>
|
|
|
640
650
|
"vehicles will no longer be let in through this camera. Vehicle " +
|
|
641
651
|
"records and the plates on your other cameras are not affected."
|
|
642
652
|
: "Every plate on this camera's allowed list will be cleared from the " +
|
|
643
|
-
"camera, including plates added directly on it, because
|
|
644
|
-
|
|
653
|
+
"camera, including plates added directly on it, because " +
|
|
654
|
+
`${directionLabel(camera.value.direction)} cameras must not hold ` +
|
|
655
|
+
"whitelist or season pass plates. Those vehicles " +
|
|
645
656
|
"will no longer be let in through this camera. Blocked plates stay on " +
|
|
646
657
|
"it. Vehicle records and the plates on your other cameras are not " +
|
|
647
658
|
"affected."
|
|
@@ -726,8 +737,8 @@ const addVehiclesMessage = computed(
|
|
|
726
737
|
`Yes adds ${plateSetWords.value} to this camera now, one plate at a ` +
|
|
727
738
|
`time, and you can watch each one go on. ` +
|
|
728
739
|
(prop.mode === "edit"
|
|
729
|
-
? `
|
|
730
|
-
: `
|
|
740
|
+
? `Save changes only saves your changes without the vehicles - `
|
|
741
|
+
: `Add camera only adds the camera without the vehicles - `) +
|
|
731
742
|
`you can add them later with Sync vehicles in the camera list.`
|
|
732
743
|
);
|
|
733
744
|
|
|
@@ -848,7 +859,7 @@ const clearOnAddMessage = computed(() => {
|
|
|
848
859
|
(what
|
|
849
860
|
? ` Either way, ${what} is then added to it, one plate at a time, and ` +
|
|
850
861
|
`you can watch each one go on.`
|
|
851
|
-
: `
|
|
862
|
+
: ` This camera receives no vehicle plates.`)
|
|
852
863
|
);
|
|
853
864
|
});
|
|
854
865
|
|
|
@@ -865,7 +876,7 @@ function submit() {
|
|
|
865
876
|
return;
|
|
866
877
|
}
|
|
867
878
|
|
|
868
|
-
// A new
|
|
879
|
+
// A new camera that receives no plates: only the clear question applies.
|
|
869
880
|
if (isNewActiveAnpr.value) {
|
|
870
881
|
askClearOnAdd.value = true;
|
|
871
882
|
return;
|
|
@@ -912,6 +923,7 @@ async function save(
|
|
|
912
923
|
...(camera.value.password ? { password: camera.value.password } : {}),
|
|
913
924
|
category: camera.value.category,
|
|
914
925
|
guardPost: camera.value.guardPost,
|
|
926
|
+
...(isANPR.value && camera.value.gate ? { gate: camera.value.gate } : {}),
|
|
915
927
|
direction: camera.value.direction,
|
|
916
928
|
status: camera.value.status,
|
|
917
929
|
name: camera.value.name,
|
|
@@ -958,6 +970,50 @@ async function save(
|
|
|
958
970
|
}
|
|
959
971
|
}
|
|
960
972
|
|
|
973
|
+
/*
|
|
974
|
+
* Gate 1-20. The gates other ANPR cameras at the site already use are shown
|
|
975
|
+
* but cannot be picked (the server refuses them too). Read in the background;
|
|
976
|
+
* a failed read leaves every gate open and the server decides.
|
|
977
|
+
*/
|
|
978
|
+
const usedGates = ref<Map<number, string>>(new Map());
|
|
979
|
+
|
|
980
|
+
async function loadUsedGates() {
|
|
981
|
+
if (!isANPR.value || !prop.site) return;
|
|
982
|
+
try {
|
|
983
|
+
const res: any = await getAllSiteCameras({ site: prop.site, type: "anpr", limit: 100 });
|
|
984
|
+
const items: any[] = Array.isArray(res?.items) ? res.items : [];
|
|
985
|
+
const used = new Map<number, string>();
|
|
986
|
+
for (const item of items) {
|
|
987
|
+
const gate = Number(item?.gate);
|
|
988
|
+
if (!Number.isInteger(gate) || gate < 1) continue;
|
|
989
|
+
if (camera.value._id && String(item?._id) === String(camera.value._id)) continue;
|
|
990
|
+
used.set(gate, String(item?.direction ?? ""));
|
|
991
|
+
}
|
|
992
|
+
usedGates.value = used;
|
|
993
|
+
} catch {
|
|
994
|
+
usedGates.value = new Map();
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
onMounted(loadUsedGates);
|
|
998
|
+
|
|
999
|
+
const gateOptions = computed(() =>
|
|
1000
|
+
Array.from({ length: 20 }, (_, index) => {
|
|
1001
|
+
const gate = index + 1;
|
|
1002
|
+
const taken = usedGates.value.has(gate);
|
|
1003
|
+
return {
|
|
1004
|
+
title: taken ? `Gate ${gate} (used by another camera)` : `Gate ${gate}`,
|
|
1005
|
+
value: gate,
|
|
1006
|
+
disabled: taken,
|
|
1007
|
+
};
|
|
1008
|
+
})
|
|
1009
|
+
);
|
|
1010
|
+
|
|
1011
|
+
const gateRule = (value: unknown) => {
|
|
1012
|
+
if (!value) return "Gate is required.";
|
|
1013
|
+
if (usedGates.value.has(Number(value))) return "This gate is used by another camera.";
|
|
1014
|
+
return true;
|
|
1015
|
+
};
|
|
1016
|
+
|
|
961
1017
|
const guardPostOptions = computed(() => {
|
|
962
1018
|
const items = [];
|
|
963
1019
|
|
|
@@ -58,6 +58,12 @@
|
|
|
58
58
|
<template #item.host="{ value }">
|
|
59
59
|
{{ maskHostUrl(value) }}
|
|
60
60
|
</template>
|
|
61
|
+
<template #item.gate="{ value }">
|
|
62
|
+
{{ value ? `Gate ${value}` : "-" }}
|
|
63
|
+
</template>
|
|
64
|
+
<template #item.status="{ value }">
|
|
65
|
+
<StatusChip class="text-capitalize" :status="value || 'active'" :label="value || 'active'" />
|
|
66
|
+
</template>
|
|
61
67
|
</v-data-table>
|
|
62
68
|
</AppCard>
|
|
63
69
|
</v-col>
|
|
@@ -161,9 +167,9 @@
|
|
|
161
167
|
</v-col>
|
|
162
168
|
|
|
163
169
|
<v-col v-if="camera.type === 'anpr'" cols="12">
|
|
164
|
-
|
|
170
|
+
Gate:
|
|
165
171
|
<span class="font-weight-bold">
|
|
166
|
-
{{ camera.
|
|
172
|
+
{{ camera.gate ? `Gate ${camera.gate}` : "-" }}
|
|
167
173
|
</span>
|
|
168
174
|
</v-col>
|
|
169
175
|
</v-row>
|
|
@@ -378,9 +384,12 @@ const computedHeaders = computed(() => {
|
|
|
378
384
|
{ title: "URL", key: "host" },
|
|
379
385
|
]
|
|
380
386
|
}else if(prop.type === "anpr"){
|
|
387
|
+
// Gate and Status beside the type (owner, 2026-10-23).
|
|
381
388
|
return [
|
|
382
389
|
{ title: "URL", key: "host" },
|
|
383
390
|
{ title: "Type", key: "direction" },
|
|
391
|
+
{ title: "Gate", key: "gate" },
|
|
392
|
+
{ title: "Status", key: "status" },
|
|
384
393
|
]
|
|
385
394
|
}
|
|
386
395
|
// A THIRD TYPE FELL OFF THE END. Both arms above are early returns and the
|
|
@@ -600,7 +609,7 @@ const dialogSyncAsk = ref(false);
|
|
|
600
609
|
const dialogSync = ref(false);
|
|
601
610
|
const syncClear = ref(false);
|
|
602
611
|
|
|
603
|
-
/** An active ANPR camera of a type that receives plates (
|
|
612
|
+
/** An active ANPR camera of a type that receives plates (every type, Exit included). */
|
|
604
613
|
const canSyncVehicles = computed(
|
|
605
614
|
() =>
|
|
606
615
|
camera.value.type === "anpr" &&
|
|
@@ -8,6 +8,11 @@
|
|
|
8
8
|
<h3 class="screen-card-title">
|
|
9
9
|
{{ type === "workOrder" ? "Work Order" : "Feedback" }} Chat
|
|
10
10
|
</h3>
|
|
11
|
+
<div class="text-body-2 text-medium-emphasis">
|
|
12
|
+
Between the Managing Agent and the service provider on this
|
|
13
|
+
{{ type === "workOrder" ? "work order" : "feedback" }}. The other side
|
|
14
|
+
is notified of each new message.
|
|
15
|
+
</div>
|
|
11
16
|
</div>
|
|
12
17
|
|
|
13
18
|
<v-sheet
|
|
@@ -23,7 +28,7 @@
|
|
|
23
28
|
v-else-if="!days.length"
|
|
24
29
|
class="text-center text-medium-emphasis py-6"
|
|
25
30
|
>
|
|
26
|
-
No messages yet
|
|
31
|
+
No messages yet. Send a message or a photo below to start.
|
|
27
32
|
</div>
|
|
28
33
|
|
|
29
34
|
<template v-for="group in days" :key="group.day">
|
|
@@ -48,7 +53,7 @@
|
|
|
48
53
|
:message="msg.comment"
|
|
49
54
|
:timestamp="formatTime(msg.createdAt)"
|
|
50
55
|
:senderName="isMine(msg) ? 'You' : msg.createdByName || 'User'"
|
|
51
|
-
:role="msg.createdByType"
|
|
56
|
+
:role="senderSideLabel(msg.createdByMemberType) || msg.createdByType"
|
|
52
57
|
:attachments="msg.attachments"
|
|
53
58
|
:avatarId="msg.createdByProfile"
|
|
54
59
|
@preview="openPreview"
|
|
@@ -186,7 +191,7 @@
|
|
|
186
191
|
</template>
|
|
187
192
|
|
|
188
193
|
<script setup lang="ts">
|
|
189
|
-
import { groupChatByDay } from "../../utils/chat-sender.ts";
|
|
194
|
+
import { groupChatByDay, senderSideLabel } from "../../utils/chat-sender.ts";
|
|
190
195
|
|
|
191
196
|
/**
|
|
192
197
|
* The comment thread under a feedback or a work order.
|
package/components/Snackbar.vue
CHANGED
|
@@ -6,6 +6,16 @@
|
|
|
6
6
|
:z-index="100000000"
|
|
7
7
|
>
|
|
8
8
|
{{ props.text }}
|
|
9
|
+
<!-- A second line ("Opening Barrier Success."); a failure is red on a
|
|
10
|
+
light pill so it reads on every toast colour. -->
|
|
11
|
+
<div
|
|
12
|
+
v-if="props.detail"
|
|
13
|
+
class="app-toast__detail"
|
|
14
|
+
:class="{ 'app-toast__detail--error': props.detailTone === 'error' }"
|
|
15
|
+
data-test="app-toast-detail"
|
|
16
|
+
>
|
|
17
|
+
{{ props.detail }}
|
|
18
|
+
</div>
|
|
9
19
|
|
|
10
20
|
<template #actions>
|
|
11
21
|
<v-btn class="app-toast__close" variant="text" @click="snackbar = false">
|
|
@@ -52,5 +62,31 @@ const props = defineProps({
|
|
|
52
62
|
type: String,
|
|
53
63
|
default: "",
|
|
54
64
|
},
|
|
65
|
+
/** A second line under `text` (e.g. "Opening Barrier Failed."). */
|
|
66
|
+
detail: {
|
|
67
|
+
type: String,
|
|
68
|
+
default: "",
|
|
69
|
+
},
|
|
70
|
+
/** "error" draws `detail` in red. */
|
|
71
|
+
detailTone: {
|
|
72
|
+
type: String as PropType<"default" | "error">,
|
|
73
|
+
default: "default",
|
|
74
|
+
},
|
|
55
75
|
});
|
|
56
76
|
</script>
|
|
77
|
+
|
|
78
|
+
<style scoped>
|
|
79
|
+
.app-toast__detail {
|
|
80
|
+
margin-top: 4px;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.app-toast__detail--error {
|
|
84
|
+
display: inline-block;
|
|
85
|
+
margin-top: 6px;
|
|
86
|
+
padding: 2px 8px;
|
|
87
|
+
border-radius: 6px;
|
|
88
|
+
background: rgb(var(--v-theme-surface));
|
|
89
|
+
color: rgb(var(--v-theme-error));
|
|
90
|
+
font-weight: 700;
|
|
91
|
+
}
|
|
92
|
+
</style>
|
package/components/TableMain.vue
CHANGED
|
@@ -131,7 +131,9 @@
|
|
|
131
131
|
/>
|
|
132
132
|
|
|
133
133
|
<!-- Toolbar -->
|
|
134
|
-
<CardToolbar
|
|
134
|
+
<CardToolbar
|
|
135
|
+
:count="$slots['pagination-prepend'] ? '' : pageRange"
|
|
136
|
+
>
|
|
135
137
|
<!--
|
|
136
138
|
The design draws a screen's tabs ON the toolbar row, not on a
|
|
137
139
|
stacked strip above the table. `CardToolbar` has always had the
|
|
@@ -174,6 +176,15 @@
|
|
|
174
176
|
</template>
|
|
175
177
|
|
|
176
178
|
<template #right>
|
|
179
|
+
<!--
|
|
180
|
+
Before the range and pager, e.g. the visitor list's default-camera
|
|
181
|
+
button. With it filled, the range is drawn here (not by CardToolbar)
|
|
182
|
+
so the button can sit in front of it.
|
|
183
|
+
-->
|
|
184
|
+
<template v-if="$slots['pagination-prepend']">
|
|
185
|
+
<slot name="pagination-prepend" />
|
|
186
|
+
<span v-if="pageRange" class="app-toolbar__count">{{ pageRange }}</span>
|
|
187
|
+
</template>
|
|
177
188
|
<local-pagination
|
|
178
189
|
v-model="internalPage"
|
|
179
190
|
:length="pages"
|
|
@@ -29,16 +29,23 @@ type TVehicleAddChoice = TVehicleType | "seasonpass" | "whitelist-bulk";
|
|
|
29
29
|
const prop = defineProps({
|
|
30
30
|
// Security catalogue v2: false hides the Blocklist choice. Default = today.
|
|
31
31
|
canManageBlocklist: { type: Boolean, default: true },
|
|
32
|
+
// Add Vehicle: false hides Whitelist, Whitelist (Bulk) and Season Pass, so a
|
|
33
|
+
// role with only Manage Blocklist is offered the Blocklist form alone.
|
|
34
|
+
canAddWhitelist: { type: Boolean, default: true },
|
|
32
35
|
});
|
|
33
36
|
|
|
34
37
|
const emit = defineEmits(['cancel', 'select']);
|
|
35
38
|
|
|
36
39
|
const selection = computed<{label: string, value: TVehicleAddChoice}[]>(() => {
|
|
37
40
|
return [
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
41
|
+
...(prop.canAddWhitelist
|
|
42
|
+
? [
|
|
43
|
+
{ label: "Whitelist", value: "whitelist" as TVehicleAddChoice },
|
|
44
|
+
// Many whitelist vehicles at once, typed or pasted into a sheet.
|
|
45
|
+
{ label: "Whitelist (Bulk)", value: "whitelist-bulk" as TVehicleAddChoice },
|
|
46
|
+
{ label: "Season Pass", value: "seasonpass" as TVehicleAddChoice },
|
|
47
|
+
]
|
|
48
|
+
: []),
|
|
42
49
|
...(prop.canManageBlocklist
|
|
43
50
|
? [{ label: "Blocklist", value: "blocklist" as TVehicleAddChoice }]
|
|
44
51
|
: []),
|