mirror of
https://github.com/open-webui/open-webui.git
synced 2026-10-14 04:08:46 +00:00
refac
This commit is contained in:
parent
cc4fe3eab3
commit
e9635596dd
7 changed files with 276 additions and 162 deletions
|
|
@ -385,19 +385,31 @@ async def connect_app_by_id(
|
|||
db: AsyncSession = Depends(get_async_session),
|
||||
):
|
||||
app = await get_app_by_id(app_id, db=db)
|
||||
webui_url = str(request.base_url).rstrip('/')
|
||||
webui_url = str(await Config.get('webui.url') or request.base_url).strip().rstrip('/')
|
||||
try:
|
||||
validate_app_url(webui_url)
|
||||
origin = urlsplit(webui_url)
|
||||
if origin.path or origin.query or origin.fragment:
|
||||
raise ValueError()
|
||||
if (
|
||||
origin.scheme not in ('http', 'https')
|
||||
or not origin.hostname
|
||||
or origin.username is not None
|
||||
or origin.password is not None
|
||||
or origin.path
|
||||
or origin.query
|
||||
or origin.fragment
|
||||
or '\\' in webui_url
|
||||
or len(webui_url) > 2048
|
||||
or any(c.isspace() for c in webui_url)
|
||||
):
|
||||
raise ValueError('Invalid WebUI origin')
|
||||
host = f'[{origin.hostname}]' if ':' in origin.hostname else origin.hostname.lower()
|
||||
port = origin.port or (443 if origin.scheme == 'https' else 80)
|
||||
suffix = f':{port}' if port != (443 if origin.scheme == 'https' else 80) else ''
|
||||
webui_url = f'{origin.scheme}://{host}{suffix}'
|
||||
except ValueError:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST, detail='Connect through your public HTTPS WebUI URL'
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail='Invalid WebUI URL. Set WebUI URL in Settings > Admin > General to an HTTP(S) origin '
|
||||
'without a path, query, fragment or credentials.',
|
||||
) from None
|
||||
if form_data.api_key is not None:
|
||||
app.api_key = get_app_encryption_cipher().encrypt(form_data.api_key.get_secret_value().encode()).decode()
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@
|
|||
import Modal from '$lib/components/common/Modal.svelte';
|
||||
import XMark from '$lib/components/icons/XMark.svelte';
|
||||
import Search from '$lib/components/icons/Search.svelte';
|
||||
import Refresh from '$lib/components/icons/Refresh.svelte';
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||
import Switch from '$lib/components/common/Switch.svelte';
|
||||
import AccessButton from '$lib/components/common/AccessButton.svelte';
|
||||
|
|
@ -93,9 +95,16 @@
|
|||
showScopes = true;
|
||||
} else {
|
||||
if (manual) {
|
||||
manifest = { ...manifest, id: manifest.id || slug, name, requested_scopes: scopes };
|
||||
manifest = {
|
||||
...manifest,
|
||||
id: manifest.id || slug,
|
||||
name: app ? manifest.name : name,
|
||||
requested_scopes: [...new Set([...manifest.requested_scopes, ...scopes])]
|
||||
};
|
||||
manifestUpdated = !!app && JSON.stringify(manifest) !== JSON.stringify(app.data.manifest);
|
||||
}
|
||||
if (!app) {
|
||||
manifest = { ...manifest, name };
|
||||
app = await createNewApp(localStorage.token, {
|
||||
manifest,
|
||||
url,
|
||||
|
|
@ -141,7 +150,7 @@
|
|||
let opened = false;
|
||||
async function initialize() {
|
||||
manifestUpdated = false;
|
||||
manual = false;
|
||||
manual = !!app;
|
||||
showScopes = false;
|
||||
scopeQuery = '';
|
||||
url = app?.data.url ?? '';
|
||||
|
|
@ -174,7 +183,7 @@
|
|||
bind:accessGrants={grants}
|
||||
accessRoles={['read']}
|
||||
labels={{
|
||||
title: $i18n.t('Who can use this app?'),
|
||||
title: $i18n.t('Access'),
|
||||
public: $i18n.t('Everyone'),
|
||||
private: $i18n.t('Restricted'),
|
||||
read: $i18n.t('Use'),
|
||||
|
|
@ -305,7 +314,7 @@
|
|||
bind:value={apiKey}
|
||||
required={!app?.configured}
|
||||
maxlength="4096"
|
||||
placeholder={app?.configured ? '••••••••' : ''}
|
||||
placeholder={app?.configured ? '••••••••' : $i18n.t('API Key')}
|
||||
/>
|
||||
</AdminSettingField>
|
||||
{:else}
|
||||
|
|
@ -315,7 +324,6 @@
|
|||
id="app-name"
|
||||
class={inputClass}
|
||||
bind:value={name}
|
||||
disabled={!app && !manual}
|
||||
required
|
||||
maxlength="100"
|
||||
/></AdminSettingField
|
||||
|
|
@ -354,30 +362,35 @@
|
|||
</AdminSettingField>
|
||||
{/if}
|
||||
<AdminSettingSection title={$i18n.t('Connection')}>
|
||||
<p class="break-all text-xs text-gray-600 dark:text-gray-400">{url}</p>
|
||||
{#if app && !manual}
|
||||
<button
|
||||
type="button"
|
||||
disabled={busy}
|
||||
class="mt-2 text-xs text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white hover:underline"
|
||||
on:click={() =>
|
||||
run(async () => {
|
||||
manifest = await previewAppManifest(localStorage.token, url, {
|
||||
...(apiKey ? { api_key: apiKey } : {}),
|
||||
...(app ? { app_id: app.id } : {})
|
||||
});
|
||||
scopes = scopes.filter((scope) => manifest.requested_scopes.includes(scope));
|
||||
manifestUpdated = true;
|
||||
showScopes = true;
|
||||
})}>{$i18n.t('Update app')}</button
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
disabled={busy}
|
||||
class="ml-3 mt-2 text-xs text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white hover:underline"
|
||||
on:click={editDetailsManually}>{$i18n.t('Edit details manually')}</button
|
||||
>
|
||||
{/if}
|
||||
<div class="flex items-center gap-2">
|
||||
<p class="min-w-0 flex-1 break-all text-xs text-gray-600 dark:text-gray-400">{url}</p>
|
||||
{#if app}
|
||||
<Tooltip content={$i18n.t('Update app')}>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={$i18n.t('Update app')}
|
||||
disabled={busy}
|
||||
class="self-center flex size-6 shrink-0 items-center justify-center bg-transparent hover:bg-gray-100 dark:hover:bg-gray-850 rounded-lg transition disabled:opacity-50"
|
||||
on:click={() =>
|
||||
run(async () => {
|
||||
manifest = await previewAppManifest(localStorage.token, url, {
|
||||
...(apiKey ? { api_key: apiKey } : {}),
|
||||
app_id: app!.id
|
||||
});
|
||||
scopes = scopes.filter((scope) =>
|
||||
manifest.requested_scopes.includes(scope)
|
||||
);
|
||||
manifestUpdated = true;
|
||||
showScopes = true;
|
||||
})}
|
||||
>
|
||||
{#if busy}<Spinner className="size-3.5" />{:else}<Refresh
|
||||
className="size-3.5"
|
||||
/>{/if}
|
||||
</button>
|
||||
</Tooltip>
|
||||
{/if}
|
||||
</div>
|
||||
{#if manifestUpdated}
|
||||
<p class="mt-2 text-xs text-gray-500">
|
||||
{$i18n.t('Saving approves the updated app identity and selected scopes.')}
|
||||
|
|
@ -399,7 +412,7 @@
|
|||
bind:value={apiKey}
|
||||
required={!app?.configured}
|
||||
maxlength="4096"
|
||||
placeholder={app?.configured ? '••••••••' : ''}
|
||||
placeholder={app?.configured ? '••••••••' : $i18n.t('API Key')}
|
||||
/>
|
||||
</AdminSettingField>
|
||||
</AdminSettingSection>
|
||||
|
|
@ -425,7 +438,7 @@
|
|||
</AdminSettingRow>
|
||||
</AdminSettingSection>
|
||||
<AccessButton
|
||||
label={$i18n.t('Who can use this app?')}
|
||||
label={$i18n.t('Access')}
|
||||
className="mt-5"
|
||||
on:click={() => {
|
||||
showGrants = true;
|
||||
|
|
|
|||
|
|
@ -1,82 +0,0 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { user } from '$lib/stores';
|
||||
import { getApps, type App } from '$lib/apis/apps';
|
||||
import { getChannelAppsById, inviteAppToChannel, removeAppFromChannel } from '$lib/apis/channels';
|
||||
export let channel: any;
|
||||
let joined: App[] = [];
|
||||
let available: App[] = [];
|
||||
let selected = '';
|
||||
let busy = false;
|
||||
$: canManage = channel?.is_manager || $user?.role === 'admin' || channel?.user_id === $user?.id;
|
||||
async function load() {
|
||||
try {
|
||||
joined = await getChannelAppsById(localStorage.token, channel.id);
|
||||
available = (await getApps(localStorage.token)).filter(
|
||||
(a: App) => a.is_active && !joined.some((j) => j.id === a.id)
|
||||
);
|
||||
} catch (error) {
|
||||
toast.error(String(error));
|
||||
}
|
||||
}
|
||||
async function change(appId: string, remove = false) {
|
||||
busy = true;
|
||||
try {
|
||||
if (remove) {
|
||||
await removeAppFromChannel(localStorage.token, channel.id, appId);
|
||||
} else {
|
||||
await inviteAppToChannel(localStorage.token, channel.id, appId);
|
||||
}
|
||||
selected = '';
|
||||
await load();
|
||||
} catch (error) {
|
||||
toast.error(String(error));
|
||||
} finally {
|
||||
busy = false;
|
||||
}
|
||||
}
|
||||
onMount(load);
|
||||
</script>
|
||||
|
||||
<section class="space-y-2 px-1 mt-3" aria-label="Apps">
|
||||
<h3 class="text-[0.6875rem] text-gray-400 dark:text-gray-600 dark:text-gray-400">Apps</h3>
|
||||
{#each joined as app}
|
||||
<div class="flex items-center justify-between gap-2 text-xs text-gray-600 dark:text-gray-400">
|
||||
<span
|
||||
>{app.name}
|
||||
<span class="text-[0.6875rem] text-gray-400 dark:text-gray-600"
|
||||
>{app.is_active ? 'App' : 'Inactive'}</span
|
||||
></span
|
||||
>
|
||||
{#if canManage}<button
|
||||
type="button"
|
||||
disabled={busy}
|
||||
class="text-xs text-gray-400 hover:text-red-600 transition disabled:opacity-50"
|
||||
on:click={() => change(app.id, true)}>Remove</button
|
||||
>{/if}
|
||||
</div>
|
||||
{:else}<p class="text-[0.6875rem] text-gray-400 dark:text-gray-600">
|
||||
No apps have been invited.
|
||||
</p>{/each}
|
||||
{#if canManage && available.length}
|
||||
<div class="flex gap-2">
|
||||
<select
|
||||
aria-label="App to invite"
|
||||
bind:value={selected}
|
||||
class="min-w-0 flex-1 bg-transparent text-xs text-gray-600 dark:text-gray-400 outline-hidden"
|
||||
>
|
||||
<option value="">Choose an app</option>{#each available as app}<option value={app.id}
|
||||
>{app.name}</option
|
||||
>{/each}
|
||||
</select>
|
||||
<button
|
||||
type="button"
|
||||
disabled={!selected || busy}
|
||||
on:click={() => change(selected)}
|
||||
class="h-7 rounded-lg px-2.5 text-xs bg-gray-900 text-white dark:bg-gray-100 dark:text-gray-900 transition hover:bg-black dark:hover:bg-white disabled:opacity-50"
|
||||
>Invite</button
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
||||
|
|
@ -1,13 +1,12 @@
|
|||
<script lang="ts">
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { getContext, onMount } from 'svelte';
|
||||
const i18n = getContext('i18n');
|
||||
import { getContext } from 'svelte';
|
||||
const i18n = getContext<any>('i18n');
|
||||
|
||||
import Apps from './Apps.svelte';
|
||||
import { config } from '$lib/stores';
|
||||
import { removeMembersById } from '$lib/apis/channels';
|
||||
import { config, user } from '$lib/stores';
|
||||
import type { App } from '$lib/apis/apps';
|
||||
import { removeMembersById, getChannelAppsById, removeAppFromChannel } from '$lib/apis/channels';
|
||||
|
||||
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||
import Modal from '$lib/components/common/Modal.svelte';
|
||||
|
||||
import XMark from '$lib/components/icons/XMark.svelte';
|
||||
|
|
@ -22,6 +21,22 @@
|
|||
export let onUpdate = () => {};
|
||||
|
||||
let showAddMembersModal = false;
|
||||
let joinedApps: App[] = [];
|
||||
$: canManage =
|
||||
channel?.type !== 'dm' &&
|
||||
(channel?.is_manager || $user?.role === 'admin' || channel?.user_id === $user?.id);
|
||||
const updateMembers = async () => {
|
||||
await init();
|
||||
onUpdate();
|
||||
};
|
||||
const removeAppHandler = async (appId: string) => {
|
||||
try {
|
||||
await removeAppFromChannel(localStorage.token, channel.id, appId);
|
||||
await updateMembers();
|
||||
} catch (error) {
|
||||
toast.error(`${error}`);
|
||||
}
|
||||
};
|
||||
const submitHandler = async () => {};
|
||||
|
||||
const hasPublicReadGrant = (grants: any) =>
|
||||
|
|
@ -59,19 +74,28 @@
|
|||
}
|
||||
};
|
||||
|
||||
const init = () => {};
|
||||
const init = async () => {
|
||||
joinedApps = [];
|
||||
if (show && channel?.type !== 'dm' && $config?.features?.enable_apps) {
|
||||
joinedApps = await getChannelAppsById(localStorage.token, channel.id).catch((error) => {
|
||||
toast.error(`${error}`);
|
||||
return [];
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$: if (show) {
|
||||
init();
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
init();
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if channel}
|
||||
<AddMembersModal bind:show={showAddMembersModal} {channel} {onUpdate} />
|
||||
<AddMembersModal
|
||||
bind:show={showAddMembersModal}
|
||||
{channel}
|
||||
{joinedApps}
|
||||
onUpdate={updateMembers}
|
||||
/>
|
||||
<Modal size="sm" bind:show>
|
||||
<div>
|
||||
<div class=" flex justify-between dark:text-gray-100 px-4 pt-3 mb-1">
|
||||
|
|
@ -118,7 +142,9 @@
|
|||
<div class="flex flex-col w-full h-full pb-2">
|
||||
<UserList
|
||||
{channel}
|
||||
onAdd={channel?.type === 'group' && channel?.is_manager
|
||||
apps={joinedApps}
|
||||
onRemoveApp={canManage ? removeAppHandler : null}
|
||||
onAdd={canManage && (channel?.type === 'group' || $config?.features?.enable_apps)
|
||||
? () => {
|
||||
showAddMembersModal = true;
|
||||
}
|
||||
|
|
@ -132,7 +158,6 @@
|
|||
sort={channel?.type !== 'dm'}
|
||||
/>
|
||||
</div>
|
||||
{#if $config?.features?.enable_apps && channel.type !== 'dm'}<Apps {channel} />{/if}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
<script lang="ts">
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { getContext, onMount } from 'svelte';
|
||||
const i18n = getContext('i18n');
|
||||
import { getContext } from 'svelte';
|
||||
const i18n = getContext<any>('i18n');
|
||||
|
||||
import { addMembersById } from '$lib/apis/channels';
|
||||
import { addMembersById, inviteAppToChannel } from '$lib/apis/channels';
|
||||
import { getApps, type App } from '$lib/apis/apps';
|
||||
import { config } from '$lib/stores';
|
||||
|
||||
import Modal from '$lib/components/common/Modal.svelte';
|
||||
import XMark from '$lib/components/icons/XMark.svelte';
|
||||
|
|
@ -11,36 +13,67 @@
|
|||
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||
|
||||
export let show = false;
|
||||
export let channel = null;
|
||||
export let channel: any = null;
|
||||
export let joinedApps: App[] = [];
|
||||
let apps: App[] = [];
|
||||
let appIds: string[] = [];
|
||||
$: availableApps = apps.filter(
|
||||
(app) => app.is_active && !joinedApps.some((joined) => joined.id === app.id)
|
||||
);
|
||||
|
||||
export let onUpdate = () => {};
|
||||
|
||||
let groupIds = [];
|
||||
let userIds = [];
|
||||
let groupIds: string[] = [];
|
||||
let userIds: string[] = [];
|
||||
|
||||
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) {
|
||||
if (loading) return;
|
||||
loading = true;
|
||||
let changed = false;
|
||||
try {
|
||||
if (userIds.length || groupIds.length) {
|
||||
await addMembersById(localStorage.token, channel.id, {
|
||||
user_ids: userIds,
|
||||
group_ids: groupIds
|
||||
});
|
||||
userIds = [];
|
||||
groupIds = [];
|
||||
changed = true;
|
||||
}
|
||||
for (const id of [...appIds]) {
|
||||
await inviteAppToChannel(localStorage.token, channel.id, id);
|
||||
appIds = appIds.filter((appId) => appId !== id);
|
||||
changed = true;
|
||||
}
|
||||
toast.success($i18n.t('Members added successfully'));
|
||||
onUpdate();
|
||||
show = false;
|
||||
} else {
|
||||
toast.error($i18n.t('Failed to add members'));
|
||||
} catch (error) {
|
||||
toast.error(`${error}`);
|
||||
} finally {
|
||||
if (changed) onUpdate();
|
||||
loading = false;
|
||||
}
|
||||
};
|
||||
|
||||
let opened = false;
|
||||
$: if (show && !opened) {
|
||||
opened = true;
|
||||
if ($config?.features?.enable_apps && channel?.type !== 'dm') {
|
||||
getApps(localStorage.token)
|
||||
.then((value) => (apps = value))
|
||||
.catch((error) => toast.error(`${error}`));
|
||||
}
|
||||
} else if (!show) {
|
||||
opened = false;
|
||||
apps = [];
|
||||
}
|
||||
|
||||
const reset = () => {
|
||||
userIds = [];
|
||||
groupIds = [];
|
||||
appIds = [];
|
||||
loading = false;
|
||||
};
|
||||
|
||||
|
|
@ -78,14 +111,21 @@
|
|||
}}
|
||||
>
|
||||
<div class="flex flex-col w-full h-full pb-2">
|
||||
<MemberSelector bind:userIds bind:groupIds includeGroups={true} />
|
||||
<MemberSelector
|
||||
bind:userIds
|
||||
bind:groupIds
|
||||
bind:appIds
|
||||
apps={availableApps}
|
||||
includeGroups={channel.type === 'group'}
|
||||
includeUsers={channel.type === 'group'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end pt-3 text-sm font-normal gap-1.5">
|
||||
<button
|
||||
class="flex h-7 shrink-0 items-center justify-center gap-1.5 rounded-lg bg-gray-900 px-2.5 text-xs font-normal text-white transition hover:bg-black disabled:opacity-60 dark:bg-gray-100 dark:text-gray-900 dark:hover:bg-white disabled:cursor-not-allowed"
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
disabled={loading || !(userIds.length || groupIds.length || appIds.length)}
|
||||
>
|
||||
{$i18n.t('Add')}
|
||||
|
||||
|
|
|
|||
|
|
@ -20,10 +20,18 @@
|
|||
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||
import ProfilePreview from '../Messages/Message/ProfilePreview.svelte';
|
||||
import XMark from '$lib/components/icons/XMark.svelte';
|
||||
import Grid from '$lib/components/icons/Grid.svelte';
|
||||
import type { App } from '$lib/apis/apps';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
const i18n = getContext<any>('i18n');
|
||||
|
||||
export let channel = null;
|
||||
export let apps: App[] = [];
|
||||
export let onRemoveApp: ((appId: string) => Promise<void>) | null = null;
|
||||
let removingApp = '';
|
||||
$: filteredApps = apps.filter((app) =>
|
||||
`${app.name} ${app.slug}`.toLowerCase().includes(query.toLowerCase())
|
||||
);
|
||||
|
||||
export let onAdd = null;
|
||||
export let onRemove = null;
|
||||
|
|
@ -108,7 +116,7 @@
|
|||
<span class="text-sm">
|
||||
{$i18n.t('Members')}
|
||||
</span>
|
||||
<span class="text-sm text-gray-500">{total}</span>
|
||||
<span class="text-sm text-gray-500">{total + filteredApps.length}</span>
|
||||
</div>
|
||||
|
||||
{#if onAdd}
|
||||
|
|
@ -155,7 +163,7 @@
|
|||
</div>
|
||||
{/if}
|
||||
|
||||
{#if users.length > 0}
|
||||
{#if users.length > 0 || filteredApps.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
|
||||
|
|
@ -214,6 +222,39 @@
|
|||
</div>
|
||||
</div> -->
|
||||
<div class="w-full">
|
||||
{#each filteredApps as app (app.id)}
|
||||
<div class="text-xs flex items-center justify-between">
|
||||
<div class="px-2 py-1.5 font-normal text-gray-900 dark:text-white flex-1 min-w-0">
|
||||
<div class="flex items-center gap-2">
|
||||
<div
|
||||
class="size-6 rounded-lg bg-gray-100 dark:bg-gray-850 flex items-center justify-center shrink-0"
|
||||
>
|
||||
<Grid className="size-3.5" />
|
||||
</div>
|
||||
<span class="truncate">{app.name}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="px-2 py-1 flex items-center gap-1 translate-y-0.5">
|
||||
<Badge type="muted" content={$i18n.t('App')} />
|
||||
{#if onRemoveApp}
|
||||
<button
|
||||
type="button"
|
||||
aria-label={$i18n.t('Remove {{name}}', { name: app.name })}
|
||||
disabled={!!removingApp}
|
||||
class="rounded-lg p-1 hover:bg-gray-100 dark:hover:bg-gray-850 transition disabled:opacity-50"
|
||||
on:click={async () => {
|
||||
removingApp = app.id;
|
||||
try {
|
||||
await onRemoveApp?.(app.id);
|
||||
} finally {
|
||||
removingApp = '';
|
||||
}
|
||||
}}><XMark /></button
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
{#each users as user, userIdx (user.id)}
|
||||
<div class=" dark:border-gray-850 text-xs flex items-center justify-between">
|
||||
<div class="px-2 py-1.5 font-normal text-gray-900 dark:text-white flex-1">
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@
|
|||
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||
import Checkbox from '$lib/components/common/Checkbox.svelte';
|
||||
import { getGroups } from '$lib/apis/groups';
|
||||
import type { App } from '$lib/apis/apps';
|
||||
import Grid from '$lib/components/icons/Grid.svelte';
|
||||
|
||||
export let includeGroups = true;
|
||||
export let includeUsers = true;
|
||||
|
|
@ -26,6 +28,11 @@
|
|||
|
||||
export let groupIds: string[] = [];
|
||||
export let userIds: string[] = [];
|
||||
export let appIds: string[] = [];
|
||||
export let apps: App[] = [];
|
||||
$: filteredApps = apps.filter((app) =>
|
||||
`${app.name} ${app.slug}`.toLowerCase().includes(query.toLowerCase())
|
||||
);
|
||||
|
||||
let groups: any[] | null = null;
|
||||
let filteredGroups: any[] = [];
|
||||
|
|
@ -79,7 +86,7 @@
|
|||
const handleSearchInput = () => {
|
||||
clearTimeout(searchDebounceTimer);
|
||||
searchDebounceTimer = setTimeout(() => {
|
||||
getUserList();
|
||||
if (includeUsers) getUserList();
|
||||
}, 300);
|
||||
};
|
||||
|
||||
|
|
@ -87,15 +94,20 @@
|
|||
clearTimeout(searchDebounceTimer);
|
||||
});
|
||||
|
||||
$: if (page !== null && orderBy !== null && direction !== null) {
|
||||
$: if (includeUsers && page !== null && orderBy !== null && direction !== null) {
|
||||
getUserList();
|
||||
} else if (!includeUsers) {
|
||||
users = [];
|
||||
total = 0;
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
groups = await getGroups(localStorage.token, true).catch((error) => {
|
||||
console.error(error);
|
||||
return [];
|
||||
});
|
||||
groups = includeGroups
|
||||
? await getGroups(localStorage.token, true).catch((error) => {
|
||||
console.error(error);
|
||||
return [];
|
||||
})
|
||||
: [];
|
||||
|
||||
if (userIds.length > 0) {
|
||||
userIds.forEach(async (id) => {
|
||||
|
|
@ -180,6 +192,22 @@
|
|||
</div>
|
||||
{/if}
|
||||
|
||||
{#if appIds.length > 0}
|
||||
<div class="mx-1 mb-1.5">
|
||||
<div class="text-xs text-gray-500 mx-0.5 mb-1">{appIds.length} {$i18n.t('Apps')}</div>
|
||||
<div class="flex gap-1 flex-wrap">
|
||||
{#each apps.filter((app) => appIds.includes(app.id)) as app (app.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={() => (appIds = appIds.filter((id) => id !== app.id))}
|
||||
>
|
||||
<span>{app.name}</span><XMark className="size-3" />
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="flex gap-1 mb-1">
|
||||
<div class=" flex w-full space-x-2">
|
||||
<div class="flex flex-1">
|
||||
|
|
@ -207,7 +235,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{#if filteredUsers.length > 0 || filteredGroups.length > 0}
|
||||
{#if (includeUsers && filteredUsers.length > 0) || (includeGroups && filteredGroups.length > 0) || filteredApps.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 class="w-full max-h-96 overflow-y-auto rounded-lg">
|
||||
|
|
@ -311,6 +339,43 @@
|
|||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if filteredApps.length > 0}
|
||||
<div class="text-xs text-gray-500 mb-1 mx-1 mt-3">{$i18n.t('Apps')}</div>
|
||||
<div>
|
||||
{#each filteredApps as app (app.id)}
|
||||
<div class="text-xs flex items-center justify-between w-full">
|
||||
<button
|
||||
type="button"
|
||||
class="px-3 py-1.5 font-normal text-gray-900 dark:text-white flex-1 min-w-0"
|
||||
on:click={() =>
|
||||
(appIds = appIds.includes(app.id)
|
||||
? appIds.filter((id) => id !== app.id)
|
||||
: [...appIds, app.id])}
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<div
|
||||
class="size-6 rounded-lg bg-gray-100 dark:bg-gray-850 flex items-center justify-center shrink-0"
|
||||
>
|
||||
<Grid className="size-3.5" />
|
||||
</div>
|
||||
<span class="truncate">{app.name}</span>
|
||||
</div>
|
||||
</button>
|
||||
<div class="px-3 py-1">
|
||||
<Checkbox
|
||||
ariaLabel={app.name}
|
||||
state={appIds.includes(app.id) ? 'checked' : 'unchecked'}
|
||||
on:change={() =>
|
||||
(appIds = appIds.includes(app.id)
|
||||
? appIds.filter((id) => id !== app.id)
|
||||
: [...appIds, app.id])}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue