Browse Source

enh: update channel

Timothy Jaeryang Baek 6 months ago
parent
commit
7ad8918cd9

+ 25 - 0
backend/open_webui/routers/channels.py

@@ -78,6 +78,31 @@ async def get_channel_by_id(id: str, user=Depends(get_verified_user)):
     return ChannelModel(**channel.model_dump())
 
 
+############################
+# UpdateChannelById
+############################
+
+
+@router.post("/{id}/update", response_model=Optional[ChannelModel])
+async def update_channel_by_id(
+    id: str, form_data: ChannelForm, user=Depends(get_admin_user)
+):
+    channel = Channels.get_channel_by_id(id)
+    if not channel:
+        raise HTTPException(
+            status_code=status.HTTP_404_NOT_FOUND, detail=ERROR_MESSAGES.NOT_FOUND
+        )
+
+    try:
+        channel = Channels.update_channel_by_id(id, form_data)
+        return ChannelModel(**channel.model_dump())
+    except Exception as e:
+        log.exception(e)
+        raise HTTPException(
+            status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT()
+        )
+
+
 ############################
 # GetChannelMessages
 ############################

+ 32 - 0
src/lib/apis/channels/index.ts

@@ -102,6 +102,38 @@ export const getChannelById = async (token: string = '', channel_id: string) =>
 	return res;
 }
 
+export const updateChannelById = async (token: string = '', channel_id: string, channel: ChannelForm) => {
+	let error = null;
+
+	const res = await fetch(`${WEBUI_API_BASE_URL}/channels/${channel_id}/update`, {
+		method: 'POST',
+		headers: {
+			Accept: 'application/json',
+			'Content-Type': 'application/json',
+			authorization: `Bearer ${token}`
+		},
+		body: JSON.stringify({ ...channel })
+	})
+		.then(async (res) => {
+			if (!res.ok) throw await res.json();
+			return res.json();
+		})
+		.then((json) => {
+			return json;
+		})
+		.catch((err) => {
+			error = err.detail;
+			console.log(err);
+			return null;
+		});
+
+	if (error) {
+		throw error;
+	}
+
+	return res;
+}
+
 
 export const getChannelMessages = async (token: string = '', channel_id: string, page: number = 1) => {
 	let error = null;

+ 2 - 2
src/lib/components/channel/Messages.svelte

@@ -65,7 +65,7 @@
 			{($settings?.widescreenMode ?? null) ? 'max-w-full' : 'max-w-5xl'} mx-auto"
 			>
 				{#if channel}
-					<div class="flex flex-col py-1 gap-1.5 py-5">
+					<div class="flex flex-col gap-1.5 py-5">
 						<div class="text-2xl font-medium capitalize">{channel.name}</div>
 
 						<div class=" text-gray-500">
@@ -76,7 +76,7 @@
 						</div>
 					</div>
 				{:else}
-					<div class="flex justify-center py-1 text-xs items-center gap-2 py-5">
+					<div class="flex justify-center text-xs items-center gap-2 py-5">
 						<div class=" ">Start of the channel</div>
 					</div>
 				{/if}

+ 1 - 1
src/lib/components/channel/Messages/Message.svelte

@@ -70,7 +70,7 @@
 
 						{#if message.created_at}
 							<span
-								class=" self-center invisible group-hover:visible text-gray-400 text-xs font-medium capitalize ml-0.5 -mt-0.5"
+								class=" self-center invisible group-hover:visible text-gray-400 text-xs font-medium first-letter:capitalize ml-0.5 -mt-0.5"
 							>
 								{formatDate(message.created_at / 1000000)}
 							</span>

+ 7 - 4
src/lib/components/common/ConfirmDialog.svelte

@@ -37,7 +37,6 @@
 
 	const confirmHandler = async () => {
 		show = false;
-
 		await onConfirm();
 		dispatch('confirm', inputValue);
 	};
@@ -47,11 +46,15 @@
 	});
 
 	$: if (mounted) {
-		if (show) {
+		if (show && modalElement) {
+			document.body.appendChild(modalElement);
+
 			window.addEventListener('keydown', handleKeyDown);
 			document.body.style.overflow = 'hidden';
-		} else {
+		} else if (modalElement) {
 			window.removeEventListener('keydown', handleKeyDown);
+			document.body.removeChild(modalElement);
+
 			document.body.style.overflow = 'unset';
 		}
 	}
@@ -62,7 +65,7 @@
 	<!-- svelte-ignore a11y-no-static-element-interactions -->
 	<div
 		bind:this={modalElement}
-		class=" fixed top-0 right-0 left-0 bottom-0 bg-black/60 w-full h-screen max-h-[100dvh] flex justify-center z-[99999] overflow-hidden overscroll-contain"
+		class=" fixed top-0 right-0 left-0 bottom-0 bg-black/60 w-full h-screen max-h-[100dvh] flex justify-center z-[99999999] overflow-hidden overscroll-contain"
 		in:fade={{ duration: 10 }}
 		on:mousedown={() => {
 			show = false;

+ 12 - 0
src/lib/components/icons/Cog6Solid.svelte

@@ -0,0 +1,12 @@
+<script lang="ts">
+	export let className = 'w-4 h-4';
+	export let strokeWidth = '1.5';
+</script>
+
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class={className}>
+	<path
+		fill-rule="evenodd"
+		d="M6.455 1.45A.5.5 0 0 1 6.952 1h2.096a.5.5 0 0 1 .497.45l.186 1.858a4.996 4.996 0 0 1 1.466.848l1.703-.769a.5.5 0 0 1 .639.206l1.047 1.814a.5.5 0 0 1-.14.656l-1.517 1.09a5.026 5.026 0 0 1 0 1.694l1.516 1.09a.5.5 0 0 1 .141.656l-1.047 1.814a.5.5 0 0 1-.639.206l-1.703-.768c-.433.36-.928.649-1.466.847l-.186 1.858a.5.5 0 0 1-.497.45H6.952a.5.5 0 0 1-.497-.45l-.186-1.858a4.993 4.993 0 0 1-1.466-.848l-1.703.769a.5.5 0 0 1-.639-.206l-1.047-1.814a.5.5 0 0 1 .14-.656l1.517-1.09a5.033 5.033 0 0 1 0-1.694l-1.516-1.09a.5.5 0 0 1-.141-.656L2.46 3.593a.5.5 0 0 1 .639-.206l1.703.769c.433-.36.928-.65 1.466-.848l.186-1.858Zm-.177 7.567-.022-.037a2 2 0 0 1 3.466-1.997l.022.037a2 2 0 0 1-3.466 1.997Z"
+		clip-rule="evenodd"
+	/>
+</svg>

+ 21 - 5
src/lib/components/layout/Sidebar.svelte

@@ -53,7 +53,7 @@
 	import Tooltip from '../common/Tooltip.svelte';
 	import Folders from './Sidebar/Folders.svelte';
 	import { getChannels, createNewChannel } from '$lib/apis/channels';
-	import CreateChannelModal from './Sidebar/CreateChannelModal.svelte';
+	import ChannelModal from './Sidebar/ChannelModal.svelte';
 	import ChannelItem from './Sidebar/ChannelItem.svelte';
 	import PencilSquare from '../icons/PencilSquare.svelte';
 
@@ -403,10 +403,21 @@
 	}}
 />
 
-<CreateChannelModal
+<ChannelModal
 	bind:show={showCreateChannel}
-	onChange={async () => {
-		await initChannels();
+	onSubmit={async ({ name, access_control }) => {
+		const res = await createNewChannel(localStorage.token, {
+			name: name,
+			access_control: access_control
+		}).catch((error) => {
+			toast.error(error);
+			return null;
+		});
+
+		if (res) {
+			await initChannels();
+			showCreateChannel = false;
+		}
 	}}
 />
 
@@ -642,7 +653,12 @@
 					onAddLabel={$i18n.t('Create Channel')}
 				>
 					{#each $channels as channel}
-						<ChannelItem id={channel.id} name={channel.name} />
+						<ChannelItem
+							{channel}
+							onUpdate={async () => {
+								await initChannels();
+							}}
+						/>
 					{/each}
 				</Folder>
 			{/if}

+ 35 - 11
src/lib/components/layout/Sidebar/ChannelItem.svelte

@@ -1,33 +1,55 @@
 <script lang="ts">
 	import { toast } from 'svelte-sonner';
-	import { onMount, getContext, createEventDispatcher, tick, onDestroy } from 'svelte';
+	import { onMount, getContext, tick, onDestroy } from 'svelte';
 	const i18n = getContext('i18n');
 
-	const dispatch = createEventDispatcher();
-
 	import { page } from '$app/stores';
-
 	import { mobile, showSidebar, user } from '$lib/stores';
-	import EllipsisHorizontal from '$lib/components/icons/EllipsisHorizontal.svelte';
+	import { updateChannelById } from '$lib/apis/channels';
+
+	import Cog6 from '$lib/components/icons/Cog6.svelte';
+	import ChannelModal from './ChannelModal.svelte';
+
+	export let onUpdate: Function = () => {};
 
 	export let className = '';
+	export let channel;
 
-	export let id;
-	export let name;
+	let showEditChannelModal = false;
 
 	let itemElement;
 </script>
 
+<ChannelModal
+	bind:show={showEditChannelModal}
+	{channel}
+	edit={true}
+	onSubmit={async ({ name, access_control }) => {
+		const res = await updateChannelById(localStorage.token, channel.id, {
+			name,
+			access_control
+		}).catch((error) => {
+			toast.error(error.message);
+		});
+
+		if (res) {
+			toast.success('Channel updated successfully');
+		}
+
+		onUpdate();
+	}}
+/>
+
 <div
 	bind:this={itemElement}
 	class=" w-full {className} rounded-lg flex relative group hover:bg-gray-100 dark:hover:bg-gray-900 {$page
-		.url.pathname === `/channels/${id}`
+		.url.pathname === `/channels/${channel.id}`
 		? 'bg-gray-100 dark:bg-gray-900'
 		: ''} px-2.5 py-1"
 >
 	<a
 		class=" w-full flex justify-between"
-		href="/channels/{id}"
+		href="/channels/{channel.id}"
 		on:click={() => {
 			if ($mobile) {
 				showSidebar.set(false);
@@ -50,7 +72,7 @@
 			</svg>
 
 			<div class=" text-left self-center overflow-hidden w-full line-clamp-1">
-				{name}
+				{channel.name}
 			</div>
 		</div>
 	</a>
@@ -60,10 +82,12 @@
 			class="absolute z-10 right-2 invisible group-hover:visible self-center flex items-center dark:text-gray-300"
 			on:pointerup={(e) => {
 				e.stopPropagation();
+
+				showEditChannelModal = true;
 			}}
 		>
 			<button class="p-0.5 dark:hover:bg-gray-850 rounded-lg touch-auto" on:click={(e) => {}}>
-				<EllipsisHorizontal className="size-4" strokeWidth="2.5" />
+				<Cog6 className="size-3.5" />
 			</button>
 		</button>
 	{/if}

+ 57 - 12
src/lib/components/layout/Sidebar/CreateChannelModal.svelte → src/lib/components/layout/Sidebar/ChannelModal.svelte

@@ -1,15 +1,19 @@
 <script lang="ts">
-	import { getContext, createEventDispatcher } from 'svelte';
-
+	import { getContext, createEventDispatcher, onMount } from 'svelte';
 	import { createNewChannel } from '$lib/apis/channels';
 
 	import Modal from '$lib/components/common/Modal.svelte';
 	import AccessControl from '$lib/components/workspace/common/AccessControl.svelte';
+	import DeleteConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
+
 	import { toast } from 'svelte-sonner';
 	const i18n = getContext('i18n');
 
 	export let show = false;
-	export let onChange: Function = () => {};
+	export let onSubmit: Function = () => {};
+
+	export let channel = null;
+	export let edit = false;
 
 	let name = '';
 	let accessControl = null;
@@ -22,25 +26,41 @@
 
 	const submitHandler = async () => {
 		loading = true;
-		const res = await createNewChannel(localStorage.token, {
+		await onSubmit({
 			name: name.replace(/\s/g, '-'),
 			access_control: accessControl
-		}).catch((error) => {
-			toast.error(error);
-			return null;
 		});
-
-		onChange();
 		show = false;
-
 		loading = false;
 	};
+
+	const init = () => {
+		name = channel.name;
+		accessControl = channel.access_control;
+	};
+
+	$: if (channel) {
+		init();
+	}
+
+	let showDeleteConfirmDialog = false;
+
+	const deleteHandler = async () => {
+		showDeleteConfirmDialog = false;
+		show = false;
+	};
 </script>
 
 <Modal size="sm" bind:show>
 	<div>
 		<div class=" flex justify-between dark:text-gray-300 px-5 pt-4 pb-1">
-			<div class=" text-lg font-medium self-center">{$i18n.t('Create Channel')}</div>
+			<div class=" text-lg font-medium self-center">
+				{#if edit}
+					{$i18n.t('Edit Channel')}
+				{:else}
+					{$i18n.t('Create Channel')}
+				{/if}
+			</div>
 			<button
 				class="self-center"
 				on:click={() => {
@@ -93,6 +113,18 @@
 					</div>
 
 					<div class="flex justify-end pt-3 text-sm font-medium gap-1.5">
+						{#if edit}
+							<button
+								class="px-3.5 py-1.5 text-sm font-medium dark:bg-black dark:hover:bg-black/90 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;
+								}}
+							>
+								{$i18n.t('Delete')}
+							</button>
+						{/if}
+
 						<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'
@@ -100,7 +132,11 @@
 							type="submit"
 							disabled={loading}
 						>
-							{$i18n.t('Create')}
+							{#if edit}
+								{$i18n.t('Update')}
+							{:else}
+								{$i18n.t('Create')}
+							{/if}
 
 							{#if loading}
 								<div class="ml-2 self-center">
@@ -136,3 +172,12 @@
 		</div>
 	</div>
 </Modal>
+
+<DeleteConfirmDialog
+	bind:show={showDeleteConfirmDialog}
+	message={$i18n.t('Are you sure you want to delete this channel?')}
+	confirmLabel={$i18n.t('Delete')}
+	on:confirm={() => {
+		deleteHandler();
+	}}
+/>