feat: hide apps without launch url on My Apps page

This commit is contained in:
Elias Schneider
2026-08-10 22:53:35 +02:00
parent 84a58cd757
commit 3ca9a55c71
21 changed files with 547 additions and 108 deletions
+172 -56
View File
@@ -1,28 +1,65 @@
<script lang="ts">
import { openConfirmDialog } from '$lib/components/confirm-dialog';
import { Button } from '$lib/components/ui/button';
import * as Empty from '$lib/components/ui/empty';
import * as Pagination from '$lib/components/ui/pagination';
import { Separator } from '$lib/components/ui/separator';
import { m } from '$lib/paraglide/messages';
import OIDCService from '$lib/services/oidc-service';
import type { ListRequestOptions, Paginated } from '$lib/types/list-request.type';
import type { AccessibleOidcClient, OidcClientMetaData } from '$lib/types/oidc.type';
import type {
AccessibleOidcClient,
AuthorizedOidcClient,
OidcClientMetaData
} from '$lib/types/oidc.type';
import { axiosErrorToast } from '$lib/utils/error-util';
import { LayoutDashboard } from '@lucide/svelte';
import { cn } from '$lib/utils/style';
import { ChevronDown, LayoutDashboard } from '@lucide/svelte';
import { toast } from 'svelte-sonner';
import { slide } from 'svelte/transition';
import AuthorizedOidcClientCard from './authorized-oidc-client-card.svelte';
let { data } = $props();
let clients: Paginated<AccessibleOidcClient> = $state(data.clients);
let requestOptions: ListRequestOptions = $state(data.appRequestOptions);
let authorizedClientsWithoutLaunchURL: Paginated<AuthorizedOidcClient> = $state(
data.authorizedClientsWithoutLaunchURL
);
let authorizedClientRequestOptions: ListRequestOptions = $state(
data.authorizedClientRequestOptions
);
let showAllApps = $state(false);
const hiddenAuthorizedClients = $derived(
authorizedClientsWithoutLaunchURL.data.map(({ client, lastUsedAt }) => ({
...client,
lastUsedAt
}))
);
const oidcService = new OIDCService();
async function onRefresh(options: ListRequestOptions) {
clients = await oidcService.listOwnAccessibleClients(options);
async function refreshClients() {
[clients, authorizedClientsWithoutLaunchURL] = await Promise.all([
oidcService.listOwnAccessibleClients(requestOptions),
oidcService.listOwnAuthorizedClients(authorizedClientRequestOptions)
]);
if (authorizedClientsWithoutLaunchURL.pagination.totalItems === 0) {
showAllApps = false;
}
}
async function onPageChange(page: number) {
requestOptions.pagination = { limit: clients.pagination.itemsPerPage, page };
onRefresh(requestOptions);
clients = await oidcService.listOwnAccessibleClients(requestOptions);
}
async function onAuthorizedClientPageChange(page: number) {
authorizedClientRequestOptions.pagination = {
limit: authorizedClientsWithoutLaunchURL.pagination.itemsPerPage,
page
};
authorizedClientsWithoutLaunchURL = await oidcService.listOwnAuthorizedClients(
authorizedClientRequestOptions
);
}
async function revokeAuthorizedClient(client: OidcClientMetaData) {
@@ -38,7 +75,7 @@
action: async () => {
try {
await oidcService.revokeOwnAuthorizedClient(client.id);
onRefresh(requestOptions);
await refreshClients();
toast.success(
m.revoke_access_successful({
clientName: client.name
@@ -56,74 +93,153 @@
<svelte:head>
<title>{m.my_apps()}</title>
</svelte:head>
<div class="space-y-6">
<div>
<div>
<h1 class="flex items-center gap-2 text-2xl font-bold">
<h1 class="flex items-center gap-2 text-2xl font-bold mb-5">
<LayoutDashboard class="text-primary/80 size-6" />
{m.my_apps()}
</h1>
</div>
{#if clients.data.length === 0}
<div class="py-16 text-center">
<LayoutDashboard class="text-muted-foreground mx-auto mb-4 size-16" />
<h3 class="text-muted-foreground mb-2 text-lg font-medium">
{m.no_apps_available()}
</h3>
<p class="text-muted-foreground mx-auto max-w-md text-sm">
{m.contact_your_administrator_for_app_access()}
</p>
</div>
{#if clients.data.length === 0 && !showAllApps}
<Empty.Root>
<Empty.Header>
<Empty.Media variant="icon">
<LayoutDashboard />
</Empty.Media>
<Empty.Title>{m.no_apps_available()}</Empty.Title>
<Empty.Description>
{m.contact_your_administrator_for_app_access()}
</Empty.Description>
</Empty.Header>
<Empty.Content>
<Button variant="outline" size="sm" onclick={() => (showAllApps = !showAllApps)}
>{m.show_hidden_apps()}</Button
>
</Empty.Content>
</Empty.Root>
{:else}
<div class="space-y-8">
{#if clients.data.length > 0}
<div
class="grid gap-3"
style="grid-template-columns: repeat(auto-fit, minmax(min(280px, 100%), 1fr));"
style="grid-template-columns: repeat(auto-fit, minmax(min(300px, 100%), 1fr));"
>
{#each clients.data as client (client.id)}
<AuthorizedOidcClientCard {client} onRevoke={revokeAuthorizedClient} />
{/each}
<!-- Gap fix if two elements are present-->
{#if clients.data.length == 2}
{#if clients.data.length === 2}
<div></div>
{/if}
</div>
{/if}
{#if clients.pagination.totalPages > 1}
<div class="border-border flex items-center justify-center border-t pt-3">
<Pagination.Root
class="mx-0 w-auto"
count={clients.pagination.totalItems}
perPage={clients.pagination.itemsPerPage}
{onPageChange}
page={clients.pagination.currentPage}
>
{#snippet children({ pages })}
<Pagination.Content class="flex justify-center">
<Pagination.Item>
<Pagination.PrevButton />
</Pagination.Item>
{#each pages as page (page.key)}
{#if page.type !== 'ellipsis' && page.value != 0}
<Pagination.Item>
<Pagination.Link
{page}
isActive={clients.pagination.currentPage === page.value}
>
{page.value}
</Pagination.Link>
</Pagination.Item>
{/if}
{/each}
<Pagination.Item>
<Pagination.NextButton />
</Pagination.Item>
</Pagination.Content>
{/snippet}
</Pagination.Root>
{#if clients.pagination.totalPages > 1}
<div class="flex items-center justify-center mt-5">
<Pagination.Root
class="mx-0 w-auto"
count={clients.pagination.totalItems}
perPage={clients.pagination.itemsPerPage}
{onPageChange}
page={clients.pagination.currentPage}
>
{#snippet children({ pages })}
<Pagination.Content class="flex justify-center">
<Pagination.Item>
<Pagination.PrevButton />
</Pagination.Item>
{#each pages as page (page.key)}
{#if page.type !== 'ellipsis' && page.value != 0}
<Pagination.Item>
<Pagination.Link
{page}
isActive={clients.pagination.currentPage === page.value}
>
{page.value}
</Pagination.Link>
</Pagination.Item>
{/if}
{/each}
<Pagination.Item>
<Pagination.NextButton />
</Pagination.Item>
</Pagination.Content>
{/snippet}
</Pagination.Root>
</div>
{/if}
{#if showAllApps}
<div transition:slide={{ duration: 200 }}>
{#if clients.data.length > 0}
<Separator class="my-8" />
{/if}
<div
class="grid gap-3"
style="grid-template-columns: repeat(auto-fit, minmax(min(300px, 100%), 1fr));"
>
{#each hiddenAuthorizedClients as client (client.id)}
<AuthorizedOidcClientCard {client} onRevoke={revokeAuthorizedClient} />
{/each}
<!-- Gap fix if two elements are present-->
{#if hiddenAuthorizedClients.length === 2}
<div></div>
{/if}
</div>
{/if}
{#if authorizedClientsWithoutLaunchURL.pagination.totalPages > 1}
<div class="flex items-center justify-center mt-5">
<Pagination.Root
class="mx-0 w-auto"
count={authorizedClientsWithoutLaunchURL.pagination.totalItems}
perPage={authorizedClientsWithoutLaunchURL.pagination.itemsPerPage}
onPageChange={onAuthorizedClientPageChange}
page={authorizedClientsWithoutLaunchURL.pagination.currentPage}
>
{#snippet children({ pages })}
<Pagination.Content class="flex justify-center">
<Pagination.Item>
<Pagination.PrevButton />
</Pagination.Item>
{#each pages as page (page.key)}
{#if page.type !== 'ellipsis' && page.value != 0}
<Pagination.Item>
<Pagination.Link
{page}
isActive={authorizedClientsWithoutLaunchURL.pagination.currentPage ===
page.value}
>
{page.value}
</Pagination.Link>
</Pagination.Item>
{/if}
{/each}
<Pagination.Item>
<Pagination.NextButton />
</Pagination.Item>
</Pagination.Content>
{/snippet}
</Pagination.Root>
</div>
{/if}
</div>
{/if}
{/if}
{#if authorizedClientsWithoutLaunchURL.pagination.totalItems > 0 && clients.data.length !== 0}
<div class="flex justify-center mt-10">
<Button
variant="ghost"
class="text-muted-foreground"
onclick={() => (showAllApps = !showAllApps)}
>
{showAllApps ? m.hide_all_apps() : m.show_all_apps()}
({authorizedClientsWithoutLaunchURL.pagination.totalItems})
<ChevronDown
data-icon="inline-end"
class={cn('transition-transform duration-200', showAllApps && 'rotate-180 transform')}
/>
</Button>
</div>
{/if}
</div>
+27 -2
View File
@@ -13,10 +13,35 @@ export const load: PageLoad = async () => {
sort: {
column: 'lastUsedAt',
direction: 'desc'
},
filters: {
hasLaunchURL: [true]
}
};
const clients = await oidcService.listOwnAccessibleClients(appRequestOptions);
const authorizedClientRequestOptions: ListRequestOptions = {
pagination: {
page: 1,
limit: 20
},
sort: {
column: 'lastUsedAt',
direction: 'desc'
},
filters: {
hasLaunchURL: [false]
}
};
return { clients, appRequestOptions };
const [clients, authorizedClientsWithoutLaunchURL] = await Promise.all([
oidcService.listOwnAccessibleClients(appRequestOptions),
oidcService.listOwnAuthorizedClients(authorizedClientRequestOptions)
]);
return {
clients,
appRequestOptions,
authorizedClientsWithoutLaunchURL,
authorizedClientRequestOptions
};
};
@@ -33,10 +33,11 @@
</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-[430px]"
data-testid="authorized-oidc-client-card"
class="border-muted group relative h-[160px] p-5 hover:shadow-md sm:max-w-[50vw] md:max-w-[450px]"
role="article"
aria-label={client.name}
>
<Card.Content class=" p-0">
<Card.Content class="p-0">
<div class="flex gap-3">
<div class="aspect-square h-[56px]">
{#if client.hasLogo}
@@ -124,17 +125,18 @@
{:else}
<div></div>
{/if}
<Button
href={client.launchURL}
target="_blank"
size="sm"
class="h-8 text-xs"
rel="noopener noreferrer"
disabled={!client.launchURL}
>
{m.launch()}
<LucideExternalLink class="ml-1 size-3" />
</Button>
{#if client.launchURL}
<Button
href={client.launchURL}
target="_blank"
size="sm"
class="h-8 text-xs"
rel="noopener noreferrer"
>
{m.launch()}
<LucideExternalLink data-icon="inline-end" />
</Button>
{/if}
</div>
</Card.Content>
</Card.Root>