mirror of
https://github.com/pocket-id/pocket-id.git
synced 2026-07-16 15:59:53 +00:00
feat: add tab bar navigation for crowded pages
This commit is contained in:
@@ -28,7 +28,7 @@
|
||||
>
|
||||
<div
|
||||
class="{!isAuthPage
|
||||
? 'max-w-410'
|
||||
? 'max-w-[1720px]'
|
||||
: ''} mx-auto flex w-full items-center justify-between px-4 md:px-10"
|
||||
>
|
||||
<div class="flex h-16 items-center">
|
||||
|
||||
@@ -12,6 +12,9 @@
|
||||
<TabsPrimitive.Content
|
||||
bind:ref
|
||||
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}
|
||||
/>
|
||||
|
||||
@@ -19,22 +19,105 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { Tabs as TabsPrimitive } from 'bits-ui';
|
||||
import { onMount } from 'svelte';
|
||||
import { cn } from '$lib/utils/style.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
variant = 'default',
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: TabsPrimitive.ListProps & {
|
||||
variant?: TabsListVariant;
|
||||
} = $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>
|
||||
|
||||
<TabsPrimitive.List
|
||||
bind:ref
|
||||
data-slot="tabs-list"
|
||||
data-variant={variant}
|
||||
class={cn(tabsListVariants({ variant }), className)}
|
||||
class={cn(tabsListVariants({ variant }), variant === 'line' && 'relative', className)}
|
||||
{...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",
|
||||
'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',
|
||||
'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
|
||||
)}
|
||||
{...restProps}
|
||||
|
||||
@@ -1,18 +1,36 @@
|
||||
<script lang="ts">
|
||||
import { Tabs as TabsPrimitive } from 'bits-ui';
|
||||
import { page } from '$app/state';
|
||||
import { cn } from '$lib/utils/style.js';
|
||||
import { Tabs as TabsPrimitive } from 'bits-ui';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
value = $bindable(''),
|
||||
useHash = false,
|
||||
class: className,
|
||||
...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>
|
||||
|
||||
<TabsPrimitive.Root
|
||||
bind:ref
|
||||
bind:value
|
||||
onValueChange={onTabChange}
|
||||
data-slot="tabs"
|
||||
class={cn('gap-2 group/tabs flex data-[orientation=horizontal]:flex-col', className)}
|
||||
{...restProps}
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
>
|
||||
<main
|
||||
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 in:fly={{ x: -15, duration: 200 }} class="sticky top-6">
|
||||
|
||||
@@ -1,18 +1,14 @@
|
||||
<script lang="ts">
|
||||
import CollapsibleCard from '$lib/components/collapsible-card.svelte';
|
||||
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 AppConfigService from '$lib/services/app-config-service';
|
||||
import appConfigStore from '$lib/stores/application-configuration-store';
|
||||
import type { AllAppConfig } from '$lib/types/application-configuration.type';
|
||||
import { axiosErrorToast } from '$lib/utils/error-util';
|
||||
import {
|
||||
LucideImage,
|
||||
LucideInfo,
|
||||
Mail,
|
||||
SlidersHorizontal,
|
||||
UserSearch,
|
||||
Users
|
||||
LucideInfo
|
||||
} from '@lucide/svelte';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import AppConfigEmailForm from './forms/app-config-email-form.svelte';
|
||||
@@ -101,57 +97,85 @@
|
||||
</Alert.Description>
|
||||
</Alert.Root>
|
||||
{/if}
|
||||
<div>
|
||||
<CollapsibleCard
|
||||
id="application-configuration-general"
|
||||
icon={SlidersHorizontal}
|
||||
title={m.general()}
|
||||
defaultExpanded
|
||||
>
|
||||
<AppConfigGeneralForm {appConfig} callback={updateAppConfig} />
|
||||
</CollapsibleCard>
|
||||
</div>
|
||||
<Tabs.Root value="general" useHash class="gap-4">
|
||||
<div class="overflow-x-auto pb-1">
|
||||
<Tabs.List variant="line" class="min-w-max">
|
||||
<Tabs.Trigger value="general">
|
||||
{m.general()}
|
||||
</Tabs.Trigger>
|
||||
<Tabs.Trigger value="user-creation">
|
||||
{m.user_creation()}
|
||||
</Tabs.Trigger>
|
||||
<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>
|
||||
<CollapsibleCard
|
||||
id="application-configuration-signup-defaults"
|
||||
icon={Users}
|
||||
title={m.user_creation()}
|
||||
description={m.configure_user_creation()}
|
||||
>
|
||||
<AppConfigSignupDefaultsForm {appConfig} callback={updateAppConfig} />
|
||||
</CollapsibleCard>
|
||||
</div>
|
||||
<Tabs.Content value="general" id="application-configuration-general">
|
||||
<Card.Root>
|
||||
<Card.Header>
|
||||
<Card.Title>{m.general()}</Card.Title>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
<AppConfigGeneralForm {appConfig} callback={updateAppConfig} />
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
</Tabs.Content>
|
||||
|
||||
<div>
|
||||
<CollapsibleCard
|
||||
id="application-configuration-email"
|
||||
icon={Mail}
|
||||
title={m.email()}
|
||||
description={m.configure_smtp_to_send_emails()}
|
||||
>
|
||||
<AppConfigEmailForm {appConfig} callback={updateAppConfig} />
|
||||
</CollapsibleCard>
|
||||
</div>
|
||||
<Tabs.Content value="user-creation" id="application-configuration-signup-defaults">
|
||||
<Card.Root>
|
||||
<Card.Header>
|
||||
<Card.Title>{m.user_creation()}</Card.Title>
|
||||
<Card.Description>{m.configure_user_creation()}</Card.Description>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
<AppConfigSignupDefaultsForm {appConfig} callback={updateAppConfig} />
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
</Tabs.Content>
|
||||
|
||||
<div>
|
||||
<CollapsibleCard
|
||||
id="application-configuration-ldap"
|
||||
icon={UserSearch}
|
||||
title={m.ldap()}
|
||||
description={m.configure_ldap_settings_to_sync_users_and_groups_from_an_ldap_server()}
|
||||
>
|
||||
<AppConfigLdapForm {appConfig} callback={updateAppConfig} />
|
||||
</CollapsibleCard>
|
||||
</div>
|
||||
<Tabs.Content value="email" id="application-configuration-email">
|
||||
<Card.Root>
|
||||
<Card.Header>
|
||||
<Card.Title>{m.email()}</Card.Title>
|
||||
<Card.Description>{m.configure_smtp_to_send_emails()}</Card.Description>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
<AppConfigEmailForm {appConfig} callback={updateAppConfig} />
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
</Tabs.Content>
|
||||
|
||||
<div>
|
||||
<CollapsibleCard
|
||||
id="application-configuration-images"
|
||||
icon={LucideImage}
|
||||
title={m.images()}
|
||||
description={m.configure_application_images()}
|
||||
>
|
||||
<UpdateApplicationImages callback={updateImages} />
|
||||
</CollapsibleCard>
|
||||
</div>
|
||||
<Tabs.Content value="ldap" id="application-configuration-ldap">
|
||||
<Card.Root>
|
||||
<Card.Header>
|
||||
<Card.Title>{m.ldap()}</Card.Title>
|
||||
<Card.Description>
|
||||
{m.configure_ldap_settings_to_sync_users_and_groups_from_an_ldap_server()}
|
||||
</Card.Description>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
<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">
|
||||
import { beforeNavigate } from '$app/navigation';
|
||||
import { page } from '$app/state';
|
||||
import CollapsibleCard from '$lib/components/collapsible-card.svelte';
|
||||
import { openConfirmDialog } from '$lib/components/confirm-dialog';
|
||||
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 { Button } from '$lib/components/ui/button';
|
||||
import * as Card from '$lib/components/ui/card';
|
||||
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 { m } from '$lib/paraglide/messages';
|
||||
import OidcService from '$lib/services/oidc-service';
|
||||
@@ -17,7 +18,7 @@
|
||||
import type { ScimServiceProviderCreate } from '$lib/types/scim.type';
|
||||
import { cachedOidcClientLogo } from '$lib/utils/cached-image-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 { slide } from 'svelte/transition';
|
||||
import { backNavigate } from '../../users/navigate-back-util';
|
||||
@@ -230,122 +231,176 @@
|
||||
>
|
||||
</div>
|
||||
|
||||
<Card.Root>
|
||||
<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>
|
||||
</CopyToClipboard>
|
||||
</div>
|
||||
{#if !client.isPublic}
|
||||
<div class="mt-1 mb-2 flex flex-col sm:flex-row sm:items-center">
|
||||
<Field.Label class="w-52">{m.client_secret()}</Field.Label>
|
||||
{#if $clientSecretStore}
|
||||
<CopyToClipboard value={$clientSecretStore}>
|
||||
<span class="text-muted-foreground text-sm" data-testid="client-secret">
|
||||
{$clientSecretStore}
|
||||
<Tabs.Root value="general" useHash class="gap-4">
|
||||
<div class="overflow-x-auto pb-1">
|
||||
<Tabs.List variant="line" class="min-w-max">
|
||||
<Tabs.Trigger value="general">{m.general()}</Tabs.Trigger>
|
||||
<Tabs.Trigger value="user-groups">
|
||||
{m.allowed_user_groups()}
|
||||
{#if client.isGroupRestricted && client.allowedUserGroupIds.length === 0}
|
||||
<LucideTriangleAlert class="ml-0.5 size-4 text-yellow-600 dark:text-yellow-400" />
|
||||
{/if}</Tabs.Trigger
|
||||
>
|
||||
<Tabs.Trigger value="api-access">{m.api_access()}</Tabs.Trigger>
|
||||
<Tabs.Trigger value="scim">{m.scim_provisioning()}</Tabs.Trigger>
|
||||
<Tabs.Trigger value="preview">{m.oidc_data_preview()}</Tabs.Trigger>
|
||||
</Tabs.List>
|
||||
</div>
|
||||
|
||||
<Tabs.Content value="general" class="flex flex-col gap-4">
|
||||
<Card.Root>
|
||||
<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>
|
||||
</CopyToClipboard>
|
||||
{: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 !client.isPublic}
|
||||
<div class="mt-1 mb-2 flex flex-col sm:flex-row sm:items-center">
|
||||
<Field.Label class="w-52">{m.client_secret()}</Field.Label>
|
||||
{#if $clientSecretStore}
|
||||
<CopyToClipboard value={$clientSecretStore}>
|
||||
<span class="text-muted-foreground text-sm" data-testid="client-secret">
|
||||
{$clientSecretStore}
|
||||
</span>
|
||||
</CopyToClipboard>
|
||||
{: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>
|
||||
{/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}
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
{#if !showAllDetails}
|
||||
<div class="mt-4 flex justify-center">
|
||||
<Button onclick={() => (showAllDetails = true)} size="sm" variant="ghost"
|
||||
>{m.show_more_details()}</Button
|
||||
>
|
||||
</div>
|
||||
{/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>
|
||||
<Card.Root>
|
||||
<Card.Content>
|
||||
<OidcForm mode="update" existingClient={client} callback={updateClient} />
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
</Tabs.Content>
|
||||
|
||||
<Button usePromiseLoading onclick={() => updateUserGroupClients(client.allowedUserGroupIds)}
|
||||
>{m.save()}</Button
|
||||
>
|
||||
</div>
|
||||
</CollapsibleCard>
|
||||
<CollapsibleCard id="api-access" title={m.api_access()} description={m.api_access_description()}>
|
||||
<ApiAccessCard clientId={client.id} isPublicClient={client.isPublic} />
|
||||
</CollapsibleCard>
|
||||
<CollapsibleCard
|
||||
id="scim-provisioning"
|
||||
title={m.scim_provisioning()}
|
||||
description={m.scim_provisioning_description()}
|
||||
>
|
||||
<ScimResourceProviderForm
|
||||
oidcClientId={client.id}
|
||||
existingProvider={scimServiceProvider}
|
||||
onSave={saveScimServiceProvider}
|
||||
/>
|
||||
</CollapsibleCard>
|
||||
<Card.Root>
|
||||
<Card.Header>
|
||||
<div class="flex flex-col items-start justify-between gap-3 sm:flex-row sm:items-center">
|
||||
<div>
|
||||
<Card.Title>
|
||||
{m.oidc_data_preview()}
|
||||
</Card.Title>
|
||||
<Tabs.Content value="user-groups" id="allowed-user-groups">
|
||||
<Card.Root>
|
||||
<Card.Header>
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<Card.Title>{m.allowed_user_groups()}</Card.Title>
|
||||
<Card.Description>
|
||||
{client.isGroupRestricted
|
||||
? m.allowed_user_groups_description()
|
||||
: m.allowed_user_groups_status_unrestricted_description()}
|
||||
</Card.Description>
|
||||
</div>
|
||||
{#if !client.isGroupRestricted}
|
||||
{@render UnrestrictButton()}
|
||||
{/if}
|
||||
</div>
|
||||
</Card.Header>
|
||||
{#if client.isGroupRestricted}
|
||||
<Card.Content>
|
||||
<UserGroupSelection bind:selectedGroupIds={client.allowedUserGroupIds} />
|
||||
<div class="mt-5 flex justify-end gap-3">
|
||||
<Button onclick={disableGroupRestriction} variant="secondary">{m.unrestrict()}</Button>
|
||||
|
||||
<Button
|
||||
usePromiseLoading
|
||||
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>
|
||||
{m.preview_the_oidc_data_that_would_be_sent_for_different_users()}
|
||||
<FormattedMessage m={m.scim_provisioning_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)}>
|
||||
{m.show()}
|
||||
</Button>
|
||||
</div>
|
||||
</Card.Header>
|
||||
</Card.Root>
|
||||
<Tabs.Content value="preview">
|
||||
<Card.Root>
|
||||
<Card.Header>
|
||||
<div class="flex flex-col items-start justify-between gap-3 sm:flex-row sm:items-center">
|
||||
<div>
|
||||
<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} />
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<script lang="ts">
|
||||
import CollapsibleCard from '$lib/components/collapsible-card.svelte';
|
||||
import CustomClaimsInput from '$lib/components/form/custom-claims-input.svelte';
|
||||
import { Badge } from '$lib/components/ui/badge';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import * as Card from '$lib/components/ui/card';
|
||||
import * as Tabs from '$lib/components/ui/tabs';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
import CustomClaimService from '$lib/services/custom-claim-service';
|
||||
import UserGroupService from '$lib/services/user-group-service';
|
||||
@@ -86,59 +86,84 @@
|
||||
<Badge class="rounded-full" variant="default">{m.ldap()}</Badge>
|
||||
{/if}
|
||||
</div>
|
||||
<Card.Root>
|
||||
<Card.Header>
|
||||
<Card.Title>{m.general()}</Card.Title>
|
||||
</Card.Header>
|
||||
|
||||
<Card.Content>
|
||||
<UserGroupForm existingUserGroup={userGroup} callback={updateUserGroup} />
|
||||
</Card.Content>
|
||||
</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>
|
||||
<Tabs.Root value="general" useHash class="gap-4">
|
||||
<div class="overflow-x-auto pb-1">
|
||||
<Tabs.List variant="line" class="min-w-max">
|
||||
<Tabs.Trigger value="general">{m.general()}</Tabs.Trigger>
|
||||
<Tabs.Trigger value="users">{m.users()}</Tabs.Trigger>
|
||||
<Tabs.Trigger value="clients">{m.allowed_oidc_clients()}</Tabs.Trigger>
|
||||
<Tabs.Trigger value="custom-claims">{m.custom_claims()}</Tabs.Trigger>
|
||||
</Tabs.List>
|
||||
</div>
|
||||
</CollapsibleCard>
|
||||
|
||||
<CollapsibleCard
|
||||
id="user-group-oidc-clients"
|
||||
title={m.allowed_oidc_clients()}
|
||||
description={m.allowed_oidc_clients_description()}
|
||||
>
|
||||
<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>
|
||||
</CollapsibleCard>
|
||||
<Tabs.Content value="general">
|
||||
<Card.Root>
|
||||
<Card.Header>
|
||||
<Card.Title>{m.general()}</Card.Title>
|
||||
</Card.Header>
|
||||
|
||||
<Card.Content>
|
||||
<UserGroupForm existingUserGroup={userGroup} callback={updateUserGroup} />
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
</Tabs.Content>
|
||||
|
||||
<Tabs.Content value="users">
|
||||
<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>
|
||||
</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">
|
||||
import CollapsibleCard from '$lib/components/collapsible-card.svelte';
|
||||
import CustomClaimsInput from '$lib/components/form/custom-claims-input.svelte';
|
||||
import ProfilePictureSettings from '$lib/components/form/profile-picture-settings.svelte';
|
||||
import Badge from '$lib/components/ui/badge/badge.svelte';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import * as Card from '$lib/components/ui/card';
|
||||
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 { m } from '$lib/paraglide/messages';
|
||||
import CustomClaimService from '$lib/services/custom-claim-service';
|
||||
@@ -94,70 +94,95 @@
|
||||
<Badge class="rounded-full" variant="default">{m.ldap()}</Badge>
|
||||
{/if}
|
||||
</div>
|
||||
<Card.Root>
|
||||
<Card.Header>
|
||||
<Card.Title>{m.general()}</Card.Title>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
<UserForm existingUser={user} callback={updateUser} />
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
<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
|
||||
>
|
||||
<Tabs.Root value="general" useHash class="gap-4">
|
||||
<div class="overflow-x-auto pb-1">
|
||||
<Tabs.List variant="line" class="min-w-max">
|
||||
<Tabs.Trigger value="general">{m.general()}</Tabs.Trigger>
|
||||
<Tabs.Trigger value="groups">{m.user_groups()}</Tabs.Trigger>
|
||||
<Tabs.Trigger value="passkeys">{m.passkeys()}</Tabs.Trigger>
|
||||
<Tabs.Trigger value="custom-claims">{m.custom_claims()}</Tabs.Trigger>
|
||||
</Tabs.List>
|
||||
</div>
|
||||
</CollapsibleCard>
|
||||
|
||||
<Item.Group class="bg-card border shadow-sm rounded-4xl p-5">
|
||||
<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 value="general" class="flex flex-col gap-4">
|
||||
<Card.Root>
|
||||
<Card.Header>
|
||||
<Card.Title>{m.general()}</Card.Title>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
<UserForm existingUser={user} callback={updateUser} />
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
<CollapsibleCard
|
||||
id="user-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()}
|
||||
>
|
||||
<CustomClaimsInput bind:customClaims={user.customClaims} />
|
||||
<div class="mt-5 flex justify-end">
|
||||
<Button onclick={updateCustomClaims} type="submit">{m.save()}</Button>
|
||||
</div>
|
||||
</CollapsibleCard>
|
||||
<Card.Root>
|
||||
<Card.Content>
|
||||
<ProfilePictureSettings
|
||||
userId={user.id}
|
||||
isLdapUser={!!user.ldapId}
|
||||
updateCallback={updateProfilePicture}
|
||||
resetCallback={resetProfilePicture}
|
||||
/>
|
||||
</Card.Content>
|
||||
</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>
|
||||
|
||||
<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"
|
||||
>
|
||||
<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
|
||||
await page.goto(`/settings/admin/oidc-clients/${oidcClients.nextcloud.id}`);
|
||||
|
||||
// Expand the API access card, then edit the Orders API row
|
||||
await page.getByText('API access', { exact: true }).click();
|
||||
// Open the API access tab, then edit the Orders API row
|
||||
await page.getByRole('tab', { name: 'API access' }).click();
|
||||
await page
|
||||
.getByRole('row', { name: apis.orders.name })
|
||||
.getByRole('button', { name: 'Edit' })
|
||||
|
||||
@@ -30,14 +30,14 @@ test('Update general configuration', async ({ page }) => {
|
||||
|
||||
test.describe('Update user creation configuration', () => {
|
||||
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 }) => {
|
||||
await page.getByRole('button', { name: 'Enable User Signups' }).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(
|
||||
'User creation settings updated successfully.'
|
||||
@@ -59,7 +59,7 @@ test.describe('Update user creation configuration', () => {
|
||||
await expect(designersOption).toBeChecked();
|
||||
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(
|
||||
'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('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(
|
||||
'User creation settings updated successfully.'
|
||||
@@ -97,7 +97,7 @@ test.describe('Update user creation configuration', () => {
|
||||
});
|
||||
|
||||
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 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('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(
|
||||
'Email configuration updated successfully'
|
||||
@@ -130,7 +130,7 @@ test('Update email configuration', async ({ page }) => {
|
||||
|
||||
test.describe('Update application images', () => {
|
||||
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 }) => {
|
||||
@@ -144,7 +144,7 @@ test.describe('Update application images', () => {
|
||||
.getByLabel('Default Profile Picture')
|
||||
.setInputFiles('resources/images/pingvin-share-logo.png');
|
||||
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(
|
||||
'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');
|
||||
|
||||
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(
|
||||
'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 }) => {
|
||||
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.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 }) => {
|
||||
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('row', { name: userGroups.designers.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();
|
||||
|
||||
|
||||
@@ -1520,7 +1520,9 @@ test.describe('OIDC skip consent', () => {
|
||||
|
||||
// Disabling it and saving must persist across a reload
|
||||
await toggle.click();
|
||||
await expect(toggle).not.toBeChecked();
|
||||
await page.getByRole('button', { name: /save/i }).click();
|
||||
await expect(page.getByText('OIDC client updated successfully', { exact: true })).toBeVisible();
|
||||
await page.reload();
|
||||
|
||||
await expect(page.getByRole('switch', { name: 'Skip Consent Screen' })).not.toBeChecked();
|
||||
|
||||
@@ -5,7 +5,7 @@ import { oidcClients, userGroups, users } from '../data';
|
||||
async function configureOidcClient(page: Page) {
|
||||
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')
|
||||
@@ -16,6 +16,7 @@ async function configureOidcClient(page: Page) {
|
||||
|
||||
async function syncScimServiceProvider(page: Page) {
|
||||
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.waitForSelector('[data-type="success"]');
|
||||
@@ -29,7 +30,7 @@ test.describe('SCIM Configuration', () => {
|
||||
test('Enable SCIM for OIDC client', async ({ page }) => {
|
||||
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 Token').fill('supersecrettoken');
|
||||
@@ -48,11 +49,12 @@ test.describe('SCIM Configuration', () => {
|
||||
await configureOidcClient(page);
|
||||
|
||||
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 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(
|
||||
'SCIM configuration updated successfully.'
|
||||
@@ -68,6 +70,7 @@ test.describe('SCIM Configuration', () => {
|
||||
await configureOidcClient(page);
|
||||
|
||||
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' }).nth(1).click();
|
||||
@@ -123,15 +126,16 @@ test.describe('SCIM Sync', () => {
|
||||
test('Remove allowed group and sync', async ({ 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('cell')
|
||||
.first()
|
||||
.click();
|
||||
.getByRole('checkbox');
|
||||
await developersCheckbox.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);
|
||||
|
||||
@@ -149,7 +153,7 @@ test.describe('SCIM Sync', () => {
|
||||
test('Remove group restrictions and sync', async ({ 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' }).nth(1).click();
|
||||
|
||||
@@ -44,11 +44,12 @@ test('Edit user group', async ({ page }) => {
|
||||
test('Update user group users', async ({ page }) => {
|
||||
const group = userGroups.designers;
|
||||
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.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');
|
||||
|
||||
@@ -77,7 +78,7 @@ test('Delete user group', async ({ page }) => {
|
||||
test('Update user group custom claims', async ({ page }) => {
|
||||
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
|
||||
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('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(
|
||||
'Custom claims updated successfully'
|
||||
@@ -106,7 +107,7 @@ test('Update user group custom claims', async ({ page }) => {
|
||||
|
||||
// Remove one custom claim
|
||||
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(
|
||||
'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 }) => {
|
||||
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
|
||||
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.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(
|
||||
'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 }) => {
|
||||
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
|
||||
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('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(
|
||||
'Custom claims updated successfully'
|
||||
@@ -215,7 +215,7 @@ test('Update user custom claims', async ({ page }) => {
|
||||
|
||||
// Remove one custom claim
|
||||
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(
|
||||
'Custom claims updated successfully'
|
||||
@@ -232,12 +232,12 @@ test('Update user group assignments', async ({ page }) => {
|
||||
const user = users.craig;
|
||||
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.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(
|
||||
'User groups updated successfully'
|
||||
@@ -255,6 +255,7 @@ test('Update user group assignments', async ({ page }) => {
|
||||
|
||||
test('Admin can view another user passkeys', async ({ page }) => {
|
||||
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(/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 }) => {
|
||||
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 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.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('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(
|
||||
'User creation settings updated successfully.'
|
||||
|
||||
Reference in New Issue
Block a user