mirror of
https://github.com/pocket-id/pocket-id.git
synced 2026-10-28 11:39:04 +01:00
feat: add tab bar navigation for crowded pages
This commit is contained in:
@@ -28,7 +28,7 @@
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="{!isAuthPage
|
class="{!isAuthPage
|
||||||
? 'max-w-410'
|
? 'max-w-[1720px]'
|
||||||
: ''} mx-auto flex w-full items-center justify-between px-4 md:px-10"
|
: ''} mx-auto flex w-full items-center justify-between px-4 md:px-10"
|
||||||
>
|
>
|
||||||
<div class="flex h-16 items-center">
|
<div class="flex h-16 items-center">
|
||||||
|
|||||||
@@ -12,6 +12,9 @@
|
|||||||
<TabsPrimitive.Content
|
<TabsPrimitive.Content
|
||||||
bind:ref
|
bind:ref
|
||||||
data-slot="tabs-content"
|
data-slot="tabs-content"
|
||||||
class={cn('text-sm flex-1 outline-none', className)}
|
class={cn(
|
||||||
|
'text-sm flex-1 outline-none data-[state=active]:animate-in data-[state=active]:fade-in-0 data-[state=active]:slide-in-from-bottom-1 data-[state=active]:duration-200 motion-reduce:data-[state=active]:animate-none',
|
||||||
|
className
|
||||||
|
)}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -19,22 +19,105 @@
|
|||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Tabs as TabsPrimitive } from 'bits-ui';
|
import { Tabs as TabsPrimitive } from 'bits-ui';
|
||||||
|
import { onMount } from 'svelte';
|
||||||
import { cn } from '$lib/utils/style.js';
|
import { cn } from '$lib/utils/style.js';
|
||||||
|
|
||||||
let {
|
let {
|
||||||
ref = $bindable(null),
|
ref = $bindable(null),
|
||||||
variant = 'default',
|
variant = 'default',
|
||||||
class: className,
|
class: className,
|
||||||
|
children,
|
||||||
...restProps
|
...restProps
|
||||||
}: TabsPrimitive.ListProps & {
|
}: TabsPrimitive.ListProps & {
|
||||||
variant?: TabsListVariant;
|
variant?: TabsListVariant;
|
||||||
} = $props();
|
} = $props();
|
||||||
|
|
||||||
|
let indicatorOffset = $state(0);
|
||||||
|
let indicatorSize = $state(0);
|
||||||
|
let indicatorVisible = $state(false);
|
||||||
|
let indicatorOrientation = $state<'horizontal' | 'vertical'>('horizontal');
|
||||||
|
|
||||||
|
function updateIndicator() {
|
||||||
|
if (!ref || variant !== 'line') {
|
||||||
|
indicatorVisible = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const activeTrigger = ref.querySelector<HTMLElement>(
|
||||||
|
'[data-slot="tabs-trigger"][data-state="active"]'
|
||||||
|
);
|
||||||
|
if (!activeTrigger) {
|
||||||
|
indicatorVisible = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const listRect = ref.getBoundingClientRect();
|
||||||
|
const triggerRect = activeTrigger.getBoundingClientRect();
|
||||||
|
indicatorOrientation = ref.dataset.orientation === 'vertical' ? 'vertical' : 'horizontal';
|
||||||
|
|
||||||
|
if (indicatorOrientation === 'vertical') {
|
||||||
|
indicatorOffset = triggerRect.top - listRect.top;
|
||||||
|
indicatorSize = triggerRect.height;
|
||||||
|
} else {
|
||||||
|
const triggerStyle = getComputedStyle(activeTrigger);
|
||||||
|
const paddingLeft = Number.parseFloat(triggerStyle.paddingLeft);
|
||||||
|
const paddingRight = Number.parseFloat(triggerStyle.paddingRight);
|
||||||
|
indicatorOffset = triggerRect.left - listRect.left + paddingLeft;
|
||||||
|
indicatorSize = triggerRect.width - paddingLeft - paddingRight;
|
||||||
|
}
|
||||||
|
indicatorVisible = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
if (!ref || variant !== 'line') return;
|
||||||
|
|
||||||
|
const resizeObserver = new ResizeObserver(updateIndicator);
|
||||||
|
const observeTriggers = () => {
|
||||||
|
ref
|
||||||
|
?.querySelectorAll<HTMLElement>('[data-slot="tabs-trigger"]')
|
||||||
|
.forEach((trigger) => resizeObserver.observe(trigger));
|
||||||
|
updateIndicator();
|
||||||
|
};
|
||||||
|
const mutationObserver = new MutationObserver(observeTriggers);
|
||||||
|
|
||||||
|
resizeObserver.observe(ref);
|
||||||
|
observeTriggers();
|
||||||
|
mutationObserver.observe(ref, {
|
||||||
|
attributes: true,
|
||||||
|
attributeFilter: ['data-state'],
|
||||||
|
childList: true,
|
||||||
|
subtree: true
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
mutationObserver.disconnect();
|
||||||
|
resizeObserver.disconnect();
|
||||||
|
};
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<TabsPrimitive.List
|
<TabsPrimitive.List
|
||||||
bind:ref
|
bind:ref
|
||||||
data-slot="tabs-list"
|
data-slot="tabs-list"
|
||||||
data-variant={variant}
|
data-variant={variant}
|
||||||
class={cn(tabsListVariants({ variant }), className)}
|
class={cn(tabsListVariants({ variant }), variant === 'line' && 'relative', className)}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
/>
|
>
|
||||||
|
{@render children?.()}
|
||||||
|
{#if variant === 'line'}
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
data-slot="tabs-indicator"
|
||||||
|
class={cn(
|
||||||
|
'bg-foreground pointer-events-none absolute rounded-full opacity-0 transition-[transform,width,height,opacity] duration-300 ease-out motion-reduce:transition-none',
|
||||||
|
indicatorOrientation === 'horizontal' ? 'bottom-0 left-0 h-0.5' : 'top-0 right-0 w-0.5',
|
||||||
|
indicatorVisible && 'opacity-100'
|
||||||
|
)}
|
||||||
|
style:width={indicatorOrientation === 'horizontal' ? `${indicatorSize}px` : undefined}
|
||||||
|
style:height={indicatorOrientation === 'vertical' ? `${indicatorSize}px` : undefined}
|
||||||
|
style:transform={indicatorOrientation === 'horizontal'
|
||||||
|
? `translate3d(${indicatorOffset}px, 0, 0)`
|
||||||
|
: `translate3d(0, ${indicatorOffset}px, 0)`}
|
||||||
|
></span>
|
||||||
|
{/if}
|
||||||
|
</TabsPrimitive.List>
|
||||||
|
|||||||
@@ -16,7 +16,6 @@
|
|||||||
"gap-2 rounded-full border border-transparent! px-3 py-1 text-sm font-medium group-data-vertical/tabs:rounded-2xl group-data-vertical/tabs:px-3 group-data-vertical/tabs:py-1.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2 [&_svg:not([class*='size-'])]:size-4 focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring text-foreground/60 hover:text-foreground dark:text-muted-foreground dark:hover:text-foreground relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center whitespace-nowrap transition-all group-data-[orientation=vertical]/tabs:w-full group-data-[orientation=vertical]/tabs:justify-start focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
"gap-2 rounded-full border border-transparent! px-3 py-1 text-sm font-medium group-data-vertical/tabs:rounded-2xl group-data-vertical/tabs:px-3 group-data-vertical/tabs:py-1.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2 [&_svg:not([class*='size-'])]:size-4 focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring text-foreground/60 hover:text-foreground dark:text-muted-foreground dark:hover:text-foreground relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center whitespace-nowrap transition-all group-data-[orientation=vertical]/tabs:w-full group-data-[orientation=vertical]/tabs:justify-start focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||||
'group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-active:bg-transparent dark:group-data-[variant=line]/tabs-list:data-active:border-transparent dark:group-data-[variant=line]/tabs-list:data-active:bg-transparent',
|
'group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-active:bg-transparent dark:group-data-[variant=line]/tabs-list:data-active:border-transparent dark:group-data-[variant=line]/tabs-list:data-active:bg-transparent',
|
||||||
'data-active:bg-background dark:data-active:text-foreground dark:data-active:border-input dark:data-active:bg-input/30 data-active:text-foreground',
|
'data-active:bg-background dark:data-active:text-foreground dark:data-active:border-input dark:data-active:bg-input/30 data-active:text-foreground',
|
||||||
'after:bg-foreground after:absolute after:opacity-0 after:transition-opacity group-data-[orientation=horizontal]/tabs:after:inset-x-0 group-data-[orientation=horizontal]/tabs:after:bottom-[-5px] group-data-[orientation=horizontal]/tabs:after:h-0.5 group-data-[orientation=vertical]/tabs:after:inset-y-0 group-data-[orientation=vertical]/tabs:after:-right-1 group-data-[orientation=vertical]/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-active:after:opacity-100',
|
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
|
|||||||
@@ -1,18 +1,36 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Tabs as TabsPrimitive } from 'bits-ui';
|
import { page } from '$app/state';
|
||||||
import { cn } from '$lib/utils/style.js';
|
import { cn } from '$lib/utils/style.js';
|
||||||
|
import { Tabs as TabsPrimitive } from 'bits-ui';
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
let {
|
let {
|
||||||
ref = $bindable(null),
|
ref = $bindable(null),
|
||||||
value = $bindable(''),
|
value = $bindable(''),
|
||||||
|
useHash = false,
|
||||||
class: className,
|
class: className,
|
||||||
...restProps
|
...restProps
|
||||||
}: TabsPrimitive.RootProps = $props();
|
}: TabsPrimitive.RootProps & {
|
||||||
|
useHash?: boolean;
|
||||||
|
} = $props();
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
if (useHash && page.url.hash) {
|
||||||
|
value = page.url.hash.substring(1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function onTabChange(newValue: string) {
|
||||||
|
if (useHash && page.url.hash !== newValue) {
|
||||||
|
window.location.hash = newValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<TabsPrimitive.Root
|
<TabsPrimitive.Root
|
||||||
bind:ref
|
bind:ref
|
||||||
bind:value
|
bind:value
|
||||||
|
onValueChange={onTabChange}
|
||||||
data-slot="tabs"
|
data-slot="tabs"
|
||||||
class={cn('gap-2 group/tabs flex data-[orientation=horizontal]:flex-col', className)}
|
class={cn('gap-2 group/tabs flex data-[orientation=horizontal]:flex-col', className)}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
|
|||||||
@@ -50,7 +50,7 @@
|
|||||||
>
|
>
|
||||||
<main
|
<main
|
||||||
in:fade={{ duration: 200 }}
|
in:fade={{ duration: 200 }}
|
||||||
class="mx-auto flex w-full max-w-[1640px] flex-col gap-x-8 gap-y-8 p-4 md:p-8 lg:flex-row"
|
class="mx-auto flex w-full max-w-[1720px] flex-col gap-x-8 gap-y-8 p-4 md:p-8 lg:flex-row"
|
||||||
>
|
>
|
||||||
<div class="min-w-[200px] xl:min-w-[250px]">
|
<div class="min-w-[200px] xl:min-w-[250px]">
|
||||||
<div in:fly={{ x: -15, duration: 200 }} class="sticky top-6">
|
<div in:fly={{ x: -15, duration: 200 }} class="sticky top-6">
|
||||||
|
|||||||
@@ -1,18 +1,14 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import CollapsibleCard from '$lib/components/collapsible-card.svelte';
|
|
||||||
import * as Alert from '$lib/components/ui/alert';
|
import * as Alert from '$lib/components/ui/alert';
|
||||||
|
import * as Card from '$lib/components/ui/card';
|
||||||
|
import * as Tabs from '$lib/components/ui/tabs';
|
||||||
import { m } from '$lib/paraglide/messages';
|
import { m } from '$lib/paraglide/messages';
|
||||||
import AppConfigService from '$lib/services/app-config-service';
|
import AppConfigService from '$lib/services/app-config-service';
|
||||||
import appConfigStore from '$lib/stores/application-configuration-store';
|
import appConfigStore from '$lib/stores/application-configuration-store';
|
||||||
import type { AllAppConfig } from '$lib/types/application-configuration.type';
|
import type { AllAppConfig } from '$lib/types/application-configuration.type';
|
||||||
import { axiosErrorToast } from '$lib/utils/error-util';
|
import { axiosErrorToast } from '$lib/utils/error-util';
|
||||||
import {
|
import {
|
||||||
LucideImage,
|
LucideInfo
|
||||||
LucideInfo,
|
|
||||||
Mail,
|
|
||||||
SlidersHorizontal,
|
|
||||||
UserSearch,
|
|
||||||
Users
|
|
||||||
} from '@lucide/svelte';
|
} from '@lucide/svelte';
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
import AppConfigEmailForm from './forms/app-config-email-form.svelte';
|
import AppConfigEmailForm from './forms/app-config-email-form.svelte';
|
||||||
@@ -101,57 +97,85 @@
|
|||||||
</Alert.Description>
|
</Alert.Description>
|
||||||
</Alert.Root>
|
</Alert.Root>
|
||||||
{/if}
|
{/if}
|
||||||
<div>
|
<Tabs.Root value="general" useHash class="gap-4">
|
||||||
<CollapsibleCard
|
<div class="overflow-x-auto pb-1">
|
||||||
id="application-configuration-general"
|
<Tabs.List variant="line" class="min-w-max">
|
||||||
icon={SlidersHorizontal}
|
<Tabs.Trigger value="general">
|
||||||
title={m.general()}
|
{m.general()}
|
||||||
defaultExpanded
|
</Tabs.Trigger>
|
||||||
>
|
<Tabs.Trigger value="user-creation">
|
||||||
<AppConfigGeneralForm {appConfig} callback={updateAppConfig} />
|
{m.user_creation()}
|
||||||
</CollapsibleCard>
|
</Tabs.Trigger>
|
||||||
</div>
|
<Tabs.Trigger value="email">
|
||||||
|
{m.email()}
|
||||||
|
</Tabs.Trigger>
|
||||||
|
<Tabs.Trigger value="ldap">
|
||||||
|
{m.ldap()}
|
||||||
|
</Tabs.Trigger>
|
||||||
|
<Tabs.Trigger value="images">
|
||||||
|
{m.images()}
|
||||||
|
</Tabs.Trigger>
|
||||||
|
</Tabs.List>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div>
|
<Tabs.Content value="general" id="application-configuration-general">
|
||||||
<CollapsibleCard
|
<Card.Root>
|
||||||
id="application-configuration-signup-defaults"
|
<Card.Header>
|
||||||
icon={Users}
|
<Card.Title>{m.general()}</Card.Title>
|
||||||
title={m.user_creation()}
|
</Card.Header>
|
||||||
description={m.configure_user_creation()}
|
<Card.Content>
|
||||||
>
|
<AppConfigGeneralForm {appConfig} callback={updateAppConfig} />
|
||||||
<AppConfigSignupDefaultsForm {appConfig} callback={updateAppConfig} />
|
</Card.Content>
|
||||||
</CollapsibleCard>
|
</Card.Root>
|
||||||
</div>
|
</Tabs.Content>
|
||||||
|
|
||||||
<div>
|
<Tabs.Content value="user-creation" id="application-configuration-signup-defaults">
|
||||||
<CollapsibleCard
|
<Card.Root>
|
||||||
id="application-configuration-email"
|
<Card.Header>
|
||||||
icon={Mail}
|
<Card.Title>{m.user_creation()}</Card.Title>
|
||||||
title={m.email()}
|
<Card.Description>{m.configure_user_creation()}</Card.Description>
|
||||||
description={m.configure_smtp_to_send_emails()}
|
</Card.Header>
|
||||||
>
|
<Card.Content>
|
||||||
<AppConfigEmailForm {appConfig} callback={updateAppConfig} />
|
<AppConfigSignupDefaultsForm {appConfig} callback={updateAppConfig} />
|
||||||
</CollapsibleCard>
|
</Card.Content>
|
||||||
</div>
|
</Card.Root>
|
||||||
|
</Tabs.Content>
|
||||||
|
|
||||||
<div>
|
<Tabs.Content value="email" id="application-configuration-email">
|
||||||
<CollapsibleCard
|
<Card.Root>
|
||||||
id="application-configuration-ldap"
|
<Card.Header>
|
||||||
icon={UserSearch}
|
<Card.Title>{m.email()}</Card.Title>
|
||||||
title={m.ldap()}
|
<Card.Description>{m.configure_smtp_to_send_emails()}</Card.Description>
|
||||||
description={m.configure_ldap_settings_to_sync_users_and_groups_from_an_ldap_server()}
|
</Card.Header>
|
||||||
>
|
<Card.Content>
|
||||||
<AppConfigLdapForm {appConfig} callback={updateAppConfig} />
|
<AppConfigEmailForm {appConfig} callback={updateAppConfig} />
|
||||||
</CollapsibleCard>
|
</Card.Content>
|
||||||
</div>
|
</Card.Root>
|
||||||
|
</Tabs.Content>
|
||||||
|
|
||||||
<div>
|
<Tabs.Content value="ldap" id="application-configuration-ldap">
|
||||||
<CollapsibleCard
|
<Card.Root>
|
||||||
id="application-configuration-images"
|
<Card.Header>
|
||||||
icon={LucideImage}
|
<Card.Title>{m.ldap()}</Card.Title>
|
||||||
title={m.images()}
|
<Card.Description>
|
||||||
description={m.configure_application_images()}
|
{m.configure_ldap_settings_to_sync_users_and_groups_from_an_ldap_server()}
|
||||||
>
|
</Card.Description>
|
||||||
<UpdateApplicationImages callback={updateImages} />
|
</Card.Header>
|
||||||
</CollapsibleCard>
|
<Card.Content>
|
||||||
</div>
|
<AppConfigLdapForm {appConfig} callback={updateAppConfig} />
|
||||||
|
</Card.Content>
|
||||||
|
</Card.Root>
|
||||||
|
</Tabs.Content>
|
||||||
|
|
||||||
|
<Tabs.Content value="images" id="application-configuration-images">
|
||||||
|
<Card.Root>
|
||||||
|
<Card.Header>
|
||||||
|
<Card.Title>{m.images()}</Card.Title>
|
||||||
|
<Card.Description>{m.configure_application_images()}</Card.Description>
|
||||||
|
</Card.Header>
|
||||||
|
<Card.Content>
|
||||||
|
<UpdateApplicationImages callback={updateImages} />
|
||||||
|
</Card.Content>
|
||||||
|
</Card.Root>
|
||||||
|
</Tabs.Content>
|
||||||
|
</Tabs.Root>
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { beforeNavigate } from '$app/navigation';
|
import { beforeNavigate } from '$app/navigation';
|
||||||
import { page } from '$app/state';
|
import { page } from '$app/state';
|
||||||
import CollapsibleCard from '$lib/components/collapsible-card.svelte';
|
|
||||||
import { openConfirmDialog } from '$lib/components/confirm-dialog';
|
import { openConfirmDialog } from '$lib/components/confirm-dialog';
|
||||||
import CopyToClipboard from '$lib/components/copy-to-clipboard.svelte';
|
import CopyToClipboard from '$lib/components/copy-to-clipboard.svelte';
|
||||||
|
import FormattedMessage from '$lib/components/formatted-message.svelte';
|
||||||
import * as Alert from '$lib/components/ui/alert';
|
import * as Alert from '$lib/components/ui/alert';
|
||||||
import { Button } from '$lib/components/ui/button';
|
import { Button } from '$lib/components/ui/button';
|
||||||
import * as Card from '$lib/components/ui/card';
|
import * as Card from '$lib/components/ui/card';
|
||||||
import * as Field from '$lib/components/ui/field';
|
import * as Field from '$lib/components/ui/field';
|
||||||
|
import * as Tabs from '$lib/components/ui/tabs';
|
||||||
import UserGroupSelection from '$lib/components/user-group-selection.svelte';
|
import UserGroupSelection from '$lib/components/user-group-selection.svelte';
|
||||||
import { m } from '$lib/paraglide/messages';
|
import { m } from '$lib/paraglide/messages';
|
||||||
import OidcService from '$lib/services/oidc-service';
|
import OidcService from '$lib/services/oidc-service';
|
||||||
@@ -17,7 +18,7 @@
|
|||||||
import type { ScimServiceProviderCreate } from '$lib/types/scim.type';
|
import type { ScimServiceProviderCreate } from '$lib/types/scim.type';
|
||||||
import { cachedOidcClientLogo } from '$lib/utils/cached-image-util';
|
import { cachedOidcClientLogo } from '$lib/utils/cached-image-util';
|
||||||
import { axiosErrorToast } from '$lib/utils/error-util';
|
import { axiosErrorToast } from '$lib/utils/error-util';
|
||||||
import { LucideChevronLeft, LucideInfo, LucideRefreshCcw } from '@lucide/svelte';
|
import { LucideChevronLeft, LucideInfo, LucideRefreshCcw, LucideTriangleAlert } from '@lucide/svelte';
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
import { slide } from 'svelte/transition';
|
import { slide } from 'svelte/transition';
|
||||||
import { backNavigate } from '../../users/navigate-back-util';
|
import { backNavigate } from '../../users/navigate-back-util';
|
||||||
@@ -230,122 +231,176 @@
|
|||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Card.Root>
|
<Tabs.Root value="general" useHash class="gap-4">
|
||||||
<Card.Header>
|
<div class="overflow-x-auto pb-1">
|
||||||
<Card.Title>{client.name}</Card.Title>
|
<Tabs.List variant="line" class="min-w-max">
|
||||||
</Card.Header>
|
<Tabs.Trigger value="general">{m.general()}</Tabs.Trigger>
|
||||||
<Card.Content>
|
<Tabs.Trigger value="user-groups">
|
||||||
<div class="flex flex-col">
|
{m.allowed_user_groups()}
|
||||||
<div class="mb-2 flex flex-col sm:flex-row sm:items-center">
|
{#if client.isGroupRestricted && client.allowedUserGroupIds.length === 0}
|
||||||
<Field.Label class="w-52">{m.client_id()}</Field.Label>
|
<LucideTriangleAlert class="ml-0.5 size-4 text-yellow-600 dark:text-yellow-400" />
|
||||||
<CopyToClipboard value={client.id}>
|
{/if}</Tabs.Trigger
|
||||||
<span class="text-muted-foreground text-sm" data-testid="client-id"> {client.id}</span>
|
>
|
||||||
</CopyToClipboard>
|
<Tabs.Trigger value="api-access">{m.api_access()}</Tabs.Trigger>
|
||||||
</div>
|
<Tabs.Trigger value="scim">{m.scim_provisioning()}</Tabs.Trigger>
|
||||||
{#if !client.isPublic}
|
<Tabs.Trigger value="preview">{m.oidc_data_preview()}</Tabs.Trigger>
|
||||||
<div class="mt-1 mb-2 flex flex-col sm:flex-row sm:items-center">
|
</Tabs.List>
|
||||||
<Field.Label class="w-52">{m.client_secret()}</Field.Label>
|
</div>
|
||||||
{#if $clientSecretStore}
|
|
||||||
<CopyToClipboard value={$clientSecretStore}>
|
<Tabs.Content value="general" class="flex flex-col gap-4">
|
||||||
<span class="text-muted-foreground text-sm" data-testid="client-secret">
|
<Card.Root>
|
||||||
{$clientSecretStore}
|
<Card.Header>
|
||||||
|
<Card.Title>{client.name}</Card.Title>
|
||||||
|
</Card.Header>
|
||||||
|
<Card.Content>
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<div class="mb-2 flex flex-col sm:flex-row sm:items-center">
|
||||||
|
<Field.Label class="w-52">{m.client_id()}</Field.Label>
|
||||||
|
<CopyToClipboard value={client.id}>
|
||||||
|
<span class="text-muted-foreground text-sm" data-testid="client-id">
|
||||||
|
{client.id}
|
||||||
</span>
|
</span>
|
||||||
</CopyToClipboard>
|
</CopyToClipboard>
|
||||||
{:else}
|
</div>
|
||||||
<div>
|
{#if !client.isPublic}
|
||||||
<span class="text-muted-foreground text-sm" data-testid="client-secret"
|
<div class="mt-1 mb-2 flex flex-col sm:flex-row sm:items-center">
|
||||||
>••••••••••••••••••••••••••••••••</span
|
<Field.Label class="w-52">{m.client_secret()}</Field.Label>
|
||||||
>
|
{#if $clientSecretStore}
|
||||||
<Button
|
<CopyToClipboard value={$clientSecretStore}>
|
||||||
class="ml-2"
|
<span class="text-muted-foreground text-sm" data-testid="client-secret">
|
||||||
onclick={createClientSecret}
|
{$clientSecretStore}
|
||||||
size="sm"
|
</span>
|
||||||
variant="ghost"
|
</CopyToClipboard>
|
||||||
aria-label="Create new client secret"><LucideRefreshCcw class="size-3" /></Button
|
{:else}
|
||||||
|
<div>
|
||||||
|
<span class="text-muted-foreground text-sm" data-testid="client-secret"
|
||||||
|
>••••••••••••••••••••••••••••••••</span
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
class="ml-2"
|
||||||
|
onclick={createClientSecret}
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
aria-label="Create new client secret"
|
||||||
|
><LucideRefreshCcw class="size-3" /></Button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{#if showAllDetails}
|
||||||
|
<div transition:slide>
|
||||||
|
{#each Object.entries(setupDetails) as [key, value]}
|
||||||
|
<div class="mb-2 flex flex-col sm:flex-row sm:items-center">
|
||||||
|
<Field.Label class="w-52">{key}</Field.Label>
|
||||||
|
<CopyToClipboard {value}>
|
||||||
|
<span class="text-muted-foreground text-sm">{value}</span>
|
||||||
|
</CopyToClipboard>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if !showAllDetails}
|
||||||
|
<div class="mt-4 flex justify-center">
|
||||||
|
<Button onclick={() => (showAllDetails = true)} size="sm" variant="ghost"
|
||||||
|
>{m.show_more_details()}</Button
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
</Card.Content>
|
||||||
{#if showAllDetails}
|
</Card.Root>
|
||||||
<div transition:slide>
|
|
||||||
{#each Object.entries(setupDetails) as [key, value]}
|
|
||||||
<div class="mb-2 flex flex-col sm:flex-row sm:items-center">
|
|
||||||
<Field.Label class="w-52">{key}</Field.Label>
|
|
||||||
<CopyToClipboard {value}>
|
|
||||||
<span class="text-muted-foreground text-sm">{value}</span>
|
|
||||||
</CopyToClipboard>
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if !showAllDetails}
|
<Card.Root>
|
||||||
<div class="mt-4 flex justify-center">
|
<Card.Content>
|
||||||
<Button onclick={() => (showAllDetails = true)} size="sm" variant="ghost"
|
<OidcForm mode="update" existingClient={client} callback={updateClient} />
|
||||||
>{m.show_more_details()}</Button
|
</Card.Content>
|
||||||
>
|
</Card.Root>
|
||||||
</div>
|
</Tabs.Content>
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</Card.Content>
|
|
||||||
</Card.Root>
|
|
||||||
<Card.Root>
|
|
||||||
<Card.Content>
|
|
||||||
<OidcForm mode="update" existingClient={client} callback={updateClient} />
|
|
||||||
</Card.Content>
|
|
||||||
</Card.Root>
|
|
||||||
<CollapsibleCard
|
|
||||||
id="allowed-user-groups"
|
|
||||||
title={m.allowed_user_groups()}
|
|
||||||
button={!client.isGroupRestricted ? UnrestrictButton : undefined}
|
|
||||||
forcedExpanded={client.isGroupRestricted ? undefined : false}
|
|
||||||
description={client.isGroupRestricted
|
|
||||||
? m.allowed_user_groups_description()
|
|
||||||
: m.allowed_user_groups_status_unrestricted_description()}
|
|
||||||
>
|
|
||||||
<UserGroupSelection
|
|
||||||
bind:selectedGroupIds={client.allowedUserGroupIds}
|
|
||||||
selectionDisabled={!client.isGroupRestricted}
|
|
||||||
/>
|
|
||||||
<div class="mt-5 flex justify-end gap-3">
|
|
||||||
<Button onclick={disableGroupRestriction} variant="secondary">{m.unrestrict()}</Button>
|
|
||||||
|
|
||||||
<Button usePromiseLoading onclick={() => updateUserGroupClients(client.allowedUserGroupIds)}
|
<Tabs.Content value="user-groups" id="allowed-user-groups">
|
||||||
>{m.save()}</Button
|
<Card.Root>
|
||||||
>
|
<Card.Header>
|
||||||
</div>
|
<div class="flex items-center justify-between gap-4">
|
||||||
</CollapsibleCard>
|
<div>
|
||||||
<CollapsibleCard id="api-access" title={m.api_access()} description={m.api_access_description()}>
|
<Card.Title>{m.allowed_user_groups()}</Card.Title>
|
||||||
<ApiAccessCard clientId={client.id} isPublicClient={client.isPublic} />
|
<Card.Description>
|
||||||
</CollapsibleCard>
|
{client.isGroupRestricted
|
||||||
<CollapsibleCard
|
? m.allowed_user_groups_description()
|
||||||
id="scim-provisioning"
|
: m.allowed_user_groups_status_unrestricted_description()}
|
||||||
title={m.scim_provisioning()}
|
</Card.Description>
|
||||||
description={m.scim_provisioning_description()}
|
</div>
|
||||||
>
|
{#if !client.isGroupRestricted}
|
||||||
<ScimResourceProviderForm
|
{@render UnrestrictButton()}
|
||||||
oidcClientId={client.id}
|
{/if}
|
||||||
existingProvider={scimServiceProvider}
|
</div>
|
||||||
onSave={saveScimServiceProvider}
|
</Card.Header>
|
||||||
/>
|
{#if client.isGroupRestricted}
|
||||||
</CollapsibleCard>
|
<Card.Content>
|
||||||
<Card.Root>
|
<UserGroupSelection bind:selectedGroupIds={client.allowedUserGroupIds} />
|
||||||
<Card.Header>
|
<div class="mt-5 flex justify-end gap-3">
|
||||||
<div class="flex flex-col items-start justify-between gap-3 sm:flex-row sm:items-center">
|
<Button onclick={disableGroupRestriction} variant="secondary">{m.unrestrict()}</Button>
|
||||||
<div>
|
|
||||||
<Card.Title>
|
<Button
|
||||||
{m.oidc_data_preview()}
|
usePromiseLoading
|
||||||
</Card.Title>
|
onclick={() => updateUserGroupClients(client.allowedUserGroupIds)}>{m.save()}</Button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</Card.Content>
|
||||||
|
{/if}
|
||||||
|
</Card.Root>
|
||||||
|
</Tabs.Content>
|
||||||
|
|
||||||
|
<Tabs.Content value="api-access" id="api-access">
|
||||||
|
<Card.Root>
|
||||||
|
<Card.Header>
|
||||||
|
<Card.Title>{m.api_access()}</Card.Title>
|
||||||
|
<Card.Description>{m.api_access_description()}</Card.Description>
|
||||||
|
</Card.Header>
|
||||||
|
<Card.Content>
|
||||||
|
<ApiAccessCard clientId={client.id} isPublicClient={client.isPublic} />
|
||||||
|
</Card.Content>
|
||||||
|
</Card.Root>
|
||||||
|
</Tabs.Content>
|
||||||
|
|
||||||
|
<Tabs.Content value="scim" id="scim-provisioning">
|
||||||
|
<Card.Root>
|
||||||
|
<Card.Header>
|
||||||
|
<Card.Title>{m.scim_provisioning()}</Card.Title>
|
||||||
<Card.Description>
|
<Card.Description>
|
||||||
{m.preview_the_oidc_data_that_would_be_sent_for_different_users()}
|
<FormattedMessage m={m.scim_provisioning_description()} />
|
||||||
</Card.Description>
|
</Card.Description>
|
||||||
</div>
|
</Card.Header>
|
||||||
|
<Card.Content>
|
||||||
|
<ScimResourceProviderForm
|
||||||
|
oidcClientId={client.id}
|
||||||
|
existingProvider={scimServiceProvider}
|
||||||
|
onSave={saveScimServiceProvider}
|
||||||
|
/>
|
||||||
|
</Card.Content>
|
||||||
|
</Card.Root>
|
||||||
|
</Tabs.Content>
|
||||||
|
|
||||||
<Button variant="outline" onclick={() => (showPreview = true)}>
|
<Tabs.Content value="preview">
|
||||||
{m.show()}
|
<Card.Root>
|
||||||
</Button>
|
<Card.Header>
|
||||||
</div>
|
<div class="flex flex-col items-start justify-between gap-3 sm:flex-row sm:items-center">
|
||||||
</Card.Header>
|
<div>
|
||||||
</Card.Root>
|
<Card.Title>
|
||||||
|
{m.oidc_data_preview()}
|
||||||
|
</Card.Title>
|
||||||
|
<Card.Description>
|
||||||
|
{m.preview_the_oidc_data_that_would_be_sent_for_different_users()}
|
||||||
|
</Card.Description>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button variant="outline" onclick={() => (showPreview = true)}>
|
||||||
|
{m.show()}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Card.Header>
|
||||||
|
</Card.Root>
|
||||||
|
</Tabs.Content>
|
||||||
|
</Tabs.Root>
|
||||||
<OidcClientPreviewModal bind:open={showPreview} clientId={client.id} />
|
<OidcClientPreviewModal bind:open={showPreview} clientId={client.id} />
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import CollapsibleCard from '$lib/components/collapsible-card.svelte';
|
|
||||||
import CustomClaimsInput from '$lib/components/form/custom-claims-input.svelte';
|
import CustomClaimsInput from '$lib/components/form/custom-claims-input.svelte';
|
||||||
import { Badge } from '$lib/components/ui/badge';
|
import { Badge } from '$lib/components/ui/badge';
|
||||||
import { Button } from '$lib/components/ui/button';
|
import { Button } from '$lib/components/ui/button';
|
||||||
import * as Card from '$lib/components/ui/card';
|
import * as Card from '$lib/components/ui/card';
|
||||||
|
import * as Tabs from '$lib/components/ui/tabs';
|
||||||
import { m } from '$lib/paraglide/messages';
|
import { m } from '$lib/paraglide/messages';
|
||||||
import CustomClaimService from '$lib/services/custom-claim-service';
|
import CustomClaimService from '$lib/services/custom-claim-service';
|
||||||
import UserGroupService from '$lib/services/user-group-service';
|
import UserGroupService from '$lib/services/user-group-service';
|
||||||
@@ -86,59 +86,84 @@
|
|||||||
<Badge class="rounded-full" variant="default">{m.ldap()}</Badge>
|
<Badge class="rounded-full" variant="default">{m.ldap()}</Badge>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<Card.Root>
|
<Tabs.Root value="general" useHash class="gap-4">
|
||||||
<Card.Header>
|
<div class="overflow-x-auto pb-1">
|
||||||
<Card.Title>{m.general()}</Card.Title>
|
<Tabs.List variant="line" class="min-w-max">
|
||||||
</Card.Header>
|
<Tabs.Trigger value="general">{m.general()}</Tabs.Trigger>
|
||||||
|
<Tabs.Trigger value="users">{m.users()}</Tabs.Trigger>
|
||||||
<Card.Content>
|
<Tabs.Trigger value="clients">{m.allowed_oidc_clients()}</Tabs.Trigger>
|
||||||
<UserGroupForm existingUserGroup={userGroup} callback={updateUserGroup} />
|
<Tabs.Trigger value="custom-claims">{m.custom_claims()}</Tabs.Trigger>
|
||||||
</Card.Content>
|
</Tabs.List>
|
||||||
</Card.Root>
|
|
||||||
|
|
||||||
<Card.Root>
|
|
||||||
<Card.Header>
|
|
||||||
<Card.Title>{m.users()}</Card.Title>
|
|
||||||
<Card.Description>{m.assign_users_to_this_group()}</Card.Description>
|
|
||||||
</Card.Header>
|
|
||||||
|
|
||||||
<Card.Content>
|
|
||||||
<UserSelection
|
|
||||||
bind:selectedUserIds={userGroup.userIds}
|
|
||||||
selectionDisabled={!!userGroup.ldapId && $appConfigStore.ldapEnabled}
|
|
||||||
/>
|
|
||||||
<div class="mt-5 flex justify-end">
|
|
||||||
<Button
|
|
||||||
disabled={!!userGroup.ldapId && $appConfigStore.ldapEnabled}
|
|
||||||
onclick={() => updateUserGroupUsers(userGroup.userIds)}>{m.save()}</Button
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</Card.Content>
|
|
||||||
</Card.Root>
|
|
||||||
|
|
||||||
<CollapsibleCard
|
|
||||||
id="user-group-custom-claims"
|
|
||||||
title={m.custom_claims()}
|
|
||||||
description={m.custom_claims_are_key_value_pairs_that_can_be_used_to_store_additional_information_about_a_user_prioritized()}
|
|
||||||
>
|
|
||||||
<CustomClaimsInput bind:customClaims={userGroup.customClaims} />
|
|
||||||
<div class="mt-5 flex justify-end">
|
|
||||||
<Button onclick={updateCustomClaims} type="submit">{m.save()}</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</CollapsibleCard>
|
|
||||||
|
|
||||||
<CollapsibleCard
|
<Tabs.Content value="general">
|
||||||
id="user-group-oidc-clients"
|
<Card.Root>
|
||||||
title={m.allowed_oidc_clients()}
|
<Card.Header>
|
||||||
description={m.allowed_oidc_clients_description()}
|
<Card.Title>{m.general()}</Card.Title>
|
||||||
>
|
</Card.Header>
|
||||||
<OidcClientSelection
|
|
||||||
bind:this={oidcClientSelectionRef}
|
<Card.Content>
|
||||||
bind:selectedGroupIds={userGroup.allowedOidcClientIds}
|
<UserGroupForm existingUserGroup={userGroup} callback={updateUserGroup} />
|
||||||
/>
|
</Card.Content>
|
||||||
<div class="mt-5 flex justify-end gap-3">
|
</Card.Root>
|
||||||
<Button onclick={() => updateAllowedOidcClients(userGroup.allowedOidcClientIds)}
|
</Tabs.Content>
|
||||||
>{m.save()}</Button
|
|
||||||
>
|
<Tabs.Content value="users">
|
||||||
</div>
|
<Card.Root>
|
||||||
</CollapsibleCard>
|
<Card.Header>
|
||||||
|
<Card.Title>{m.users()}</Card.Title>
|
||||||
|
<Card.Description>{m.assign_users_to_this_group()}</Card.Description>
|
||||||
|
</Card.Header>
|
||||||
|
|
||||||
|
<Card.Content>
|
||||||
|
<UserSelection
|
||||||
|
bind:selectedUserIds={userGroup.userIds}
|
||||||
|
selectionDisabled={!!userGroup.ldapId && $appConfigStore.ldapEnabled}
|
||||||
|
/>
|
||||||
|
<div class="mt-5 flex justify-end">
|
||||||
|
<Button
|
||||||
|
disabled={!!userGroup.ldapId && $appConfigStore.ldapEnabled}
|
||||||
|
onclick={() => updateUserGroupUsers(userGroup.userIds)}>{m.save()}</Button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</Card.Content>
|
||||||
|
</Card.Root>
|
||||||
|
</Tabs.Content>
|
||||||
|
|
||||||
|
<Tabs.Content value="clients" id="user-group-oidc-clients">
|
||||||
|
<Card.Root>
|
||||||
|
<Card.Header>
|
||||||
|
<Card.Title>{m.allowed_oidc_clients()}</Card.Title>
|
||||||
|
<Card.Description>{m.allowed_oidc_clients_description()}</Card.Description>
|
||||||
|
</Card.Header>
|
||||||
|
<Card.Content>
|
||||||
|
<OidcClientSelection
|
||||||
|
bind:this={oidcClientSelectionRef}
|
||||||
|
bind:selectedGroupIds={userGroup.allowedOidcClientIds}
|
||||||
|
/>
|
||||||
|
<div class="mt-5 flex justify-end gap-3">
|
||||||
|
<Button onclick={() => updateAllowedOidcClients(userGroup.allowedOidcClientIds)}
|
||||||
|
>{m.save()}</Button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</Card.Content>
|
||||||
|
</Card.Root>
|
||||||
|
</Tabs.Content>
|
||||||
|
|
||||||
|
<Tabs.Content value="custom-claims" id="user-group-custom-claims">
|
||||||
|
<Card.Root>
|
||||||
|
<Card.Header>
|
||||||
|
<Card.Title>{m.custom_claims()}</Card.Title>
|
||||||
|
<Card.Description>
|
||||||
|
{m.custom_claims_are_key_value_pairs_that_can_be_used_to_store_additional_information_about_a_user_prioritized()}
|
||||||
|
</Card.Description>
|
||||||
|
</Card.Header>
|
||||||
|
<Card.Content>
|
||||||
|
<CustomClaimsInput bind:customClaims={userGroup.customClaims} />
|
||||||
|
<div class="mt-5 flex justify-end">
|
||||||
|
<Button onclick={updateCustomClaims} type="submit">{m.save()}</Button>
|
||||||
|
</div>
|
||||||
|
</Card.Content>
|
||||||
|
</Card.Root>
|
||||||
|
</Tabs.Content>
|
||||||
|
</Tabs.Root>
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import CollapsibleCard from '$lib/components/collapsible-card.svelte';
|
|
||||||
import CustomClaimsInput from '$lib/components/form/custom-claims-input.svelte';
|
import CustomClaimsInput from '$lib/components/form/custom-claims-input.svelte';
|
||||||
import ProfilePictureSettings from '$lib/components/form/profile-picture-settings.svelte';
|
import ProfilePictureSettings from '$lib/components/form/profile-picture-settings.svelte';
|
||||||
import Badge from '$lib/components/ui/badge/badge.svelte';
|
import Badge from '$lib/components/ui/badge/badge.svelte';
|
||||||
import { Button } from '$lib/components/ui/button';
|
import { Button } from '$lib/components/ui/button';
|
||||||
import * as Card from '$lib/components/ui/card';
|
import * as Card from '$lib/components/ui/card';
|
||||||
import * as Item from '$lib/components/ui/item/index.js';
|
import * as Item from '$lib/components/ui/item/index.js';
|
||||||
|
import * as Tabs from '$lib/components/ui/tabs';
|
||||||
import UserGroupSelection from '$lib/components/user-group-selection.svelte';
|
import UserGroupSelection from '$lib/components/user-group-selection.svelte';
|
||||||
import { m } from '$lib/paraglide/messages';
|
import { m } from '$lib/paraglide/messages';
|
||||||
import CustomClaimService from '$lib/services/custom-claim-service';
|
import CustomClaimService from '$lib/services/custom-claim-service';
|
||||||
@@ -94,70 +94,95 @@
|
|||||||
<Badge class="rounded-full" variant="default">{m.ldap()}</Badge>
|
<Badge class="rounded-full" variant="default">{m.ldap()}</Badge>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<Card.Root>
|
<Tabs.Root value="general" useHash class="gap-4">
|
||||||
<Card.Header>
|
<div class="overflow-x-auto pb-1">
|
||||||
<Card.Title>{m.general()}</Card.Title>
|
<Tabs.List variant="line" class="min-w-max">
|
||||||
</Card.Header>
|
<Tabs.Trigger value="general">{m.general()}</Tabs.Trigger>
|
||||||
<Card.Content>
|
<Tabs.Trigger value="groups">{m.user_groups()}</Tabs.Trigger>
|
||||||
<UserForm existingUser={user} callback={updateUser} />
|
<Tabs.Trigger value="passkeys">{m.passkeys()}</Tabs.Trigger>
|
||||||
</Card.Content>
|
<Tabs.Trigger value="custom-claims">{m.custom_claims()}</Tabs.Trigger>
|
||||||
</Card.Root>
|
</Tabs.List>
|
||||||
|
|
||||||
<Card.Root>
|
|
||||||
<Card.Content>
|
|
||||||
<ProfilePictureSettings
|
|
||||||
userId={user.id}
|
|
||||||
isLdapUser={!!user.ldapId}
|
|
||||||
updateCallback={updateProfilePicture}
|
|
||||||
resetCallback={resetProfilePicture}
|
|
||||||
/>
|
|
||||||
</Card.Content>
|
|
||||||
</Card.Root>
|
|
||||||
|
|
||||||
<CollapsibleCard
|
|
||||||
id="user-groups"
|
|
||||||
title={m.user_groups()}
|
|
||||||
description={m.manage_which_groups_this_user_belongs_to()}
|
|
||||||
>
|
|
||||||
<UserGroupSelection
|
|
||||||
bind:selectedGroupIds={user.userGroupIds}
|
|
||||||
selectionDisabled={!!user.ldapId && $appConfigStore.ldapEnabled}
|
|
||||||
/>
|
|
||||||
<div class="mt-5 flex justify-end">
|
|
||||||
<Button
|
|
||||||
onclick={() => updateUserGroups(user.userGroupIds)}
|
|
||||||
disabled={!!user.ldapId && $appConfigStore.ldapEnabled}
|
|
||||||
type="submit">{m.save()}</Button
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
</CollapsibleCard>
|
|
||||||
|
|
||||||
<Item.Group class="bg-card border shadow-sm rounded-4xl p-5">
|
<Tabs.Content value="general" class="flex flex-col gap-4">
|
||||||
<Item.Root class="border-none bg-transparent p-0">
|
<Card.Root>
|
||||||
<Item.Media class="text-primary/80">
|
<Card.Header>
|
||||||
<KeyRound class="size-5" />
|
<Card.Title>{m.general()}</Card.Title>
|
||||||
</Item.Media>
|
</Card.Header>
|
||||||
<Item.Content class="min-w-52">
|
<Card.Content>
|
||||||
<Item.Title class="text-xl font-semibold">{m.passkeys()}</Item.Title>
|
<UserForm existingUser={user} callback={updateUser} />
|
||||||
<Item.Description
|
</Card.Content>
|
||||||
>{passkeys.length > 0
|
</Card.Root>
|
||||||
? m.manage_this_users_passkeys()
|
|
||||||
: m.user_has_no_passkeys_yet()}</Item.Description
|
|
||||||
>
|
|
||||||
</Item.Content>
|
|
||||||
</Item.Root>
|
|
||||||
{#if passkeys.length > 0}
|
|
||||||
<AdminPasskeyList userId={user.id} bind:passkeys />
|
|
||||||
{/if}
|
|
||||||
</Item.Group>
|
|
||||||
|
|
||||||
<CollapsibleCard
|
<Card.Root>
|
||||||
id="user-custom-claims"
|
<Card.Content>
|
||||||
title={m.custom_claims()}
|
<ProfilePictureSettings
|
||||||
description={m.custom_claims_are_key_value_pairs_that_can_be_used_to_store_additional_information_about_a_user()}
|
userId={user.id}
|
||||||
>
|
isLdapUser={!!user.ldapId}
|
||||||
<CustomClaimsInput bind:customClaims={user.customClaims} />
|
updateCallback={updateProfilePicture}
|
||||||
<div class="mt-5 flex justify-end">
|
resetCallback={resetProfilePicture}
|
||||||
<Button onclick={updateCustomClaims} type="submit">{m.save()}</Button>
|
/>
|
||||||
</div>
|
</Card.Content>
|
||||||
</CollapsibleCard>
|
</Card.Root>
|
||||||
|
</Tabs.Content>
|
||||||
|
|
||||||
|
<Tabs.Content value="groups" id="user-groups">
|
||||||
|
<Card.Root>
|
||||||
|
<Card.Header>
|
||||||
|
<Card.Title>{m.user_groups()}</Card.Title>
|
||||||
|
<Card.Description>{m.manage_which_groups_this_user_belongs_to()}</Card.Description>
|
||||||
|
</Card.Header>
|
||||||
|
<Card.Content>
|
||||||
|
<UserGroupSelection
|
||||||
|
bind:selectedGroupIds={user.userGroupIds}
|
||||||
|
selectionDisabled={!!user.ldapId && $appConfigStore.ldapEnabled}
|
||||||
|
/>
|
||||||
|
<div class="mt-5 flex justify-end">
|
||||||
|
<Button
|
||||||
|
onclick={() => updateUserGroups(user.userGroupIds)}
|
||||||
|
disabled={!!user.ldapId && $appConfigStore.ldapEnabled}
|
||||||
|
type="submit">{m.save()}</Button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</Card.Content>
|
||||||
|
</Card.Root>
|
||||||
|
</Tabs.Content>
|
||||||
|
|
||||||
|
<Tabs.Content value="passkeys">
|
||||||
|
<Item.Group class="bg-card rounded-4xl border p-5 shadow-sm">
|
||||||
|
<Item.Root class="border-none bg-transparent p-0">
|
||||||
|
<Item.Media class="text-primary/80">
|
||||||
|
<KeyRound class="size-5" />
|
||||||
|
</Item.Media>
|
||||||
|
<Item.Content class="min-w-52">
|
||||||
|
<Item.Title class="text-xl font-semibold">{m.passkeys()}</Item.Title>
|
||||||
|
<Item.Description
|
||||||
|
>{passkeys.length > 0
|
||||||
|
? m.manage_this_users_passkeys()
|
||||||
|
: m.user_has_no_passkeys_yet()}</Item.Description
|
||||||
|
>
|
||||||
|
</Item.Content>
|
||||||
|
</Item.Root>
|
||||||
|
{#if passkeys.length > 0}
|
||||||
|
<AdminPasskeyList userId={user.id} bind:passkeys />
|
||||||
|
{/if}
|
||||||
|
</Item.Group>
|
||||||
|
</Tabs.Content>
|
||||||
|
|
||||||
|
<Tabs.Content value="custom-claims" id="user-custom-claims">
|
||||||
|
<Card.Root>
|
||||||
|
<Card.Header>
|
||||||
|
<Card.Title>{m.custom_claims()}</Card.Title>
|
||||||
|
<Card.Description>
|
||||||
|
{m.custom_claims_are_key_value_pairs_that_can_be_used_to_store_additional_information_about_a_user()}
|
||||||
|
</Card.Description>
|
||||||
|
</Card.Header>
|
||||||
|
<Card.Content>
|
||||||
|
<CustomClaimsInput bind:customClaims={user.customClaims} />
|
||||||
|
<div class="mt-5 flex justify-end">
|
||||||
|
<Button onclick={updateCustomClaims} type="submit">{m.save()}</Button>
|
||||||
|
</div>
|
||||||
|
</Card.Content>
|
||||||
|
</Card.Root>
|
||||||
|
</Tabs.Content>
|
||||||
|
</Tabs.Root>
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Card.Root
|
<Card.Root
|
||||||
class="border-muted group relative h-[160px] p-5 transition-all duration-200 hover:shadow-md sm:max-w-[50vw] md:max-w-[400px]"
|
class="border-muted group relative h-[160px] p-5 transition-all duration-200 hover:shadow-md sm:max-w-[50vw] md:max-w-[430px]"
|
||||||
data-testid="authorized-oidc-client-card"
|
data-testid="authorized-oidc-client-card"
|
||||||
>
|
>
|
||||||
<Card.Content class=" p-0">
|
<Card.Content class=" p-0">
|
||||||
|
|||||||
@@ -108,8 +108,8 @@ test('Grant a client user-delegated and client access to API permissions', async
|
|||||||
// Nextcloud has no API access granted by default
|
// Nextcloud has no API access granted by default
|
||||||
await page.goto(`/settings/admin/oidc-clients/${oidcClients.nextcloud.id}`);
|
await page.goto(`/settings/admin/oidc-clients/${oidcClients.nextcloud.id}`);
|
||||||
|
|
||||||
// Expand the API access card, then edit the Orders API row
|
// Open the API access tab, then edit the Orders API row
|
||||||
await page.getByText('API access', { exact: true }).click();
|
await page.getByRole('tab', { name: 'API access' }).click();
|
||||||
await page
|
await page
|
||||||
.getByRole('row', { name: apis.orders.name })
|
.getByRole('row', { name: apis.orders.name })
|
||||||
.getByRole('button', { name: 'Edit' })
|
.getByRole('button', { name: 'Edit' })
|
||||||
|
|||||||
@@ -30,14 +30,14 @@ test('Update general configuration', async ({ page }) => {
|
|||||||
|
|
||||||
test.describe('Update user creation configuration', () => {
|
test.describe('Update user creation configuration', () => {
|
||||||
test.beforeEach(async ({ page }) => {
|
test.beforeEach(async ({ page }) => {
|
||||||
await page.getByRole('button', { name: 'Expand card' }).nth(1).click();
|
await page.getByRole('tab', { name: 'User Creation' }).click();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should save sign up mode', async ({ page }) => {
|
test('should save sign up mode', async ({ page }) => {
|
||||||
await page.getByRole('button', { name: 'Enable User Signups' }).click();
|
await page.getByRole('button', { name: 'Enable User Signups' }).click();
|
||||||
await page.getByRole('option', { name: 'Open Signup' }).click();
|
await page.getByRole('option', { name: 'Open Signup' }).click();
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Save' }).nth(1).click();
|
await page.getByRole('button', { name: 'Save' }).click();
|
||||||
|
|
||||||
await expect(page.locator('[data-type="success"]').last()).toHaveText(
|
await expect(page.locator('[data-type="success"]').last()).toHaveText(
|
||||||
'User creation settings updated successfully.'
|
'User creation settings updated successfully.'
|
||||||
@@ -59,7 +59,7 @@ test.describe('Update user creation configuration', () => {
|
|||||||
await expect(designersOption).toBeChecked();
|
await expect(designersOption).toBeChecked();
|
||||||
await page.keyboard.press('Escape');
|
await page.keyboard.press('Escape');
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Save' }).nth(1).click();
|
await page.getByRole('button', { name: 'Save' }).click();
|
||||||
|
|
||||||
await expect(page.locator('[data-type="success"]').last()).toHaveText(
|
await expect(page.locator('[data-type="success"]').last()).toHaveText(
|
||||||
'User creation settings updated successfully.'
|
'User creation settings updated successfully.'
|
||||||
@@ -81,7 +81,7 @@ test.describe('Update user creation configuration', () => {
|
|||||||
await page.getByPlaceholder('Key').nth(1).fill('another-claim');
|
await page.getByPlaceholder('Key').nth(1).fill('another-claim');
|
||||||
await page.getByPlaceholder('Value').nth(1).fill('another-value');
|
await page.getByPlaceholder('Value').nth(1).fill('another-value');
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Save' }).nth(1).click();
|
await page.getByRole('button', { name: 'Save' }).click();
|
||||||
|
|
||||||
await expect(page.locator('[data-type="success"]').last()).toHaveText(
|
await expect(page.locator('[data-type="success"]').last()).toHaveText(
|
||||||
'User creation settings updated successfully.'
|
'User creation settings updated successfully.'
|
||||||
@@ -97,7 +97,7 @@ test.describe('Update user creation configuration', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('Update email configuration', async ({ page }) => {
|
test('Update email configuration', async ({ page }) => {
|
||||||
await page.getByRole('button', { name: 'Expand card' }).nth(2).click();
|
await page.getByRole('tab', { name: 'Email' }).click();
|
||||||
|
|
||||||
await page.getByLabel('SMTP Host').fill('smtp.gmail.com');
|
await page.getByLabel('SMTP Host').fill('smtp.gmail.com');
|
||||||
await page.getByLabel('SMTP Port').fill('587');
|
await page.getByLabel('SMTP Port').fill('587');
|
||||||
@@ -109,7 +109,7 @@ test('Update email configuration', async ({ page }) => {
|
|||||||
await page.getByLabel('Email Login Code from Admin').click();
|
await page.getByLabel('Email Login Code from Admin').click();
|
||||||
await page.getByLabel('API Key Expiration').click();
|
await page.getByLabel('API Key Expiration').click();
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Save' }).nth(1).click();
|
await page.getByRole('button', { name: 'Save' }).click();
|
||||||
|
|
||||||
await expect(page.locator('[data-type="success"]')).toHaveText(
|
await expect(page.locator('[data-type="success"]')).toHaveText(
|
||||||
'Email configuration updated successfully'
|
'Email configuration updated successfully'
|
||||||
@@ -130,7 +130,7 @@ test('Update email configuration', async ({ page }) => {
|
|||||||
|
|
||||||
test.describe('Update application images', () => {
|
test.describe('Update application images', () => {
|
||||||
test.beforeEach(async ({ page }) => {
|
test.beforeEach(async ({ page }) => {
|
||||||
await page.getByRole('button', { name: 'Expand card' }).nth(4).click();
|
await page.getByRole('tab', { name: 'Images' }).click();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should upload images', async ({ page }) => {
|
test('should upload images', async ({ page }) => {
|
||||||
@@ -144,7 +144,7 @@ test.describe('Update application images', () => {
|
|||||||
.getByLabel('Default Profile Picture')
|
.getByLabel('Default Profile Picture')
|
||||||
.setInputFiles('resources/images/pingvin-share-logo.png');
|
.setInputFiles('resources/images/pingvin-share-logo.png');
|
||||||
await page.getByLabel('Background Image').setInputFiles('resources/images/clouds.jpg');
|
await page.getByLabel('Background Image').setInputFiles('resources/images/clouds.jpg');
|
||||||
await page.getByRole('button', { name: 'Save' }).last().click();
|
await page.getByRole('button', { name: 'Save' }).click();
|
||||||
|
|
||||||
await expect(page.locator('[data-type="success"]')).toHaveText(
|
await expect(page.locator('[data-type="success"]')).toHaveText(
|
||||||
'Images updated successfully. It may take a few minutes to update.'
|
'Images updated successfully. It may take a few minutes to update.'
|
||||||
@@ -171,7 +171,7 @@ test.describe('Update application images', () => {
|
|||||||
const emailLogoInput = page.getByLabel('Email Logo');
|
const emailLogoInput = page.getByLabel('Email Logo');
|
||||||
|
|
||||||
await emailLogoInput.setInputFiles('resources/images/cloud-logo.svg');
|
await emailLogoInput.setInputFiles('resources/images/cloud-logo.svg');
|
||||||
await page.getByRole('button', { name: 'Save' }).last().click();
|
await page.getByRole('button', { name: 'Save' }).click();
|
||||||
|
|
||||||
await expect(page.locator('[data-type="error"]')).toHaveText(
|
await expect(page.locator('[data-type="error"]')).toHaveText(
|
||||||
'File must be of type .png or .jpg/jpeg'
|
'File must be of type .png or .jpg/jpeg'
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ test.describe('LDAP Integration', () => {
|
|||||||
test('LDAP configuration is working properly', async ({ page }) => {
|
test('LDAP configuration is working properly', async ({ page }) => {
|
||||||
await page.goto('/settings/admin/application-configuration');
|
await page.goto('/settings/admin/application-configuration');
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Expand card' }).nth(3).click();
|
await page.getByRole('tab', { name: 'LDAP' }).click();
|
||||||
|
|
||||||
await expect(page.getByRole('button', { name: 'Disable', exact: true })).toBeVisible();
|
await expect(page.getByRole('button', { name: 'Disable', exact: true })).toBeVisible();
|
||||||
await expect(page.getByLabel('LDAP URL')).toHaveValue(/ldap:\/\/.*/);
|
await expect(page.getByLabel('LDAP URL')).toHaveValue(/ldap:\/\/.*/);
|
||||||
|
|||||||
@@ -161,13 +161,14 @@ test('Filter OIDC clients by PAR requirement', async ({ page, request }) => {
|
|||||||
|
|
||||||
test('Update OIDC client allowed user groups', async ({ page }) => {
|
test('Update OIDC client allowed user groups', async ({ page }) => {
|
||||||
await page.goto(`/settings/admin/oidc-clients/${oidcClients.nextcloud.id}`);
|
await page.goto(`/settings/admin/oidc-clients/${oidcClients.nextcloud.id}`);
|
||||||
|
await page.getByRole('tab', { name: 'Allowed user groups' }).click();
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Restrict' }).click();
|
await page.getByRole('button', { name: 'Restrict' }).click();
|
||||||
|
|
||||||
await page.getByRole('row', { name: userGroups.designers.name }).getByRole('checkbox').click();
|
await page.getByRole('row', { name: userGroups.designers.name }).getByRole('checkbox').click();
|
||||||
await page.getByRole('row', { name: userGroups.developers.name }).getByRole('checkbox').click();
|
await page.getByRole('row', { name: userGroups.developers.name }).getByRole('checkbox').click();
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Save' }).nth(1).click();
|
await page.getByRole('button', { name: 'Save' }).click();
|
||||||
|
|
||||||
await expect(page.getByText('Allowed user groups updated successfully')).toBeVisible();
|
await expect(page.getByText('Allowed user groups updated successfully')).toBeVisible();
|
||||||
|
|
||||||
|
|||||||
@@ -1520,7 +1520,9 @@ test.describe('OIDC skip consent', () => {
|
|||||||
|
|
||||||
// Disabling it and saving must persist across a reload
|
// Disabling it and saving must persist across a reload
|
||||||
await toggle.click();
|
await toggle.click();
|
||||||
|
await expect(toggle).not.toBeChecked();
|
||||||
await page.getByRole('button', { name: /save/i }).click();
|
await page.getByRole('button', { name: /save/i }).click();
|
||||||
|
await expect(page.getByText('OIDC client updated successfully', { exact: true })).toBeVisible();
|
||||||
await page.reload();
|
await page.reload();
|
||||||
|
|
||||||
await expect(page.getByRole('switch', { name: 'Skip Consent Screen' })).not.toBeChecked();
|
await expect(page.getByRole('switch', { name: 'Skip Consent Screen' })).not.toBeChecked();
|
||||||
|
|||||||
+14
-10
@@ -5,7 +5,7 @@ import { oidcClients, userGroups, users } from '../data';
|
|||||||
async function configureOidcClient(page: Page) {
|
async function configureOidcClient(page: Page) {
|
||||||
await page.goto(`/settings/admin/oidc-clients/${oidcClients.scim.id}`);
|
await page.goto(`/settings/admin/oidc-clients/${oidcClients.scim.id}`);
|
||||||
|
|
||||||
await page.getByText('SCIM Provisioning', { exact: true }).click();
|
await page.getByRole('tab', { name: 'SCIM Provisioning' }).click();
|
||||||
|
|
||||||
await page
|
await page
|
||||||
.getByLabel('SCIM Endpoint')
|
.getByLabel('SCIM Endpoint')
|
||||||
@@ -16,6 +16,7 @@ async function configureOidcClient(page: Page) {
|
|||||||
|
|
||||||
async function syncScimServiceProvider(page: Page) {
|
async function syncScimServiceProvider(page: Page) {
|
||||||
await page.goto(`/settings/admin/oidc-clients/${oidcClients.scim.id}`);
|
await page.goto(`/settings/admin/oidc-clients/${oidcClients.scim.id}`);
|
||||||
|
await page.getByRole('tab', { name: 'SCIM Provisioning' }).click();
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Sync now' }).click();
|
await page.getByRole('button', { name: 'Sync now' }).click();
|
||||||
await page.waitForSelector('[data-type="success"]');
|
await page.waitForSelector('[data-type="success"]');
|
||||||
@@ -29,7 +30,7 @@ test.describe('SCIM Configuration', () => {
|
|||||||
test('Enable SCIM for OIDC client', async ({ page }) => {
|
test('Enable SCIM for OIDC client', async ({ page }) => {
|
||||||
await page.goto(`/settings/admin/oidc-clients/${oidcClients.scim.id}`);
|
await page.goto(`/settings/admin/oidc-clients/${oidcClients.scim.id}`);
|
||||||
|
|
||||||
await page.getByText('SCIM Provisioning', { exact: true }).click();
|
await page.getByRole('tab', { name: 'SCIM Provisioning' }).click();
|
||||||
|
|
||||||
await page.getByLabel('SCIM Endpoint').fill('http://scim.provider/api');
|
await page.getByLabel('SCIM Endpoint').fill('http://scim.provider/api');
|
||||||
await page.getByLabel('SCIM Token').fill('supersecrettoken');
|
await page.getByLabel('SCIM Token').fill('supersecrettoken');
|
||||||
@@ -48,11 +49,12 @@ test.describe('SCIM Configuration', () => {
|
|||||||
await configureOidcClient(page);
|
await configureOidcClient(page);
|
||||||
|
|
||||||
await page.goto(`/settings/admin/oidc-clients/${oidcClients.scim.id}`);
|
await page.goto(`/settings/admin/oidc-clients/${oidcClients.scim.id}`);
|
||||||
|
await page.getByRole('tab', { name: 'SCIM Provisioning' }).click();
|
||||||
|
|
||||||
await page.getByLabel('SCIM Endpoint').fill('http://new.scim.provider/api');
|
await page.getByLabel('SCIM Endpoint').fill('http://new.scim.provider/api');
|
||||||
await page.getByLabel('SCIM Token').fill('evenmoresecrettoken');
|
await page.getByLabel('SCIM Token').fill('evenmoresecrettoken');
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Save' }).nth(1).click();
|
await page.getByRole('button', { name: 'Save' }).click();
|
||||||
|
|
||||||
await expect(page.locator('[data-type="success"]')).toHaveText(
|
await expect(page.locator('[data-type="success"]')).toHaveText(
|
||||||
'SCIM configuration updated successfully.'
|
'SCIM configuration updated successfully.'
|
||||||
@@ -68,6 +70,7 @@ test.describe('SCIM Configuration', () => {
|
|||||||
await configureOidcClient(page);
|
await configureOidcClient(page);
|
||||||
|
|
||||||
await page.goto(`/settings/admin/oidc-clients/${oidcClients.scim.id}`);
|
await page.goto(`/settings/admin/oidc-clients/${oidcClients.scim.id}`);
|
||||||
|
await page.getByRole('tab', { name: 'SCIM Provisioning' }).click();
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Disable' }).click();
|
await page.getByRole('button', { name: 'Disable' }).click();
|
||||||
await page.getByRole('button', { name: 'Disable' }).nth(1).click();
|
await page.getByRole('button', { name: 'Disable' }).nth(1).click();
|
||||||
@@ -123,15 +126,16 @@ test.describe('SCIM Sync', () => {
|
|||||||
test('Remove allowed group and sync', async ({ page }) => {
|
test('Remove allowed group and sync', async ({ page }) => {
|
||||||
await syncScimServiceProvider(page);
|
await syncScimServiceProvider(page);
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Expand card' }).first().click();
|
await page.getByRole('tab', { name: 'Allowed user groups' }).click();
|
||||||
|
|
||||||
await page
|
const developersCheckbox = page
|
||||||
.getByRole('row', { name: userGroups.developers.name })
|
.getByRole('row', { name: userGroups.developers.name })
|
||||||
.getByRole('cell')
|
.getByRole('checkbox');
|
||||||
.first()
|
await developersCheckbox.click();
|
||||||
.click();
|
await expect(developersCheckbox).toHaveAttribute('data-state', 'unchecked');
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Save' }).nth(1).click();
|
await page.getByRole('button', { name: 'Save' }).click();
|
||||||
|
await expect(page.getByText('Allowed user groups updated successfully', { exact: true })).toBeVisible();
|
||||||
|
|
||||||
await syncScimServiceProvider(page);
|
await syncScimServiceProvider(page);
|
||||||
|
|
||||||
@@ -149,7 +153,7 @@ test.describe('SCIM Sync', () => {
|
|||||||
test('Remove group restrictions and sync', async ({ page }) => {
|
test('Remove group restrictions and sync', async ({ page }) => {
|
||||||
await syncScimServiceProvider(page);
|
await syncScimServiceProvider(page);
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Expand card' }).first().click();
|
await page.getByRole('tab', { name: 'Allowed user groups' }).click();
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Unrestrict' }).click();
|
await page.getByRole('button', { name: 'Unrestrict' }).click();
|
||||||
await page.getByRole('button', { name: 'Unrestrict' }).nth(1).click();
|
await page.getByRole('button', { name: 'Unrestrict' }).nth(1).click();
|
||||||
|
|||||||
@@ -44,11 +44,12 @@ test('Edit user group', async ({ page }) => {
|
|||||||
test('Update user group users', async ({ page }) => {
|
test('Update user group users', async ({ page }) => {
|
||||||
const group = userGroups.designers;
|
const group = userGroups.designers;
|
||||||
await page.goto(`/settings/admin/user-groups/${group.id}`);
|
await page.goto(`/settings/admin/user-groups/${group.id}`);
|
||||||
|
await page.getByRole('tab', { name: 'Users' }).click();
|
||||||
|
|
||||||
await page.getByRole('row', { name: users.tim.username }).getByRole('checkbox').click();
|
await page.getByRole('row', { name: users.tim.username }).getByRole('checkbox').click();
|
||||||
await page.getByRole('row', { name: users.craig.username }).getByRole('checkbox').click();
|
await page.getByRole('row', { name: users.craig.username }).getByRole('checkbox').click();
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Save' }).nth(1).click();
|
await page.getByRole('button', { name: 'Save' }).click();
|
||||||
|
|
||||||
await expect(page.locator('[data-type="success"]')).toHaveText('Users updated successfully');
|
await expect(page.locator('[data-type="success"]')).toHaveText('Users updated successfully');
|
||||||
|
|
||||||
@@ -77,7 +78,7 @@ test('Delete user group', async ({ page }) => {
|
|||||||
test('Update user group custom claims', async ({ page }) => {
|
test('Update user group custom claims', async ({ page }) => {
|
||||||
await page.goto(`/settings/admin/user-groups/${userGroups.designers.id}`);
|
await page.goto(`/settings/admin/user-groups/${userGroups.designers.id}`);
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Expand card' }).first().click();
|
await page.getByRole('tab', { name: 'Custom Claims' }).click();
|
||||||
|
|
||||||
// Add two custom claims
|
// Add two custom claims
|
||||||
await page.getByRole('button', { name: 'Add custom claim' }).click();
|
await page.getByRole('button', { name: 'Add custom claim' }).click();
|
||||||
@@ -89,7 +90,7 @@ test('Update user group custom claims', async ({ page }) => {
|
|||||||
await page.getByPlaceholder('Key').nth(1).fill('customClaim2');
|
await page.getByPlaceholder('Key').nth(1).fill('customClaim2');
|
||||||
await page.getByPlaceholder('Value').nth(1).fill('customClaim2_value');
|
await page.getByPlaceholder('Value').nth(1).fill('customClaim2_value');
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Save' }).nth(2).click();
|
await page.getByRole('button', { name: 'Save' }).click();
|
||||||
|
|
||||||
await expect(page.locator('[data-type="success"]')).toHaveText(
|
await expect(page.locator('[data-type="success"]')).toHaveText(
|
||||||
'Custom claims updated successfully'
|
'Custom claims updated successfully'
|
||||||
@@ -106,7 +107,7 @@ test('Update user group custom claims', async ({ page }) => {
|
|||||||
|
|
||||||
// Remove one custom claim
|
// Remove one custom claim
|
||||||
await page.getByLabel('Remove custom claim').first().click();
|
await page.getByLabel('Remove custom claim').first().click();
|
||||||
await page.getByRole('button', { name: 'Save' }).nth(2).click();
|
await page.getByRole('button', { name: 'Save' }).click();
|
||||||
|
|
||||||
await expect(page.locator('[data-type="success"]')).toHaveText(
|
await expect(page.locator('[data-type="success"]')).toHaveText(
|
||||||
'Custom claims updated successfully'
|
'Custom claims updated successfully'
|
||||||
@@ -123,7 +124,7 @@ test('Update user group custom claims', async ({ page }) => {
|
|||||||
test('Update user group allowed user groups', async ({ page }) => {
|
test('Update user group allowed user groups', async ({ page }) => {
|
||||||
await page.goto(`/settings/admin/user-groups/${userGroups.designers.id}`);
|
await page.goto(`/settings/admin/user-groups/${userGroups.designers.id}`);
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Expand card' }).nth(1).click();
|
await page.getByRole('tab', { name: 'Allowed OIDC Clients' }).click();
|
||||||
|
|
||||||
// Unrestricted OIDC clients should be checked and disabled
|
// Unrestricted OIDC clients should be checked and disabled
|
||||||
const nextcloudRow = page
|
const nextcloudRow = page
|
||||||
@@ -135,7 +136,7 @@ test('Update user group allowed user groups', async ({ page }) => {
|
|||||||
await page.getByRole('row', { name: oidcClients.tailscale.name }).getByRole('checkbox').click();
|
await page.getByRole('row', { name: oidcClients.tailscale.name }).getByRole('checkbox').click();
|
||||||
await page.getByRole('row', { name: oidcClients.immich.name }).getByRole('checkbox').click();
|
await page.getByRole('row', { name: oidcClients.immich.name }).getByRole('checkbox').click();
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Save' }).nth(2).click();
|
await page.getByRole('button', { name: 'Save' }).click();
|
||||||
|
|
||||||
await expect(page.locator('[data-type="success"]')).toHaveText(
|
await expect(page.locator('[data-type="success"]')).toHaveText(
|
||||||
'Allowed OIDC clients updated successfully'
|
'Allowed OIDC clients updated successfully'
|
||||||
|
|||||||
@@ -187,7 +187,7 @@ test('Update user fails with already taken username in different casing', async
|
|||||||
test('Update user custom claims', async ({ page }) => {
|
test('Update user custom claims', async ({ page }) => {
|
||||||
await page.goto(`/settings/admin/users/${users.craig.id}`);
|
await page.goto(`/settings/admin/users/${users.craig.id}`);
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Expand card' }).nth(1).click();
|
await page.getByRole('tab', { name: 'Custom Claims' }).click();
|
||||||
|
|
||||||
// Add two custom claims
|
// Add two custom claims
|
||||||
await page.getByRole('button', { name: 'Add custom claim' }).click();
|
await page.getByRole('button', { name: 'Add custom claim' }).click();
|
||||||
@@ -199,7 +199,7 @@ test('Update user custom claims', async ({ page }) => {
|
|||||||
await page.getByPlaceholder('Key').nth(1).fill('customClaim2');
|
await page.getByPlaceholder('Key').nth(1).fill('customClaim2');
|
||||||
await page.getByPlaceholder('Value').nth(1).fill('customClaim2_value');
|
await page.getByPlaceholder('Value').nth(1).fill('customClaim2_value');
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Save' }).nth(1).click();
|
await page.getByRole('button', { name: 'Save' }).click();
|
||||||
|
|
||||||
await expect(page.locator('[data-type="success"]')).toHaveText(
|
await expect(page.locator('[data-type="success"]')).toHaveText(
|
||||||
'Custom claims updated successfully'
|
'Custom claims updated successfully'
|
||||||
@@ -215,7 +215,7 @@ test('Update user custom claims', async ({ page }) => {
|
|||||||
|
|
||||||
// Remove one custom claim
|
// Remove one custom claim
|
||||||
await page.getByLabel('Remove custom claim').first().click();
|
await page.getByLabel('Remove custom claim').first().click();
|
||||||
await page.getByRole('button', { name: 'Save' }).nth(1).click();
|
await page.getByRole('button', { name: 'Save' }).click();
|
||||||
|
|
||||||
await expect(page.locator('[data-type="success"]')).toHaveText(
|
await expect(page.locator('[data-type="success"]')).toHaveText(
|
||||||
'Custom claims updated successfully'
|
'Custom claims updated successfully'
|
||||||
@@ -232,12 +232,12 @@ test('Update user group assignments', async ({ page }) => {
|
|||||||
const user = users.craig;
|
const user = users.craig;
|
||||||
await page.goto(`/settings/admin/users/${user.id}`);
|
await page.goto(`/settings/admin/users/${user.id}`);
|
||||||
|
|
||||||
page.getByRole('button', { name: 'Expand card' }).first().click();
|
await page.getByRole('tab', { name: 'User Groups' }).click();
|
||||||
|
|
||||||
await page.getByRole('row', { name: userGroups.developers.name }).getByRole('checkbox').click();
|
await page.getByRole('row', { name: userGroups.developers.name }).getByRole('checkbox').click();
|
||||||
await page.getByRole('row', { name: userGroups.designers.name }).getByRole('checkbox').click();
|
await page.getByRole('row', { name: userGroups.designers.name }).getByRole('checkbox').click();
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Save' }).nth(1).click();
|
await page.getByRole('button', { name: 'Save' }).click();
|
||||||
|
|
||||||
await expect(page.locator('[data-type="success"]')).toHaveText(
|
await expect(page.locator('[data-type="success"]')).toHaveText(
|
||||||
'User groups updated successfully'
|
'User groups updated successfully'
|
||||||
@@ -255,6 +255,7 @@ test('Update user group assignments', async ({ page }) => {
|
|||||||
|
|
||||||
test('Admin can view another user passkeys', async ({ page }) => {
|
test('Admin can view another user passkeys', async ({ page }) => {
|
||||||
await page.goto(`/settings/admin/users/${users.craig.id}`);
|
await page.goto(`/settings/admin/users/${users.craig.id}`);
|
||||||
|
await page.getByRole('tab', { name: 'Passkeys' }).click();
|
||||||
|
|
||||||
await expect(page.getByText('Passkey 2')).toBeVisible();
|
await expect(page.getByText('Passkey 2')).toBeVisible();
|
||||||
await expect(page.getByText(/Added on/)).toBeVisible();
|
await expect(page.getByText(/Added on/)).toBeVisible();
|
||||||
@@ -262,8 +263,13 @@ test('Admin can view another user passkeys', async ({ page }) => {
|
|||||||
|
|
||||||
test('Admin can delete another user passkey and audit log is created', async ({ page }) => {
|
test('Admin can delete another user passkey and audit log is created', async ({ page }) => {
|
||||||
await page.goto(`/settings/admin/users/${users.craig.id}`);
|
await page.goto(`/settings/admin/users/${users.craig.id}`);
|
||||||
|
await page.getByRole('tab', { name: 'Passkeys' }).click();
|
||||||
|
|
||||||
await page.locator('[data-slot="item"]').filter({ hasText: 'Passkey 2' }).getByLabel('Delete').click();
|
await page
|
||||||
|
.locator('[data-slot="item"]')
|
||||||
|
.filter({ hasText: 'Passkey 2' })
|
||||||
|
.getByLabel('Delete')
|
||||||
|
.click();
|
||||||
await page.getByRole('alertdialog').getByRole('button', { name: 'Delete' }).click();
|
await page.getByRole('alertdialog').getByRole('button', { name: 'Delete' }).click();
|
||||||
|
|
||||||
await expect(page.locator('[data-type="success"]')).toHaveText('Passkey deleted successfully');
|
await expect(page.locator('[data-type="success"]')).toHaveText('Passkey deleted successfully');
|
||||||
|
|||||||
@@ -10,10 +10,10 @@ async function setSignupMode(
|
|||||||
) {
|
) {
|
||||||
await page.goto('/settings/admin/application-configuration');
|
await page.goto('/settings/admin/application-configuration');
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Expand card' }).nth(1).click();
|
await page.getByRole('tab', { name: 'User Creation' }).click();
|
||||||
await page.getByRole('button', { name: 'Enable User Signups' }).click();
|
await page.getByRole('button', { name: 'Enable User Signups' }).click();
|
||||||
await page.getByRole('option', { name: mode }).click();
|
await page.getByRole('option', { name: mode }).click();
|
||||||
await page.getByRole('button', { name: 'Save' }).nth(1).click();
|
await page.getByRole('button', { name: 'Save' }).click();
|
||||||
|
|
||||||
await expect(page.locator('[data-type="success"]').last()).toHaveText(
|
await expect(page.locator('[data-type="success"]').last()).toHaveText(
|
||||||
'User creation settings updated successfully.'
|
'User creation settings updated successfully.'
|
||||||
|
|||||||
Reference in New Issue
Block a user