Skip to content

Commit 0445af0

Browse files
rmunnhahn-kevmyieye
authored
Migrate Lexbox UI to Svelte 5 (#1634)
* Rename "derived" uses that mess up migration script The `pnpx sv migrate svelte-5` script has trouble with changing `$:` statements to `derived` when there are existing uses of `derived` in the code, so we'll rename the three cases that have that problem before running the migration script. * Result of svelte-5 migration script This is what `pnpx sv migrate svelte-5` produced, with no changes. There is one file, `src/routes/(unauthenticated)/login/+page.svelte`, which might need further attention: a `@migration task` commit was created saying "review uses of `navigating`" (which used to be imported from $app/stores and now comes from $app/state instead). There might not be any changes needed, but if there are, that work will go in a separate commit. * Navigating state is guaranteed non-null In SvelteKit version 2.12 or later, the `navigating` object is guaranteed never to be null; its properties (from, to, etc.) will be null if no navigation is going on, but you can safely check `navigating.to` rather than `navigating?.to`. * Fix now-incorrect ordering of variables IconButton was using the `size` prop before it was declared. In Svelte 4 the compiler was reordering this, in Svelte 5 this is no longer allowed; the variable that uses `size` needs to be moved after the $props call. * Just use `$bindable()`, not `$bindable(undefined)` In Svelte 5, if you have set a fallback value for a bindable prop then you're not allowed to have the caller pass `undefined` to it. But if the "fallback value" was just `undefined` in the first place, then that's the default behavior of just plain `$bindable()` with no fallback value. * Use `page` from `$app/state`, not `$app/stores` Running the `pnpx sv migrate svelte-5` a second time allowed it to migrate several uses of the `$page` store into the new `page` state. This commit is the unchanged result of the migration tool. * Allow eslint to fix auto-fixable errors These are errors that used to be suppressed but are no longer errors, and eslint automatically removed the error-suppressing comments. * Fix consistent-type-imports errors Types like `import('svelte').Snippet` are now forbidden, so we replace them with explicit type imports at the top of the file. Prettier made other code changes when I saved these files, which I've placed into a separate commit to make refactoring slightly easier should that prove necessary. * Changes made by Prettier when fixing lint errors Separated into separate commit so these changes will be slightly easier to revert if they are wrong. * Fix remaining lint errors * Fix type errors * Remove now-unused createBubbler() imports * change custom overlay to use passed callbacks rather than custom events * Pass rest props through intermediate Select components * Fix errors on project page * Address review comments * Get rid of infinite loop on register page * Fix PasswordStrengthMeter without infinite loop Instead of setting a bindable prop based on a derived from the props list, which was triggering an infinite loop, we will fall back to function props and pass an onScoreUpdated function into the password strength meter component. It's slightly more verbose, but it avoids the infinite loop of effects triggering updates to themselves that our previous approach was causing. * Avoid infinite recursion during SSR This works just fine in client-side rendering, but causes an infinite recursive call to headerContent() when rendered server-side. Putting a `$derived` in the middle breaks the loop and makes the page render correctly in both SSR and CSR. --------- Co-authored-by: Kevin Hahn <[email protected]> Co-authored-by: Tim Haasdyk <[email protected]>
1 parent 2719cd8 commit 0445af0

File tree

176 files changed

+4255
-2695
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

176 files changed

+4255
-2695
lines changed

frontend/src/hooks.server.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ async function initI18n(event: RequestEvent): Promise<void> {
2828
await loadI18n();
2929
}
3030

31-
// eslint-disable-next-line func-style, @typescript-eslint/unbound-method
31+
// eslint-disable-next-line func-style
3232
export const handle: Handle = ({event, resolve}) => {
3333
console.log(`HTTP request: ${event.request.method} ${event.request.url}`);
3434
event.locals.getUser = () => getUser(event.cookies);

frontend/src/lib/components/Badges/ActionBadge.svelte

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
11
<script lang="ts">
2+
import type { Snippet } from 'svelte';
23
import { Icon, type IconString } from '$lib/icons';
34
import { createEventDispatcher } from 'svelte';
4-
export let disabled = false;
5-
export let actionIcon: IconString;
6-
export let variant: 'btn-neutral' | 'btn-primary' | 'btn-secondary' = 'btn-neutral';
7-
$: iconHoverColor = variant === 'btn-neutral' ? 'group-hover:bg-base-200' : 'group-hover:bg-neutral/50';
5+
interface Props {
6+
disabled?: boolean;
7+
actionIcon: IconString;
8+
variant?: 'btn-neutral' | 'btn-primary' | 'btn-secondary';
9+
children?: Snippet;
10+
}
11+
12+
let { disabled = false, actionIcon, variant = 'btn-neutral', children }: Props = $props();
13+
let iconHoverColor = $derived(variant === 'btn-neutral' ? 'group-hover:bg-base-200' : 'group-hover:bg-neutral/50');
814
915
const dispatch = createEventDispatcher<{
1016
action: void;
@@ -16,16 +22,17 @@
1622
}
1723
}
1824
19-
$: pr = disabled ? '!pr-0' : '!pr-1';
20-
$: br = disabled ? 'border-r-0' : '';
25+
let pr = $derived(disabled ? '!pr-0' : '!pr-1');
26+
let br = $derived(disabled ? 'border-r-0' : '');
2127
</script>
2228

2329
<button
24-
on:click={onAction}
25-
on:keypress={onAction}
26-
class="btn badge {variant} group transition whitespace-nowrap gap-1 {pr} {br}" class:pointer-events-none={disabled}
30+
onclick={onAction}
31+
onkeypress={onAction}
32+
class="btn badge {variant} group transition whitespace-nowrap gap-1 {pr} {br}"
33+
class:pointer-events-none={disabled}
2734
>
28-
<slot />
35+
{@render children?.()}
2936

3037
{#if !disabled}
3138
<span class="btn btn-circle btn-xs btn-ghost transition {iconHoverColor}">

frontend/src/lib/components/Badges/Badge.svelte

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,40 @@
1-
<script lang="ts" context="module">
1+
<script lang="ts" module>
2+
import type { Snippet } from 'svelte';
23
// Add more as necessary. Should be as limited as possible to maximize consistency. https://daisyui.com/components/badge/
3-
export type BadgeVariant = 'badge-neutral' | 'badge-info' | 'badge-primary' | 'badge-warning' | 'badge-success' | undefined;
4+
export type BadgeVariant =
5+
| 'badge-neutral'
6+
| 'badge-info'
7+
| 'badge-primary'
8+
| 'badge-warning'
9+
| 'badge-success'
10+
| undefined;
411
</script>
512

613
<script lang="ts">
714
import { Icon, type IconString } from '$lib/icons';
815
9-
export let variant: BadgeVariant = 'badge-neutral';
10-
export let icon: IconString | undefined = undefined;
11-
export let hoverIcon: IconString | undefined = undefined;
12-
export let outline = false;
16+
interface Props {
17+
variant?: BadgeVariant;
18+
icon?: IconString | undefined;
19+
hoverIcon?: IconString | undefined;
20+
outline?: boolean;
21+
children?: Snippet;
22+
}
23+
24+
let {
25+
variant = 'badge-neutral',
26+
icon = undefined,
27+
hoverIcon = undefined,
28+
outline = false,
29+
children,
30+
}: Props = $props();
1331
</script>
1432

1533
<span
1634
class="badge {variant ?? ''} whitespace-nowrap inline-flex gap-2 items-center group"
1735
class:badge-outline={outline}
1836
>
19-
<slot />
37+
{@render children?.()}
2038
<span class="contents" class:group-hover:hidden={!!hoverIcon}>
2139
<Icon {icon} />
2240
</span>
Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,36 @@
11
<script lang="ts">
2+
import type { Snippet } from 'svelte';
23
import type { IconString } from '$lib/icons';
34
import Badge from './Badge.svelte';
45
56
// Add more as necessary. Should be as limited as possible to maximize consistency.
67
type BadgeButtonVariant = 'badge-success' | 'badge-warning' | 'badge-neutral' | undefined;
7-
export let variant: BadgeButtonVariant = undefined;
8-
export let icon: IconString | undefined = undefined;
9-
export let hoverIcon: IconString | undefined = undefined;
10-
export let disabled = false;
8+
interface Props {
9+
variant?: BadgeButtonVariant;
10+
icon?: IconString | undefined;
11+
hoverIcon?: IconString | undefined;
12+
disabled?: boolean;
13+
onclick?: () => void;
14+
children?: Snippet;
15+
}
16+
17+
let {
18+
variant = undefined,
19+
icon = undefined,
20+
hoverIcon = undefined,
21+
disabled = false,
22+
onclick,
23+
children,
24+
}: Props = $props();
1125
</script>
1226

13-
<button on:click {disabled} class="badge btn btn-sm !p-0 bright transition-all border-0" class:hover:brightness-90={!disabled}>
27+
<button
28+
{onclick}
29+
{disabled}
30+
class="badge btn btn-sm !p-0 bright transition-all border-0"
31+
class:hover:brightness-90={!disabled}
32+
>
1433
<Badge {variant} {icon} hoverIcon={disabled ? undefined : hoverIcon}>
15-
<slot />
34+
{@render children?.()}
1635
</Badge>
1736
</button>

frontend/src/lib/components/Badges/BadgeList.svelte

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
11
<script lang="ts">
2-
export let grid = false;
2+
import type { Snippet } from 'svelte';
3+
interface Props {
4+
grid?: boolean;
5+
children?: Snippet;
6+
}
7+
8+
let { grid = false, children }: Props = $props();
39
</script>
410

511
<span class:grid class:inline-flex={!grid} class="badge-list flex-wrap gap-3 justify-items-stretch">
6-
<slot />
12+
{@render children?.()}
713
</span>
814

915
<style>

frontend/src/lib/components/Badges/MemberBadge.svelte

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,16 @@
33
import FormatUserProjectRole from '../Projects/FormatUserProjectRole.svelte';
44
import ActionBadge from './ActionBadge.svelte';
55
import Badge from './Badge.svelte';
6-
export let member: { name: string; role: ProjectRole };
7-
export let canManage = false;
86
9-
export let type: 'existing' | 'new' = 'existing';
10-
$: actionIcon = (type === 'existing' ? 'i-mdi-dots-vertical' as const : 'i-mdi-close' as const);
11-
$: variant = member.role === ProjectRole.Manager ? 'btn-primary' as const : 'btn-secondary' as const;
7+
interface Props {
8+
member: { name: string; role: ProjectRole };
9+
canManage?: boolean;
10+
type?: 'existing' | 'new';
11+
}
12+
13+
let { member, canManage = false, type = 'existing' }: Props = $props();
14+
let actionIcon = ($derived(type === 'existing' ? 'i-mdi-dots-vertical' as const : 'i-mdi-close' as const));
15+
let variant = $derived(member.role === ProjectRole.Manager ? 'btn-primary' as const : 'btn-secondary' as const);
1216
</script>
1317

1418
<ActionBadge {actionIcon} {variant} disabled={!canManage} on:action>
@@ -17,7 +21,7 @@
1721
</span>
1822

1923
<!-- justify the name left and the role right -->
20-
<span class="flex-grow" />
24+
<span class="flex-grow"></span>
2125

2226
<Badge>
2327
<FormatUserProjectRole role={member.role} />

frontend/src/lib/components/Badges/OrgMemberBadge.svelte

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,16 @@
33
import FormatUserOrgRole from '../Orgs/FormatUserOrgRole.svelte';
44
import ActionBadge from './ActionBadge.svelte';
55
import Badge from './Badge.svelte';
6-
export let member: { name: string; role: OrgRole };
7-
export let canManage = false;
86
9-
export let type: 'existing' | 'new' = 'existing';
10-
$: actionIcon = (type === 'existing' ? 'i-mdi-dots-vertical' as const : 'i-mdi-close' as const);
11-
$: variant = member.role === OrgRole.Admin ? 'btn-primary' as const : 'btn-secondary' as const;
7+
interface Props {
8+
member: { name: string; role: OrgRole };
9+
canManage?: boolean;
10+
type?: 'existing' | 'new';
11+
}
12+
13+
let { member, canManage = false, type = 'existing' }: Props = $props();
14+
let actionIcon = ($derived(type === 'existing' ? 'i-mdi-dots-vertical' as const : 'i-mdi-close' as const));
15+
let variant = $derived(member.role === OrgRole.Admin ? 'btn-primary' as const : 'btn-secondary' as const);
1216
</script>
1317

1418
<ActionBadge {actionIcon} {variant} disabled={!canManage} on:action>
@@ -17,7 +21,7 @@
1721
</span>
1822

1923
<!-- justify the name left and the role right -->
20-
<span class="flex-grow" />
24+
<span class="flex-grow"></span>
2125

2226
<Badge>
2327
<FormatUserOrgRole role={member.role} />

frontend/src/lib/components/ButtonToggle.svelte

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,29 @@
11
<script lang="ts">
2-
export let icon1: string;
3-
export let icon2: string;
4-
export let text1: string;
5-
export let text2: string;
6-
export let style: string;
7-
let isIconOne = true;
2+
import { preventDefault } from 'svelte/legacy';
3+
4+
interface Props {
5+
icon1: string;
6+
icon2: string;
7+
text1: string;
8+
text2: string;
9+
style: string;
10+
}
11+
12+
let {
13+
icon1,
14+
icon2,
15+
text1,
16+
text2,
17+
style
18+
}: Props = $props();
19+
let isIconOne = $state(true);
820
921
function handleClick(): void {
1022
isIconOne = !isIconOne;
1123
}
1224
</script>
1325

14-
<button on:click|preventDefault={handleClick} class="btn {style} flex items-center">
26+
<button onclick={preventDefault(handleClick)} class="btn {style} flex items-center">
1527
{#if isIconOne}
1628
<span>{text1} </span>
1729
<span class="{icon1} text-lg"></span>
Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,16 @@
1-
<!--cloudflare replaces email addresses with a hash, however this can mess up the dom and break svelte, so sometimes we need to bypass it-->
1+
<script lang="ts">
2+
import type { Snippet } from 'svelte';
3+
interface Props {
4+
children?: Snippet;
5+
}
6+
7+
let { children }: Props = $props();
8+
</script>
9+
10+
<!--cloudflare replaces email addresses with a hash, however this can mess up the dom and break svelte, so sometimes we need to bypass it-->
211
<!--because normal html comments are trimmed in svelte we need to use @html to avoid the comment being trimmed-->
312
<!--eslint-disable-next-line svelte/no-at-html-tags-->
413
{@html '<!--email_off-->'}
5-
<slot />
14+
{@render children?.()}
615
<!--eslint-disable-next-line svelte/no-at-html-tags-->
716
{@html '<!--/email_off-->'}

frontend/src/lib/components/CopyToClipboardButton.svelte

Lines changed: 25 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,17 @@
33
import IconButton from './IconButton.svelte';
44
import t from '$lib/i18n';
55
6-
var copyingToClipboard = false;
7-
var copiedToClipboard = false;
6+
var copyingToClipboard = $state(false);
7+
var copiedToClipboard = $state(false);
88
9-
export let textToCopy: string;
10-
export let delayMs: Duration | number = Duration.Default;
11-
export let size: 'btn-sm' | undefined = undefined;
12-
export let outline: boolean = true;
9+
interface Props {
10+
textToCopy: string;
11+
delayMs?: Duration | number;
12+
size?: 'btn-sm' | undefined;
13+
outline?: boolean;
14+
}
15+
16+
let { textToCopy, delayMs = Duration.Default, size = undefined, outline = true }: Props = $props();
1317
1418
async function copyToClipboard(): Promise<void> {
1519
copyingToClipboard = true;
@@ -22,15 +26,21 @@
2226
</script>
2327

2428
{#if copiedToClipboard}
25-
<div class="tooltip tooltip-open" data-tip={$t('clipboard.copied')}>
26-
<IconButton fake icon="i-mdi-check" {size} variant={outline ? undefined : 'btn-ghost'} class={outline ? 'btn-success' : 'text-success'} />
27-
</div>
29+
<div class="tooltip tooltip-open" data-tip={$t('clipboard.copied')}>
30+
<IconButton
31+
fake
32+
icon="i-mdi-check"
33+
{size}
34+
variant={outline ? undefined : 'btn-ghost'}
35+
class={outline ? 'btn-success' : 'text-success'}
36+
/>
37+
</div>
2838
{:else}
2939
<IconButton
30-
loading={copyingToClipboard}
31-
icon="i-mdi-content-copy"
32-
{size}
33-
variant={outline ? undefined : 'btn-ghost'}
34-
on:click={copyToClipboard}
35-
/>
40+
loading={copyingToClipboard}
41+
icon="i-mdi-content-copy"
42+
{size}
43+
variant={outline ? undefined : 'btn-ghost'}
44+
onclick={copyToClipboard}
45+
/>
3646
{/if}

0 commit comments

Comments
 (0)