diff --git a/.github/workflows/maven-build.yml b/.github/workflows/maven-build.yml index 0b09dac..44f6219 100644 --- a/.github/workflows/maven-build.yml +++ b/.github/workflows/maven-build.yml @@ -13,10 +13,10 @@ jobs: steps: - uses: actions/checkout@v2 - - name: Set up JDK 11 + - name: Set up JDK 21 uses: actions/setup-java@v2 with: - java-version: '11' + java-version: '21' distribution: 'adopt' cache: maven - name: Build with Maven Wrapper diff --git a/.gitignore b/.gitignore index a8d9fd9..3fb6e1e 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,5 @@ node_modules/ frontend/generated/ pnpmfile.js webpack.generated.js + +/src/main/frontend/generated/ diff --git a/frontend/index.html b/frontend/index.html deleted file mode 100644 index 35bc55e..0000000 --- a/frontend/index.html +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - - - - - - PetClinic :: a Spring Framework demonstration - - - - - - -
- - diff --git a/frontend/index.ts b/frontend/index.ts deleted file mode 100644 index 1768d5e..0000000 --- a/frontend/index.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { Router, RouterLocation } from '@vaadin/router'; -import { routes } from './routes'; -import { appStore } from './stores/app-store'; -/* - * TODO: This style-modules.js import here is a workaround to fix broken theme. - * Remove when https://github.com/vaadin/flow/pull/12017 is resolved. - */ -import '@vaadin/polymer-legacy-adapter/style-modules.js'; - -export const router = new Router(document.querySelector('#outlet')); - -router.setRoutes(routes); - -type RouterLocationChangedEvent = CustomEvent<{ - router: Router; - location: RouterLocation; -}>; - -window.addEventListener( - 'vaadin-router-location-changed', - (e: RouterLocationChangedEvent) => { - appStore.setLocation(e.detail.location); - const title = appStore.currentViewTitle; - if (title) { - document.title = title + ' | ' + appStore.applicationName; - } else { - document.title = appStore.applicationName; - } - } -); diff --git a/frontend/routes.ts b/frontend/routes.ts deleted file mode 100644 index a3d6986..0000000 --- a/frontend/routes.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { Route } from '@vaadin/router'; -import './views/error-view'; -import './views/home-view'; -import './views/main-layout'; -import './views/owners/create-or-update-owner-view'; -import './views/owners/find-owners-view'; -import './views/owners/owner-details-view'; -import './views/pets/create-or-update-pet-view'; -import './views/pets/create-or-update-visit-view'; - -export type ViewRoute = Route & { - title?: string; - icon?: string; - children?: ViewRoute[]; -}; - -export const views: ViewRoute[] = [ - // place routes below (more info https://hilla.dev/docs/routing/defining) - { - path: '/', - name: 'home', - component: 'home-view', - icon: 'la la-home', - title: 'Home', - }, - { - // Included on root level to include in navigation menu. - path: '/owners/find', - name: 'find-owners', - component: 'find-owners-view', - icon: 'la la-search', - title: 'Find owners', - }, - { - path: '/owners', - name: 'owners-base', - component: 'span', - children: [ - { - path: '/', - component: 'find-owners-view', - }, - { - path: '/new', - name: 'new-owner', - component: 'create-or-update-owner-view', - }, - { - path: '/:ownerId([0-9]+)', - children: [ - { - path: '/', - name: 'owner-details', - component: 'owner-details-view', - }, - { - path: '/edit', - name: 'edit-owner', - component: 'create-or-update-owner-view', - }, - { - path: '/pets/new', - name: 'add-pet', - component: 'create-or-update-pet-view', - }, - { - path: '/pets/:petId([0-9]+)/edit', - name: 'edit-pet', - component: 'create-or-update-pet-view', - }, - { - path: '/pets/:petId([0-9]+)/visits/new', - name: 'add-visit', - component: 'create-or-update-visit-view', - }, - ], - }, - ], - }, - { - path: '/vets', - name: 'vets-list', - component: 'vets-view', - icon: 'la la-th-list', - title: 'Veterinarians', - // Defer the load of the Vets view until it is accessed - action: async () => { - await import('./views/vets-view'); - }, - }, - { - path: '/oups', - component: 'error-view', - icon: 'la la-exclamation-triangle', - title: 'Error', - }, -]; - -export const routes: ViewRoute[] = [ - { - path: '/', - component: 'main-layout', - children: [...views], - }, -]; diff --git a/frontend/stores/app-store.ts b/frontend/stores/app-store.ts deleted file mode 100644 index 8483fda..0000000 --- a/frontend/stores/app-store.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { RouterLocation } from '@vaadin/router'; -import { makeAutoObservable } from 'mobx'; - -export class AppStore { - applicationName = 'Hilla PetClinic'; - - // The location, relative to the base path, e.g. "hello" when viewing "/hello" - location = ''; - - currentViewTitle = ''; - - constructor() { - makeAutoObservable(this); - } - - setLocation(location: RouterLocation) { - if (location.route) { - this.location = location.route.path; - } else if (location.pathname.startsWith(location.baseUrl)) { - this.location = location.pathname.substr(location.baseUrl.length); - } else { - this.location = location.pathname; - } - this.currentViewTitle = (location?.route as any)?.title || ''; - } -} -export const appStore = new AppStore(); diff --git a/frontend/themes/petclinic/main-layout.css b/frontend/themes/petclinic/main-layout.css deleted file mode 100644 index 92876af..0000000 --- a/frontend/themes/petclinic/main-layout.css +++ /dev/null @@ -1,36 +0,0 @@ -[slot='navbar'] { - /* background-image: linear-gradient(0deg, var(--lumo-shade-5pct), var(--lumo-shade-5pct)); */ -} - -[slot='navbar'] nav a { - text-decoration: none; - transition: color 140ms; -} - -[slot='navbar'] nav a .la { - margin-top: calc(var(--lumo-space-xs) * 0.5); -} - -[slot='navbar'] nav a::before { - border-radius: var(--lumo-border-radius); - bottom: calc(var(--lumo-space-xs) * 0.5); - content: ''; - left: 0; - position: absolute; - right: 0; - top: calc(var(--lumo-space-xs) * 0.5); - transition: background-color 140ms; -} - -[slot='navbar'] nav a[highlight] { - color: var(--lumo-primary-text-color); -} - -[slot='navbar'] nav a[highlight]::before { - background-color: var(--lumo-primary-color-10pct); -} - -[slot='navbar'] footer vaadin-context-menu { - align-items: center; - display: flex; -} diff --git a/frontend/views/error-view.ts b/frontend/views/error-view.ts deleted file mode 100644 index c1e588b..0000000 --- a/frontend/views/error-view.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { html } from 'lit'; -import { customElement, state } from 'lit/decorators.js'; -import { View } from '../views/view'; -import { CrashEndpoint } from 'Frontend/generated/endpoints'; - -@customElement('error-view') -export class ErrorView extends View { - @state() - result?: string; - - async connectedCallback() { - super.connectedCallback(); - this.result = await CrashEndpoint.triggerException(); - } - - render() { - return html` -

See JS console for errors.

-

- For more information see the Hilla - - Error Handling - - documentation. -

- `; - } -} diff --git a/frontend/views/home-view.ts b/frontend/views/home-view.ts deleted file mode 100644 index 57288b5..0000000 --- a/frontend/views/home-view.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { html } from 'lit'; -import { customElement } from 'lit/decorators.js'; -import { View } from '../views/view'; - -@customElement('home-view') -export class HomeView extends View { - render() { - return html` -

Welcome

-
-
- -
-
- `; - } -} diff --git a/frontend/views/main-layout.ts b/frontend/views/main-layout.ts deleted file mode 100644 index a86a162..0000000 --- a/frontend/views/main-layout.ts +++ /dev/null @@ -1,99 +0,0 @@ -import '@vaadin/app-layout'; -import '@vaadin/vaadin-lumo-styles/utility'; -import { html } from 'lit'; -import { customElement } from 'lit/decorators.js'; -import { router } from '../index'; -import { views } from '../routes'; -import { appStore } from '../stores/app-store'; -import { Layout } from './view'; -import type { RouterLocation } from '@vaadin/router'; - -@customElement('main-layout') -export class MainLayout extends Layout { - render() { - return html` - -
- -
-
-
-
- -
- - -
-
-
- `; - } - - isSubRoute(location: RouterLocation, parentRouteName: string) { - return ( - location.routes.find((r) => r.name === parentRouteName) !== undefined - ); - } - - highlightNav(routePath: string) { - // Highlight route '/' - if (routePath === '/' && appStore.location === routePath) { - return true; - } - // Generic highlight for most routes - if (routePath !== '/' && appStore.location.startsWith(routePath)) { - return true; - } - // Highlight "Find owners" if this is any view under the owners-base route - if ( - routePath === router.urlForName('find-owners') && - this.isSubRoute(router.location, 'owners-base') - ) { - return true; - } - return false; - } - - connectedCallback() { - super.connectedCallback(); - this.classList.add('block', 'h-full'); - } - - private getMenuRoutes() { - return views.filter((route) => route.title); - } -} diff --git a/frontend/views/owners/blocks.ts b/frontend/views/owners/blocks.ts deleted file mode 100644 index 22ddcfb..0000000 --- a/frontend/views/owners/blocks.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { html } from 'lit'; -import { field } from '@hilla/form'; -import '@vaadin/form-layout'; -import '@vaadin/text-field'; -import OwnerModel from 'Frontend/generated/org/springframework/samples/petclinic/owner/OwnerModel'; - -export function ownerForm(model: OwnerModel, readonly = false) { - return html` -
- - - - - -
- `; -} diff --git a/frontend/views/owners/create-or-update-owner-view.ts b/frontend/views/owners/create-or-update-owner-view.ts deleted file mode 100644 index a4c9d5e..0000000 --- a/frontend/views/owners/create-or-update-owner-view.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { html, nothing } from 'lit'; -import { customElement, state } from 'lit/decorators.js'; -import { Binder, ValidationError } from '@hilla/form'; -import { Router } from '@vaadin/router'; -import '@vaadin/button'; -import '@vaadin/form-layout'; -import '@vaadin/text-field'; -import { View } from '../../views/view'; -import { router } from 'Frontend/index'; -import { OwnerEndpoint } from 'Frontend/generated/endpoints'; -import Owner from 'Frontend/generated/org/springframework/samples/petclinic/owner/Owner'; -import OwnerModel from 'Frontend/generated/org/springframework/samples/petclinic/owner/OwnerModel'; -import { EndpointError } from '@hilla/frontend'; -import { ownerForm } from 'Frontend/views/owners/blocks'; - -@customElement('create-or-update-owner-view') -export class CreateOrUpdateOwnerView extends View { - @state() owner?: Owner; - @state() error = ''; - - private binder = new Binder(this, OwnerModel); - - connectedCallback() { - super.connectedCallback(); - if (router.location.route?.name === 'edit-owner') { - const id = parseInt(router.location.params.ownerId as string); - this.fetchOwner(id); - } - } - - async fetchOwner(id: number) { - this.binder.clear(); - try { - this.owner = await OwnerEndpoint.findById(id); - } finally { - if (this.owner) { - this.binder.read(this.owner); - } else { - this.error = `No owner found with id ${id}`; - } - } - } - - render() { - const model = this.binder.model; - const submitButtonText = this.owner ? 'Update Owner' : 'Add Owner'; - return html` -
-

Owner

- - ${ownerForm(model)} - - ${submitButtonText} - - - ${this.error ? html`

${this.error}

` : nothing} -
- `; - } - - async submit() { - let ownerId: number; - - try { - ownerId = await this.binder.submitTo(OwnerEndpoint.save); - } catch (e) { - if (e instanceof EndpointError) { - this.error = 'Saving owner failed due to server error'; - } else if (e instanceof ValidationError) { - this.error = 'Saving owner failed due to validation error(s).'; - } else { - this.error = - 'Saving owner failed due to network error. Try again later.'; - } - console.error(e); - return; - } - - Router.go(`/owners/${ownerId}`); - } -} diff --git a/frontend/views/owners/find-owners-view.ts b/frontend/views/owners/find-owners-view.ts deleted file mode 100644 index 7f0c59f..0000000 --- a/frontend/views/owners/find-owners-view.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { html, nothing, PropertyValueMap, render } from 'lit'; -import { customElement, property, state } from 'lit/decorators.js'; -import '@vaadin/button'; -import '@vaadin/icons'; -import '@vaadin/icon'; -import '@vaadin/grid'; -import '@vaadin/text-field'; -import type { TextField } from '@vaadin/text-field'; -import { View } from '../../views/view'; -import { router } from 'Frontend/index'; -import { Router } from '@vaadin/router'; -import Owner from 'Frontend/generated/org/springframework/samples/petclinic/owner/Owner'; -import { OwnerEndpoint } from 'Frontend/generated/endpoints'; -import { GridBodyRenderer } from '@vaadin/grid'; - -@customElement('find-owners-view') -export class FindOwnersView extends View { - @state() lastName = ''; - @state() owners: Owner[] = []; - - async firstUpdated() { - this.owners = await OwnerEndpoint.findByLastName(''); - } - - render() { - return html` -
-

Find Owners

- -
- - - - - Find Owner - - Add Owner -
- - ${this.owners.length > 0 - ? html` - - - - - - - - ` - : nothing} -
- `; - } - - lastNameChanged(event: Event) { - const textField = event.target as TextField; - this.lastName = textField.value; - } - - textFieldKeyUp(event: KeyboardEvent) { - if (event.key === 'Enter') { - this.findOwner(); - } - } - - async findOwner() { - this.owners = await OwnerEndpoint.findByLastName(this.lastName); - } - - addOwner() { - Router.go(router.urlForName('new-owner')); - } - - private nameRenderer: GridBodyRenderer = (root, _, model) => { - const owner = model.item; - render( - html` - ${owner.firstName} ${owner.lastName} - `, - root - ); - }; - - private petRenderer: GridBodyRenderer = (root, _, model) => { - const owner = model.item; - render(html`${owner.pets.flatMap((p) => p.name).join(', ')}`, root); - }; -} diff --git a/frontend/views/owners/owner-details-view.ts b/frontend/views/owners/owner-details-view.ts deleted file mode 100644 index 003cea1..0000000 --- a/frontend/views/owners/owner-details-view.ts +++ /dev/null @@ -1,127 +0,0 @@ -import { html } from 'lit'; -import { customElement, state } from 'lit/decorators.js'; -import '@vaadin/button'; -import '@vaadin/form-layout'; -import '@vaadin/form-layout/vaadin-form-item'; -import '@vaadin/text-field'; -import '@vaadin/grid'; -import '@vaadin/grid/vaadin-grid-column'; -import '@vaadin/grid/vaadin-grid-sort-column'; -import { View } from '../../views/view'; -import { OwnerEndpoint, VisitEndpoint } from 'Frontend/generated/endpoints'; -import Owner from 'Frontend/generated/org/springframework/samples/petclinic/owner/Owner'; -import Pet from 'Frontend/generated/org/springframework/samples/petclinic/owner/Pet'; -import { Binder } from '@hilla/form'; -import OwnerModel from 'Frontend/generated/org/springframework/samples/petclinic/owner/OwnerModel'; -import { ownerForm } from 'Frontend/views/owners/blocks'; -import { BeforeEnterObserver, RouterLocation } from '@vaadin/router'; - -@customElement('owner-details-view') -export class OwnerDetailsView extends View implements BeforeEnterObserver { - @state() - private owner?: Owner; - private binder = new Binder(this, OwnerModel); - - onBeforeEnter(location: RouterLocation) { - const id = parseInt(location.params.ownerId as string); - this.fetchOwner(id); - } - - async fetchOwner(id: number) { - this.binder.clear(); - this.owner = await OwnerEndpoint.findById(id); - if (!this.owner) return; - - // Fetch visits for pets - if (this.owner.pets) { - let pets: Pet[] = []; - for (const pet of this.owner.pets) { - const visits = await VisitEndpoint.findByPetId(pet.id); - pets.push({ ...pet, visits }); - } - this.owner = { ...this.owner, pets }; - } - this.binder.read(this.owner); - } - - render() { - const { model } = this.binder; - - return html` -
- -
- -

Owner Information

- - ${ownerForm(model, true)} - - -
- -
- -

Pets and Visits

- -
- ${this.owner?.pets.map((pet) => this.petLayout(pet))} -
-
-
- - `; - } - - petLayout(pet: Pet) { - return html` -
-
- - - -
- - -
- `; - } -} diff --git a/frontend/views/pets/create-or-update-pet-view.ts b/frontend/views/pets/create-or-update-pet-view.ts deleted file mode 100644 index ebc2b42..0000000 --- a/frontend/views/pets/create-or-update-pet-view.ts +++ /dev/null @@ -1,135 +0,0 @@ -import { html, nothing, PropertyValues } from 'lit'; -import { customElement, state } from 'lit/decorators.js'; -import { createRef, ref, Ref } from 'lit/directives/ref.js'; -import { Binder, field, ValidationError } from '@hilla/form'; -import { BeforeEnterObserver, Router, RouterLocation } from '@vaadin/router'; -import { selectRenderer } from 'lit-vaadin-helpers'; -import { formatISO } from 'date-fns'; -import '@vaadin/button'; -import '@vaadin/date-picker'; -import '@vaadin/form-layout'; -import '@vaadin/form-layout/vaadin-form-item'; -import '@vaadin/item'; -import '@vaadin/list-box'; -import '@vaadin/select'; -import type { Select, SelectItem } from '@vaadin/select'; -import '@vaadin/text-field'; -import { View } from '../../views/view'; -import { OwnerEndpoint, PetEndpoint } from 'Frontend/generated/endpoints'; -import Owner from 'Frontend/generated/org/springframework/samples/petclinic/owner/Owner'; -import PetDTO from 'Frontend/generated/org/springframework/samples/petclinic/dto/PetDTO'; -import PetDTOModel from 'Frontend/generated/org/springframework/samples/petclinic/dto/PetDTOModel'; -import { EndpointError } from '@hilla/frontend'; -import PetType from 'Frontend/generated/org/springframework/samples/petclinic/owner/PetType'; - -@customElement('create-or-update-pet-view') -export class CreateOrUpdatePetView extends View implements BeforeEnterObserver { - @state() owner?: Owner; - @state() pet?: PetDTO; - @state() petTypes: SelectItem[] = []; - @state() error = ''; - @state() today = formatISO(Date.now(), { representation: 'date' }); - - private binder = new Binder(this, PetDTOModel); - private selectRef: Ref