Skip to content

Commit

Permalink
chore: move FormPage to its own component (#7472)
Browse files Browse the repository at this point in the history
* chore: move FormPage to its own component

remove tinro dependency in the UI component

keep a tinro FormPage in renderer part that is just delegating the calls

fixes #6920
Signed-off-by: Florent Benoit <fbenoit@redhat.com>
  • Loading branch information
benoitf committed Jun 10, 2024
1 parent c822920 commit f25dc1b
Show file tree
Hide file tree
Showing 8 changed files with 257 additions and 94 deletions.
73 changes: 16 additions & 57 deletions packages/renderer/src/lib/ui/FormPage.svelte
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script lang="ts">
import { CloseButton, LinearProgress, Link } from '@podman-desktop/ui-svelte';
import { FormPage } from '@podman-desktop/ui-svelte';
import { router } from 'tinro';
import { currentPage, lastPage } from '../../stores/breadcrumb';
Expand All @@ -8,63 +8,22 @@ export let title: string;
export let showBreadcrumb = true;
export let inProgress = false;
export function close(): void {
export function goToPreviousPage(): void {
router.goto($lastPage.path);
}
function handleKeydown(e: KeyboardEvent) {
if (e.key === 'Escape') {
close();
e.preventDefault();
}
}
</script>

<svelte:window on:keydown="{handleKeydown}" />

<div class="flex flex-col w-full h-full shadow-pageheader">
<div class="flex flex-row w-full h-fit px-5 pt-4" class:pb-3.5="{inProgress}" class:pb-4="{!inProgress}">
<div class="flex flex-col w-full h-fit">
{#if showBreadcrumb}
<div class="flex flew-row items-center text-sm text-[var(--pd-content-breadcrumb)]">
<Link aria-label="back" on:click="{() => router.goto($lastPage.path)}" title="Go back to {$lastPage.name}"
>{$lastPage.name}</Link>
<div class="mx-2">&gt;</div>
<div class="grow font-extralight" aria-label="name">{$currentPage.name}</div>
<CloseButton class="justify-self-end" on:click="{() => router.goto($lastPage.path)}" />
</div>
{/if}
<div class="flex flex-row items-center pt-1">
{#if $$slots.icon}
<div class="pr-3 text-[var(--pd-content-header-icon)]">
<slot name="icon" />
</div>
{/if}
<h1 aria-label="{title}" class="grow text-xl first-letter:uppercase text-[var(--pd-content-header)]">
{title}
</h1>
<div class="flex items-center space-x-3">
{#if $$slots.actions}
<div class="flex flex-nowrap justify-self-end pl-3 space-x-2">
<slot name="actions" />
</div>
{/if}
{#if !showBreadcrumb}
<CloseButton class="justify-self-end" on:click="{() => router.goto($lastPage.path)}" />
{/if}
</div>
</div>
</div>
</div>
{#if inProgress}
<LinearProgress />
{/if}
{#if $$slots.tabs}
<div class="flex flex-row px-2 border-b border-[var(--pd-content-divider)]">
<slot name="tabs" />
</div>
{/if}
<div class="flex w-full h-full bg-[var(--pd-content-bg)] overflow-auto">
<slot name="content" />
</div>
</div>
<FormPage
title="{title}"
showBreadcrumb="{showBreadcrumb}"
inProgress="{inProgress}"
breadcrumbLeftPart="{$lastPage.name}"
breadcrumbRightPart="{$currentPage.name}"
breadcrumbTitle="Go back to {$lastPage.name}"
on:close="{goToPreviousPage}"
on:breadcrumbClick="{goToPreviousPage}">
<slot slot="icon" name="icon" />
<slot slot="actions" name="actions" />
<slot slot="tabs" name="tabs" />
<slot slot="content" name="content" />
</FormPage>
23 changes: 0 additions & 23 deletions packages/renderer/src/lib/ui/FormPageSpec.svelte

This file was deleted.

4 changes: 4 additions & 0 deletions packages/ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,10 @@
"types": "./dist/progress/LinearProgress.d.ts",
"svelte": "./dist/progress/LinearProgress.svelte"
},
"./FormPage": {
"types": "./dist/layouts/FormPage.d.ts",
"svelte": "./dist/layouts/FormPage.svelte"
},
"./NavPage": {
"types": "./dist/layouts/NavPage.d.ts",
"svelte": "./dist/layouts/NavPage.svelte"
Expand Down
2 changes: 2 additions & 0 deletions packages/ui/src/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import Checkbox from './checkbox/Checkbox.svelte';
import DropdownMenu from './dropdownMenu';
import Input from './inputs/Input.svelte';
import SearchInput from './inputs/SearchInput.svelte';
import FormPage from './layouts/FormPage.svelte';
import NavPage from './layouts/NavPage.svelte';
import Link from './link/Link.svelte';
import Modal from './modal/Modal.svelte';
Expand All @@ -48,6 +49,7 @@ export {
EmptyScreen,
ErrorMessage,
FilteredEmptyScreen,
FormPage,
Input,
LinearProgress,
Link,
Expand Down
136 changes: 136 additions & 0 deletions packages/ui/src/lib/layouts/FormPage.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
/**********************************************************************
* Copyright (C) 2023-2024 Red Hat, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
***********************************************************************/

import '@testing-library/jest-dom/vitest';

import { fireEvent, getByRole, render, screen } from '@testing-library/svelte';
import userEvent from '@testing-library/user-event';
import { beforeEach, expect, test, vi } from 'vitest';

import FormPage from './FormPage.svelte';

beforeEach(() => {
vi.clearAllMocks();
});

test('Expect title is defined', async () => {
const title = 'My Dummy Title';
render(FormPage, {
title,
});

const titleElement = screen.getByRole('heading', { level: 1, name: title });
expect(titleElement).toBeInTheDocument();
expect(titleElement).toHaveTextContent(title);
});

test('Expect no backlink or close is defined', async () => {
render(FormPage, {
title: 'No Title',
showBreadcrumb: false,
});

const backElement = screen.queryByLabelText('back');
expect(backElement).not.toBeInTheDocument();

const closeElement = screen.queryByTitle('Close');
expect(closeElement).toBeInTheDocument();
});

test('Expect name is defined', async () => {
const name = 'My Dummy Name';
render(FormPage, {
title: 'No Title',
breadcrumbRightPart: name,
});

const nameElement = screen.getByLabelText('name');
expect(nameElement).toBeInTheDocument();
expect(nameElement).toHaveTextContent(name);
});

test('Expect backlink is defined', async () => {
const backName = 'Last page';
const breadcrumbClickMock = vi.fn();

const comp = render(FormPage, {
breadcrumbLeftPart: 'Last page',
breadcrumbRightPart: 'hello',
title: 'No Title',
});

comp.component.$on('breadcrumbClick', breadcrumbClickMock);

const backElement = screen.getByLabelText('back');
expect(backElement).toBeInTheDocument();
expect(backElement).toHaveTextContent(backName);

await fireEvent.click(backElement);

expect(breadcrumbClickMock).toHaveBeenCalled();
});

test('Expect close link is defined', async () => {
const closeClickMock = vi.fn();

const comp = render(FormPage, {
title: 'No Title',
breadcrumbLeftPart: 'back',
breadcrumbRightPart: 'hello',
});

comp.component.$on('close', closeClickMock);
const closeElement = getByRole(document.body, 'button', { name: 'Close' });
expect(closeElement).toBeInTheDocument();
await fireEvent.click(closeElement);

expect(closeClickMock).toHaveBeenCalled();
});

test('Expect Escape key works', async () => {
const closeClickMock = vi.fn();

const comp = render(FormPage, {
title: 'No Title',
breadcrumbLeftPart: 'back',
});
comp.component.$on('close', closeClickMock);
await userEvent.keyboard('{Escape}');

expect(closeClickMock).toHaveBeenCalled();
});

test('Expect no progress', async () => {
render(FormPage, {
title: 'No Title',
inProgress: false,
});

const progress = screen.queryByRole('progressbar');
expect(progress).toBeNull();
});

test('Expect progress', async () => {
render(FormPage, {
title: 'No Title',
inProgress: true,
});

const progress = screen.getByRole('progressbar');
expect(progress).toBeInTheDocument();
});
85 changes: 85 additions & 0 deletions packages/ui/src/lib/layouts/FormPage.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte';
import CloseButton from '../button/CloseButton.svelte';
import Link from '../link/Link.svelte';
import LinearProgress from '../progress/LinearProgress.svelte';
export let title: string;
export let showBreadcrumb = true;
export let inProgress = false;
export let breadcrumbLeftPart: string | undefined = undefined;
export let breadcrumbRightPart: string | undefined = undefined;
export let breadcrumbTitle: string | undefined = '';
const dispatchClose = createEventDispatcher<{ close: undefined }>();
function close(): void {
dispatchClose('close');
}
const dispatchBreadCrumb = createEventDispatcher<{ breadcrumbClick: undefined }>();
function breadcrumbClick(): void {
dispatchBreadCrumb('breadcrumbClick');
}
function handleKeydown(e: KeyboardEvent): void {
if (e.key === 'Escape') {
close();
e.preventDefault();
}
}
</script>

<svelte:window on:keydown="{handleKeydown}" />

<div class="flex flex-col w-full h-full shadow-pageheader">
<div class="flex flex-row w-full h-fit px-5 pt-4" class:pb-3.5="{inProgress}" class:pb-4="{!inProgress}">
<div class="flex flex-col w-full h-fit">
{#if showBreadcrumb}
<div class="flex flew-row items-center text-sm text-[var(--pd-content-breadcrumb)]">
{#if breadcrumbLeftPart}
<Link aria-label="back" on:click="{breadcrumbClick}" title="{breadcrumbTitle}">{breadcrumbLeftPart}</Link>
{/if}
{#if breadcrumbRightPart}
<div class="mx-2">&gt;</div>
<div class="grow font-extralight" aria-label="name">{breadcrumbRightPart}</div>
{/if}
<CloseButton class="justify-self-end" on:click="{close}" />
</div>
{/if}
<div class="flex flex-row items-center pt-1">
{#if $$slots.icon}
<div class="pr-3 text-[var(--pd-content-header-icon)]">
<slot name="icon" />
</div>
{/if}
<h1 aria-label="{title}" class="grow text-xl first-letter:uppercase text-[var(--pd-content-header)]">
{title}
</h1>
<div class="flex items-center space-x-3">
{#if $$slots.actions}
<div class="flex flex-nowrap justify-self-end pl-3 space-x-2">
<slot name="actions" />
</div>
{/if}
{#if !showBreadcrumb}
<CloseButton class="justify-self-end" on:click="{close}" />
{/if}
</div>
</div>
</div>
</div>
{#if inProgress}
<LinearProgress />
{/if}
{#if $$slots.tabs}
<div class="flex flex-row px-2 border-b border-[var(--pd-content-divider)]">
<slot name="tabs" />
</div>
{/if}
<div class="flex w-full h-full bg-[var(--pd-content-bg)] overflow-auto">
<slot name="content" />
</div>
</div>
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**********************************************************************
* Copyright (C) 2023 Red Hat, Inc.
* Copyright (C) 2023-2024 Red Hat, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -43,16 +43,3 @@ test('Expect content slot is defined', async () => {
const element = screen.getByLabelText('content');
expect(element).toBeInTheDocument();
});

test('Expect tabs slot is defined', async () => {
render(FormPageSpec);

const fooTabElement = screen.getByRole('link', { name: 'Foo' });
expect(fooTabElement).toBeInTheDocument();

const barTabElement = screen.getByRole('link', { name: 'Bar' });
expect(barTabElement).toBeInTheDocument();

const bazTabElement = screen.getByRole('link', { name: 'Baz' });
expect(bazTabElement).toBeInTheDocument();
});
13 changes: 13 additions & 0 deletions packages/ui/src/lib/layouts/FormPageSpec.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<script lang="ts">
import FormPage from './FormPage.svelte';
</script>

<FormPage title="Test component" showBreadcrumb="{false}">
<i slot="icon" class="fas fa-lightbulb fa-2x" aria-label="icon"></i>

<i slot="actions" class="fas fa-lightbulb fa-2x" aria-label="actions"></i>

<div slot="content" class="flex flex-col">
<i class="fas fa-lightbulb fa-2x" aria-label="content"></i>
</div>
</FormPage>

0 comments on commit f25dc1b

Please sign in to comment.