mirror of
https://github.com/sysadminsmedia/homebox.git
synced 2025-12-27 23:46:37 +01:00
feat: progress
This commit is contained in:
@@ -1,126 +1,121 @@
|
||||
<script setup lang="ts">
|
||||
import { reactive, watch, ref } from "vue";
|
||||
import { reactive, ref, onMounted, onUnmounted } from "vue";
|
||||
import type { User as MockUser, Collection as MockCollection } from "~/mock/collections";
|
||||
import { api } from "~/mock/collections";
|
||||
import { DialogContent, DialogHeader, DialogFooter, DialogTitle, DialogDescription } from "@/components/ui/dialog";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogFooter,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
} from "@/components/ui/dialog";
|
||||
import { DialogID } from "@/components/ui/dialog-provider/utils";
|
||||
import { useDialog } from "@/components/ui/dialog-provider";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Select, SelectTrigger, SelectContent, SelectItem, SelectValue } from "@/components/ui/select";
|
||||
import MdiClose from "~icons/mdi/close";
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: boolean;
|
||||
// editing matches the mock User shape: collections are {id, role} tuples
|
||||
editing: MockUser | null;
|
||||
collections: MockCollection[];
|
||||
editingCollectionIds: string[];
|
||||
newPassword: string;
|
||||
}>();
|
||||
// dialog provider
|
||||
const { closeDialog, registerOpenDialogCallback } = useDialog();
|
||||
|
||||
const emit = defineEmits([
|
||||
"update:modelValue",
|
||||
"update:editing",
|
||||
"update:editingCollectionIds",
|
||||
"update:newPassword",
|
||||
"save",
|
||||
"cancel",
|
||||
"collections-changed",
|
||||
] as const);
|
||||
// local collections snapshot used for checkbox list
|
||||
const availableCollections = ref<MockCollection[]>(api.getCollections() as MockCollection[]);
|
||||
const isNew = ref(true);
|
||||
|
||||
const localEditing = reactive<MockUser>(
|
||||
props.editing
|
||||
? { ...props.editing }
|
||||
: { id: String(Date.now()), name: "", email: "", role: "user", password_set: false, collections: [] }
|
||||
);
|
||||
const localEditing = reactive<MockUser>({
|
||||
id: String(Date.now()),
|
||||
name: "",
|
||||
email: "",
|
||||
role: "user",
|
||||
password_set: false,
|
||||
collections: [],
|
||||
});
|
||||
|
||||
const localCollectionIds = ref<string[]>([...(props.editingCollectionIds ?? [])]);
|
||||
const localNewPassword = ref(props.newPassword ?? "");
|
||||
const localCollectionIds = ref<string[]>([]);
|
||||
const localNewPassword = ref("");
|
||||
const newAddCollectionId = ref<string>("");
|
||||
|
||||
onMounted(() => {
|
||||
const cleanup = registerOpenDialogCallback(DialogID.EditUser, params => {
|
||||
// refresh available collections each time
|
||||
availableCollections.value = api.getCollections() as MockCollection[];
|
||||
|
||||
if (params && (params as { userId?: string }).userId) {
|
||||
const u = api.getUser(params.userId!);
|
||||
if (u) {
|
||||
Object.assign(localEditing, u as MockUser);
|
||||
localCollectionIds.value = (u.collections ?? []).map(c => c.id);
|
||||
isNew.value = false;
|
||||
} else {
|
||||
reset();
|
||||
isNew.value = true;
|
||||
}
|
||||
} else {
|
||||
// new user
|
||||
reset();
|
||||
isNew.value = true;
|
||||
}
|
||||
localNewPassword.value = "";
|
||||
});
|
||||
|
||||
onUnmounted(cleanup);
|
||||
});
|
||||
|
||||
type Membership = { id: string; role: "owner" | "admin" | "editor" | "viewer" };
|
||||
|
||||
watch(
|
||||
() => props.editing,
|
||||
v => {
|
||||
if (v) Object.assign(localEditing, v);
|
||||
else {
|
||||
localEditing.id = String(Date.now());
|
||||
localEditing.name = "";
|
||||
localEditing.email = "";
|
||||
localEditing.role = "user";
|
||||
localEditing.password_set = false;
|
||||
localEditing.collections = [];
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
function getCollectionName(id: string) {
|
||||
const found = availableCollections.value.find(c => c.id === id);
|
||||
return found ? found.name : id;
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.editingCollectionIds,
|
||||
v => {
|
||||
localCollectionIds.value = [...(v ?? [])];
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.newPassword,
|
||||
v => (localNewPassword.value = v ?? ""),
|
||||
{ immediate: true }
|
||||
);
|
||||
// localEditing will be set when dialog opens via registerOpenDialogCallback
|
||||
|
||||
function close() {
|
||||
emit("update:modelValue", false);
|
||||
emit("cancel");
|
||||
reset();
|
||||
closeDialog(DialogID.EditUser);
|
||||
}
|
||||
|
||||
function onSave() {
|
||||
// propagate changes back to parent
|
||||
emit("update:editing", { ...localEditing });
|
||||
emit("update:editingCollectionIds", [...localCollectionIds.value]);
|
||||
emit("update:newPassword", localNewPassword.value);
|
||||
emit("save");
|
||||
emit("update:modelValue", false);
|
||||
if (!localEditing.name.trim() || !localEditing.email.trim()) {
|
||||
alert("Name and email are required");
|
||||
return;
|
||||
}
|
||||
|
||||
if (localNewPassword.value && localEditing) localEditing.password_set = true;
|
||||
|
||||
const existing = api.getUser(localEditing.id);
|
||||
if (existing) {
|
||||
const updated = {
|
||||
...existing,
|
||||
name: localEditing.name,
|
||||
email: localEditing.email,
|
||||
role: localEditing.role,
|
||||
password_set: localEditing.password_set,
|
||||
} as MockUser;
|
||||
api.updateUser(updated);
|
||||
} else {
|
||||
const toCreate = { ...localEditing, collections: [] } as MockUser;
|
||||
const created = api.addUser(toCreate);
|
||||
localCollectionIds.value.forEach(id => api.addUserToCollection(created.id, id, "viewer"));
|
||||
}
|
||||
|
||||
// close and signal caller to refresh
|
||||
closeDialog(DialogID.EditUser, true);
|
||||
reset();
|
||||
}
|
||||
|
||||
function onCheckboxChange(e: Event, id: string) {
|
||||
const checked = (e.target as HTMLInputElement).checked;
|
||||
// if this user exists in the api, call the api to add/remove membership immediately
|
||||
const existsInApi = !!api.getUser(localEditing.id);
|
||||
if (!checked) {
|
||||
// unchecking
|
||||
if (existsInApi) {
|
||||
// will confirm inside removeMembership
|
||||
const ok = api.removeUserFromCollection(localEditing.id, id);
|
||||
if (ok) {
|
||||
// update local state to reflect api change
|
||||
localCollectionIds.value = localCollectionIds.value.filter(x => x !== id);
|
||||
localEditing.collections = (localEditing.collections ?? []).filter((x: Membership) => x.id !== id);
|
||||
}
|
||||
return;
|
||||
}
|
||||
// not in API yet (new user) — just update local state
|
||||
localCollectionIds.value = localCollectionIds.value.filter(x => x !== id);
|
||||
localEditing.collections = (localEditing.collections ?? []).filter((x: Membership) => x.id !== id);
|
||||
return;
|
||||
}
|
||||
|
||||
// checking
|
||||
if (existsInApi) {
|
||||
const mem = api.addUserToCollection(localEditing.id, id, "viewer");
|
||||
if (mem) {
|
||||
if (!localCollectionIds.value.includes(id)) localCollectionIds.value.push(id);
|
||||
localEditing.collections = (localEditing.collections ?? []).filter((x: Membership) => x.id !== id);
|
||||
localEditing.collections.push(mem as Membership);
|
||||
emit("update:editing", { ...api.getUser(localEditing.id) });
|
||||
emit("update:editingCollectionIds", [...localCollectionIds.value]);
|
||||
emit("collections-changed");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// new user — just add locally
|
||||
if (!localCollectionIds.value.includes(id)) localCollectionIds.value.push(id);
|
||||
localEditing.collections = (localEditing.collections ?? []).filter((x: Membership) => x.id !== id);
|
||||
localEditing.collections.push({ id, role: "viewer" });
|
||||
function reset() {
|
||||
localEditing.id = String(Date.now());
|
||||
localEditing.name = "";
|
||||
localEditing.email = "";
|
||||
localEditing.role = "user";
|
||||
localEditing.password_set = false;
|
||||
localEditing.collections = [];
|
||||
localCollectionIds.value = [];
|
||||
localNewPassword.value = "";
|
||||
}
|
||||
|
||||
function removeMembership(id: string) {
|
||||
@@ -139,9 +134,8 @@
|
||||
if (ok) {
|
||||
localCollectionIds.value = localCollectionIds.value.filter(x => x !== id);
|
||||
localEditing.collections = (localEditing.collections ?? []).filter((x: Membership) => x.id !== id);
|
||||
emit("update:editing", { ...api.getUser(localEditing.id) });
|
||||
emit("update:editingCollectionIds", [...localCollectionIds.value]);
|
||||
emit("collections-changed");
|
||||
const refreshed = api.getUser(localEditing.id);
|
||||
if (refreshed) Object.assign(localEditing, refreshed as MockUser);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -150,51 +144,144 @@
|
||||
localCollectionIds.value = localCollectionIds.value.filter(x => x !== id);
|
||||
localEditing.collections = (localEditing.collections ?? []).filter((x: Membership) => x.id !== id);
|
||||
}
|
||||
|
||||
function addMembership(id: string) {
|
||||
if (!id) return;
|
||||
const existsInApi = !!api.getUser(localEditing.id);
|
||||
if (existsInApi) {
|
||||
const mem = api.addUserToCollection(localEditing.id, id, "viewer");
|
||||
if (mem) {
|
||||
if (!localCollectionIds.value.includes(id)) localCollectionIds.value.push(id);
|
||||
localEditing.collections = (localEditing.collections ?? []).filter((x: Membership) => x.id !== id);
|
||||
localEditing.collections.push(mem as Membership);
|
||||
const refreshed = api.getUser(localEditing.id);
|
||||
if (refreshed) Object.assign(localEditing, refreshed as MockUser);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// new user — add locally
|
||||
if (!localCollectionIds.value.includes(id)) localCollectionIds.value.push(id);
|
||||
localEditing.collections = (localEditing.collections ?? []).filter((x: Membership) => x.id !== id);
|
||||
localEditing.collections.push({ id, role: "viewer" });
|
||||
}
|
||||
|
||||
function updateMembershipRole(id: string, role: Membership["role"]) {
|
||||
const existsInApi = !!api.getUser(localEditing.id);
|
||||
if (existsInApi) {
|
||||
// best-effort: remove then re-add with new role if API doesn't expose direct update
|
||||
api.removeUserFromCollection(localEditing.id, id);
|
||||
const mem = api.addUserToCollection(localEditing.id, id, role);
|
||||
if (mem) {
|
||||
const refreshed = api.getUser(localEditing.id);
|
||||
if (refreshed) Object.assign(localEditing, refreshed as MockUser);
|
||||
localCollectionIds.value = (localEditing.collections ?? []).map((c: Membership) => c.id);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// local-only
|
||||
const existing = (localEditing.collections ?? []) as Membership[];
|
||||
const found = existing.find(x => x.id === id);
|
||||
if (found) found.role = role;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DialogContent v-if="props.modelValue">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{{ props.editing ? "Edit User" : "Add User" }}</DialogTitle>
|
||||
<DialogDescription> Manage user details and collection memberships. </DialogDescription>
|
||||
</DialogHeader>
|
||||
<Dialog :dialog-id="DialogID.EditUser">
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{{ isNew ? "Add User" : "Edit User" }}</DialogTitle>
|
||||
<DialogDescription>Manage user details and collection memberships.</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div class="grid gap-3">
|
||||
<label class="block">
|
||||
<div class="mb-1 text-sm">Name</div>
|
||||
<Input v-model="localEditing.name" />
|
||||
</label>
|
||||
<form class="flex flex-col gap-3" @submit.prevent="onSave">
|
||||
<label class="block">
|
||||
<div class="mb-1 text-sm">Name</div>
|
||||
<Input v-model="localEditing.name" />
|
||||
</label>
|
||||
|
||||
<label class="block">
|
||||
<div class="mb-1 text-sm">Email</div>
|
||||
<Input v-model="localEditing.email" />
|
||||
</label>
|
||||
<label class="block">
|
||||
<div class="mb-1 text-sm">Email</div>
|
||||
<Input v-model="localEditing.email" />
|
||||
</label>
|
||||
|
||||
<label class="block">
|
||||
<div class="mb-1 text-sm">Password</div>
|
||||
<Input v-model="localNewPassword" type="password" placeholder="Leave blank to keep" />
|
||||
</label>
|
||||
<label class="block">
|
||||
<div class="mb-1 text-sm">Password</div>
|
||||
<Input v-model="localNewPassword" type="password" placeholder="Leave blank to keep" />
|
||||
</label>
|
||||
|
||||
<div>
|
||||
<div class="mb-1 text-sm">Collections</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<label v-for="c in props.collections" :key="c.id" class="inline-flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
:value="c.id"
|
||||
:checked="localCollectionIds.includes(c.id)"
|
||||
@change="onCheckboxChange($event, c.id)"
|
||||
/>
|
||||
<Badge class="whitespace-nowrap">{{ c.name }}</Badge>
|
||||
<button type="button" class="ml-2 text-destructive" @click.prevent="removeMembership(c.id)">×</button>
|
||||
</label>
|
||||
<div>
|
||||
<div class="mb-1 text-sm">Collections</div>
|
||||
<div class="flex flex-col gap-3">
|
||||
<div
|
||||
v-for="m in localEditing.collections ?? []"
|
||||
:key="m.id"
|
||||
class="flex items-center justify-between rounded-lg border py-1 pl-3 pr-1"
|
||||
>
|
||||
<div class="text-lg font-medium">
|
||||
<Badge>
|
||||
{{ getCollectionName(m.id) }}
|
||||
</Badge>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<Select v-model="m.role" @update:model-value="val => updateMembershipRole(m.id, val)">
|
||||
<SelectTrigger class="w-40">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="owner">Owner</SelectItem>
|
||||
<SelectItem value="admin">Admin</SelectItem>
|
||||
<SelectItem value="editor">Editor</SelectItem>
|
||||
<SelectItem value="viewer">Viewer</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="icon"
|
||||
class="ml-2"
|
||||
:title="$t ? $t('global.remove') : 'Remove'"
|
||||
@click.prevent="removeMembership(m.id)"
|
||||
>
|
||||
<MdiClose class="size-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-2 flex items-center gap-2">
|
||||
<Select v-model="newAddCollectionId">
|
||||
<SelectTrigger class="flex-1">
|
||||
<SelectValue placeholder="Select collection to add" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem
|
||||
v-for="c in availableCollections.filter(c => !localCollectionIds.includes(c.id))"
|
||||
:key="c.id"
|
||||
:value="c.id"
|
||||
>
|
||||
{{ c.name }}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button
|
||||
type="button"
|
||||
class="ml-2 w-10 px-0"
|
||||
variant="default"
|
||||
size="lg"
|
||||
:disabled="!newAddCollectionId"
|
||||
@click="addMembership(newAddCollectionId)"
|
||||
>
|
||||
+
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" @click="close">Cancel</Button>
|
||||
<Button @click="onSave">Save</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" type="button" @click="close">Cancel</Button>
|
||||
<Button type="submit">Save</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
@@ -30,6 +30,7 @@ export enum DialogID {
|
||||
UpdateLabel = "update-label",
|
||||
UpdateLocation = "update-location",
|
||||
CreateInvite = "create-invite",
|
||||
EditUser = "edit-user",
|
||||
UpdateTemplate = "update-template",
|
||||
ItemChangeDetails = "item-table-updater",
|
||||
}
|
||||
@@ -57,6 +58,7 @@ export type DialogParamsMap = {
|
||||
attachmentId: string;
|
||||
};
|
||||
[DialogID.CreateItem]?: { product?: BarcodeProduct };
|
||||
[DialogID.EditUser]?: { userId?: string };
|
||||
[DialogID.ProductImport]?: { barcode?: string };
|
||||
[DialogID.EditMaintenance]:
|
||||
| { type: "create"; itemId: string | string[] }
|
||||
@@ -78,6 +80,7 @@ export type DialogResultMap = {
|
||||
[DialogID.EditMaintenance]?: boolean;
|
||||
[DialogID.CreateInvite]?: boolean;
|
||||
[DialogID.ItemChangeDetails]?: boolean;
|
||||
[DialogID.EditUser]?: boolean;
|
||||
};
|
||||
|
||||
/** Helpers to split IDs by requirement */
|
||||
|
||||
@@ -13,10 +13,15 @@
|
||||
import BaseSectionHeader from "@/components/Base/SectionHeader.vue";
|
||||
import MdiPencil from "~icons/mdi/pencil";
|
||||
import MdiDelete from "~icons/mdi/delete";
|
||||
import MdiCheck from "~icons/mdi/check";
|
||||
import MdiClose from "~icons/mdi/close";
|
||||
// import MdiOpenInNew from "~icons/mdi/open-in-new";
|
||||
// Badge component for collections display
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import UserFormDialog from "@/components/Admin/UserFormDialog.vue";
|
||||
import { useDialog } from "@/components/ui/dialog-provider";
|
||||
import { DialogID } from "@/components/ui/dialog-provider/utils";
|
||||
|
||||
import { api, type Collection as MockCollection, type User } from "~/mock/collections";
|
||||
|
||||
@@ -35,10 +40,7 @@
|
||||
});
|
||||
});
|
||||
|
||||
const editing = ref<User | null>(null);
|
||||
const showForm = ref(false);
|
||||
const newPassword = ref("");
|
||||
const editingCollectionIds = ref<string[]>([]);
|
||||
const { openDialog } = useDialog();
|
||||
const confirm = useConfirm();
|
||||
const { t } = useI18n();
|
||||
|
||||
@@ -55,61 +57,26 @@
|
||||
}
|
||||
|
||||
function openAdd() {
|
||||
editing.value = { id: String(Date.now()), name: "", email: "", role: "user", password_set: false, collections: [] };
|
||||
newPassword.value = "";
|
||||
editingCollectionIds.value = [];
|
||||
showForm.value = true;
|
||||
openDialog(DialogID.EditUser, {
|
||||
onClose: result => {
|
||||
if (result) {
|
||||
users.value = api.getUsers();
|
||||
collections.value = api.getCollections();
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function openEdit(u: User) {
|
||||
editing.value = { ...u };
|
||||
editingCollectionIds.value = (u.collections ?? []).map(c => c.id);
|
||||
newPassword.value = "";
|
||||
showForm.value = true;
|
||||
}
|
||||
|
||||
function saveUser() {
|
||||
if (!editing.value) return;
|
||||
// basic validation
|
||||
if (!editing.value.name.trim() || !editing.value.email.trim()) {
|
||||
// keep UX simple: alert for now
|
||||
// Replace with a nicer notification component when available
|
||||
alert("Name and email are required");
|
||||
return;
|
||||
}
|
||||
|
||||
// apply password flag if new password was set locally
|
||||
if (newPassword.value && editing.value) editing.value.password_set = true;
|
||||
|
||||
const existing = api.getUser(editing.value.id);
|
||||
if (existing) {
|
||||
// update only scalar fields; collections are managed via the add/remove API
|
||||
const updated = {
|
||||
...existing,
|
||||
name: editing.value.name,
|
||||
email: editing.value.email,
|
||||
role: editing.value.role,
|
||||
password_set: editing.value.password_set,
|
||||
} as User;
|
||||
api.updateUser(updated);
|
||||
} else {
|
||||
// create user without collections first, then add memberships
|
||||
const toCreate = { ...editing.value, collections: [] } as User;
|
||||
const created = api.addUser(toCreate);
|
||||
editingCollectionIds.value.forEach(id => api.addUserToCollection(created.id, id, "viewer"));
|
||||
}
|
||||
|
||||
// refresh local cache
|
||||
users.value = api.getUsers();
|
||||
|
||||
editing.value = null;
|
||||
showForm.value = false;
|
||||
// TODO: call backend API to persist changes when available
|
||||
}
|
||||
|
||||
function cancelForm() {
|
||||
editing.value = null;
|
||||
showForm.value = false;
|
||||
openDialog(DialogID.EditUser, {
|
||||
params: { userId: u.id },
|
||||
onClose: result => {
|
||||
if (result) {
|
||||
users.value = api.getUsers();
|
||||
collections.value = api.getCollections();
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function confirmDelete(u: User) {
|
||||
@@ -130,17 +97,13 @@
|
||||
return col ? col.name : id;
|
||||
}
|
||||
|
||||
function onUpdateEditing(val: User | null) {
|
||||
editing.value = val;
|
||||
function roleVariant(role: string | undefined) {
|
||||
if (role === "owner") return "default";
|
||||
if (role === "admin") return "secondary";
|
||||
return "outline";
|
||||
}
|
||||
|
||||
function onUpdateEditingCollectionIds(val: string[]) {
|
||||
editingCollectionIds.value = val;
|
||||
}
|
||||
|
||||
function onUpdateNewPassword(val: string) {
|
||||
newPassword.value = val;
|
||||
}
|
||||
// dialog handles editing state now via dialog provider
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -156,12 +119,12 @@
|
||||
<Table class="w-full">
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>{{ t("global.name") }}</TableHead>
|
||||
<TableHead>{{ t("global.email") }}</TableHead>
|
||||
<TableHead>Is Admin</TableHead>
|
||||
<TableHead>Collections</TableHead>
|
||||
<TableHead class="w-32 text-center">Auth</TableHead>
|
||||
<TableHead class="w-40 text-center">{{ t("global.details") }}</TableHead>
|
||||
<TableHead class="min-w-[160px]">{{ t("global.name") }}</TableHead>
|
||||
<TableHead class="min-w-[220px]">{{ t("global.email") }}</TableHead>
|
||||
<TableHead class="min-w-[96px] text-center">Is Admin</TableHead>
|
||||
<TableHead class="min-w-[220px]">Collections</TableHead>
|
||||
<TableHead class="min-w-[96px] text-center">{{ t("global.details") }}</TableHead>
|
||||
<TableHead class="w-40 text-center"></TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
|
||||
@@ -170,24 +133,40 @@
|
||||
<TableRow v-for="u in filtered" :key="u.id">
|
||||
<TableCell>{{ u.name }}</TableCell>
|
||||
<TableCell>{{ u.email }}</TableCell>
|
||||
<TableCell class="text-center">
|
||||
<span class="font-medium">{{ u.role === "admin" ? "Yes" : "No" }}</span>
|
||||
<TableCell class="text-center align-middle">
|
||||
<div class="flex size-full items-center justify-center font-medium">
|
||||
<MdiCheck v-if="u.role === 'admin'" class="text-primary" />
|
||||
<MdiClose v-else class="text-destructive" />
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<template v-if="u.collections && u.collections.length">
|
||||
<Badge v-for="c in u.collections" :key="c.id" class="whitespace-nowrap"
|
||||
>{{ collectionName(c.id) }}<span class="ml-1 text-xs opacity-60">({{ c.role }})</span></Badge
|
||||
>
|
||||
<TooltipProvider :delay-duration="0">
|
||||
<template v-for="c in u.collections" :key="c.id">
|
||||
<Tooltip>
|
||||
<TooltipTrigger as-child>
|
||||
<Badge class="whitespace-nowrap" :variant="roleVariant(c.role)">{{
|
||||
collectionName(c.id)
|
||||
}}</Badge>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p class="text-sm">{{ c.role }}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</template>
|
||||
</TooltipProvider>
|
||||
</template>
|
||||
<span v-else class="text-muted-foreground">-</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell class="text-center">
|
||||
<span>{{ authType(u) }}</span>
|
||||
<TableCell class="text-center align-middle">
|
||||
<div class="flex size-full items-center justify-center">
|
||||
<span>{{ authType(u) }}</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell class="text-right">
|
||||
<div class="flex justify-end gap-2">
|
||||
<TableCell class="text-right align-middle">
|
||||
<div class="flex size-full items-center justify-end gap-2">
|
||||
<Button size="icon" variant="outline" class="size-8" :title="t('global.edit')" @click="openEdit(u)">
|
||||
<MdiPencil class="size-4" />
|
||||
</Button>
|
||||
@@ -215,18 +194,6 @@
|
||||
</Card>
|
||||
|
||||
<!-- Add / Edit form modal (moved to component) -->
|
||||
<UserFormDialog
|
||||
v-model="showForm"
|
||||
:editing="editing"
|
||||
:collections="collections"
|
||||
:editing-collection-ids="editingCollectionIds"
|
||||
:new-password="newPassword"
|
||||
@update:editing="onUpdateEditing"
|
||||
@update:editing-collection-ids="onUpdateEditingCollectionIds"
|
||||
@update:new-password="onUpdateNewPassword"
|
||||
@save="saveUser"
|
||||
@cancel="cancelForm"
|
||||
@collections-changed="() => (collections = api.getCollections())"
|
||||
/>
|
||||
<UserFormDialog />
|
||||
</BaseContainer>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user