fix: restore JSDoc marker, wire modal bindings, align chatDirection, drop nested form, parallelize load

- Restore the /** opener above displayFileHandler in utils/index.ts that
  was dropped during the earlier merge; the orphaned comment body made
  the file fail TypeScript parsing and broke the frontend build.
- admin Settings/Interface.svelte now imports InterfaceDefaultsModal
  and ConfirmDialog and declares the showInterfaceDefaultsModal /
  showResetConfirmDialog flags that were already being bound at the
  bottom of the template, resolving the unresolved-identifier compile
  errors.
- InterfaceDefaultsModal no longer lowercases chatDirection before
  saving. The user-side store is typed 'LTR' | 'RTL' | 'auto' and the
  toggle/render logic matches those uppercase values; lowercasing on
  the admin side made every saved RTL/LTR default silently render as
  "Auto" for end users.
- InterfaceDefaultsModal drops the outer <form> wrapper. InterfaceSettings
  already renders its own <form> and nesting forms is invalid HTML; the
  Save button is now a plain type="button" with on:click=submitHandler,
  which keeps the click-to-save behaviour without the nested-form event
  routing surprises.
- +layout.svelte fetches user settings and admin defaults in parallel
  via Promise.all, with an inline .catch fallback on the defaults call
  so the endpoint being unavailable still lets user settings apply.
  This removes an avoidable serial round-trip on every authenticated
  load.
This commit is contained in:
DrMelone 2026-04-13 00:20:48 +02:00
parent 4497226b94
commit 6712c83cf6
4 changed files with 41 additions and 28 deletions

View file

@ -10,9 +10,16 @@
import Switch from '$lib/components/common/Switch.svelte';
import Textarea from '$lib/components/common/Textarea.svelte';
import Spinner from '$lib/components/common/Spinner.svelte';
import ConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
import InterfaceDefaultsModal from './Interface/InterfaceDefaultsModal.svelte';
import { resetAllUsersInterfaceSettings } from '$lib/apis/users';
// Toggles for the admin-defaults modal and the "reset all users" confirm
// dialog rendered at the bottom of this panel.
let showInterfaceDefaultsModal = false;
let showResetConfirmDialog = false;
const dispatch = createEventDispatcher();
const i18n = getContext('i18n');

View file

@ -33,16 +33,15 @@
}
};
// Prepare settings for backend - normalize chatDirection and filter nulls
// Prepare settings for backend — strip nulls so absent keys fall through
// to the user's own setting via deep-merge on load. Do NOT lowercase
// chatDirection: the user-side store is typed 'LTR' | 'RTL' | 'auto'
// and the toggle/render logic in chat/Settings/Interface.svelte matches
// against the uppercase values. Lowercasing here made admin defaults
// silently render as "Auto" because neither 'ltr' nor 'rtl' matched.
const prepareForBackend = (settings: any): object => {
const result = { ...settings };
if (result.chatDirection) {
result.chatDirection = result.chatDirection.toLowerCase();
}
return Object.fromEntries(
Object.entries(result).filter(([_, v]) => v !== undefined && v !== null)
Object.entries({ ...settings }).filter(([_, v]) => v !== undefined && v !== null)
);
};
@ -89,12 +88,15 @@
</div>
{#if !loading}
<form
class="flex flex-col w-full"
on:submit|preventDefault={() => {
submitHandler();
}}
>
<!--
No outer <form> wrapper: InterfaceSettings already renders its
own <form>, and nesting forms is invalid HTML and produces
unpredictable submit/event routing across browsers. The
InterfaceSettings component feeds changes back through the
saveAdminSettings prop, so we just need a Save button that
pushes the accumulated adminDefaults to the backend.
-->
<div class="flex flex-col w-full">
<div class="interface-defaults-wrapper">
<InterfaceSettings initialSettings={adminDefaults} saveSettings={saveAdminSettings} />
</div>
@ -104,7 +106,8 @@
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 {saving
? ' cursor-not-allowed'
: ''}"
type="submit"
type="button"
on:click={submitHandler}
disabled={saving}
>
{$i18n.t('Save')}
@ -116,7 +119,7 @@
{/if}
</button>
</div>
</form>
</div>
{:else}
<div class="flex justify-center items-center py-8">
<Spinner className="size-5" />

View file

@ -1840,6 +1840,7 @@ export const formatSkillName = (name) => {
return name.replace(/[-_]/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
};
/**
* Open the file browser panel to display a specific file.
* Used by both the direct tool execution path (client-side) and the
* backend event path (server-side) so behaviour is consistent.

View file

@ -916,19 +916,21 @@
$socket?.on('events', chatEventHandler);
$socket?.on('events:channel', channelEventHandler);
const userSettings = await getUserSettings(localStorage.token);
// Fetch user settings and admin-configured interface defaults in
// parallel — both are independent network calls on the hot
// authenticated-load path, and serializing them added
// avoidable round-trip latency. Defaults fall back to {} if
// the endpoint is unavailable so user settings still apply.
const [userSettings, adminDefaults] = await Promise.all([
getUserSettings(localStorage.token),
getInterfaceDefaults(localStorage.token).catch((e) => {
console.warn('Failed to load admin interface defaults:', e);
return {};
})
]);
// Fetch admin-configured interface defaults and merge with user settings
let effectiveSettings = {};
try {
const adminDefaults = await getInterfaceDefaults(localStorage.token);
const userUI = userSettings?.ui ?? {};
// Admin defaults are base, user settings override
effectiveSettings = deepMerge(adminDefaults, userUI);
} catch (e) {
// Fall back to user settings if admin defaults unavailable
effectiveSettings = userSettings?.ui ?? {};
}
// Admin defaults are base, user settings override.
const effectiveSettings = deepMerge(adminDefaults ?? {}, userSettings?.ui ?? {});
if (userSettings) {
settings.set(effectiveSettings);