@7365admin1/layer-common 1.11.37 → 1.11.39

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.
@@ -24,7 +24,11 @@
24
24
  </v-row>
25
25
  </v-col>
26
26
 
27
- <v-col v-if="APP === 'organization'" cols="12" class="mt-2">
27
+ <v-col
28
+ v-if="!props.hideApp && props.adminState && APP === 'organization'"
29
+ cols="12"
30
+ class="mt-2"
31
+ >
28
32
  <v-row no-gutters>
29
33
  <InputLabel class="text-capitalize" title="App" required />
30
34
  <v-col cols="12">
@@ -55,7 +59,7 @@
55
59
  </v-row>
56
60
  </v-col>
57
61
 
58
- <v-col v-if="hasSite" cols="12">
62
+ <v-col v-if="hasSite && props.adminState" cols="12">
59
63
  <v-row no-gutters>
60
64
  <InputLabel class="text-capitalize" title="Site" />
61
65
  <v-col cols="12">
@@ -139,6 +143,8 @@ import useUser from '../composables/useUser';
139
143
  import useUtils from '../composables/useUtils';
140
144
 
141
145
  const APP = useRuntimeConfig().public.APP;
146
+
147
+
142
148
  const props = defineProps({
143
149
  title: {
144
150
  type: String,
@@ -177,13 +183,21 @@ const props = defineProps({
177
183
  metadata: { app: "organization", role: "" },
178
184
  }),
179
185
  },
186
+ adminState: {
187
+ type: Boolean,
188
+ default: true
189
+ },
190
+ hideApp: {
191
+ type: Boolean,
192
+ default: false
193
+ }
180
194
  });
181
195
 
182
196
  const emit = defineEmits(["cancel", "success", "success:create-more"]);
183
197
 
184
198
  const validForm = ref(false);
185
199
  const form = ref<HTMLFormElement | null>(null);
186
- const app = computed(() => useRuntimeConfig().public.APP ?? "");
200
+ // const app = computed(() => useRuntimeConfig().public.APP ?? "");
187
201
 
188
202
  const loading = reactive({
189
203
  submittingForm: false,
@@ -200,8 +214,24 @@ const invite = ref<Record<string, any>>({
200
214
  isMemberInvite: false,
201
215
  });
202
216
 
203
- invite.value.app = props.app;
204
- invite.value.org = props.org ?? "";
217
+ watch(
218
+ () => [props.hideApp, props.adminState, props.app, props.org],
219
+ () => {
220
+ invite.value.org = props.org || ""
221
+
222
+ if (props.hideApp) {
223
+ invite.value.app = "property_management_agency"
224
+ return
225
+ }
226
+
227
+ invite.value.app = props.adminState
228
+ ? (props.app || "")
229
+ : "property_management_agency"
230
+ },
231
+ { immediate: true }
232
+ )
233
+ // invite.value.app = props.app;
234
+ // invite.value.org = props.org ?? "";
205
235
 
206
236
  if (props.mode === "edit") {
207
237
  invite.value.email = props.invite.email;
@@ -1,45 +1,68 @@
1
1
  <template>
2
- <v-navigation-drawer v-model="drawer" permanent floating class="bg-primary">
2
+ <v-navigation-drawer
3
+ v-model="drawer"
4
+ permanent
5
+ floating
6
+ class="bg-primary"
7
+ >
3
8
  <v-list>
4
9
  <v-list-item>
5
10
  <v-list-item-title class="text-h6 text-white">
6
11
  {{ props.title || APP_NAME }}
7
12
  </v-list-item-title>
8
13
  </v-list-item>
14
+
9
15
  <slot name="action" />
10
- <template
11
- v-for="(navigationItem, navigationIndex) in props.navigationItems"
12
- :key="`${navigationItem.title}-${navigationIndex}`"
16
+
17
+ <!-- NAVIGATION ITEMS -->
18
+ <v-tooltip
19
+ v-for="(item, index) in props.navigationItems"
20
+ :key="`${item.title}-${index}`"
21
+ location="right"
13
22
  >
14
- <NavigationItem
15
- v-if="navigationItem.children?.length"
16
- :title="navigationItem.title"
17
- :icon="navigationItem.icon"
18
- :route="navigationItem.route"
19
- :children="navigationItem.children"
20
- />
21
-
22
- <NavigationItem
23
- v-else-if="navigationItem.route"
24
- :title="navigationItem.title"
25
- :icon="navigationItem.icon"
26
- :link="navigationItem.link"
27
- :route="navigationItem.route"
28
- />
29
- </template>
30
-
31
- <slot name="services"> </slot>
23
+ <template #activator="{ props: tooltipProps }">
24
+ <div v-bind="tooltipProps">
25
+ <NavigationItem
26
+ :title="item.title"
27
+ :icon="item.icon"
28
+ :route="item.route"
29
+ :link="item.link"
30
+ :class="{
31
+ 'opacity-50 pointer-events-none': item.disabled
32
+ }"
33
+ @click="() => handleClick(item)"
34
+ />
35
+ </div>
36
+ </template>
37
+
38
+ <!-- TOOLTIP ONLY WHEN DISABLED -->
39
+ <span v-if="item.disabled">
40
+ Please complete onboarding first
41
+ </span>
42
+ </v-tooltip>
43
+
44
+ <!-- SERVICES SLOT -->
45
+ <slot name="services" />
32
46
  </v-list>
33
47
  </v-navigation-drawer>
34
48
  </template>
35
49
 
36
50
  <script setup lang="ts">
37
51
  const props = defineProps({
38
- navigationItems: { type: Array<TNavigationItem>, required: true },
52
+ navigationItems: { type: Array, required: true },
39
53
  title: { type: String, default: "" }
40
- });
54
+ })
55
+
56
+ const emit = defineEmits(['navigate'])
41
57
 
42
- const { drawer } = useLocal();
58
+ const { drawer } = useLocal()
59
+ const { APP_NAME } = useRuntimeConfig().public
43
60
 
44
- const { APP_NAME } = useRuntimeConfig().public;
61
+ /**
62
+ * GLOBAL CLICK CONTROL
63
+ */
64
+ const handleClick = (item: any) => {
65
+ if (item.disabled) return
66
+ emit('navigate', item)
67
+ }
45
68
  </script>
@@ -26,17 +26,11 @@
26
26
  </v-row>
27
27
  </v-col>
28
28
 
29
- <v-col cols="12" class="mt-2">
29
+ <v-col v-if="props.siteState" cols="12" class="mt-2">
30
30
  <v-row no-gutters>
31
31
  <InputLabel title="Site" />
32
32
  <v-col cols="12">
33
- <v-text-field
34
- :model-value="siteName"
35
- density="comfortable"
36
- readonly
37
- disabled
38
- :loading="loadSite"
39
- />
33
+ <v-text-field :model-value="siteName" density="comfortable" readonly disabled :loading="loadSite" />
40
34
  </v-col>
41
35
  </v-row>
42
36
  </v-col>
@@ -53,12 +47,21 @@
53
47
  </v-radio-group>
54
48
  </v-col>
55
49
 
50
+ <v-col cols="12" class="mt-2">
51
+ <v-btn
52
+ size="small"
53
+ variant="tonal"
54
+ class="text-none mb-2"
55
+ @click="() => toggleSelectAll(!isAllSelected)"
56
+ >
57
+ {{ isAllSelected ? 'Unselect All' : 'Select All' }}
58
+ </v-btn>
59
+ </v-col>
60
+
56
61
  <v-col cols="12" class="mt-3">
57
62
  <v-card variant="outlined" border="thin" rounded="lg">
58
63
  <template
59
- v-for="(
60
- actions, resourceKey, resourceIndex
61
- ) in props.permissions"
64
+ v-for="(actions, resourceKey, resourceIndex) in safePermissions"
62
65
  :key="resourceKey"
63
66
  >
64
67
  <v-divider v-if="resourceIndex > 0" />
@@ -94,18 +97,14 @@
94
97
  <!-- Expanded checkboxes when toggled on -->
95
98
  <template v-if="isCategoryEnabled(String(resourceKey))">
96
99
  <template
97
- v-for="(action, actionKey) in actions"
100
+ v-for="([actionKey, action]) in Object.entries(actions || {})"
98
101
  :key="actionKey"
99
102
  >
100
103
  <v-divider />
101
104
  <v-list-item density="compact" :ripple="false">
102
105
  <template #prepend>
103
106
  <v-checkbox-btn
104
- :model-value="
105
- selectedPermissions.includes(
106
- `${String(resourceKey)}:${String(actionKey)}`
107
- )
108
- "
107
+ :model-value="permState.selected.includes(`${String(resourceKey)}:${String(actionKey)}`)"
109
108
  @update:model-value="
110
109
  toggleAction(
111
110
  String(resourceKey),
@@ -204,59 +203,90 @@ const props = defineProps({
204
203
  type: String,
205
204
  default: "app",
206
205
  },
206
+ siteState: {
207
+ type: Boolean,
208
+ default: true,
209
+ },
207
210
  });
208
211
 
209
212
  const emit = defineEmits(["cancel", "success", "success:create-more"]);
210
213
 
211
214
  const validForm = ref(false);
212
215
  const name = ref("");
213
- const selectedPermissions = ref<Array<string>>([]);
214
216
  const platform = ref("web");
215
217
  const disable = ref(false);
216
218
  const message = ref("");
217
219
 
218
- const { requiredRule, requireListRule } = useUtils();
220
+ const { requiredRule } = useUtils();
219
221
  const { createRole } = useRole();
220
222
  const { getSiteById } = useSite();
221
223
 
222
- const enabledCategories = ref<Set<string>>(new Set());
224
+ // Gom selected + enabled vào 1 shallowRef duy nhất
225
+ // để chỉ trigger 1 render cycle khi update cả 2
226
+ const permState = shallowRef({
227
+ selected: [] as string[],
228
+ enabled: [] as string[],
229
+ })
223
230
 
224
- function selectedActionCount(resource: string) {
225
- return selectedPermissions.value.filter((p) => p.startsWith(`${resource}:`))
226
- .length;
231
+ function isCategoryEnabled(resource: string) {
232
+ return permState.value.enabled.includes(resource)
227
233
  }
228
234
 
229
- function isCategoryEnabled(resource: string) {
230
- return enabledCategories.value.has(resource);
235
+ function selectedActionCount(resource: string) {
236
+ return permState.value.selected.filter(p => p.startsWith(`${resource}:`)).length
231
237
  }
232
238
 
233
239
  function toggleCategory(resource: string, value: boolean | null) {
234
- const updated = new Set(enabledCategories.value);
240
+ const { selected, enabled } = permState.value
235
241
  if (value) {
236
- updated.add(resource);
242
+ if (!enabled.includes(resource)) {
243
+ permState.value = { selected, enabled: [...enabled, resource] }
244
+ }
237
245
  } else {
238
- updated.delete(resource);
239
-
240
- selectedPermissions.value = selectedPermissions.value.filter(
241
- (p) => !p.startsWith(`${resource}:`)
242
- );
246
+ permState.value = {
247
+ selected: selected.filter(p => !p.startsWith(`${resource}:`)),
248
+ enabled: enabled.filter(r => r !== resource),
249
+ }
243
250
  }
244
- enabledCategories.value = updated;
245
251
  }
246
252
 
247
253
  function toggleAction(resource: string, action: string, value: boolean | null) {
248
- const key = `${resource}:${action}`;
254
+ if (value !== true && value !== false) return
255
+ const key = `${resource}:${action}`
256
+ const { selected, enabled } = permState.value
249
257
  if (value) {
250
- if (!selectedPermissions.value.includes(key)) {
251
- selectedPermissions.value = [...selectedPermissions.value, key];
258
+ if (!selected.includes(key)) {
259
+ permState.value = { selected: [...selected, key], enabled }
252
260
  }
253
261
  } else {
254
- selectedPermissions.value = selectedPermissions.value.filter(
255
- (p) => p !== key
256
- );
262
+ permState.value = { selected: selected.filter(p => p !== key), enabled }
257
263
  }
258
264
  }
259
265
 
266
+ function toggleSelectAll(value: boolean) {
267
+ if (!value) {
268
+ permState.value = { selected: [], enabled: [] }
269
+ return
270
+ }
271
+
272
+ const perms = safePermissions.value
273
+ if (!perms) return
274
+
275
+ const allKeys: string[] = []
276
+ const categories: string[] = []
277
+
278
+ for (const [resource, actions] of Object.entries(perms)) {
279
+ if (!resource || !actions || typeof actions !== 'object') continue
280
+ categories.push(resource)
281
+ for (const action of Object.keys(actions)) {
282
+ if (action) allKeys.push(`${resource}:${action}`)
283
+ }
284
+ }
285
+
286
+ // 1 assignment duy nhất = 1 render cycle, không crash
287
+ permState.value = { selected: allKeys, enabled: categories }
288
+ }
289
+
260
290
  const siteName = ref("");
261
291
  const loadSite = ref(false);
262
292
 
@@ -286,7 +316,7 @@ async function submit() {
286
316
  const platformValue = platform.value === "web" ? "website" : platform.value;
287
317
  await createRole({
288
318
  name: name.value,
289
- permissions: selectedPermissions.value,
319
+ permissions: permState.value.selected,
290
320
  type: props.type,
291
321
  org: props.org,
292
322
  site: props.siteId,
@@ -301,11 +331,41 @@ async function submit() {
301
331
  }
302
332
 
303
333
  function cancel() {
304
- name.value = "";
305
- selectedPermissions.value = [];
306
- enabledCategories.value = new Set();
307
- platform.value = "web";
308
- message.value = "";
309
- emit("cancel");
334
+ name.value = ""
335
+ permState.value = { selected: [], enabled: [] }
336
+ platform.value = "web"
337
+ message.value = ""
338
+ emit("cancel")
310
339
  }
340
+
341
+ const safePermissions = computed(() => {
342
+ const p = props.permissions || {}
343
+ const cleaned: Record<string, any> = {}
344
+
345
+ for (const [k, v] of Object.entries(p)) {
346
+ if (v && typeof v === 'object' && !Array.isArray(v)) {
347
+ cleaned[k] = v
348
+ } else {
349
+ cleaned[k] = {}
350
+ }
351
+ }
352
+
353
+ return cleaned
354
+ })
355
+
356
+ const isAllSelected = computed(() => {
357
+ const perms = safePermissions.value
358
+ const allKeys: string[] = []
359
+
360
+ Object.entries(perms).forEach(([resource, actions]) => {
361
+ if (!actions) return
362
+ Object.keys(actions).forEach((action) => {
363
+ if (action) allKeys.push(`${resource}:${action}`)
364
+ })
365
+ })
366
+
367
+ return allKeys.length > 0 &&
368
+ allKeys.every(k => permState.value.selected.includes(k))
369
+ })
370
+
311
371
  </script>