feat/enh: add/remove users from group channel
This commit is contained in:
@@ -194,6 +194,88 @@ export const updateChannelMemberActiveStatusById = async (
|
||||
return res;
|
||||
};
|
||||
|
||||
type UpdateMembersForm = {
|
||||
user_ids?: string[];
|
||||
group_ids?: string[];
|
||||
};
|
||||
|
||||
export const addMembersById = async (
|
||||
token: string = '',
|
||||
channel_id: string,
|
||||
formData: UpdateMembersForm
|
||||
) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/channels/${channel_id}/update/members/add`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
authorization: `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({ ...formData })
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.then((json) => {
|
||||
return json;
|
||||
})
|
||||
.catch((err) => {
|
||||
error = err.detail;
|
||||
console.error(err);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
type RemoveMembersForm = {
|
||||
user_ids?: string[];
|
||||
group_ids?: string[];
|
||||
};
|
||||
|
||||
export const removeMembersById = async (
|
||||
token: string = '',
|
||||
channel_id: string,
|
||||
formData: RemoveMembersForm
|
||||
) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/channels/${channel_id}/update/members/remove`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
authorization: `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({ ...formData })
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
})
|
||||
.then((json) => {
|
||||
return json;
|
||||
})
|
||||
.catch((err) => {
|
||||
error = err.detail;
|
||||
console.error(err);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export const updateChannelById = async (
|
||||
token: string = '',
|
||||
channel_id: string,
|
||||
|
||||
@@ -309,6 +309,11 @@
|
||||
return message;
|
||||
});
|
||||
}}
|
||||
onUpdate={async () => {
|
||||
channel = await getChannelById(localStorage.token, id).catch((error) => {
|
||||
return null;
|
||||
});
|
||||
}}
|
||||
/>
|
||||
|
||||
{#if channel && messages !== null}
|
||||
|
||||
@@ -3,22 +3,41 @@
|
||||
import { getContext, onMount } from 'svelte';
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
import { removeMembersById } from '$lib/apis/channels';
|
||||
|
||||
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||
import Modal from '$lib/components/common/Modal.svelte';
|
||||
|
||||
import UserPlusSolid from '$lib/components/icons/UserPlusSolid.svelte';
|
||||
import WrenchSolid from '$lib/components/icons/WrenchSolid.svelte';
|
||||
import ConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
|
||||
import XMark from '$lib/components/icons/XMark.svelte';
|
||||
import Hashtag from '../icons/Hashtag.svelte';
|
||||
import Lock from '../icons/Lock.svelte';
|
||||
import UserList from './ChannelInfoModal/UserList.svelte';
|
||||
import AddMembersModal from './ChannelInfoModal/AddMembersModal.svelte';
|
||||
|
||||
export let show = false;
|
||||
export let channel = null;
|
||||
|
||||
export let onUpdate = () => {};
|
||||
|
||||
let showAddMembersModal = false;
|
||||
const submitHandler = async () => {};
|
||||
|
||||
const removeMemberHandler = async (userId) => {
|
||||
const res = await removeMembersById(localStorage.token, channel.id, {
|
||||
user_ids: [userId]
|
||||
}).catch((error) => {
|
||||
toast.error(`${error}`);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (res) {
|
||||
toast.success($i18n.t('Member removed successfully'));
|
||||
onUpdate();
|
||||
} else {
|
||||
toast.error($i18n.t('Failed to remove member'));
|
||||
}
|
||||
};
|
||||
|
||||
const init = () => {};
|
||||
|
||||
$: if (show) {
|
||||
@@ -31,15 +50,14 @@
|
||||
</script>
|
||||
|
||||
{#if channel}
|
||||
<AddMembersModal bind:show={showAddMembersModal} {channel} {onUpdate} />
|
||||
<Modal size="sm" bind:show>
|
||||
<div>
|
||||
<div class=" flex justify-between dark:text-gray-100 px-5 pt-4 mb-1.5">
|
||||
<div class="self-center text-base">
|
||||
<div class="flex items-center gap-0.5 shrink-0">
|
||||
{#if channel?.type === 'dm'}
|
||||
<div
|
||||
class=" text-left self-center overflow-hidden w-full line-clamp-1 capitalize flex-1"
|
||||
>
|
||||
<div class=" text-left self-center overflow-hidden w-full line-clamp-1 flex-1">
|
||||
{$i18n.t('Direct Message')}
|
||||
</div>
|
||||
{:else}
|
||||
@@ -51,9 +69,7 @@
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div
|
||||
class=" text-left self-center overflow-hidden w-full line-clamp-1 capitalize flex-1"
|
||||
>
|
||||
<div class=" text-left self-center overflow-hidden w-full line-clamp-1 flex-1">
|
||||
{channel.name}
|
||||
</div>
|
||||
{/if}
|
||||
@@ -69,7 +85,7 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col md:flex-row w-full px-4 pb-4 md:space-x-4 dark:text-gray-200">
|
||||
<div class="flex flex-col md:flex-row w-full px-3 pb-4 md:space-x-4 dark:text-gray-200">
|
||||
<div class=" flex flex-col w-full sm:flex-row sm:justify-center sm:space-x-6">
|
||||
<form
|
||||
class="flex flex-col w-full"
|
||||
@@ -79,7 +95,21 @@
|
||||
}}
|
||||
>
|
||||
<div class="flex flex-col w-full h-full pb-2">
|
||||
<UserList {channel} search={channel?.type !== 'dm'} sort={channel?.type !== 'dm'} />
|
||||
<UserList
|
||||
{channel}
|
||||
onAdd={channel?.type === 'group' && channel?.is_manager
|
||||
? () => {
|
||||
showAddMembersModal = true;
|
||||
}
|
||||
: null}
|
||||
onRemove={channel?.type === 'group' && channel?.is_manager
|
||||
? (userId) => {
|
||||
removeMemberHandler(userId);
|
||||
}
|
||||
: null}
|
||||
search={channel?.type !== 'dm'}
|
||||
sort={channel?.type !== 'dm'}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
<script lang="ts">
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { getContext, onMount } from 'svelte';
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
import { addMembersById } from '$lib/apis/channels';
|
||||
|
||||
import Modal from '$lib/components/common/Modal.svelte';
|
||||
import XMark from '$lib/components/icons/XMark.svelte';
|
||||
import MemberSelector from '$lib/components/workspace/common/MemberSelector.svelte';
|
||||
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||
|
||||
export let show = false;
|
||||
export let channel = null;
|
||||
|
||||
export let onUpdate = () => {};
|
||||
|
||||
let groupIds = [];
|
||||
let userIds = [];
|
||||
|
||||
let loading = false;
|
||||
|
||||
const submitHandler = async () => {
|
||||
const res = await addMembersById(localStorage.token, channel.id, {
|
||||
user_ids: userIds,
|
||||
group_ids: groupIds
|
||||
}).catch((error) => {
|
||||
toast.error(`${error}`);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (res) {
|
||||
toast.success($i18n.t('Members added successfully'));
|
||||
onUpdate();
|
||||
show = false;
|
||||
} else {
|
||||
toast.error($i18n.t('Failed to add members'));
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
{#if channel}
|
||||
<Modal size="sm" bind:show>
|
||||
<div>
|
||||
<div class=" flex justify-between dark:text-gray-100 px-5 pt-4 mb-1.5">
|
||||
<div class="self-center text-base">
|
||||
<div class="flex items-center gap-0.5 shrink-0">
|
||||
{$i18n.t('Add Members')}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
class="self-center"
|
||||
on:click={() => {
|
||||
show = false;
|
||||
}}
|
||||
>
|
||||
<XMark className={'size-5'} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col md:flex-row w-full px-3 pb-4 md:space-x-4 dark:text-gray-200">
|
||||
<div class=" flex flex-col w-full sm:flex-row sm:justify-center sm:space-x-6">
|
||||
<form
|
||||
class="flex flex-col w-full"
|
||||
on:submit={(e) => {
|
||||
e.preventDefault();
|
||||
submitHandler();
|
||||
}}
|
||||
>
|
||||
<div class="flex flex-col w-full h-full pb-2">
|
||||
<MemberSelector bind:userIds bind:groupIds includeGroups={true} />
|
||||
</div>
|
||||
|
||||
<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 bg-black hover:bg-gray-950 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('Add')}
|
||||
|
||||
{#if loading}
|
||||
<div class="ml-2 self-center">
|
||||
<Spinner />
|
||||
</div>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
{/if}
|
||||
@@ -1,6 +1,6 @@
|
||||
<script>
|
||||
import { WEBUI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants';
|
||||
import { WEBUI_NAME, config, user, showSidebar } from '$lib/stores';
|
||||
import { WEBUI_NAME, config, user as _user, showSidebar } from '$lib/stores';
|
||||
import { goto } from '$app/navigation';
|
||||
import { onMount, getContext } from 'svelte';
|
||||
|
||||
@@ -14,29 +14,21 @@
|
||||
import { getChannelMembersById } from '$lib/apis/channels';
|
||||
|
||||
import Pagination from '$lib/components/common/Pagination.svelte';
|
||||
import ChatBubbles from '$lib/components/icons/ChatBubbles.svelte';
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
|
||||
import EditUserModal from '$lib/components/admin/Users/UserList/EditUserModal.svelte';
|
||||
import UserChatsModal from '$lib/components/admin/Users/UserList/UserChatsModal.svelte';
|
||||
import AddUserModal from '$lib/components/admin/Users/UserList/AddUserModal.svelte';
|
||||
|
||||
import ConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
|
||||
import RoleUpdateConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
|
||||
|
||||
import Badge from '$lib/components/common/Badge.svelte';
|
||||
import Plus from '$lib/components/icons/Plus.svelte';
|
||||
import ChevronUp from '$lib/components/icons/ChevronUp.svelte';
|
||||
import ChevronDown from '$lib/components/icons/ChevronDown.svelte';
|
||||
import About from '$lib/components/chat/Settings/About.svelte';
|
||||
import Banner from '$lib/components/common/Banner.svelte';
|
||||
import Markdown from '$lib/components/chat/Messages/Markdown.svelte';
|
||||
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||
import ProfilePreview from '../Messages/Message/ProfilePreview.svelte';
|
||||
import XMark from '$lib/components/icons/XMark.svelte';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
export let channel = null;
|
||||
|
||||
export let onAdd = null;
|
||||
export let onRemove = null;
|
||||
|
||||
export let search = true;
|
||||
export let sort = true;
|
||||
|
||||
@@ -85,7 +77,13 @@
|
||||
}
|
||||
};
|
||||
|
||||
$: if (page !== null && query !== null && orderBy !== null && direction !== null) {
|
||||
$: if (
|
||||
channel !== null &&
|
||||
page !== null &&
|
||||
query !== null &&
|
||||
orderBy !== null &&
|
||||
direction !== null
|
||||
) {
|
||||
getUserList();
|
||||
}
|
||||
</script>
|
||||
@@ -96,10 +94,33 @@
|
||||
<Spinner className="size-5" />
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex items-center justify-between px-2 mb-1">
|
||||
<div class="flex gap-1 items-center">
|
||||
<span class="text-sm">
|
||||
{$i18n.t('Members')}
|
||||
</span>
|
||||
<span class="text-sm text-gray-500">{total}</span>
|
||||
</div>
|
||||
|
||||
{#if onAdd}
|
||||
<div class="">
|
||||
<button
|
||||
type="button"
|
||||
class=" px-3 py-1.5 gap-1 rounded-xl bg-black dark:text-white dark:bg-gray-850/50 text-black transition font-medium text-xs flex items-center justify-center"
|
||||
on:click={onAdd}
|
||||
>
|
||||
<Plus className="size-3.5 " />
|
||||
<span>{$i18n.t('Add Member')}</span>
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<!-- <hr class="my-1 border-gray-100/5- dark:border-gray-850/50" /> -->
|
||||
|
||||
{#if search}
|
||||
<div class="flex gap-1 px-0.5">
|
||||
<div class="flex gap-1 px-1 mb-1">
|
||||
<div class=" flex w-full space-x-2">
|
||||
<div class="flex flex-1">
|
||||
<div class="flex flex-1 items-center">
|
||||
<div class=" self-center ml-1 mr-3">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
@@ -127,7 +148,7 @@
|
||||
{#if users.length > 0}
|
||||
<div class="scrollbar-hidden relative whitespace-nowrap w-full max-w-full">
|
||||
<div class=" text-sm text-left text-gray-500 dark:text-gray-400 w-full max-w-full">
|
||||
<div
|
||||
<!-- <div
|
||||
class="text-xs text-gray-800 uppercase bg-transparent dark:text-gray-200 w-full mb-0.5"
|
||||
>
|
||||
<div
|
||||
@@ -181,11 +202,11 @@
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
<div class="w-full">
|
||||
{#each users as user, userIdx (user.id)}
|
||||
<div class=" dark:border-gray-850 text-xs flex items-center justify-between">
|
||||
<div class="px-3 py-1.5 font-medium text-gray-900 dark:text-white flex-1">
|
||||
<div class="px-2 py-1.5 font-medium text-gray-900 dark:text-white flex-1">
|
||||
<div class="flex items-center gap-2">
|
||||
<ProfilePreview {user} side="right" align="center" sideOffset={6}>
|
||||
<img
|
||||
@@ -212,8 +233,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="px-3 py-1">
|
||||
<div class=" translate-y-0.5">
|
||||
<div class="px-2 py-1 flex items-center gap-1 translate-y-0.5">
|
||||
<div class=" ">
|
||||
<Badge
|
||||
type={user.role === 'admin'
|
||||
? 'info'
|
||||
@@ -223,6 +244,21 @@
|
||||
content={$i18n.t(user.role)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{#if onRemove}
|
||||
<div>
|
||||
<button
|
||||
class=" rounded-full p-1 hover:bg-gray-100 dark:hover:bg-gray-850 transition disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
type="button"
|
||||
disabled={user.id === $_user?.id}
|
||||
on:click={() => {
|
||||
onRemove(user.id);
|
||||
}}
|
||||
>
|
||||
<XMark />
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
@@ -29,10 +29,11 @@
|
||||
export let channel;
|
||||
|
||||
export let onPin = (messageId, pinned) => {};
|
||||
export let onUpdate = () => {};
|
||||
</script>
|
||||
|
||||
<PinnedMessagesModal bind:show={showChannelPinnedMessagesModal} {channel} {onPin} />
|
||||
<ChannelInfoModal bind:show={showChannelInfoModal} {channel} />
|
||||
<ChannelInfoModal bind:show={showChannelInfoModal} {channel} {onUpdate} />
|
||||
<nav class="sticky top-0 z-30 w-full px-1.5 py-1 -mb-8 flex items-center drag-region flex flex-col">
|
||||
<div
|
||||
id="navbar-bg-gradient-to-b"
|
||||
|
||||
@@ -18,8 +18,6 @@
|
||||
import Checkbox from '$lib/components/common/Checkbox.svelte';
|
||||
import { getGroups } from '$lib/apis/groups';
|
||||
|
||||
export let onChange: Function = () => {};
|
||||
|
||||
export let includeGroups = true;
|
||||
export let pagination = false;
|
||||
|
||||
@@ -33,6 +31,7 @@
|
||||
? groups.filter((group) => group.name.toLowerCase().includes(query.toLowerCase()))
|
||||
: [];
|
||||
|
||||
let selectedGroup = {};
|
||||
let selectedUsers = {};
|
||||
|
||||
let page = 1;
|
||||
@@ -43,15 +42,6 @@
|
||||
let orderBy = 'name'; // default sort key
|
||||
let direction = 'asc'; // default sort order
|
||||
|
||||
const setSortKey = (key) => {
|
||||
if (orderBy === key) {
|
||||
direction = direction === 'asc' ? 'desc' : 'asc';
|
||||
} else {
|
||||
orderBy = key;
|
||||
direction = 'asc';
|
||||
}
|
||||
};
|
||||
|
||||
const getUserList = async () => {
|
||||
try {
|
||||
const res = await searchUsers(localStorage.token, query, orderBy, direction, page).catch(
|
||||
@@ -96,6 +86,38 @@
|
||||
<Spinner className="size-5" />
|
||||
</div>
|
||||
{:else}
|
||||
{#if groupIds.length > 0}
|
||||
<div class="mx-1 mb-1.5">
|
||||
<div class="text-xs text-gray-500 mx-0.5 mb-1">
|
||||
{groupIds.length}
|
||||
{$i18n.t('groups')}
|
||||
</div>
|
||||
<div class="flex gap-1 flex-wrap">
|
||||
{#each groupIds as id}
|
||||
{#if selectedGroup[id]}
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center space-x-1 px-2 py-1 bg-gray-100/50 dark:bg-gray-850 rounded-lg text-xs"
|
||||
on:click={() => {
|
||||
groupIds = groupIds.filter((gid) => gid !== id);
|
||||
delete selectedGroup[id];
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
{selectedGroup[id].name}
|
||||
<span class="text-xs text-gray-500">{selectedGroup[id].member_count}</span>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<XMark className="size-3" />
|
||||
</div>
|
||||
</button>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if userIds.length > 0}
|
||||
<div class="mx-1 mb-1.5">
|
||||
<div class="text-xs text-gray-500 mx-0.5 mb-1">
|
||||
@@ -127,7 +149,7 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="flex gap-1 -mx-0.5 my-1.5">
|
||||
<div class="flex gap-1 mb-1">
|
||||
<div class=" flex w-full space-x-2">
|
||||
<div class="flex flex-1">
|
||||
<div class=" self-center ml-1 mr-3">
|
||||
@@ -170,10 +192,11 @@
|
||||
on:click={() => {
|
||||
if ((groupIds ?? []).includes(group.id)) {
|
||||
groupIds = groupIds.filter((id) => id !== group.id);
|
||||
delete selectedGroup[group.id];
|
||||
} else {
|
||||
groupIds = [...groupIds, group.id];
|
||||
selectedGroup[group.id] = group;
|
||||
}
|
||||
onChange(groupIds);
|
||||
}}
|
||||
>
|
||||
<div class="px-3 py-1.5 font-medium text-gray-900 dark:text-white flex-1">
|
||||
@@ -216,7 +239,6 @@
|
||||
userIds = [...userIds, user.id];
|
||||
selectedUsers[user.id] = user;
|
||||
}
|
||||
onChange(userIds);
|
||||
}}
|
||||
>
|
||||
<div class="px-3 py-1.5 font-medium text-gray-900 dark:text-white flex-1">
|
||||
|
||||
Reference in New Issue
Block a user