open-webui/src/lib/components/admin/Users/UserList/EditUserModal.svelte
silentoplayz 16cf973ce5 fix: truncate long usernames in UI
Long usernames were causing layout issues in several parts of the application. This change truncates long usernames with an ellipsis to prevent them from overflowing.

The following areas have been fixed:
- Edit User modal
- User Chats modal
- Edit User Group modal
- Users table in the admin overview

fix: truncate long usernames in UI

Long usernames were causing layout issues in several parts of the application. This change truncates long usernames with an ellipsis to prevent them from overflowing.

The following areas have been fixed:
- Edit User modal
- User Chats modal
- Edit User Group modal
- Users table in the admin overview

Revert "fix: truncate long usernames in UI"

This reverts commit b623fdc95d0c494228b49f9369db3bbb3042cef0.
2025-09-26 18:30:48 -04:00

229 lines
6.3 KiB
Svelte

<script lang="ts">
import { toast } from 'svelte-sonner';
import dayjs from 'dayjs';
import { createEventDispatcher } from 'svelte';
import { onMount, getContext } from 'svelte';
import { goto } from '$app/navigation';
import { updateUserById, getUserGroupsById } from '$lib/apis/users';
import Modal from '$lib/components/common/Modal.svelte';
import localizedFormat from 'dayjs/plugin/localizedFormat';
import XMark from '$lib/components/icons/XMark.svelte';
import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
const i18n = getContext('i18n');
const dispatch = createEventDispatcher();
dayjs.extend(localizedFormat);
export let show = false;
export let selectedUser;
export let sessionUser;
let _user = {
profile_image_url: '',
role: 'pending',
name: '',
email: '',
password: ''
};
let userGroups: any[] | null = null;
const submitHandler = async () => {
const res = await updateUserById(localStorage.token, selectedUser.id, _user).catch((error) => {
toast.error(`${error}`);
});
if (res) {
dispatch('save');
show = false;
}
};
const loadUserGroups = async () => {
if (!selectedUser?.id) return;
userGroups = null;
userGroups = await getUserGroupsById(localStorage.token, selectedUser.id).catch((error) => {
toast.error(`${error}`);
return null;
});
};
onMount(() => {
if (selectedUser) {
_user = selectedUser;
_user.password = '';
loadUserGroups();
}
});
</script>
<Modal size="sm" bind:show>
<div>
<div class=" flex justify-between dark:text-gray-300 px-5 pt-4 pb-2">
<div class=" text-lg font-medium self-center">{$i18n.t('Edit User')}</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 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|preventDefault={() => {
submitHandler();
}}
>
<div class=" flex items-center rounded-md px-5 py-2 w-full">
<div class=" self-center mr-5">
<img
src={selectedUser.profile_image_url}
class=" max-w-[55px] object-cover rounded-full"
alt="User profile"
/>
</div>
<div class="overflow-hidden w-full">
<div class=" self-center capitalize font-semibold truncate">{selectedUser.name}</div>
<div class="text-xs text-gray-500">
{$i18n.t('Created at')}
{dayjs(selectedUser.created_at * 1000).format('LL')}
</div>
</div>
</div>
<div class=" px-5 pt-3 pb-5">
<div class=" flex flex-col space-y-1.5">
<div class="flex flex-col w-full">
<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Role')}</div>
<div class="flex-1">
<select
class="w-full dark:bg-gray-900 text-sm bg-transparent disabled:text-gray-500 dark:disabled:text-gray-500 outline-hidden"
bind:value={_user.role}
disabled={_user.id == sessionUser.id}
required
>
<option value="admin">{$i18n.t('Admin')}</option>
<option value="user">{$i18n.t('User')}</option>
<option value="pending">{$i18n.t('Pending')}</option>
</select>
</div>
</div>
{#if userGroups}
<div class="flex flex-col w-full text-sm">
<div class="mb-1 text-xs text-gray-500">{$i18n.t('User Groups')}</div>
{#if userGroups.length}
<div class="flex flex-wrap gap-1 my-0.5 -mx-1">
{#each userGroups as userGroup}
<span class="px-2 py-0.5 rounded-full bg-gray-100 dark:bg-gray-850 text-xs">
<a
href={'/admin/users/groups?id=' + userGroup.id}
on:click|preventDefault={() =>
goto('/admin/users/groups?id=' + userGroup.id)}
>
{userGroup.name}
</a>
</span>
{/each}
</div>
{:else}
<span>-</span>
{/if}
</div>
{/if}
<div class="flex flex-col w-full">
<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Email')}</div>
<div class="flex-1">
<input
class="w-full text-sm bg-transparent disabled:text-gray-500 dark:disabled:text-gray-500 outline-hidden"
type="email"
bind:value={_user.email}
placeholder={$i18n.t('Enter Your Email')}
autocomplete="off"
required
/>
</div>
</div>
<div class="flex flex-col w-full">
<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Name')}</div>
<div class="flex-1">
<input
class="w-full text-sm bg-transparent outline-hidden"
type="text"
bind:value={_user.name}
placeholder={$i18n.t('Enter Your Name')}
autocomplete="off"
required
/>
</div>
</div>
<div class="flex flex-col w-full">
<div class=" mb-1 text-xs text-gray-500">{$i18n.t('New Password')}</div>
<div class="flex-1">
<SensitiveInput
class="w-full text-sm bg-transparent outline-hidden"
type="password"
placeholder={$i18n.t('Enter New Password')}
bind:value={_user.password}
autocomplete="new-password"
required={false}
/>
</div>
</div>
</div>
<div class="flex justify-end pt-3 text-sm font-medium">
<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"
type="submit"
>
{$i18n.t('Save')}
</button>
</div>
</div>
</form>
</div>
</div>
</div>
</Modal>
<style>
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
/* display: none; <- Crashes Chrome on hover */
-webkit-appearance: none;
margin: 0; /* <-- Apparently some margin are still there even though it's hidden */
}
.tabs::-webkit-scrollbar {
display: none; /* for Chrome, Safari and Opera */
}
.tabs {
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
}
input[type='number'] {
-moz-appearance: textfield; /* Firefox */
}
</style>