feat/refac: group members db table (#19239)
* refac: group members table db migration * refac: group members backend * refac: group members frontend * refac: group members frontend integration * refac: styling
This commit is contained in:
@@ -33,9 +33,6 @@
|
||||
|
||||
let loaded = false;
|
||||
|
||||
let users = [];
|
||||
let total = 0;
|
||||
|
||||
let groups = [];
|
||||
let filteredGroups;
|
||||
|
||||
@@ -93,16 +90,6 @@
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await getAllUsers(localStorage.token).catch((error) => {
|
||||
toast.error(`${error}`);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (res) {
|
||||
users = res.users;
|
||||
total = res.total;
|
||||
}
|
||||
|
||||
defaultPermissions = await getUserDefaultPermissions(localStorage.token);
|
||||
await setGroups();
|
||||
loaded = true;
|
||||
@@ -189,7 +176,7 @@
|
||||
|
||||
{#each filteredGroups as group}
|
||||
<div class="my-2">
|
||||
<GroupItem {group} {users} {setGroups} {defaultPermissions} />
|
||||
<GroupItem {group} {setGroups} {defaultPermissions} />
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
@@ -8,6 +8,9 @@
|
||||
export let name = '';
|
||||
export let color = '';
|
||||
export let description = '';
|
||||
|
||||
export let edit = false;
|
||||
export let onDelete: Function = () => {};
|
||||
</script>
|
||||
|
||||
<div class="flex gap-2">
|
||||
@@ -59,3 +62,18 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if edit}
|
||||
<div class="flex flex-col w-full mt-2">
|
||||
<div class=" mb-0.5 text-xs text-gray-500">{$i18n.t('Actions')}</div>
|
||||
|
||||
<div class="flex-1">
|
||||
<button
|
||||
class="text-xs bg-transparent hover:underline cursor-pointer"
|
||||
on:click={() => onDelete()}
|
||||
>
|
||||
{$i18n.t('Delete')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
export let show = false;
|
||||
export let edit = false;
|
||||
|
||||
export let users = [];
|
||||
export let group = null;
|
||||
export let defaultPermissions = {};
|
||||
|
||||
@@ -31,6 +30,8 @@
|
||||
let loading = false;
|
||||
let showDeleteConfirmDialog = false;
|
||||
|
||||
let userCount = 0;
|
||||
|
||||
export let name = '';
|
||||
export let description = '';
|
||||
|
||||
@@ -83,7 +84,6 @@
|
||||
notes: true
|
||||
}
|
||||
};
|
||||
export let userIds = [];
|
||||
|
||||
const submitHandler = async () => {
|
||||
loading = true;
|
||||
@@ -91,8 +91,7 @@
|
||||
const group = {
|
||||
name,
|
||||
description,
|
||||
permissions,
|
||||
user_ids: userIds
|
||||
permissions
|
||||
};
|
||||
|
||||
await onSubmit(group);
|
||||
@@ -107,7 +106,7 @@
|
||||
description = group.description;
|
||||
permissions = group?.permissions ?? {};
|
||||
|
||||
userIds = group?.user_ids ?? [];
|
||||
userCount = group?.member_count ?? 0;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -129,7 +128,7 @@
|
||||
}}
|
||||
/>
|
||||
|
||||
<Modal size="md" bind:show>
|
||||
<Modal size="lg" bind:show>
|
||||
<div>
|
||||
<div class=" flex justify-between dark:text-gray-100 px-5 pt-4 mb-1.5">
|
||||
<div class=" text-lg font-medium self-center font-primary">
|
||||
@@ -228,20 +227,27 @@
|
||||
<div class=" self-center mr-2">
|
||||
<UserPlusSolid />
|
||||
</div>
|
||||
<div class=" self-center">{$i18n.t('Users')} ({userIds.length})</div>
|
||||
<div class=" self-center">{$i18n.t('Users')}</div>
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex-1 mt-1 lg:mt-1 lg:h-[22rem] lg:max-h-[22rem] overflow-y-auto scrollbar-hidden"
|
||||
class="flex-1 mt-1 lg:mt-1 lg:h-[30rem] lg:max-h-[30rem] overflow-y-auto scrollbar-hidden"
|
||||
>
|
||||
{#if selectedTab == 'general'}
|
||||
<Display bind:name bind:description />
|
||||
<Display
|
||||
bind:name
|
||||
bind:description
|
||||
{edit}
|
||||
onDelete={() => {
|
||||
showDeleteConfirmDialog = true;
|
||||
}}
|
||||
/>
|
||||
{:else if selectedTab == 'permissions'}
|
||||
<Permissions bind:permissions {defaultPermissions} />
|
||||
{:else if selectedTab == 'users'}
|
||||
<Users bind:userIds {users} />
|
||||
<Users bind:userCount groupId={group?.id} />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
@@ -295,37 +301,25 @@
|
||||
{/if}
|
||||
</div> -->
|
||||
|
||||
<div class="flex justify-between pt-3 text-sm font-medium gap-1.5">
|
||||
{#if edit}
|
||||
{#if ['general', 'permissions'].includes(selectedTab)}
|
||||
<div class="flex justify-end pt-3 text-sm font-medium gap-1.5">
|
||||
<button
|
||||
class="px-3.5 py-1.5 text-sm font-medium dark:bg-black dark:hover:bg-gray-900 dark:text-white bg-white text-black hover:bg-gray-100 transition rounded-full flex flex-row space-x-1 items-center"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
showDeleteConfirmDialog = true;
|
||||
}}
|
||||
class="px-3.5 py-1.5 text-sm font-medium bg-black hover:bg-gray-900 text-white dark:bg-white dark:text-black dark:hover:bg-gray-100 transition rounded-full flex flex-row space-x-1 items-center {loading
|
||||
? ' cursor-not-allowed'
|
||||
: ''}"
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
>
|
||||
{$i18n.t('Delete')}
|
||||
{$i18n.t('Save')}
|
||||
|
||||
{#if loading}
|
||||
<div class="ml-2 self-center">
|
||||
<Spinner />
|
||||
</div>
|
||||
{/if}
|
||||
</button>
|
||||
{:else}
|
||||
<div></div>
|
||||
{/if}
|
||||
|
||||
<button
|
||||
class="px-3.5 py-1.5 text-sm font-medium bg-black hover:bg-gray-900 text-white dark:bg-white dark:text-black dark:hover:bg-gray-100 transition rounded-full flex flex-row space-x-1 items-center {loading
|
||||
? ' cursor-not-allowed'
|
||||
: ''}"
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
>
|
||||
{$i18n.t('Save')}
|
||||
|
||||
{#if loading}
|
||||
<div class="ml-2 self-center">
|
||||
<Spinner />
|
||||
</div>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
import UserCircleSolid from '$lib/components/icons/UserCircleSolid.svelte';
|
||||
import GroupModal from './EditGroupModal.svelte';
|
||||
|
||||
export let users = [];
|
||||
export let group = {
|
||||
name: 'Admins',
|
||||
user_ids: [1, 2, 3]
|
||||
@@ -58,7 +57,6 @@
|
||||
<GroupModal
|
||||
bind:show={showEdit}
|
||||
edit
|
||||
{users}
|
||||
{group}
|
||||
{defaultPermissions}
|
||||
onSubmit={updateHandler}
|
||||
@@ -81,7 +79,7 @@
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-1.5 w-fit font-medium text-right justify-end">
|
||||
{group.user_ids.length}
|
||||
{group?.member_count}
|
||||
|
||||
<div>
|
||||
<User className="size-3.5" />
|
||||
|
||||
@@ -2,50 +2,80 @@
|
||||
import { getContext } from 'svelte';
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
import { getUsers } from '$lib/apis/users';
|
||||
import { toast } from 'svelte-sonner';
|
||||
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
import Plus from '$lib/components/icons/Plus.svelte';
|
||||
import { WEBUI_BASE_URL } from '$lib/constants';
|
||||
import Checkbox from '$lib/components/common/Checkbox.svelte';
|
||||
import Badge from '$lib/components/common/Badge.svelte';
|
||||
import Search from '$lib/components/icons/Search.svelte';
|
||||
import Pagination from '$lib/components/common/Pagination.svelte';
|
||||
import { addUserToGroup, removeUserFromGroup } from '$lib/apis/groups';
|
||||
|
||||
export let users = [];
|
||||
export let userIds = [];
|
||||
export let groupId: string;
|
||||
export let userCount = 0;
|
||||
|
||||
let filteredUsers = [];
|
||||
|
||||
$: filteredUsers = users
|
||||
.filter((user) => {
|
||||
if (query === '') {
|
||||
return true;
|
||||
}
|
||||
|
||||
return (
|
||||
user.name.toLowerCase().includes(query.toLowerCase()) ||
|
||||
user.email.toLowerCase().includes(query.toLowerCase())
|
||||
);
|
||||
})
|
||||
.sort((a, b) => {
|
||||
const aUserIndex = userIds.indexOf(a.id);
|
||||
const bUserIndex = userIds.indexOf(b.id);
|
||||
|
||||
// Compare based on userIds or fall back to alphabetical order
|
||||
if (aUserIndex !== -1 && bUserIndex === -1) return -1; // 'a' has valid userId -> prioritize
|
||||
if (bUserIndex !== -1 && aUserIndex === -1) return 1; // 'b' has valid userId -> prioritize
|
||||
|
||||
// Both a and b are either in the userIds array or not, so we'll sort them by their indices
|
||||
if (aUserIndex !== -1 && bUserIndex !== -1) return aUserIndex - bUserIndex;
|
||||
|
||||
// If both are not in the userIds, fallback to alphabetical sorting by name
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
let users = [];
|
||||
let total = 0;
|
||||
|
||||
let query = '';
|
||||
let page = 1;
|
||||
|
||||
const getUserList = async () => {
|
||||
try {
|
||||
const res = await getUsers(
|
||||
localStorage.token,
|
||||
query,
|
||||
`group_id:${groupId}`,
|
||||
null,
|
||||
page
|
||||
).catch((error) => {
|
||||
toast.error(`${error}`);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (res) {
|
||||
users = res.users;
|
||||
total = res.total;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
};
|
||||
|
||||
const toggleMember = async (userId, state) => {
|
||||
if (state === 'checked') {
|
||||
await addUserToGroup(localStorage.token, groupId, [userId]).catch((error) => {
|
||||
toast.error(`${error}`);
|
||||
return null;
|
||||
});
|
||||
} else {
|
||||
await removeUserFromGroup(localStorage.token, groupId, [userId]).catch((error) => {
|
||||
toast.error(`${error}`);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
page = 1;
|
||||
getUserList();
|
||||
};
|
||||
|
||||
$: if (page) {
|
||||
getUserList();
|
||||
}
|
||||
|
||||
$: if (query !== null) {
|
||||
getUserList();
|
||||
}
|
||||
|
||||
$: if (query) {
|
||||
page = 1;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<div class="flex w-full">
|
||||
<div class="flex flex-1">
|
||||
<div class=" max-h-full h-full w-full flex flex-col overflow-y-hidden">
|
||||
<div class="w-full h-fit mb-1.5">
|
||||
<div class="flex flex-1 h-fit">
|
||||
<div class=" self-center mr-3">
|
||||
<Search />
|
||||
</div>
|
||||
@@ -57,20 +87,16 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 scrollbar-hidden">
|
||||
<div class="flex-1 overflow-y-auto scrollbar-hidden">
|
||||
<div class="flex flex-col gap-2.5">
|
||||
{#if filteredUsers.length > 0}
|
||||
{#each filteredUsers as user, userIdx (user.id)}
|
||||
{#if users.length > 0}
|
||||
{#each users as user, userIdx (user.id)}
|
||||
<div class="flex flex-row items-center gap-3 w-full text-sm">
|
||||
<div class="flex items-center">
|
||||
<Checkbox
|
||||
state={userIds.includes(user.id) ? 'checked' : 'unchecked'}
|
||||
state={(user?.group_ids ?? []).includes(groupId) ? 'checked' : 'unchecked'}
|
||||
on:change={(e) => {
|
||||
if (e.detail === 'checked') {
|
||||
userIds = [...userIds, user.id];
|
||||
} else {
|
||||
userIds = userIds.filter((id) => id !== user.id);
|
||||
}
|
||||
toggleMember(user.id, e.detail);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -82,7 +108,7 @@
|
||||
</div>
|
||||
</Tooltip>
|
||||
|
||||
{#if userIds.includes(user.id)}
|
||||
{#if (user?.group_ids ?? []).includes(groupId)}
|
||||
<Badge type="success" content="member" />
|
||||
{/if}
|
||||
</div>
|
||||
@@ -95,4 +121,8 @@
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if total > 30}
|
||||
<Pagination bind:page count={total} perPage={30} />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user