feat: import projects form

Signed-off-by: André Jaenisch <andre.jaenisch@posteo.de>
This commit is contained in:
André Jaenisch 2024-02-16 15:46:32 +01:00
parent fecd15f862
commit 34be485a49
No known key found for this signature in database
GPG key ID: 5A668E771F1ED854
12 changed files with 791 additions and 8 deletions

4
package-lock.json generated
View file

@ -1,12 +1,12 @@
{ {
"name": "anvil", "name": "anvil",
"version": "0.0.2", "version": "0.0.3",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "anvil", "name": "anvil",
"version": "0.0.2", "version": "0.0.3",
"devDependencies": { "devDependencies": {
"@playwright/test": "1.41.2", "@playwright/test": "1.41.2",
"@skeletonlabs/skeleton": "2.8.0", "@skeletonlabs/skeleton": "2.8.0",

View file

@ -1,6 +1,6 @@
{ {
"name": "anvil", "name": "anvil",
"version": "0.0.2", "version": "0.0.3",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "vite dev", "dev": "vite dev",

View file

@ -1,5 +1,5 @@
<!doctype html> <!doctype html>
<html lang="en" class="dark"> <html lang="en">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" /> <link rel="icon" href="%sveltekit.assets%/favicon.png" />

View file

@ -0,0 +1,10 @@
<script lang="ts">
import ImportProjectTemplate from '../templates/ImportProject.svelte';
/**
* Data populated by SvelteKit
*/
export let data: unknown = null;
</script>
<ImportProjectTemplate {data} />

View file

@ -0,0 +1,119 @@
<script lang="ts">
import { AlertFill16, Info16, Repo24, Upload16 } from 'svelte-octicons';
import { _ } from 'svelte-i18n';
/**
* Required context for populating the template.
*/
export const data: unknown = null;
</script>
<section class="w-full">
<form
class="w-96 flex flex-col gap-6 mx-auto px-8 py-8 bg-surface-100 border border-surface-300"
name="import-project"
>
<h2 class="h2 text-surface-500">{$_('page.import_project.heading')}</h2>
<p class="text-surface-500">{$_('page.import_project.intro')}</p>
<div class="flex flex-col gap-1">
<h3 class="text-surface-500">
<label class="label" for="project-name">
{$_('page.import_project.form.fields.name.label')}
<span class="required">*</span>
</label>
</h3>
<input
id="project-name"
name="project-name"
class="input input-error"
type="text"
placeholder={$_('page.import_project.form.fields.name.placeholder')}
/>
<div class="flex gap-2 items-center text-error-500">
<AlertFill16 fill="currentColor" />
{$_('page.import_project.form.fields.name.error')}
</div>
<div class="text-surface-400">
domain.example/projects/<span class="text-surface-500">NameUpdateRealtime</span>
</div>
</div>
<div>
<h3 class="text-surface-500">
<label class="label" for="project-description">
{$_('page.import_project.form.fields.description.label')}
</label>
</h3>
<textarea
id="project-description"
name="project-description"
class="textarea"
placeholder={$_('page.import_project.form.fields.description.placeholder')}
/>
</div>
<div>
<h3 class="text-surface-500">{$_('page.import_project.form.avatar')}</h3>
<div class="flex">
<span class="bg-surface-300 flex justify-center items-center h-12 w-12">
<Repo24 class="h-6 w-6" />
</span>
<label
for="project-avatar"
class="h-12 flex flex-1 gap-2 items-center justify-center px-4 bg-surface-025 text-surface-400"
>
<Upload16 fill="currentColor" />
{$_('page.import_project.form.fields.avatar.label')}
</label>
<input
id="project-avatar"
name="project-avatar"
class="opacity-0 w-1"
type="file"
accept="image/*"
/>
</div>
</div>
<div class="flex flex-col gap-2">
<h3 class="text-surface-500">
{$_('page.import_project.form.components')}
</h3>
<div>
<input
id="project-repository"
name="project-repository"
class="input h-6 w-6"
type="checkbox"
/>
<label for="project-repository" class="label inline text-surface-500">
{$_('page.import_project.form.fields.repository.label')}
</label>
<input
type="url"
class="input ml-8 w-auto"
placeholder={$_('page.import_project.form.fields.repository.placeholder')}
/>
<div class="flex gap-1 items-center ml-8 text-warning-700">
<Info16 fill="currentColor" />
{$_('page.import_project.form.fields.repository.hint')}
</div>
</div>
<div>
<input id="project-issues" name="project-issues" class="input h-6 w-6" type="checkbox" />
<label for="project-issues" class="label inline text-surface-500">
{$_('page.import_project.form.fields.issues.label')}
</label>
</div>
<div>
<input id="project-pr" name="project-pr" class="input h-6 w-6" type="checkbox" />
<label for="project-pr" class="label inline text-surface-500">
{$_('page.import_project.form.fields.pr.label')}
</label>
</div>
</div>
<button type="submit" class="button bg-success-600 text-white h-12">
{$_('page.import_project.form.submit')}
</button>
</form>
</section>

View file

@ -1,5 +1,39 @@
{ {
"page": { "page": {
"import_project": {
"form": {
"avatar": "Avatar",
"components": "Components",
"fields": {
"avatar": {
"label": "Upload image"
},
"description": {
"label": "Description",
"placeholder": "Describe the project in less than 100 characters…"
},
"issues": {
"label": "Issues"
},
"name": {
"error": "This field is required",
"label": "Name",
"placeholder": "Name of the project…"
},
"pr": {
"label": "Pull Requests"
},
"repository": {
"hint": "optional: import from git repo",
"label": "Repository",
"placeholder": "url of git repository…"
}
},
"submit": "Create project"
},
"heading": "Create project",
"intro": "Add a new F2 project to Anvil."
},
"welcome": "Welcome to Anvil!" "welcome": "Welcome to Anvil!"
} }
} }

View file

@ -47,7 +47,7 @@
No projects added yet. No projects added yet.
<a class="anchor" href="#">Add a project</a> <a class="anchor" href="#">Add a project</a>
or or
<a class="anchor" href="#">import a project</a>. <a class="anchor" href="/projects/import/">import a project</a>.
</p> </p>
</div> </div>
</div> </div>

View file

@ -0,0 +1,4 @@
/** @type {import('./$types').PageServerLoad} */
export async function load({ params }) {
return {};
}

View file

@ -0,0 +1,8 @@
<script>
import ImportProject from '$lib/components/pages/ImportProject.svelte';
/** @type {import('./types').PageData} */
export let data;
</script>
<ImportProject {data} />

View file

@ -0,0 +1,304 @@
import '@testing-library/jest-dom';
import { render, screen } from '@testing-library/svelte';
import { init, locale, register } from 'svelte-i18n';
import ImportProject from '../../../src/lib/components/templates/ImportProject.svelte';
import enMessages from '../../../src/lib/i18n/locales/en.json';
describe('ImportProject.svelte', () => {
beforeEach(() => {
register('en', () => import('../../../src/lib/i18n/locales/en.json'));
init({ fallbackLocale: 'en', initialLocale: 'en' });
locale.set('en');
});
it('should mount', () => {
// Arrange
// Nothing to prepare
// Act
const { container } = render(ImportProject);
// Assert
expect(container).toBeTruthy();
});
it('should have a form', () => {
// Arrange
// Nothing to prepare
// Act
render(ImportProject);
// Assert
expect(screen.getByRole('form')).toBeInTheDocument();
});
it('should have a h2', () => {
// Arrange
// Nothing to prepare
// Act
render(ImportProject);
const h2 = screen.getByRole('heading', { level: 2 });
// Assert
expect(h2).toBeInTheDocument();
expect(h2).toHaveTextContent(enMessages.page.import_project.heading);
});
it('should have an intro text', () => {
// Arrange
// Nothing to prepare
// Act
render(ImportProject);
// Assert
expect(screen.getByText(enMessages.page.import_project.intro)).toBeInTheDocument();
});
describe('name', () => {
it('should have a label', () => {
// Arrange
// Nothing to prepare
// Act
render(ImportProject);
// Turn into regular expression to account for the asterisk
const name = screen.getByLabelText(
new RegExp(enMessages.page.import_project.form.fields.name.label)
);
// Assert
expect(name).toBeInTheDocument();
});
it('should have a text input', () => {
// Arrange
// Nothing to prepare
// Act
render(ImportProject);
const name = screen.getByPlaceholderText(
enMessages.page.import_project.form.fields.name.placeholder
);
// Assert
expect(name).toBeInTheDocument();
expect(name).toHaveAttribute('type', 'text');
});
describe('when empty', () => {
it('should display an error', () => {
// Arrange
// Nothing to prepare
// Act
render(ImportProject);
// Assert
expect(
screen.getByText(enMessages.page.import_project.form.fields.name.error)
).toBeInTheDocument();
});
});
// FIXME: Enable once the dev server is running
describe.skip('when filled', () => {
it('should hide the error', () => {
// Arrange
// Nothing to prepare
// Act
render(ImportProject);
// Assert
expect(
screen.getByText(enMessages.page.import_project.form.fields.name.error)
).not.toBeInTheDocument();
});
it('should show a live updating hint', () => {
// Arrange
const value = 'Vervis';
// Act
render(ImportProject, { value });
// Assert
expect(screen.getByText(`domain.example/projects/${value}`)).toBeInTheDocument();
});
});
});
describe('description', () => {
it('should have a label', () => {
// Arrange
// Nothing to prepare
// Act
render(ImportProject);
const description = screen.getByLabelText(
enMessages.page.import_project.form.fields.description.label
);
// Assert
expect(description).toBeInTheDocument();
});
it('should have a textarea', () => {
// Arrange
// Nothing to prepare
// Act
render(ImportProject);
const description = screen.getByPlaceholderText(
enMessages.page.import_project.form.fields.description.placeholder
);
// Assert
expect(description).toBeInTheDocument();
});
});
describe('avatar', () => {
it('should have a heading', () => {
// Arrange
// Nothing to prepare
// Act
render(ImportProject);
const avatar = screen.getByRole('heading', {
level: 3,
name: enMessages.page.import_project.form.avatar
});
// Assert
expect(avatar).toBeInTheDocument();
});
it('should have an upload button', () => {
// Arrange
// Nothing to prepare
// Act
render(ImportProject);
// Turn into regular expression to account for the icon
const avatar = screen.getByLabelText(
new RegExp(enMessages.page.import_project.form.fields.avatar.label)
);
// Assert
expect(avatar).toBeInTheDocument();
});
});
describe('components', () => {
it('should have a heading', () => {
// Arrange
// Nothing to prepare
// Act
render(ImportProject);
const components = screen.getByRole('heading', {
level: 3,
name: enMessages.page.import_project.form.components
});
// Assert
expect(components).toBeInTheDocument();
});
describe('repository', () => {
it('should have a label', () => {
// Arrange
// Nothing to prepare
// Act
render(ImportProject);
// Turn into regular expression to account for the icon
const repository = screen.getByLabelText(
enMessages.page.import_project.form.fields.repository.label
);
// Assert
expect(repository).toBeInTheDocument();
});
it('should have an URL input', () => {
// Arrange
// Nothing to prepare
// Act
render(ImportProject);
// Turn into regular expression to account for the icon
const repository = screen.getByPlaceholderText(
enMessages.page.import_project.form.fields.repository.placeholder
);
// Assert
expect(repository).toBeInTheDocument();
});
it('should have a hint', () => {
// Arrange
// Nothing to prepare
// Act
render(ImportProject);
// Turn into regular expression to account for the icon
const repository = screen.getByText(
enMessages.page.import_project.form.fields.repository.hint
);
// Assert
expect(repository).toBeInTheDocument();
});
});
describe('issues', () => {
it('should have a label', () => {
// Arrange
// Nothing to prepare
// Act
render(ImportProject);
// Turn into regular expression to account for the icon
const issues = screen.getByLabelText(
enMessages.page.import_project.form.fields.issues.label
);
// Assert
expect(issues).toBeInTheDocument();
});
});
describe('pull requests', () => {
it('should have a label', () => {
// Arrange
// Nothing to prepare
// Act
render(ImportProject);
// Turn into regular expression to account for the icon
const issues = screen.getByLabelText(enMessages.page.import_project.form.fields.pr.label);
// Assert
expect(issues).toBeInTheDocument();
});
});
});
});

View file

@ -0,0 +1,304 @@
import '@testing-library/jest-dom';
import { render, screen } from '@testing-library/svelte';
import { init, locale, register } from 'svelte-i18n';
import ImportProject from '../../../src/lib/components/pages/ImportProject.svelte';
import enMessages from '../../../src/lib/i18n/locales/en.json';
describe('ImportProject.svelte', () => {
beforeEach(() => {
register('en', () => import('../../../src/lib/i18n/locales/en.json'));
init({ fallbackLocale: 'en', initialLocale: 'en' });
locale.set('en');
});
it('should mount', () => {
// Arrange
// Nothing to prepare
// Act
const { container } = render(ImportProject);
// Assert
expect(container).toBeTruthy();
});
it('should have a form', () => {
// Arrange
// Nothing to prepare
// Act
render(ImportProject);
// Assert
expect(screen.getByRole('form')).toBeInTheDocument();
});
it('should have a h2', () => {
// Arrange
// Nothing to prepare
// Act
render(ImportProject);
const h2 = screen.getByRole('heading', { level: 2 });
// Assert
expect(h2).toBeInTheDocument();
expect(h2).toHaveTextContent(enMessages.page.import_project.heading);
});
it('should have an intro text', () => {
// Arrange
// Nothing to prepare
// Act
render(ImportProject);
// Assert
expect(screen.getByText(enMessages.page.import_project.intro)).toBeInTheDocument();
});
describe('name', () => {
it('should have a label', () => {
// Arrange
// Nothing to prepare
// Act
render(ImportProject);
// Turn into regular expression to account for the asterisk
const name = screen.getByLabelText(
new RegExp(enMessages.page.import_project.form.fields.name.label)
);
// Assert
expect(name).toBeInTheDocument();
});
it('should have a text input', () => {
// Arrange
// Nothing to prepare
// Act
render(ImportProject);
const name = screen.getByPlaceholderText(
enMessages.page.import_project.form.fields.name.placeholder
);
// Assert
expect(name).toBeInTheDocument();
expect(name).toHaveAttribute('type', 'text');
});
describe('when empty', () => {
it('should display an error', () => {
// Arrange
// Nothing to prepare
// Act
render(ImportProject);
// Assert
expect(
screen.getByText(enMessages.page.import_project.form.fields.name.error)
).toBeInTheDocument();
});
});
// FIXME: Enable once the dev server is running
describe.skip('when filled', () => {
it('should hide the error', () => {
// Arrange
// Nothing to prepare
// Act
render(ImportProject);
// Assert
expect(
screen.getByText(enMessages.page.import_project.form.fields.name.error)
).not.toBeInTheDocument();
});
it('should show a live updating hint', () => {
// Arrange
const value = 'Vervis';
// Act
render(ImportProject, { value });
// Assert
expect(screen.getByText(`domain.example/projects/${value}`)).toBeInTheDocument();
});
});
});
describe('description', () => {
it('should have a label', () => {
// Arrange
// Nothing to prepare
// Act
render(ImportProject);
const description = screen.getByLabelText(
enMessages.page.import_project.form.fields.description.label
);
// Assert
expect(description).toBeInTheDocument();
});
it('should have a textarea', () => {
// Arrange
// Nothing to prepare
// Act
render(ImportProject);
const description = screen.getByPlaceholderText(
enMessages.page.import_project.form.fields.description.placeholder
);
// Assert
expect(description).toBeInTheDocument();
});
});
describe('avatar', () => {
it('should have a heading', () => {
// Arrange
// Nothing to prepare
// Act
render(ImportProject);
const avatar = screen.getByRole('heading', {
level: 3,
name: enMessages.page.import_project.form.avatar
});
// Assert
expect(avatar).toBeInTheDocument();
});
it('should have an upload button', () => {
// Arrange
// Nothing to prepare
// Act
render(ImportProject);
// Turn into regular expression to account for the icon
const avatar = screen.getByLabelText(
new RegExp(enMessages.page.import_project.form.fields.avatar.label)
);
// Assert
expect(avatar).toBeInTheDocument();
});
});
describe('components', () => {
it('should have a heading', () => {
// Arrange
// Nothing to prepare
// Act
render(ImportProject);
const components = screen.getByRole('heading', {
level: 3,
name: enMessages.page.import_project.form.components
});
// Assert
expect(components).toBeInTheDocument();
});
describe('repository', () => {
it('should have a label', () => {
// Arrange
// Nothing to prepare
// Act
render(ImportProject);
// Turn into regular expression to account for the icon
const repository = screen.getByLabelText(
enMessages.page.import_project.form.fields.repository.label
);
// Assert
expect(repository).toBeInTheDocument();
});
it('should have an URL input', () => {
// Arrange
// Nothing to prepare
// Act
render(ImportProject);
// Turn into regular expression to account for the icon
const repository = screen.getByPlaceholderText(
enMessages.page.import_project.form.fields.repository.placeholder
);
// Assert
expect(repository).toBeInTheDocument();
});
it('should have a hint', () => {
// Arrange
// Nothing to prepare
// Act
render(ImportProject);
// Turn into regular expression to account for the icon
const repository = screen.getByText(
enMessages.page.import_project.form.fields.repository.hint
);
// Assert
expect(repository).toBeInTheDocument();
});
});
describe('issues', () => {
it('should have a label', () => {
// Arrange
// Nothing to prepare
// Act
render(ImportProject);
// Turn into regular expression to account for the icon
const issues = screen.getByLabelText(
enMessages.page.import_project.form.fields.issues.label
);
// Assert
expect(issues).toBeInTheDocument();
});
});
describe('pull requests', () => {
it('should have a label', () => {
// Arrange
// Nothing to prepare
// Act
render(ImportProject);
// Turn into regular expression to account for the icon
const issues = screen.getByLabelText(enMessages.page.import_project.form.fields.pr.label);
// Assert
expect(issues).toBeInTheDocument();
});
});
});
});