forked from open-webui/open-webui
		
	refac
This commit is contained in:
		
							parent
							
								
									8a9cf44dbc
								
							
						
					
					
						commit
						b25e3ed364
					
				
					 5 changed files with 87 additions and 281 deletions
				
			
		
							
								
								
									
										42
									
								
								src/lib/components/common/Pagination.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								src/lib/components/common/Pagination.svelte
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,42 @@ | |||
| <script lang="ts"> | ||||
| 	import { Pagination } from 'bits-ui'; | ||||
| 	import { createEventDispatcher } from 'svelte'; | ||||
| 
 | ||||
| 	import ChevronLeft from '../icons/ChevronLeft.svelte'; | ||||
| 	import ChevronRight from '../icons/ChevronRight.svelte'; | ||||
| 
 | ||||
| 	export let page = 0; | ||||
| 	export let count = 0; | ||||
| 	export let perPage = 20; | ||||
| </script> | ||||
| 
 | ||||
| <div class="flex justify-center"> | ||||
| 	<Pagination.Root bind:page {count} {perPage} let:pages> | ||||
| 		<div class="my-2 flex items-center"> | ||||
| 			<Pagination.PrevButton | ||||
| 				class="mr-[25px] inline-flex size-8 items-center justify-center rounded-[9px] bg-transparent hover:bg-gray-100 dark:hover:bg-gray-800 active:scale-98 disabled:cursor-not-allowed disabled:text-gray-400 dark:disabled:text-gray-700 hover:disabled:bg-transparent dark:hover:disabled:bg-transparent" | ||||
| 			> | ||||
| 				<ChevronLeft className="size-4" strokeWidth="2" /> | ||||
| 			</Pagination.PrevButton> | ||||
| 			<div class="flex items-center gap-2.5"> | ||||
| 				{#each pages as page (page.key)} | ||||
| 					{#if page.type === 'ellipsis'} | ||||
| 						<div class="text-sm font-medium text-foreground-alt">...</div> | ||||
| 					{:else} | ||||
| 						<Pagination.Page | ||||
| 							{page} | ||||
| 							class="inline-flex size-8 items-center justify-center rounded-[9px] bg-transparent hover:bg-gray-100 dark:hover:bg-gray-800 text-sm font-medium hover:bg-dark-10 active:scale-98 disabled:cursor-not-allowed disabled:opacity-50 hover:disabled:bg-transparent data-[selected]:bg-black data-[selected]:text-gray-100 data-[selected]:hover:bg-black dark:data-[selected]:bg-white dark:data-[selected]:text-gray-900 dark:data-[selected]:hover:bg-white" | ||||
| 						> | ||||
| 							{page.value} | ||||
| 						</Pagination.Page> | ||||
| 					{/if} | ||||
| 				{/each} | ||||
| 			</div> | ||||
| 			<Pagination.NextButton | ||||
| 				class="ml-[29px]  inline-flex size-8 items-center justify-center rounded-[9px] bg-transparent hover:bg-gray-100 dark:hover:bg-gray-800 active:scale-98 disabled:cursor-not-allowed disabled:text-gray-400 dark:disabled:text-gray-700 hover:disabled:bg-transparent dark:hover:disabled:bg-transparent" | ||||
| 			> | ||||
| 				<ChevronRight className="size-4" strokeWidth="2" /> | ||||
| 			</Pagination.NextButton> | ||||
| 		</div> | ||||
| 	</Pagination.Root> | ||||
| </div> | ||||
|  | @ -1,254 +0,0 @@ | |||
| <script lang="ts"> | ||||
| 	import { createEventDispatcher } from 'svelte'; | ||||
| 
 | ||||
| 	// Event Dispatcher | ||||
| 	type PaginatorEvent = { | ||||
| 		amount: number; | ||||
| 		page: number; | ||||
| 	}; | ||||
| 	const dispatch = createEventDispatcher<PaginatorEvent>(); | ||||
| 
 | ||||
| 	// Props | ||||
| 	/** Pass the page setting object. */ | ||||
| 	export let settings = { page: 0, limit: 5, size: 0, amounts: [1, 2, 5, 10] }; | ||||
| 	/** Sets selection and buttons to disabled state on-demand. */ | ||||
| 	export let disabled = false; | ||||
| 	/** Show Previous and Next buttons. */ | ||||
| 	export let showPreviousNextButtons = true; | ||||
| 	/** Show First and Last buttons. */ | ||||
| 	export let showFirstLastButtons = false; | ||||
| 	/** Displays a numeric row of page buttons. */ | ||||
| 	export let showNumerals = false; | ||||
| 	/** Maximum number of active page siblings in the numeric row.*/ | ||||
| 	export let maxNumerals = 1; | ||||
| 	/** Provide classes to set flexbox justification. */ | ||||
| 	export let justify: string = 'justify-between'; | ||||
| 
 | ||||
| 	// Props (select) | ||||
| 	/** Set the text for the amount selection input. */ | ||||
| 	export let amountText = 'Items'; | ||||
| 
 | ||||
| 	// Props (buttons) | ||||
| 	/** Provide arbitrary classes to the active page buttons. */ | ||||
| 	export let active: string = 'bg-gray-100 dark:bg-gray-700'; | ||||
| 	/*** Set the base button classes. */ | ||||
| 	export let buttonClasses: string = '!px-3 !py-1.5'; | ||||
| 
 | ||||
| 	/** Set the label for the pages separator. */ | ||||
| 	export let separatorText = 'of'; | ||||
| 
 | ||||
| 	// Base Classes | ||||
| 	const cBase = 'flex flex-col md:flex-row items-center space-y-4 md:space-y-0 md:space-x-4'; | ||||
| 	const cLabel = 'w-full md:w-auto'; | ||||
| 
 | ||||
| 	// Local | ||||
| 	let lastPage = Math.max(0, Math.ceil(settings.size / settings.limit - 1)); | ||||
| 	let controlPages: number[] = getNumerals(); | ||||
| 
 | ||||
| 	function onChangeLength(): void { | ||||
| 		/** @event {{ length: number }} amount - Fires when the amount selection input changes.  */ | ||||
| 		dispatch('amount', settings.limit); | ||||
| 
 | ||||
| 		lastPage = Math.max(0, Math.ceil(settings.size / settings.limit - 1)); | ||||
| 
 | ||||
| 		// ensure page in limit range | ||||
| 		if (settings.page > lastPage) { | ||||
| 			settings.page = lastPage; | ||||
| 		} | ||||
| 
 | ||||
| 		controlPages = getNumerals(); | ||||
| 	} | ||||
| 
 | ||||
| 	function gotoPage(page: number) { | ||||
| 		if (page < 0) return; | ||||
| 
 | ||||
| 		settings.page = page; | ||||
| 		/** @event {{ page: number }} page Fires when the next/back buttons are pressed. */ | ||||
| 		dispatch('page', settings.page); | ||||
| 		controlPages = getNumerals(); | ||||
| 	} | ||||
| 
 | ||||
| 	// Full row - no ellipsis | ||||
| 	function getFullNumerals() { | ||||
| 		const pages = []; | ||||
| 		for (let index = 0; index <= lastPage; index++) { | ||||
| 			pages.push(index); | ||||
| 		} | ||||
| 		return pages; | ||||
| 	} | ||||
| 
 | ||||
| 	function getNumerals() { | ||||
| 		if (lastPage <= maxNumerals * 2 + 1) return getFullNumerals(); | ||||
| 
 | ||||
| 		const pages = []; | ||||
| 		const isWithinLeftSection = settings.page < maxNumerals + 2; | ||||
| 		const isWithinRightSection = settings.page > lastPage - (maxNumerals + 2); | ||||
| 
 | ||||
| 		pages.push(0); | ||||
| 		if (!isWithinLeftSection) pages.push(-1); | ||||
| 
 | ||||
| 		if (isWithinLeftSection || isWithinRightSection) { | ||||
| 			// mid section - with only one ellipsis | ||||
| 			const sectionStart = isWithinLeftSection ? 1 : lastPage - (maxNumerals + 2); | ||||
| 			const sectionEnd = isWithinRightSection ? lastPage - 1 : maxNumerals + 2; | ||||
| 			for (let i = sectionStart; i <= sectionEnd; i++) { | ||||
| 				pages.push(i); | ||||
| 			} | ||||
| 		} else { | ||||
| 			// mid section - with both ellipses | ||||
| 			for (let i = settings.page - maxNumerals; i <= settings.page + maxNumerals; i++) { | ||||
| 				pages.push(i); | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		if (!isWithinRightSection) pages.push(-1); | ||||
| 		pages.push(lastPage); | ||||
| 
 | ||||
| 		return pages; | ||||
| 	} | ||||
| 
 | ||||
| 	function updateSize(size: number) { | ||||
| 		lastPage = Math.max(0, Math.ceil(size / settings.limit - 1)); | ||||
| 		controlPages = getNumerals(); | ||||
| 	} | ||||
| 
 | ||||
| 	// State | ||||
| 	$: classesButtonActive = (page: number) => { | ||||
| 		return page === settings.page ? `${active}` : ''; | ||||
| 	}; | ||||
| 	$: maxNumerals, onChangeLength(); | ||||
| 	$: updateSize(settings.size); | ||||
| 
 | ||||
| 	// Reactive Classes | ||||
| 	$: classesBase = `${cBase} ${justify} ${$$props.class ?? ''}`; | ||||
| </script> | ||||
| 
 | ||||
| <div class={classesBase}> | ||||
| 	<!-- Select Amount --> | ||||
| 	{#if settings.amounts.length} | ||||
| 		<select | ||||
| 			bind:value={settings.limit} | ||||
| 			on:change={onChangeLength} | ||||
| 			class="dark:bg-gray-900 w-fit pr-8 rounded py-2 px-2 text-sm bg-transparent outline-none" | ||||
| 			{disabled} | ||||
| 		> | ||||
| 			{#each settings.amounts as amount} | ||||
| 				<option value={amount}>{amount} {amountText}</option> | ||||
| 			{/each} | ||||
| 		</select> | ||||
| 	{/if} | ||||
| 	<!-- Controls --> | ||||
| 	<div> | ||||
| 		<!-- Button: First --> | ||||
| 		{#if showFirstLastButtons} | ||||
| 			<button | ||||
| 				type="button" | ||||
| 				class={buttonClasses} | ||||
| 				on:click={() => { | ||||
| 					gotoPage(0); | ||||
| 				}} | ||||
| 				disabled={disabled || settings.page === 0} | ||||
| 			> | ||||
| 				<svg | ||||
| 					xmlns="http://www.w3.org/2000/svg" | ||||
| 					viewBox="0 0 512 512" | ||||
| 					fill="currentColor" | ||||
| 					class="w-4 h-4" | ||||
| 				> | ||||
| 					<path | ||||
| 						d="M41.4 233.4c-12.5 12.5-12.5 32.8 0 45.3l160 160c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L109.3 256 246.6 118.6c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0l-160 160zm352-160l-160 160c-12.5 12.5-12.5 32.8 0 45.3l160 160c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L301.3 256 438.6 118.6c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0z" | ||||
| 					/> | ||||
| 				</svg> | ||||
| 			</button> | ||||
| 		{/if} | ||||
| 		<!-- Button: Back --> | ||||
| 		{#if showPreviousNextButtons} | ||||
| 			<button | ||||
| 				type="button" | ||||
| 				aria-label="Previous Page" | ||||
| 				class={buttonClasses} | ||||
| 				on:click={() => { | ||||
| 					gotoPage(settings.page - 1); | ||||
| 				}} | ||||
| 				disabled={disabled || settings.page === 0} | ||||
| 			> | ||||
| 				<svg | ||||
| 					xmlns="http://www.w3.org/2000/svg" | ||||
| 					viewBox="0 0 448 512" | ||||
| 					fill="currentColor" | ||||
| 					class="w-4 h-4" | ||||
| 				> | ||||
| 					<path | ||||
| 						d="M9.4 233.4c-12.5 12.5-12.5 32.8 0 45.3l160 160c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L109.2 288 416 288c17.7 0 32-14.3 32-32s-14.3-32-32-32l-306.7 0L214.6 118.6c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0l-160 160z" | ||||
| 					/> | ||||
| 				</svg> | ||||
| 			</button> | ||||
| 		{/if} | ||||
| 		<!-- Center --> | ||||
| 		{#if showNumerals === false} | ||||
| 			<!-- Details --> | ||||
| 			<button type="button" class="{buttonClasses} !text-sm"> | ||||
| 				{settings.page * settings.limit + 1}-{Math.min( | ||||
| 					settings.page * settings.limit + settings.limit, | ||||
| 					settings.size | ||||
| 				)} <span class="opacity-50">{separatorText} {settings.size}</span> | ||||
| 			</button> | ||||
| 		{:else} | ||||
| 			<!-- Numeric Row --> | ||||
| 			{#each controlPages as page} | ||||
| 				<button | ||||
| 					type="button" | ||||
| 					{disabled} | ||||
| 					class="{buttonClasses} {classesButtonActive(page)}" | ||||
| 					on:click={() => gotoPage(page)} | ||||
| 				> | ||||
| 					{page >= 0 ? page + 1 : '...'} | ||||
| 				</button> | ||||
| 			{/each} | ||||
| 		{/if} | ||||
| 		<!-- Button: Next --> | ||||
| 		{#if showPreviousNextButtons} | ||||
| 			<button | ||||
| 				type="button" | ||||
| 				class={buttonClasses} | ||||
| 				on:click={() => { | ||||
| 					gotoPage(settings.page + 1); | ||||
| 				}} | ||||
| 				disabled={disabled || (settings.page + 1) * settings.limit >= settings.size} | ||||
| 			> | ||||
| 				<svg | ||||
| 					xmlns="http://www.w3.org/2000/svg" | ||||
| 					viewBox="0 0 448 512" | ||||
| 					fill="currentColor" | ||||
| 					class="w-4 h-4" | ||||
| 				> | ||||
| 					<path | ||||
| 						d="M438.6 278.6c12.5-12.5 12.5-32.8 0-45.3l-160-160c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3L338.8 224 32 224c-17.7 0-32 14.3-32 32s14.3 32 32 32l306.7 0L233.4 393.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0l160-160z" | ||||
| 					/> | ||||
| 				</svg> | ||||
| 			</button> | ||||
| 		{/if} | ||||
| 		<!-- Button: last --> | ||||
| 		{#if showFirstLastButtons} | ||||
| 			<button | ||||
| 				type="button" | ||||
| 				class={buttonClasses} | ||||
| 				on:click={() => { | ||||
| 					gotoPage(lastPage); | ||||
| 				}} | ||||
| 				disabled={disabled || (settings.page + 1) * settings.limit >= settings.size} | ||||
| 			> | ||||
| 				<svg | ||||
| 					xmlns="http://www.w3.org/2000/svg" | ||||
| 					viewBox="0 0 512 512" | ||||
| 					fill="currentColor" | ||||
| 					class="w-4 h-4" | ||||
| 				> | ||||
| 					<path | ||||
| 						d="M470.6 278.6c12.5-12.5 12.5-32.8 0-45.3l-160-160c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3L402.7 256 265.4 393.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0l160-160zm-352 160l160-160c12.5-12.5 12.5-32.8 0-45.3l-160-160c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3L210.7 256 73.4 393.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0z" | ||||
| 					/> | ||||
| 				</svg> | ||||
| 			</button> | ||||
| 		{/if} | ||||
| 	</div> | ||||
| </div> | ||||
							
								
								
									
										15
									
								
								src/lib/components/icons/ChevronLeft.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								src/lib/components/icons/ChevronLeft.svelte
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,15 @@ | |||
| <script lang="ts"> | ||||
| 	export let className = 'w-4 h-4'; | ||||
| 	export let strokeWidth = '1.5'; | ||||
| </script> | ||||
| 
 | ||||
| <svg | ||||
| 	xmlns="http://www.w3.org/2000/svg" | ||||
| 	fill="none" | ||||
| 	viewBox="0 0 24 24" | ||||
| 	stroke-width={strokeWidth} | ||||
| 	stroke="currentColor" | ||||
| 	class={className} | ||||
| > | ||||
| 	<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 19.5 8.25 12l7.5-7.5" /> | ||||
| </svg> | ||||
							
								
								
									
										15
									
								
								src/lib/components/icons/ChevronRight.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								src/lib/components/icons/ChevronRight.svelte
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,15 @@ | |||
| <script lang="ts"> | ||||
| 	export let className = 'w-4 h-4'; | ||||
| 	export let strokeWidth = '1.5'; | ||||
| </script> | ||||
| 
 | ||||
| <svg | ||||
| 	xmlns="http://www.w3.org/2000/svg" | ||||
| 	fill="none" | ||||
| 	viewBox="0 0 24 24" | ||||
| 	stroke-width={strokeWidth} | ||||
| 	stroke="currentColor" | ||||
| 	class={className} | ||||
| > | ||||
| 	<path stroke-linecap="round" stroke-linejoin="round" d="m8.25 4.5 7.5 7.5-7.5 7.5" /> | ||||
| </svg> | ||||
|  | @ -12,7 +12,7 @@ | |||
| 	import { getSignUpEnabledStatus, toggleSignUpEnabledStatus } from '$lib/apis/auths'; | ||||
| 	import EditUserModal from '$lib/components/admin/EditUserModal.svelte'; | ||||
| 	import SettingsModal from '$lib/components/admin/SettingsModal.svelte'; | ||||
| 	import Paginator from '$lib/components/common/Paginator.svelte'; | ||||
| 	import Pagination from '$lib/components/common/Pagination.svelte'; | ||||
| 
 | ||||
| 	const i18n = getContext('i18n'); | ||||
| 
 | ||||
|  | @ -22,16 +22,11 @@ | |||
| 	let search = ''; | ||||
| 	let selectedUser = null; | ||||
| 
 | ||||
| 	let page = 1; | ||||
| 
 | ||||
| 	let showSettingsModal = false; | ||||
| 	let showEditUserModal = false; | ||||
| 
 | ||||
| 	let paginatorSettings = { | ||||
| 		page: 0, | ||||
| 		limit: 5, | ||||
| 		size: users.length, | ||||
| 		amounts: [5, 10, 15, 20] | ||||
| 	}; | ||||
| 
 | ||||
| 	const updateRoleHandler = async (id, role) => { | ||||
| 		const res = await updateUserRole(localStorage.token, id, role).catch((error) => { | ||||
| 			toast.error(error); | ||||
|  | @ -72,23 +67,6 @@ | |||
| 		} | ||||
| 		loaded = true; | ||||
| 	}); | ||||
| 
 | ||||
| 	$: paginatedSource = users | ||||
| 		.filter((user) => { | ||||
| 			if (search === '') { | ||||
| 				return true; | ||||
| 			} else { | ||||
| 				let name = user.name.toLowerCase(); | ||||
| 				const query = search.toLowerCase(); | ||||
| 				return name.includes(query); | ||||
| 			} | ||||
| 		}) | ||||
| 		.slice( | ||||
| 			paginatorSettings.page * paginatorSettings.limit, | ||||
| 			paginatorSettings.page * paginatorSettings.limit + paginatorSettings.limit | ||||
| 		); | ||||
| 
 | ||||
| 	$: paginatorSettings.size = users.length; | ||||
| </script> | ||||
| 
 | ||||
| <svelte:head> | ||||
|  | @ -184,7 +162,17 @@ | |||
| 										</tr> | ||||
| 									</thead> | ||||
| 									<tbody> | ||||
| 										{#each paginatedSource as user} | ||||
| 										{#each users | ||||
| 											.filter((user) => { | ||||
| 												if (search === '') { | ||||
| 													return true; | ||||
| 												} else { | ||||
| 													let name = user.name.toLowerCase(); | ||||
| 													const query = search.toLowerCase(); | ||||
| 													return name.includes(query); | ||||
| 												} | ||||
| 											}) | ||||
| 											.slice((page - 1) * 20, page * 20) as user} | ||||
| 											<tr class="bg-white border-b dark:bg-gray-900 dark:border-gray-700 text-xs"> | ||||
| 												<td class="px-3 py-2 min-w-[7rem] w-28"> | ||||
| 													<button | ||||
|  | @ -288,7 +276,7 @@ | |||
| 								ⓘ {$i18n.t("Click on the user role button to change a user's role.")} | ||||
| 							</div> | ||||
| 
 | ||||
| 							<Paginator bind:settings={paginatorSettings} showNumerals /> | ||||
| 							<Pagination bind:page count={users.length} /> | ||||
| 						</div> | ||||
| 					</div> | ||||
| 				</div> | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Timothy J. Baek
						Timothy J. Baek