refac: groups ui
This commit is contained in:
@@ -12,18 +12,15 @@
|
||||
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
import Plus from '$lib/components/icons/Plus.svelte';
|
||||
import Badge from '$lib/components/common/Badge.svelte';
|
||||
import UsersSolid from '$lib/components/icons/UsersSolid.svelte';
|
||||
import ChevronRight from '$lib/components/icons/ChevronRight.svelte';
|
||||
import EllipsisHorizontal from '$lib/components/icons/EllipsisHorizontal.svelte';
|
||||
import Search from '$lib/components/icons/Search.svelte';
|
||||
import User from '$lib/components/icons/User.svelte';
|
||||
import UserCircleSolid from '$lib/components/icons/UserCircleSolid.svelte';
|
||||
import EditGroupModal from './Groups/EditGroupModal.svelte';
|
||||
import Pencil from '$lib/components/icons/Pencil.svelte';
|
||||
import ChevronUp from '$lib/components/icons/ChevronUp.svelte';
|
||||
import ChevronDown from '$lib/components/icons/ChevronDown.svelte';
|
||||
import GroupItem from './Groups/GroupItem.svelte';
|
||||
import XMark from '$lib/components/icons/XMark.svelte';
|
||||
import ChevronDown from '$lib/components/icons/ChevronDown.svelte';
|
||||
import Check from '$lib/components/icons/Check.svelte';
|
||||
import { Select } from 'bits-ui';
|
||||
import { createNewGroup, getGroups } from '$lib/apis/groups';
|
||||
import {
|
||||
getUserDefaultPermissions,
|
||||
@@ -38,17 +35,12 @@
|
||||
let groups = [];
|
||||
|
||||
let query = '';
|
||||
let orderBy = '';
|
||||
let direction = 'asc';
|
||||
let sortBy = 'members';
|
||||
|
||||
const setSortKey = (key) => {
|
||||
if (orderBy === key) {
|
||||
direction = direction === 'asc' ? 'desc' : 'asc';
|
||||
} else {
|
||||
orderBy = key;
|
||||
direction = 'asc';
|
||||
}
|
||||
};
|
||||
const sortItems = [
|
||||
{ value: 'name', label: $i18n.t('Name') },
|
||||
{ value: 'members', label: $i18n.t('Members') }
|
||||
];
|
||||
|
||||
$: filteredGroups = groups
|
||||
.filter((group) => {
|
||||
@@ -61,12 +53,10 @@
|
||||
}
|
||||
})
|
||||
.sort((a, b) => {
|
||||
if (orderBy === 'name') {
|
||||
const cmp = a.name.localeCompare(b.name);
|
||||
return direction === 'asc' ? cmp : -cmp;
|
||||
} else if (orderBy === 'members') {
|
||||
const cmp = (a.member_count ?? 0) - (b.member_count ?? 0);
|
||||
return direction === 'asc' ? cmp : -cmp;
|
||||
if (sortBy === 'name') {
|
||||
return a.name.localeCompare(b.name);
|
||||
} else if (sortBy === 'members') {
|
||||
return (b.member_count ?? 0) - (a.member_count ?? 0);
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
@@ -129,144 +119,153 @@
|
||||
onSubmit={addGroupHandler}
|
||||
/>
|
||||
|
||||
<div class="mt-0.5 mb-2 gap-1 flex flex-col md:flex-row justify-between">
|
||||
<div class="flex items-center md:self-center text-xl font-medium px-0.5 gap-2 shrink-0">
|
||||
<div>
|
||||
{$i18n.t('Groups')}
|
||||
</div>
|
||||
|
||||
<div class="text-lg font-medium text-gray-500 dark:text-gray-500">
|
||||
{groups.length}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-1">
|
||||
<div class=" flex w-full space-x-2">
|
||||
<div class="flex flex-1">
|
||||
<div class=" self-center ml-1 mr-3">
|
||||
<Search />
|
||||
</div>
|
||||
<input
|
||||
class=" w-full text-sm pr-4 py-1 rounded-r-xl outline-hidden bg-transparent"
|
||||
bind:value={query}
|
||||
placeholder={$i18n.t('Search')}
|
||||
/>
|
||||
<div class="flex flex-col gap-1 px-1 mt-1.5 mb-3">
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="flex items-center md:self-center text-xl font-medium px-0.5 gap-2 shrink-0">
|
||||
<div>
|
||||
{$i18n.t('Groups')}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Tooltip content={$i18n.t('Create Group')}>
|
||||
<div class="text-lg font-medium text-gray-500 dark:text-gray-500">
|
||||
{groups.length}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex w-full justify-end gap-1.5">
|
||||
<button
|
||||
class="px-2 py-1.5 rounded-xl bg-black text-white dark:bg-white dark:text-black transition font-medium text-sm flex items-center"
|
||||
on:click={() => {
|
||||
showAddGroupModal = !showAddGroupModal;
|
||||
}}
|
||||
>
|
||||
<Plus className="size-3" strokeWidth="2.5" />
|
||||
|
||||
<div class="hidden md:block md:ml-1 text-xs">{$i18n.t('New Group')}</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="py-2 bg-white dark:bg-gray-900 rounded-3xl border border-gray-100/30 dark:border-gray-850/30"
|
||||
>
|
||||
<div class="flex items-center w-full space-x-2 py-0.5 px-3.5">
|
||||
<div class="flex flex-1">
|
||||
<div class="self-center ml-1 mr-3">
|
||||
<Search className="size-3.5" />
|
||||
</div>
|
||||
<input
|
||||
class="w-full text-sm py-1 rounded-r-xl outline-hidden bg-transparent"
|
||||
bind:value={query}
|
||||
placeholder={$i18n.t('Search Groups')}
|
||||
/>
|
||||
{#if query}
|
||||
<div class="self-center pl-1.5 translate-y-[0.5px] rounded-l-xl bg-transparent">
|
||||
<button
|
||||
class=" p-2 rounded-xl hover:bg-gray-100 dark:bg-gray-900 dark:hover:bg-gray-850 transition font-medium text-sm flex items-center space-x-1"
|
||||
class="p-0.5 rounded-full hover:bg-gray-100 dark:hover:bg-gray-900 transition"
|
||||
on:click={() => {
|
||||
showAddGroupModal = !showAddGroupModal;
|
||||
query = '';
|
||||
}}
|
||||
>
|
||||
<Plus className="size-3.5" />
|
||||
<XMark className="size-3" strokeWidth="2" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<Select.Root
|
||||
selected={sortItems.find((item) => item.value === sortBy)}
|
||||
items={sortItems}
|
||||
onSelectedChange={(selectedItem) => {
|
||||
sortBy = selectedItem.value;
|
||||
}}
|
||||
>
|
||||
<Select.Trigger
|
||||
class="relative flex items-center gap-0.5 px-2.5 py-1.5 text-sm bg-gray-50 dark:bg-gray-850 rounded-xl shrink-0"
|
||||
aria-label={$i18n.t('Sort by')}
|
||||
>
|
||||
<Select.Value
|
||||
class="inline-flex h-input px-0.5 outline-hidden bg-transparent truncate placeholder-gray-400 focus:outline-hidden"
|
||||
placeholder={$i18n.t('Sort by')}
|
||||
/>
|
||||
<ChevronDown className="size-3.5" strokeWidth="2.5" />
|
||||
</Select.Trigger>
|
||||
|
||||
<Select.Content
|
||||
class="rounded-2xl min-w-[170px] p-1 border border-gray-100 dark:border-gray-800 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-lg"
|
||||
sameWidth={false}
|
||||
align="end"
|
||||
>
|
||||
{#each sortItems as item}
|
||||
<Select.Item
|
||||
class="flex gap-2 items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl"
|
||||
value={item.value}
|
||||
label={item.label}
|
||||
>
|
||||
{item.label}
|
||||
|
||||
{#if sortBy === item.value}
|
||||
<div class="ml-auto">
|
||||
<Check />
|
||||
</div>
|
||||
{/if}
|
||||
</Select.Item>
|
||||
{/each}
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
</div>
|
||||
|
||||
{#if filteredGroups.length !== 0}
|
||||
<div class="my-2 px-3 grid grid-cols-1 gap-1">
|
||||
{#each filteredGroups as group}
|
||||
<GroupItem {group} {setGroups} {defaultPermissions} />
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="w-full h-full flex flex-col justify-center items-center my-16 mb-24">
|
||||
<div class="max-w-md text-center">
|
||||
<div class="text-3xl mb-3">👥</div>
|
||||
<div class="text-lg font-medium mb-1">{$i18n.t('No groups found')}</div>
|
||||
<div class="text-gray-500 text-center text-xs">
|
||||
{$i18n.t('Use groups to organize your users and assign permissions.')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<hr class="mb-2 border-gray-100/30 dark:border-gray-850/30" />
|
||||
|
||||
<EditGroupModal
|
||||
bind:show={showDefaultPermissionsModal}
|
||||
tabs={['permissions']}
|
||||
bind:permissions={defaultPermissions}
|
||||
custom={false}
|
||||
onSubmit={updateDefaultPermissionsHandler}
|
||||
/>
|
||||
|
||||
<button
|
||||
class="flex items-center justify-between rounded-lg w-full transition pt-1"
|
||||
on:click={() => {
|
||||
showDefaultPermissionsModal = true;
|
||||
}}
|
||||
>
|
||||
<div class="flex items-center gap-2.5">
|
||||
<div class="p-1.5 bg-black/5 dark:bg-white/10 rounded-full">
|
||||
<UsersSolid className="size-4" />
|
||||
</div>
|
||||
|
||||
<div class="text-left">
|
||||
<div class=" text-sm font-medium">{$i18n.t('Default permissions')}</div>
|
||||
|
||||
<div class="flex text-xs mt-0.5">
|
||||
{$i18n.t('applies to all users with the "user" role')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{#if filteredGroups.length === 0}
|
||||
<div class="flex flex-col items-center justify-center h-40">
|
||||
<div class=" text-xl font-medium">
|
||||
{$i18n.t('Organize your users')}
|
||||
</div>
|
||||
|
||||
<div class="mt-1 text-sm dark:text-gray-300">
|
||||
{$i18n.t('Use groups to group your users and assign permissions.')}
|
||||
</div>
|
||||
|
||||
<div class="mt-3">
|
||||
<button
|
||||
class=" px-4 py-1.5 text-sm rounded-full bg-black hover:bg-gray-800 text-white dark:bg-white dark:text-black dark:hover:bg-gray-100 transition font-medium flex items-center space-x-1"
|
||||
aria-label={$i18n.t('Create Group')}
|
||||
on:click={() => {
|
||||
showAddGroupModal = true;
|
||||
}}
|
||||
>
|
||||
{$i18n.t('Create Group')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div>
|
||||
<div class=" flex items-center gap-3 justify-between text-xs uppercase px-1 font-medium">
|
||||
<button
|
||||
class="w-full basis-3/5 flex items-center gap-1 hover:text-gray-900 dark:hover:text-gray-100 transition cursor-pointer"
|
||||
on:click={() => setSortKey('name')}
|
||||
>
|
||||
{$i18n.t('Group')}
|
||||
{#if orderBy === 'name'}
|
||||
{#if direction === 'asc'}
|
||||
<ChevronUp className="size-3" strokeWidth="2.5" />
|
||||
{:else}
|
||||
<ChevronDown className="size-3" strokeWidth="2.5" />
|
||||
{/if}
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="w-full basis-2/5 flex items-center gap-1 justify-end hover:text-gray-900 dark:hover:text-gray-100 transition cursor-pointer"
|
||||
on:click={() => setSortKey('members')}
|
||||
>
|
||||
{$i18n.t('Users')}
|
||||
{#if orderBy === 'members'}
|
||||
{#if direction === 'asc'}
|
||||
<ChevronUp className="size-3" strokeWidth="2.5" />
|
||||
{:else}
|
||||
<ChevronDown className="size-3" strokeWidth="2.5" />
|
||||
{/if}
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<hr class="mt-1.5 border-gray-100/30 dark:border-gray-850/30" />
|
||||
|
||||
{#each filteredGroups as group}
|
||||
<div class="my-2">
|
||||
<GroupItem {group} {setGroups} {defaultPermissions} />
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<hr class="mb-2 border-gray-100/30 dark:border-gray-850/30" />
|
||||
|
||||
<EditGroupModal
|
||||
bind:show={showDefaultPermissionsModal}
|
||||
tabs={['permissions']}
|
||||
bind:permissions={defaultPermissions}
|
||||
custom={false}
|
||||
onSubmit={updateDefaultPermissionsHandler}
|
||||
/>
|
||||
|
||||
<button
|
||||
class="flex items-center justify-between rounded-lg w-full transition pt-1"
|
||||
on:click={() => {
|
||||
showDefaultPermissionsModal = true;
|
||||
}}
|
||||
>
|
||||
<div class="flex items-center gap-2.5">
|
||||
<div class="p-1.5 bg-black/5 dark:bg-white/10 rounded-full">
|
||||
<UsersSolid className="size-4" />
|
||||
</div>
|
||||
|
||||
<div class="text-left">
|
||||
<div class=" text-sm font-medium">{$i18n.t('Default permissions')}</div>
|
||||
|
||||
<div class="flex text-xs mt-0.5">
|
||||
{$i18n.t('applies to all users with the "user" role')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<ChevronRight strokeWidth="2.5" />
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<ChevronRight strokeWidth="2.5" />
|
||||
</div>
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
import { deleteGroupById, updateGroupById } from '$lib/apis/groups';
|
||||
|
||||
import Pencil from '$lib/components/icons/Pencil.svelte';
|
||||
import User from '$lib/components/icons/User.svelte';
|
||||
import EditGroupModal from './EditGroupModal.svelte';
|
||||
|
||||
export let group = {
|
||||
@@ -63,26 +62,34 @@
|
||||
/>
|
||||
|
||||
<button
|
||||
class="flex items-center gap-3 justify-between px-1 text-xs w-full transition"
|
||||
class="flex space-x-4 cursor-pointer text-left w-full px-3.5 py-2.5 dark:hover:bg-gray-850/50 hover:bg-gray-50 transition rounded-2xl"
|
||||
on:click={() => {
|
||||
showEdit = true;
|
||||
}}
|
||||
>
|
||||
<div class="flex items-center gap-1.5 w-full font-medium flex-1">
|
||||
<div class="line-clamp-1">
|
||||
{group.name}
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="text-sm font-medium line-clamp-1">{group.name}</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-1.5 w-fit font-medium text-right justify-end">
|
||||
{group?.member_count}
|
||||
<div class="flex items-center gap-2 mt-0.5 line-clamp-1">
|
||||
<div class="text-xs text-gray-500 shrink-0">
|
||||
{$i18n.t('{{COUNT}} members', { COUNT: group?.member_count ?? 0 })}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<User className="size-3.5" />
|
||||
</div>
|
||||
{#if group?.description}
|
||||
<div class="text-xs text-gray-500 line-clamp-1">
|
||||
{group.description}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class=" rounded-lg p-1 hover:bg-gray-100 dark:hover:bg-gray-850 transition">
|
||||
<Pencil className="size-3.5" />
|
||||
<div class="flex self-center ml-2">
|
||||
<Pencil className="size-3.5" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user