fynally fixed device assignation to user
This commit is contained in:
@@ -25,8 +25,8 @@ const isTasksOpen = ref(false)
|
|||||||
const itCertsOpen = ref(false)
|
const itCertsOpen = ref(false)
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'refresh'): void
|
(e: 'updated', payload: { name: string; userIds: string[] }): void
|
||||||
(e: 'error', err: unknown): void
|
(e: 'deleted'): void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
function onDeleteConfirmed() {
|
function onDeleteConfirmed() {
|
||||||
@@ -65,7 +65,7 @@ function onCertsConfirm() {
|
|||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
|
|
||||||
<EditDeviceDialog v-model:modelValue="isEditOpen" :device="props.row" :all-users="props.allUsers" @confirm="onEditConfirm" />
|
<EditDeviceDialog v-model:modelValue="isEditOpen" :device="props.row" :all-users="props.allUsers" @updated="onEditConfirm" />
|
||||||
<DeleteDeviceDialog v-model:modelValue="isDeleteOpen" :device="props.row" @confirm="onDeleteConfirmed" />
|
<DeleteDeviceDialog v-model:modelValue="isDeleteOpen" :device="props.row" @confirm="onDeleteConfirmed" />
|
||||||
<DeviceCertificateDialog v-model:modelValue="itCertsOpen" :device="props.row" @confirm="onCertsConfirm" />
|
<DeviceCertificateDialog v-model:modelValue="itCertsOpen" :device="props.row" @confirm="onCertsConfirm" />
|
||||||
<DeviceTasksDialog v-model:modelValue="isTasksOpen" :device="props.row" @confirm="onTaskConfirm" />
|
<DeviceTasksDialog v-model:modelValue="isTasksOpen" :device="props.row" @confirm="onTaskConfirm" />
|
||||||
|
|||||||
@@ -210,6 +210,7 @@ onBeforeUnmount(() => {
|
|||||||
:dropdownComponent="AdminDeviceDropdown"
|
:dropdownComponent="AdminDeviceDropdown"
|
||||||
:onRowUpdated="handleDeviceUpdated"
|
:onRowUpdated="handleDeviceUpdated"
|
||||||
:onRowDeleted="handleDeviceDeleted"
|
:onRowDeleted="handleDeviceDeleted"
|
||||||
|
:dropdownProps="{ allUsers: user_data }"
|
||||||
/>
|
/>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
@@ -220,6 +221,7 @@ onBeforeUnmount(() => {
|
|||||||
:dropdownComponent="AdminDeviceDropdown"
|
:dropdownComponent="AdminDeviceDropdown"
|
||||||
:onRowUpdated="handleTrackerUpdated"
|
:onRowUpdated="handleTrackerUpdated"
|
||||||
:onRowDeleted="handleTrackerDeleted"
|
:onRowDeleted="handleTrackerDeleted"
|
||||||
|
:dropdownProps="{ allUsers: user_data }"
|
||||||
/>
|
/>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|||||||
@@ -73,6 +73,16 @@ function onSelect(ev: CustomEvent) {
|
|||||||
|
|
||||||
// When parent switches between external/internal data at runtime, refetch if needed.
|
// When parent switches between external/internal data at runtime, refetch if needed.
|
||||||
watch(() => props.allUsers, () => { if (!usingExternal.value) loadUsers() })
|
watch(() => props.allUsers, () => { if (!usingExternal.value) loadUsers() })
|
||||||
|
|
||||||
|
// Debug: whenever modelValue changes (from dialog)
|
||||||
|
watch(() => props.modelValue, (v) => {
|
||||||
|
console.log('[AssignDevice:props.modelValue]', v)
|
||||||
|
}, { immediate: true })
|
||||||
|
|
||||||
|
// Debug: when effective users list is ready
|
||||||
|
watch(() => effectiveUsers.value, (v) => {
|
||||||
|
console.log('[AssignDevice:effectiveUsers]', v?.length, 'items')
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@@ -33,13 +33,14 @@ const props = defineProps<{
|
|||||||
* :dropdownComponent="MyActionsMenu"
|
* :dropdownComponent="MyActionsMenu"
|
||||||
* />
|
* />
|
||||||
*/
|
*/
|
||||||
// dropdownComponent?: DefineComponent<{ row: TData }, any, any>
|
dropdownComponent?: DefineComponent<{ row: TData }, any, any>
|
||||||
dropdownComponent?: DefineComponent<{ row: TData } & {
|
// dropdownComponent?: DefineComponent<{ row: TData } & {
|
||||||
onRowUpdated?: (row: TData, payload: any) => void
|
// onRowUpdated?: (row: TData, payload: any) => void
|
||||||
onRowDeleted?: (row: TData) => void
|
// onRowDeleted?: (row: TData) => void
|
||||||
}, any, any>
|
// }, any, any>
|
||||||
onRowUpdated?: (row: TData, payload: any) => void // <-- NEW
|
// onRowUpdated?: (row: TData, payload: any) => void // <-- NEW
|
||||||
onRowDeleted?: (row: TData) => void // <-- NEW
|
// onRowDeleted?: (row: TData) => void // <-- NEW
|
||||||
|
dropdownProps?: Record<string, any> // <-- NEW
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
// ——— Table setup ———
|
// ——— Table setup ———
|
||||||
@@ -48,6 +49,11 @@ const table = useVueTable({
|
|||||||
get columns() { return props.columns },
|
get columns() { return props.columns },
|
||||||
getCoreRowModel: getCoreRowModel(),
|
getCoreRowModel: getCoreRowModel(),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'row-updated', row: TData, payload: any): void
|
||||||
|
(e: 'row-deleted', row: TData): void
|
||||||
|
}>()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -99,12 +105,19 @@ const table = useVueTable({
|
|||||||
|
|
||||||
<!-- dropdown cell -->
|
<!-- dropdown cell -->
|
||||||
<TableCell v-if="props.dropdownComponent" class="text-right">
|
<TableCell v-if="props.dropdownComponent" class="text-right">
|
||||||
<component
|
<!-- <component
|
||||||
:is="props.dropdownComponent"
|
:is="props.dropdownComponent"
|
||||||
:row="row.original"
|
:row="row.original"
|
||||||
:key="row.id"
|
:key="row.id"
|
||||||
:on-row-updated="props.onRowUpdated"
|
:on-row-updated="props.onRowUpdated"
|
||||||
:on-row-deleted="props.onRowDeleted"
|
:on-row-deleted="props.onRowDeleted"
|
||||||
|
/> -->
|
||||||
|
<component
|
||||||
|
:is="props.dropdownComponent"
|
||||||
|
:row="row.original"
|
||||||
|
v-bind="props.dropdownProps"
|
||||||
|
@updated="(payload: any) => emit('row-updated', row.original, payload)"
|
||||||
|
@deleted="() => emit('row-deleted', row.original)"
|
||||||
/>
|
/>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
|
|||||||
@@ -1,61 +1,147 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter,
|
||||||
DialogContent,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
} from '@/components/ui/dialog'
|
} from '@/components/ui/dialog'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import { Label } from '@/components/ui/label'
|
import { Label } from '@/components/ui/label'
|
||||||
import { defineProps, defineEmits, ref, watch, computed } from 'vue'
|
|
||||||
import type { PropType } from 'vue'
|
|
||||||
import AssignDevice from './AssignDevice.vue'
|
import AssignDevice from './AssignDevice.vue'
|
||||||
import type { Device, Users } from '@/lib/interfaces'
|
import type { Device, Users } from '@/lib/interfaces'
|
||||||
|
import { defineProps, defineEmits, ref, watch, computed } from 'vue'
|
||||||
|
import type { PropType } from 'vue'
|
||||||
import { api } from '@/lib/api'
|
import { api } from '@/lib/api'
|
||||||
|
|
||||||
|
|
||||||
type DeviceWithUsers =
|
type DeviceWithUsers =
|
||||||
& Partial<Device>
|
& Partial<Device>
|
||||||
& { guid?: string; name?: string; devicename?: string }
|
& { guid?: string; name?: string; devicename?: string; assigned_users?: string }
|
||||||
& { users?: Array<{ id: number; username: string; role?: string }> }
|
& { users?: Array<{ id: number; username: string; role?: string }> }
|
||||||
|
|
||||||
// 1) runtime props so Vue + TS agree
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
modelValue: { type: Boolean as PropType<boolean>, required: true },
|
modelValue: { type: Boolean as PropType<boolean>, required: true },
|
||||||
device: { type: Object as PropType<DeviceWithUsers>, required: true }, // must have guid
|
device: { type: Object as PropType<DeviceWithUsers>, required: true },
|
||||||
allUsers: { type: Array as PropType<Users[]>, required: false },
|
allUsers: { type: Array as PropType<Users[]>, required: false },
|
||||||
})
|
})
|
||||||
|
|
||||||
// 2) two emits: v-model and confirm
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'update:modelValue', v: boolean): void
|
(e: 'update:modelValue', v: boolean): void
|
||||||
// (e: 'confirm'): void
|
|
||||||
(e: 'updated', payload: { name: string; userIds: string[] }): void
|
(e: 'updated', payload: { name: string; userIds: string[] }): void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
// ---------- Local form state ----------
|
|
||||||
const guid = computed(() => String(props.device?.guid ?? ''))
|
const guid = computed(() => String(props.device?.guid ?? ''))
|
||||||
const originalName = computed(() => (props.device?.devicename ?? props.device?.name ?? ''))
|
const originalName = computed(() => (props.device?.devicename ?? props.device?.name ?? ''))
|
||||||
const originalIds = computed<string[]>(() =>
|
|
||||||
Array.isArray(props.device?.users) ? props.device!.users.map(u => String(u.id)) : []
|
|
||||||
)
|
|
||||||
|
|
||||||
|
const originalIds = ref<string[]>([]) // baseline for compare
|
||||||
const name = ref('')
|
const name = ref('')
|
||||||
const selectedUserIds = ref<string[]>([])
|
const selectedUserIds = ref<string[]>([]) // live edited selection
|
||||||
|
|
||||||
|
// ----- init control: only init once per open/guid -----
|
||||||
|
const initedForGuid = ref<string | null>(null)
|
||||||
|
|
||||||
|
function usernameToId(u: string): string | null {
|
||||||
|
const id = props.allUsers?.find(x => x.username === u)?.id
|
||||||
|
return typeof id === 'number' ? String(id) : null
|
||||||
|
}
|
||||||
|
|
||||||
|
function initFromDevice() {
|
||||||
|
const dev = props.device
|
||||||
|
name.value = originalName.value
|
||||||
|
|
||||||
|
if (Array.isArray(dev?.users) && dev!.users.length) {
|
||||||
|
originalIds.value = dev!.users.map(u => String(u.id))
|
||||||
|
} else {
|
||||||
|
// Map from assigned_users (comma-separated usernames) -> ids (requires allUsers)
|
||||||
|
const usernames = (dev?.assigned_users ?? '')
|
||||||
|
.split(',')
|
||||||
|
.map(s => s.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
|
||||||
|
if (usernames.length && props.allUsers?.length) {
|
||||||
|
originalIds.value = usernames
|
||||||
|
.map(usernameToId)
|
||||||
|
.filter((x): x is string => !!x)
|
||||||
|
} else {
|
||||||
|
originalIds.value = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedUserIds.value = [...originalIds.value]
|
||||||
|
initedForGuid.value = guid.value
|
||||||
|
|
||||||
|
console.log('[EditDeviceDialog:init]', {
|
||||||
|
guid: guid.value,
|
||||||
|
originalName: originalName.value,
|
||||||
|
originalIds: [...originalIds.value],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// When dialog opens, initialize once for this guid
|
||||||
watch(
|
watch(
|
||||||
() => props.device,
|
() => props.modelValue,
|
||||||
() => {
|
(open) => {
|
||||||
name.value = originalName.value
|
if (open && guid.value && initedForGuid.value !== guid.value) {
|
||||||
selectedUserIds.value = [...originalIds.value]
|
initFromDevice()
|
||||||
|
}
|
||||||
|
if (!open) {
|
||||||
|
// reset flag so next open re-inits
|
||||||
|
initedForGuid.value = null
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{ immediate: true }
|
{ immediate: true }
|
||||||
)
|
)
|
||||||
|
|
||||||
// ---------- Save (conditional API calls) ----------
|
// If allUsers arrives later AND we haven't successfully initialized for this guid,
|
||||||
|
// try again (but only when open).
|
||||||
|
// watch(
|
||||||
|
// () => props.allUsers,
|
||||||
|
// () => {
|
||||||
|
// if (props.modelValue && guid.value && initedForGuid.value !== guid.value) {
|
||||||
|
// initFromDevice()
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// )
|
||||||
|
|
||||||
|
// If allUsers arrives later, and we couldn't map on first init, map now.
|
||||||
|
watch(
|
||||||
|
() => props.allUsers,
|
||||||
|
() => {
|
||||||
|
if (!props.modelValue || !guid.value) return
|
||||||
|
|
||||||
|
const dev = props.device
|
||||||
|
const hasUsersArray = Array.isArray(dev?.users) && dev!.users.length > 0
|
||||||
|
const hasAssigned = typeof dev?.assigned_users === 'string' && dev!.assigned_users.trim().length > 0
|
||||||
|
const canMapNow = Array.isArray(props.allUsers) && props.allUsers.length > 0
|
||||||
|
|
||||||
|
// Only re-map if we *didn't* have users[], we *do* have assigned usernames,
|
||||||
|
// we *couldn't* map earlier (originalIds is empty), and now we have allUsers.
|
||||||
|
if (!hasUsersArray && hasAssigned && originalIds.value.length === 0 && canMapNow) {
|
||||||
|
const usernames = dev!.assigned_users!
|
||||||
|
.split(',')
|
||||||
|
.map(s => s.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
|
||||||
|
const mapped = usernames
|
||||||
|
.map(u => {
|
||||||
|
const id = props.allUsers!.find(x => x.username === u)?.id
|
||||||
|
return typeof id === 'number' ? String(id) : null
|
||||||
|
})
|
||||||
|
.filter((x): x is string => !!x)
|
||||||
|
|
||||||
|
if (mapped.length > 0) {
|
||||||
|
originalIds.value = mapped
|
||||||
|
selectedUserIds.value = [...mapped]
|
||||||
|
console.log('[EditDeviceDialog:remap-after-allUsers]', { mapped })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: false }
|
||||||
|
)
|
||||||
|
|
||||||
|
// Just to see live edits from AssignDevice
|
||||||
|
watch(selectedUserIds, (v) => {
|
||||||
|
console.log('[EditDeviceDialog:selectedUserIds]', [...v])
|
||||||
|
}, { deep: true })
|
||||||
|
|
||||||
|
// ---------- Save ----------
|
||||||
const saving = ref(false)
|
const saving = ref(false)
|
||||||
const errorText = ref<string | null>(null)
|
const errorText = ref<string | null>(null)
|
||||||
|
|
||||||
@@ -63,7 +149,6 @@ function changedName() {
|
|||||||
return name.value.trim() !== originalName.value.trim()
|
return name.value.trim() !== originalName.value.trim()
|
||||||
}
|
}
|
||||||
function changedUsers() {
|
function changedUsers() {
|
||||||
// compare as sets
|
|
||||||
const a = new Set(originalIds.value)
|
const a = new Set(originalIds.value)
|
||||||
const b = new Set(selectedUserIds.value)
|
const b = new Set(selectedUserIds.value)
|
||||||
if (a.size !== b.size) return true
|
if (a.size !== b.size) return true
|
||||||
@@ -76,26 +161,31 @@ async function onSave() {
|
|||||||
errorText.value = null
|
errorText.value = null
|
||||||
saving.value = true
|
saving.value = true
|
||||||
try {
|
try {
|
||||||
|
console.log('[EditDeviceDialog:compare]', {
|
||||||
|
originalIds: [...originalIds.value],
|
||||||
|
selectedUserIds: [...selectedUserIds.value],
|
||||||
|
})
|
||||||
|
|
||||||
const ops: Promise<any>[] = []
|
const ops: Promise<any>[] = []
|
||||||
|
|
||||||
if (changedName()) {
|
const nameChanged = changedName()
|
||||||
ops.push(
|
const usersChanged = changedUsers()
|
||||||
api.post(`/devices/${encodeURIComponent(guid.value)}/rename`, {
|
console.log('[EditDeviceDialog:onSave]', { nameChanged, usersChanged, name: name.value })
|
||||||
name: name.value.trim(),
|
|
||||||
} as { name: string })
|
if (nameChanged) {
|
||||||
)
|
ops.push(api.post(`/devices/${encodeURIComponent(guid.value)}/rename`, {
|
||||||
|
name: name.value.trim(),
|
||||||
|
} as { name: string }))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (changedUsers()) {
|
if (usersChanged) {
|
||||||
const userIdsNum = selectedUserIds.value
|
const userIdsNum = selectedUserIds.value
|
||||||
.map(v => Number(v))
|
.map(v => Number(v))
|
||||||
.filter(n => Number.isFinite(n)) as number[]
|
.filter(n => Number.isFinite(n)) as number[]
|
||||||
|
|
||||||
ops.push(
|
ops.push(api.post(`/devices/${encodeURIComponent(guid.value)}/set_users`, {
|
||||||
api.post(`/devices/${encodeURIComponent(guid.value)}/set_users`, {
|
userIds: userIdsNum,
|
||||||
userIds: userIdsNum,
|
} as { userIds: number[] }))
|
||||||
} as { userIds: number[] })
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ops.length > 0) {
|
if (ops.length > 0) {
|
||||||
@@ -105,7 +195,7 @@ async function onSave() {
|
|||||||
emit('updated', { name: name.value, userIds: [...selectedUserIds.value] })
|
emit('updated', { name: name.value, userIds: [...selectedUserIds.value] })
|
||||||
emit('update:modelValue', false)
|
emit('update:modelValue', false)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error(err)
|
console.error('[EditDeviceDialog:onSave] error', err)
|
||||||
errorText.value = err?.response?.data?.message || 'Failed to save changes.'
|
errorText.value = err?.response?.data?.message || 'Failed to save changes.'
|
||||||
} finally {
|
} finally {
|
||||||
saving.value = false
|
saving.value = false
|
||||||
@@ -114,30 +204,37 @@ async function onSave() {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Dialog :open="props.modelValue" @update:open="(v: boolean) => emit('update:modelValue', v)">
|
<Dialog :open="props.modelValue" @update:open="(v:boolean)=>emit('update:modelValue', v)">
|
||||||
<DialogContent class="sm:min-w-[500px]">
|
<DialogContent class="sm:min-w-[500px]">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Edit device</DialogTitle>
|
<DialogTitle>Edit device</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>Make changes to device settings below.</DialogDescription>
|
||||||
Make changes to device settings below.
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<div class="grid gap-4 py-4">
|
<div class="grid gap-4 py-4">
|
||||||
<div class="grid grid-cols-4 items-center gap-4">
|
<div class="grid grid-cols-4 items-center gap-4">
|
||||||
<Label for="guid" class="text-right">GUID</Label>
|
<Label for="guid" class="text-right">GUID</Label>
|
||||||
<p id="guid" class="col-span-3 break-all text-muted-foreground"> {{ props.device?.guid }}</p>
|
<p id="guid" class="col-span-3 break-all text-muted-foreground">{{ guid }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-cols-4 items-center gap-4">
|
<div class="grid grid-cols-4 items-center gap-4">
|
||||||
<Label for="name" class="text-right">Name</Label>
|
<Label for="name" class="text-right">Name</Label>
|
||||||
<Input id="name" class="col-span-3" v-model="name" :disabled="saving" placeholder="Device name"/>
|
<Input id="name" class="col-span-3" v-model="name" :disabled="saving" placeholder="Device name" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-cols-4 items-center gap-4">
|
<div class="grid grid-cols-4 items-center gap-4">
|
||||||
<Label for="users" class="text-right">Allowed users</Label>
|
<Label for="users" class="text-right">Allowed users</Label>
|
||||||
<AssignDevice id="users" v-model="selectedUserIds" :all-users="props.allUsers" class="col-span-3 w-full" />
|
<AssignDevice
|
||||||
|
id="users"
|
||||||
|
class="col-span-3 w-full"
|
||||||
|
v-model="selectedUserIds"
|
||||||
|
:all-users="props.allUsers"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p v-if="errorText" class="text-sm text-red-600 mt-1 col-span-4">{{ errorText }}</p>
|
<p v-if="errorText" class="text-sm text-red-600 mt-1 col-span-4">{{ errorText }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button :disabled="saving" @click="onSave">
|
<Button :disabled="saving" @click="onSave">
|
||||||
{{ saving ? 'Saving…' : 'Save changes' }}
|
{{ saving ? 'Saving…' : 'Save changes' }}
|
||||||
@@ -145,4 +242,4 @@ async function onSave() {
|
|||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
Reference in New Issue
Block a user