diff --git a/packages/nde-erfgoed-components/lib/demo/demo-form.component.ts b/packages/nde-erfgoed-components/lib/demo/demo-form.component.ts index a49f9a12..0cdd9386 100644 --- a/packages/nde-erfgoed-components/lib/demo/demo-form.component.ts +++ b/packages/nde-erfgoed-components/lib/demo/demo-form.component.ts @@ -36,15 +36,15 @@ export class DemoFormComponent extends RxLitElement { public translator: Translator = new MemoryTranslator([ { key: 'demo-form.name.required', - locale: 'nl-BE', + locale: 'nl-NL', value: 'Name is required.', }, { key: 'demo-form.uri.required', - locale: 'nl-BE', + locale: 'nl-NL', value: 'URI is required.', }, - ], 'nl-BE'); + ], 'nl-NL'); /** * The actor controlling this component. diff --git a/packages/nde-erfgoed-components/lib/forms/form-element.component.ts b/packages/nde-erfgoed-components/lib/forms/form-element.component.ts index 7a6ae6ad..ee6249eb 100644 --- a/packages/nde-erfgoed-components/lib/forms/form-element.component.ts +++ b/packages/nde-erfgoed-components/lib/forms/form-element.component.ts @@ -15,6 +15,12 @@ import { FormEvents, FormUpdatedEvent } from './form.events'; */ export class FormElementComponent extends RxLitElement { + /** + * Decides whether a border should be shown around the content + */ + @property() + public inverse = false; + /** * The component's translator. */ @@ -45,56 +51,6 @@ export class FormElementComponent extends RxLitElement { @property({type: Object}) public actor: SpawnedActorRef, State>>; - /** - * The styles associated with the component. - */ - static get styles() { - return [ - unsafeCSS(Theme), - css` - :root { - display: block; - } - .form-element { - display: flex; - flex-direction: column; - align-items: stretch; - } - .form-element .content { - display: flex; - flex-direction: row; - align-items: stretch; - } - .form-element .content .field { - display: flex; - flex-direction: row; - border: var(--border-normal) solid var(--colors-foreground-normal); - padding: var(--gap-small) var(--gap-normal); - height: 20px; - align-items: center; - flex: 1 0; - } - .form-element .label { - font-weight: var(--font-weight-bold); - margin-bottom: var(--gap-small); - } - .form-element .content .field .input { - flex: 1 0; - } - .form-element .content .field .icon { - max-height: var(--gap-normal); - max-width: var(--gap-normal); - margin-left: var(--gap-normal); - } - .form-element .results .result { - background-color: var(--colors-status-warning); - padding: var(--gap-tiny) var(--gap-normal); - font-size: var(--font-size-small); - } - `, - ]; - } - /** * Hook called on first update after connection to the DOM. * It subscribes to the actor, logs state changes, and pipes state to the properties. @@ -169,7 +125,7 @@ export class FormElementComponent extends RxLitElement {
-
+
@@ -178,7 +134,7 @@ export class FormElementComponent extends RxLitElement {
- +
@@ -190,6 +146,71 @@ export class FormElementComponent extends RxLitElement {
`; } + + /** + * The styles associated with the component. + */ + static get styles() { + return [ + unsafeCSS(Theme), + css` + :root { + display: block; + } + .no-border, .no-border ::slotted(*) { + border: none !important; + } + .form-element { + display: flex; + flex-direction: column; + align-items: stretch; + } + .form-element .label { + font-weight: var(--font-weight-bold); + margin-bottom: var(--gap-small); + } + .form-element .content { + display: flex; + flex-direction: row; + align-items: stretch; + height: 44px; + background-color: var(--colors-background-light) + } + .form-element .content .field { + display: flex; + flex-direction: row; + align-items: stretch; + justify-content: space-between; + flex: 1 0; + border: var(--border-normal) solid var(--colors-foreground-normal); + } + .form-element .content .field .input { + padding: 0 var(--gap-normal); + width: 100%; + height: 100%; + } + .form-element .content .field .input ::slotted(input) { + width: 100%; + height: 100%; + } + .form-element .content .field .icon { + padding: 0 var(--gap-normal); + height: 100%; + display: flex; + align-items: center; + } + .form-element .content .field .icon ::slotted(*) { + max-height: var(--gap-normal); + max-width: var(--gap-normal); + } + .form-element .results .result { + background-color: var(--colors-status-warning); + padding: var(--gap-tiny) var(--gap-normal); + font-size: var(--font-size-small); + } + `, + ]; + } } export default FormElementComponent; diff --git a/packages/nde-erfgoed-components/lib/forms/form.events.ts b/packages/nde-erfgoed-components/lib/forms/form.events.ts index ebcc3d10..c9332239 100644 --- a/packages/nde-erfgoed-components/lib/forms/form.events.ts +++ b/packages/nde-erfgoed-components/lib/forms/form.events.ts @@ -8,7 +8,7 @@ import { FormContext } from './form.machine'; */ export enum FormEvents { FORM_UPDATED = '[FormEvent: Updated element]', - FORM_SUBMITTED = '[FormEvent: Subitted]', + FORM_SUBMITTED = '[FormEvent: Submitted]', } /** diff --git a/packages/nde-erfgoed-components/lib/forms/form.machine.spec.ts b/packages/nde-erfgoed-components/lib/forms/form.machine.spec.ts index 4bddde12..bb2172dc 100644 --- a/packages/nde-erfgoed-components/lib/forms/form.machine.spec.ts +++ b/packages/nde-erfgoed-components/lib/forms/form.machine.spec.ts @@ -48,22 +48,24 @@ describe('FormMachine', () => { expect(machine.state.context.data).toEqual(data); // States should be updated - const stateValueMap = machine.state.value as StateValueMap; - expect(stateValueMap[FormRootStates.CLEANLINESS]).toBe(cleanliness); - expect(stateValueMap[FormRootStates.SUBMISSION]).toBe(submission); - expect(stateValueMap[FormRootStates.VALIDATION]).toBe(validation); + expect(machine.state.matches({ + [FormRootStates.CLEANLINESS]: cleanliness, + [FormRootStates.VALIDATION]: validation, + [FormRootStates.SUBMISSION]: submission, + })).toBeTruthy(); }); - it('should should submit when form is valid', () => { + it('should submit when form is valid', () => { machine.start(); machine.send(FormEvents.FORM_UPDATED, {field: 'uri', value: 'foo'}); machine.send(FormEvents.FORM_SUBMITTED); - const stateValueMap = machine.state.value as StateValueMap; - expect(stateValueMap[FormRootStates.CLEANLINESS]).toBe(FormCleanlinessStates.DIRTY); - expect(stateValueMap[FormRootStates.SUBMISSION]).toBe(FormSubmissionStates.SUBMITTED); - expect(stateValueMap[FormRootStates.VALIDATION]).toBe(FormValidationStates.VALID); + expect(machine.state.matches({ + [FormRootStates.CLEANLINESS]: FormCleanlinessStates.DIRTY, + [FormRootStates.VALIDATION]: FormValidationStates.VALID, + [FormRootStates.SUBMISSION]: FormSubmissionStates.SUBMITTED, + })).toBeTruthy(); }); it('should not change original data when form is updated', () => { @@ -79,23 +81,10 @@ describe('FormMachine', () => { machine.send(FormEvents.FORM_SUBMITTED); - const stateValueMap = machine.state.value as StateValueMap; - expect(stateValueMap[FormRootStates.CLEANLINESS]).toBe(FormCleanlinessStates.PRISTINE); - expect(stateValueMap[FormRootStates.SUBMISSION]).toBe(FormSubmissionStates.NOT_SUBMITTED); - expect(stateValueMap[FormRootStates.VALIDATION]).toBe(FormValidationStates.INVALID); - }); - - it('should allow re-submitting forms', () => { - machine.start(); - - machine.send(FormEvents.FORM_UPDATED, {field: 'uri', value: 'foo'}); - machine.send(FormEvents.FORM_SUBMITTED); - machine.send(FormEvents.FORM_UPDATED, {field: 'name', value: ''}); - machine.send(FormEvents.FORM_SUBMITTED); - - const stateValueMap = machine.state.value as StateValueMap; - expect(stateValueMap[FormRootStates.CLEANLINESS]).toBe(FormCleanlinessStates.DIRTY); - expect(stateValueMap[FormRootStates.SUBMISSION]).toBe(FormSubmissionStates.NOT_SUBMITTED); - expect(stateValueMap[FormRootStates.VALIDATION]).toBe(FormValidationStates.INVALID); + expect(machine.state.matches({ + [FormRootStates.CLEANLINESS]: FormCleanlinessStates.PRISTINE, + [FormRootStates.VALIDATION]: FormValidationStates.INVALID, + [FormRootStates.SUBMISSION]: FormSubmissionStates.NOT_SUBMITTED, + })).toBeTruthy(); }); }); diff --git a/packages/nde-erfgoed-components/lib/forms/form.machine.ts b/packages/nde-erfgoed-components/lib/forms/form.machine.ts index a4a8ed31..c46ce245 100644 --- a/packages/nde-erfgoed-components/lib/forms/form.machine.ts +++ b/packages/nde-erfgoed-components/lib/forms/form.machine.ts @@ -1,4 +1,4 @@ -import { createMachine } from 'xstate'; +import { createMachine, sendParent } from 'xstate'; import { State } from '../state/state'; import { Event } from '../state/event'; import { FormValidatorResult } from './form-validator-result'; @@ -14,6 +14,13 @@ export interface FormContext { validation?: FormValidatorResult[]; } +/** + * Actor references for this machine config. + */ +export enum FormActors { + FORM_MACHINE = 'FormMachine', +} + /** * State references of the root parallel states of the form machine. */ @@ -60,7 +67,7 @@ export type FormStates = FormRootStates | FormSubmissionStates | FormCleanliness * The form component machine. */ export const formMachine = (validator: FormValidator) => createMachine, Event, State>>({ - id: 'form', + id: FormActors.FORM_MACHINE, type: 'parallel', states: { /** @@ -79,6 +86,7 @@ export const formMachine = (validator: FormValidator) => createMachine(validator: FormValidator) => createMachine(validator: FormValidator) => createMachine(validator: FormValidator) => createMachine(validator: FormValidator) => createMachine(validator: FormValidator) => createMachine { value: 'Foo in English', }, { - locale: 'nl-BE', + locale: 'nl-NL', key: 'foo', value: 'Foo in Dutch', }, @@ -43,13 +43,13 @@ describe('MemoryTranslator', () => { }); it('Should translate return null with an existing key in an non-existing locale.', () => { - const value = service.translate('foo.bar', 'nl-BE'); + const value = service.translate('foo.bar', 'nl-NL'); expect(value).toBeFalsy(); }); it('Should translate return null with an non-existing key in an existing locale.', () => { - const value = service.translate('lorem', 'nl-BE'); + const value = service.translate('lorem', 'nl-NL'); expect(value).toBeFalsy(); }); diff --git a/packages/nde-erfgoed-core/lib/solid/solid-sdk.service.spec.ts b/packages/nde-erfgoed-core/lib/solid/solid-sdk.service.spec.ts new file mode 100644 index 00000000..590eb51e --- /dev/null +++ b/packages/nde-erfgoed-core/lib/solid/solid-sdk.service.spec.ts @@ -0,0 +1,61 @@ +// import * as solid from '@inrupt/solid-client-authn-browser'; +// import { of } from 'rxjs'; +// import { ConsoleLogger } from '../logging/console-logger'; +// import { LoggerLevel } from '../logging/logger-level'; +// import { SolidSdkService } from './solid-sdk.service'; +// jest.mock('@inrupt/solid-client-authn-browser'); + +describe('SolidService', () => { + // let service: SolidService; + + // beforeEach(async () => { + // const logger = new ConsoleLogger(LoggerLevel.silly, LoggerLevel.silly); + // service = new SolidSdkService(logger); + // }); + + // afterEach(() => { + // // clear spies + // jest.clearAllMocks(); + // }); + + it('should be correctly instantiated', () => { + expect(true).toBeTruthy(); + }); + + // describe('login()', () => { + + // it.each([ null, undefined ])('should error when WebID is %s', async () => { + // await expect(service.login(null) + // .toPromise()).rejects.toThrow('invalid-webid.no-webid'); + // }); + + // it('should error when WebID is an invalid URL', async () => { + // await expect(service.login('a') + // .toPromise()).rejects.toThrow('invalid-webid.invalid-url'); + // }); + + // it('should error when WebID does not contain profile', async () => { + // await expect(service.login('https://google.com/') + // .toPromise()).rejects.toThrow('invalid-webid.no-profile'); + // }); + + // it('should error when WebID does not contain oidcIssuer triple', async () => { + // await expect(service.login('https://pod.inrupt.com/digitatestpod/settings/publicTypeIndex.ttl') + // .toPromise()).rejects.toThrow('invalid-webid.no-oidc-issuer'); + // }); + + // it('should error when WebID does not contain valid oidcIssuer value', async () => { + // (service as any).validateOidcIssuer = jest.fn().mockImplementationOnce(() => of(false)); + // await expect(service.login('https://pod.inrupt.com/digitatestpod/profile/card#me') + // .toPromise()).rejects.toThrow('invalid-webid.invalid-oidc-issuer'); + // }); + + // it('should call login when WebID is valid', async () => { + // const loginSpy = jest.spyOn(solid, 'login'); + // // assuming webid is valid + // (service as any).validateWebId = jest.fn().mockImplementationOnce(() => of('https://broker.pod.inrupt.com/')); + // await service.login('webId').toPromise(); + // expect(loginSpy).toHaveBeenCalledTimes(1); + // }); + // }); +}); diff --git a/packages/nde-erfgoed-manage/lib/app.root.ts b/packages/nde-erfgoed-manage/lib/app.root.ts index 8dbc6390..ec12c093 100644 --- a/packages/nde-erfgoed-manage/lib/app.root.ts +++ b/packages/nde-erfgoed-manage/lib/app.root.ts @@ -1,4 +1,4 @@ -import { html, property, PropertyValues, internalProperty, unsafeCSS } from 'lit-element'; +import { html, property, PropertyValues, internalProperty, unsafeCSS, css } from 'lit-element'; import { interpret, Interpreter, State } from 'xstate'; import { from } from 'rxjs'; import { map, tap } from 'rxjs/operators'; @@ -7,7 +7,7 @@ import { Alert } from '@digita-ai/nde-erfgoed-components'; import { RxLitElement } from 'rx-lit'; import { Theme } from '@digita-ai/nde-erfgoed-theme'; import { AppActors, AppContext, AppFeatureStates, appMachine, AppRootStates } from './app.machine'; -import nlBe from './i8n/nl-BE.json'; +import nlNL from './i8n/nl-NL.json'; import { AppEvents } from './app.events'; import { CollectionsRootComponent } from './features/collections/collections-root.component'; @@ -32,7 +32,7 @@ export class AppRootComponent extends RxLitElement { * The component's translator. */ @property({type: Translator}) - public translator: Translator = new MemoryTranslator(nlBe, 'nl-BE'); + public translator: Translator = new MemoryTranslator(nlNL, 'nl-NL'); /** * The constructor of the application root component, @@ -112,8 +112,8 @@ export class AppRootComponent extends RxLitElement {

${this.translator.translate('nde.app.root.title')}

${ alerts } - ${ this.state?.matches({[AppRootStates.FEATURE]: AppFeatureStates.AUTHENTICATE}) ?? html`` } - ${ this.state?.matches({[AppRootStates.FEATURE]: AppFeatureStates.COLLECTIONS}) ?? html`` } + ${ this.state?.matches({[AppRootStates.FEATURE]: AppFeatureStates.AUTHENTICATE}) && html`` } + ${ this.state?.matches({[AppRootStates.FEATURE]: AppFeatureStates.COLLECTIONS}) && html`` } `; } @@ -123,6 +123,13 @@ export class AppRootComponent extends RxLitElement { static get styles() { return [ unsafeCSS(Theme), + css` + :host { + height: 100%; + display: flex; + background-color: var(--colors-primary-dark); + } + `, ]; } diff --git a/packages/nde-erfgoed-manage/lib/app.ts b/packages/nde-erfgoed-manage/lib/app.ts index d3938073..97216a63 100644 --- a/packages/nde-erfgoed-manage/lib/app.ts +++ b/packages/nde-erfgoed-manage/lib/app.ts @@ -1,4 +1,4 @@ -import { AlertComponent, CollectionsComponent, CollectionComponent } from '@digita-ai/nde-erfgoed-components'; +import { AlertComponent, CollectionsComponent, CollectionComponent, FormElementComponent } from '@digita-ai/nde-erfgoed-components'; import { inspect } from '@xstate/inspect'; import { AppRootComponent } from './app.root'; import { AuthenticateRootComponent } from './features/authenticate/authenticate-root.component'; @@ -10,9 +10,9 @@ import './index'; * * https://github.com/davidkpiano/xstate/tree/master/packages/xstate-inspect */ -inspect({ - iframe: false, // open in new window -}); +// inspect({ +// iframe: false, // open in new window +// }); /** * Register tags for components. @@ -23,3 +23,4 @@ customElements.define('nde-collections-root', CollectionsRootComponent); customElements.define('nde-app-root', AppRootComponent); customElements.define('nde-alert', AlertComponent); customElements.define('nde-authenticate-root', AuthenticateRootComponent); +customElements.define('nde-form-element', FormElementComponent); diff --git a/packages/nde-erfgoed-manage/lib/features/authenticate/authenticate-root.component.ts b/packages/nde-erfgoed-manage/lib/features/authenticate/authenticate-root.component.ts index 45eb8c92..117c4d6e 100644 --- a/packages/nde-erfgoed-manage/lib/features/authenticate/authenticate-root.component.ts +++ b/packages/nde-erfgoed-manage/lib/features/authenticate/authenticate-root.component.ts @@ -1,10 +1,12 @@ -import { html, property, PropertyValues, internalProperty, unsafeCSS } from 'lit-element'; -import { Logger, Translator } from '@digita-ai/nde-erfgoed-core'; -import { Event } from '@digita-ai/nde-erfgoed-components'; -import { SpawnedActorRef, State} from 'xstate'; +import { html, property, PropertyValues, internalProperty, unsafeCSS, css } from 'lit-element'; +import { ArgumentError, Logger, Translator } from '@digita-ai/nde-erfgoed-core'; +import { Event, FormActors, FormContext, FormRootStates, FormSubmissionStates, FormCleanlinessStates, FormValidationStates, FormEvents } from '@digita-ai/nde-erfgoed-components'; +import { Interpreter, SpawnedActorRef, State} from 'xstate'; import { RxLitElement } from 'rx-lit'; -import { Theme } from '@digita-ai/nde-erfgoed-theme'; -import { AuthenticateEvents } from './authenticate.events'; +import { unsafeSVG } from 'lit-html/directives/unsafe-svg'; +import { Login, NdeLogoInverse, Theme } from '@digita-ai/nde-erfgoed-theme'; +import { map } from 'rxjs/operators'; +import { from } from 'rxjs'; import { AuthenticateContext } from './authenticate.machine'; /** @@ -28,7 +30,13 @@ export class AuthenticateRootComponent extends RxLitElement { * The actor controlling this component. */ @property({type: Object}) - public actor: SpawnedActorRef, State>; + public actor: Interpreter; + + /** + * The actor responsible for form validation in this component. + */ + @internalProperty() + formActor: SpawnedActorRef, State>>; /** * The state of this component. @@ -36,12 +44,32 @@ export class AuthenticateRootComponent extends RxLitElement { @internalProperty() state?: State; + /** + * The state of this component. + */ + @internalProperty() + enableSubmit?: boolean; + /** * Hook called on first update after connection to the DOM. * It subscribes to the actor, logs state changes, and pipes state to the properties. */ firstUpdated(changed: PropertyValues) { super.firstUpdated(changed); + + this.formActor = this.actor.children.get(FormActors.FORM_MACHINE); + + if (!this.formActor) { + throw new ArgumentError('Argument this.formActor should be set.', this.formActor); + } + + this.subscribe('enableSubmit', from(this.formActor).pipe( + map((state) => state.matches({ + [FormRootStates.CLEANLINESS]: FormCleanlinessStates.DIRTY, + [FormRootStates.VALIDATION]: FormValidationStates.VALID, + [FormRootStates.SUBMISSION]: FormSubmissionStates.NOT_SUBMITTED, + })), + )); } /** @@ -50,10 +78,22 @@ export class AuthenticateRootComponent extends RxLitElement { * @returns The rendered HTML of the component. */ render() { - return html` -

${this.translator.translate('nde.authenticate.root.title')}

- - `; + return this.formActor ? html` +
+ ${ unsafeSVG(NdeLogoInverse) } +

${this.translator.translate('nde.features.authenticate.pages.login.title')}

+
+
+
+ +
+ + +
+
+
+
+ ` : html``; } /** @@ -62,6 +102,40 @@ export class AuthenticateRootComponent extends RxLitElement { static get styles() { return [ unsafeCSS(Theme), + css` + :host { + width: 400px; + height: 100%; + margin: auto; + display: flex; + flex-direction: column; + gap: 80px; + } + + .title-container { + margin-top: 30vh; + max-height: 50px; + height: 50px; + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + color: var(--colors-foreground-inverse); + } + + .title-container svg { + max-height: 50px; + height: 50px; + max-width: 50px; + width: 50px; + fill: var(--colors-foreground-inverse); + } + + .title-container h1 { + font-size: var(--font-size-header-normal); + font-weight: normal; + } + `, ]; } diff --git a/packages/nde-erfgoed-manage/lib/features/authenticate/authenticate.events.ts b/packages/nde-erfgoed-manage/lib/features/authenticate/authenticate.events.ts index 9daf5922..70e9de75 100644 --- a/packages/nde-erfgoed-manage/lib/features/authenticate/authenticate.events.ts +++ b/packages/nde-erfgoed-manage/lib/features/authenticate/authenticate.events.ts @@ -4,11 +4,9 @@ import { Event } from '@digita-ai/nde-erfgoed-components'; * Event references for the authenticate component, with readable log format. */ export enum AuthenticateEvents { - CLICKED_LOGIN = '[AuthenticateEvent: Clicked Login]', - CLICKED_LOGOUT = '[AuthenticateEvent: Clicked Logout]', + LOGIN_STARTED = '[AuthenticateEvent: Login started]', LOGIN_SUCCESS = '[AuthenticateEvent: Login Success]', LOGIN_ERROR = '[AuthenticateEvent: Login Error]', - SESSION_RESTORED = '[AuthenticateEvent: Session Restored]', } /** @@ -18,12 +16,7 @@ export enum AuthenticateEvents { /** * An event which is dispatched when a user clicks the login button. */ -export interface ClickedLoginEvent extends Event { type: AuthenticateEvents.CLICKED_LOGIN; webId: string } - -/** - * An event which is dispatched when a user clicks the logout button. - */ -export interface ClickedLogoutEvent extends Event { type: AuthenticateEvents.CLICKED_LOGOUT } +export interface LoginStartedEvent extends Event { type: AuthenticateEvents.LOGIN_STARTED; webId: string } /** * An event which is dispatched when a user login was successful. @@ -34,8 +27,3 @@ export interface LoginSuccessEvent extends Event { type: Aut * An event which is dispatched when a user login failed. */ export interface LoginErrorEvent extends Event { type: AuthenticateEvents.LOGIN_ERROR; message: string } - -/** - * An event which is dispatched when a session was able to be restored. - */ -export interface SessionRestoredEvent extends Event { type: AuthenticateEvents.SESSION_RESTORED } diff --git a/packages/nde-erfgoed-manage/lib/features/authenticate/authenticate.machine.ts b/packages/nde-erfgoed-manage/lib/features/authenticate/authenticate.machine.ts index b718fe88..344f47cf 100644 --- a/packages/nde-erfgoed-manage/lib/features/authenticate/authenticate.machine.ts +++ b/packages/nde-erfgoed-manage/lib/features/authenticate/authenticate.machine.ts @@ -1,9 +1,10 @@ -import { ConsoleLogger, LoggerLevel, SolidMockService, SolidService } from '@digita-ai/nde-erfgoed-core'; -import { Event, State } from '@digita-ai/nde-erfgoed-components'; -import { assign, createMachine, MachineConfig, StateNodeConfig } from 'xstate'; -import { log } from 'xstate/lib/actions'; +import { SolidService } from '@digita-ai/nde-erfgoed-core'; +import { Event, formMachine, State, FormActors, FormContext, FormValidatorResult, FormEvents } from '@digita-ai/nde-erfgoed-components'; +import { createMachine } from 'xstate'; +import { pure, send } from 'xstate/lib/actions'; import { map, switchMap } from 'rxjs/operators'; import { throwError } from 'rxjs'; +import { addAlert } from '../collections/collections.events'; import { AuthenticateEvents } from './authenticate.events'; /** @@ -11,9 +12,7 @@ import { AuthenticateEvents } from './authenticate.events'; */ // eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface AuthenticateContext { - -} +export interface AuthenticateContext { } /** * Actor references for this machine config. @@ -27,51 +26,87 @@ export enum AuthenticateActors { */ export enum AuthenticateStates { AUTHENTICATED = '[AuthenticateState: Authenticated]', - AUTHENTICATING = '[AuthenticateState: Authenticating]', + REDIRECTING = '[AuthenticateState: Redirecting]', UNAUTHENTICATED = '[AuthenticateState: Unauthenticated]', } +/** + * Validated the WebID form. + * + * @param context The form's context, which includes the data to validate. + * @param event The even which triggered the validation. + * @returns Validation results, or an empty array when valid. + */ +const validator = (context: FormContext<{ webId: string }>, event: Event): FormValidatorResult[] => + context.data?.webId && context.data?.webId.length > 0 ? [] : [ { field: 'webId', message: 'nde.features.authenticate.error.invalid-webid.invalid-url' } ]; + /** * The authenticate machine. */ export const authenticateMachine = (solid: SolidService) => createMachine, State>({ id: AuthenticateActors.AUTHENTICATE_MACHINE, initial: AuthenticateStates.UNAUTHENTICATED, - states: { + states: { + /** + * The user is not authenticated. + */ [AuthenticateStates.UNAUTHENTICATED]: { - entry: [ - assign({ session: null }), - ], - invoke: { - src: () => solid.handleIncomingRedirect().pipe( - switchMap(() => throwError(new Error())), - map(() => ({ type: AuthenticateEvents.SESSION_RESTORED, webId: ''})), - ), - onDone: { - target: AuthenticateStates.AUTHENTICATED, + invoke: [ + /** + * Listen for redirects, and determine if a user is authenticated or not. + */ + { + src: () => solid.handleIncomingRedirect().pipe( + switchMap(() => throwError(new Error())), + map(() => ({ type: AuthenticateEvents.LOGIN_SUCCESS, webId: ''})), + ), + onDone: { actions: send(AuthenticateEvents.LOGIN_SUCCESS) }, + onError: { actions: send(AuthenticateEvents.LOGIN_ERROR) }, }, - onError: { actions: log('Could not restore session.')}, - }, + /** + * Invoke a form machine which controls the login form. + */ + { + id: FormActors.FORM_MACHINE, + src: formMachine(validator).withContext({ + data: { webId: ''}, + original: { webId: ''}, + }), + onDone: { actions: send(AuthenticateEvents.LOGIN_STARTED) }, + }, + ], on: { - [AuthenticateEvents.CLICKED_LOGIN]: AuthenticateStates.AUTHENTICATING, - [AuthenticateEvents.SESSION_RESTORED]: AuthenticateStates.AUTHENTICATED, + [AuthenticateEvents.LOGIN_STARTED]: AuthenticateStates.REDIRECTING, + [AuthenticateEvents.LOGIN_SUCCESS]: AuthenticateStates.AUTHENTICATED, }, }, - [AuthenticateStates.AUTHENTICATING]: { + /** + * The user is being redirected to his identity provider. + */ + [AuthenticateStates.REDIRECTING]: { invoke: { + /** + * Redirects the user to the identity provider. + */ src: () => solid.login().pipe( map(() => ({ type: AuthenticateEvents.LOGIN_SUCCESS, webId: ''})), ), - onDone: AuthenticateStates.AUTHENTICATED, - onError: AuthenticateStates.UNAUTHENTICATED, - }, - on: { - [AuthenticateEvents.LOGIN_SUCCESS]: AuthenticateStates.AUTHENTICATED, - [AuthenticateEvents.LOGIN_ERROR]: AuthenticateStates.UNAUTHENTICATED, + /** + * Go back to unauthenticated when something goes wrong, and show an alert. + */ + onError: { + actions: pure((_ctx, event) => addAlert({ message: event.data.toString().split('Error: ')[1], type: 'warning' })), + target: AuthenticateStates.UNAUTHENTICATED, + }, }, + type: 'final', }, + + /** + * The user has been authenticated. + */ [AuthenticateStates.AUTHENTICATED]: { type: 'final', }, diff --git a/packages/nde-erfgoed-manage/lib/i8n/nl-BE.json b/packages/nde-erfgoed-manage/lib/i8n/nl-BE.json deleted file mode 100644 index 42f34882..00000000 --- a/packages/nde-erfgoed-manage/lib/i8n/nl-BE.json +++ /dev/null @@ -1,22 +0,0 @@ -[ - { - "key": "nde.app.root.title", - "locale": "nl-BE", - "value": "App" - }, - { - "key": "nde.root.alerts.error", - "locale": "nl-BE", - "value": "Er ging iets fout" - }, - { - "key": "nde.collections.alerts.created-collection", - "locale": "nl-BE", - "value": "Collectie aangemaakt" - }, - { - "key": "nde.collections.root.title", - "locale": "nl-BE", - "value": "Collecties" - } -] \ No newline at end of file diff --git a/packages/nde-erfgoed-manage/lib/i8n/nl-NL.json b/packages/nde-erfgoed-manage/lib/i8n/nl-NL.json new file mode 100644 index 00000000..bbae893e --- /dev/null +++ b/packages/nde-erfgoed-manage/lib/i8n/nl-NL.json @@ -0,0 +1,52 @@ +[ + { + "key": "nde.app.root.title", + "locale": "nl-NL", + "value": "App" + }, + { + "key": "nde.root.alerts.error", + "locale": "nl-NL", + "value": "Er ging iets fout" + }, + { + "key": "nde.collections.alerts.created-collection", + "locale": "nl-NL", + "value": "Collectie aangemaakt" + }, + { + "key": "nde.collections.root.title", + "locale": "nl-NL", + "value": "Collecties" + }, + { + "key": "nde.features.authenticate.error.invalid-webid.invalid-url", + "locale": "nl-NL", + "value": "Gelieve een geldig WebID in te voeren. Een WebID start met https://." + }, + { + "key": "nde.features.authenticate.error.invalid-webid.no-profile", + "locale": "nl-NL", + "value": "Er werd geen actieve profielpagina gevonden voor deze WebID." + }, + { + "key": "nde.features.authenticate.error.invalid-webid.no-oidc-registration", + "locale": "nl-NL", + "value": "De opgegeven WebID is nog niet gelinkt aan een OIDC-provider. Meer info hierover vind je bij de FAQ." + }, + { + "key": "nde.features.authenticate.error.invalid-webid.invalid-oidc-registration", + "locale": "nl-NL", + "value": "De opgegeven WebID is gelinkt aan een foutieve OIDC-provider. Meer info hierover vind je bij de FAQ." + }, + { + "key": "nde.features.authenticate.pages.login.title", + "locale": "nl-NL", + "value": "Collectiebeheersysteem" + }, + { + "key": "nde.features.authenticate.pages.login.search-placeholder", + "locale": "nl-NL", + "value": "E.g. https://profile.janjanssens.nl/" + } +] diff --git a/packages/nde-erfgoed-manage/lib/index.ts b/packages/nde-erfgoed-manage/lib/index.ts index 01ea4e1b..99925e8f 100644 --- a/packages/nde-erfgoed-manage/lib/index.ts +++ b/packages/nde-erfgoed-manage/lib/index.ts @@ -1,5 +1,5 @@ /** * Exports the modules of the package. */ -export * from './features/authenticate/authenticate-root.component'; export * from './features/collections/collections-root.component'; +export * from './features/authenticate/authenticate-root.component'; diff --git a/packages/nde-erfgoed-manage/lib/styles.module.css b/packages/nde-erfgoed-manage/lib/styles.module.css index 91218102..1cdd1054 100644 --- a/packages/nde-erfgoed-manage/lib/styles.module.css +++ b/packages/nde-erfgoed-manage/lib/styles.module.css @@ -1,12 +1,9 @@ @import '../node_modules/@digita-ai/nde-erfgoed-theme/dist/style.css'; html { - background-color: var(--colors-background-normal); - padding: 0; - margin: 0; + height: 100%; } body { - max-width: 1200px; - background-color: #fff; - margin: 0 auto; + height: 100%; + background-color: var(--colors-background-normal); } diff --git a/packages/nde-erfgoed-theme/lib/common/typography.css b/packages/nde-erfgoed-theme/lib/common/typography.css index 41f39fab..f01a1836 100644 --- a/packages/nde-erfgoed-theme/lib/common/typography.css +++ b/packages/nde-erfgoed-theme/lib/common/typography.css @@ -3,12 +3,14 @@ :root { --font-size-small: 14px; --font-size-normal: 16.5px; + --font-size-header-normal: 24px; --font-weight-bold: bold; + --font-family: 'Poppins', sans-serif; } body { color: var(--colors-foreground-normal); - font-family: 'Poppins', sans-serif; + font-family: var(--font-family); font-size: var(--font-size-normal); } diff --git a/packages/nde-erfgoed-theme/lib/elements/buttons.css b/packages/nde-erfgoed-theme/lib/elements/buttons.css index 85402eee..dee75b9f 100644 --- a/packages/nde-erfgoed-theme/lib/elements/buttons.css +++ b/packages/nde-erfgoed-theme/lib/elements/buttons.css @@ -1,6 +1,6 @@ button { + border: none; background-color: var(--colors-primary-dark); - border: var(--border-normal) solid var(--colors-primary-dark); text-transform: uppercase; padding: var(--gap-small); color: var(--colors-foreground-inverse); @@ -10,7 +10,6 @@ button { button.primary { background-color: var(--colors-primary-normal); - border: var(--border-normal) solid var(--colors-primary-normal); } button svg { @@ -19,8 +18,11 @@ button svg { } button:disabled { - border: var(--border-normal) solid var(--colors-background-normal); - background-color: var(--colors-background-light); + background-color: var(--colors-background-normal); color: var(--colors-foreground-light); cursor: inherit; } + +button:disabled svg { + fill: var(--colors-foreground-light); +} diff --git a/packages/nde-erfgoed-theme/lib/elements/forms.css b/packages/nde-erfgoed-theme/lib/elements/forms.css index 7afe06a7..79638698 100644 --- a/packages/nde-erfgoed-theme/lib/elements/forms.css +++ b/packages/nde-erfgoed-theme/lib/elements/forms.css @@ -18,6 +18,7 @@ form nde-form-element input { padding: 0; border: none; font-size: var(--font-size-small); + font-family: var(--font-family); outline: none; display: block; width: 100%; diff --git a/packages/nde-erfgoed-theme/lib/icons/NdeLogoInverse.svg b/packages/nde-erfgoed-theme/lib/icons/NdeLogoInverse.svg new file mode 100644 index 00000000..4ba51480 --- /dev/null +++ b/packages/nde-erfgoed-theme/lib/icons/NdeLogoInverse.svg @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/nde-erfgoed-theme/lib/index.ts b/packages/nde-erfgoed-theme/lib/index.ts index a4e839d1..428fc8df 100644 --- a/packages/nde-erfgoed-theme/lib/index.ts +++ b/packages/nde-erfgoed-theme/lib/index.ts @@ -1,14 +1,13 @@ -import BellIcon from './icons/Bell.svg?raw'; -export const Bell = BellIcon; - -import DismissIcon from './icons/Dismiss.svg?raw'; -export const Dismiss = DismissIcon; - -import SearchIcon from './icons/Search.svg?raw'; -export const Search = SearchIcon; - -import LoginIcon from './icons/Login.svg?raw'; -export const Login = LoginIcon; - -import ThemeCss from './theme.css'; -export const Theme = ThemeCss; +/** + * Export icons + */ +export { default as Bell } from './icons/Bell.svg?raw'; +export { default as Dismiss } from './icons/Dismiss.svg?raw'; +export { default as Search } from './icons/Search.svg?raw'; +export { default as Login } from './icons/Login.svg?raw'; +export { default as NdeLogoInverse } from './icons/NdeLogoInverse.svg?raw'; + +/** + * Export theme + */ +export { default as Theme } from './theme.css'; diff --git a/specifications/assets/authenticate/authenticate-login-page.svg b/specifications/assets/authenticate/authenticate-login-page.svg new file mode 100644 index 00000000..ffed8361 --- /dev/null +++ b/specifications/assets/authenticate/authenticate-login-page.svg @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/specifications/assets/authenticate/button.svg b/specifications/assets/authenticate/button.svg new file mode 100644 index 00000000..0ea88b9b --- /dev/null +++ b/specifications/assets/authenticate/button.svg @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/specifications/assets/authenticate/input-field.svg b/specifications/assets/authenticate/input-field.svg new file mode 100644 index 00000000..234a79cc --- /dev/null +++ b/specifications/assets/authenticate/input-field.svg @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/specifications/assets/authenticate/notification.svg b/specifications/assets/authenticate/notification.svg new file mode 100644 index 00000000..1c78a47d --- /dev/null +++ b/specifications/assets/authenticate/notification.svg @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/specifications/pages/technical/create-authenticate-login-page.adoc b/specifications/pages/technical/create-authenticate-login-page.adoc new file mode 100644 index 00000000..c5d17c03 --- /dev/null +++ b/specifications/pages/technical/create-authenticate-login-page.adoc @@ -0,0 +1,122 @@ += Technical Documentation: Create authenticate login page + +== Author(s) + +* Stijn Taelemans + +== References + + +* https://www.wrike.com/open.htm?id=675145293[Wrike task] +* https://docs.google.com/spreadsheets/d/1onOY60hXmEPQYN_nM6CK0uRYIHq7hPtYsE8pWaVe7es/edit#gid=1865680815[Test plan] +* Branch: `feat/675145293-create-authentication-login-page` +* Projects: https://github.com/digita-ai/nde-erfgoedinstellingen[nde-erfgoed-manage, nde-erfgoed-components] + + +== Introduction + +=== Overview + +This document is about the the authentication login page. Heritage institutions can enter their WebID on this page, after which they will be redirected to the login page of their identity provider. + +=== Out of scope + +Routing for the application should not be considered for this feature. This will be implemented for the whole app later. + + +=== Assumptions + +The authenticate feature setup is complete. + +All components and services are to be made in the '@digita-ai/nde-erfgoed-manage' package, unless specified otherwise. + + +== Solution + +=== Suggested or proposed solution + + +==== Components + +All new components should be exported in 'lib/index.ts'. + +In 'nde-erfgoed-manage', register their HTML tags in app.ts. + + +===== ButtonComponent + +The finished component should look like this: + +image::../../assets/authenticate/button.svg[ButtonComponent] + +Generate in '@digita-ai/nde-erfgoed-components' package under 'lib/common/button/button.component.ts', with tag `` + +These buttons consist of three parts: + +* An icon on the left side +* The text content in the middle +* An icon on the right side + +Make use of https://lit-element.readthedocs.io/en/v0.6.4/docs/templates/slots/#slot[named slots], https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_templates_and_slots[MDN] for the icons. When no icon slot is present in the component's body, do not render it. The button should be checked for a `disabled` attribute. When present, use its boolean value to determine whether the button should be disabled or not. A disabled button should always be colored grey (#8990A9), enabled buttons, by default, are colored the 'Primary Light' blue. (#25438E) + +Example usage of a button with both icons set: + +[source, html] +---- + + + + Click me! + +---- + + +===== AuthenticateLoginPageComponent + +The finished component should look like this: + +image::../../assets/authenticate/authenticate-login-page.svg[AuthenticateLoginPageComponent] + +Generate under 'lib/features/authenticate/pages/authenticate-login-page.component.ts' with tag `` + +The page consists two main elements: + +* The header which contains both the NDE logo and a title. +* The WebID input field with a button to its right. Use the previously mentioned and . + +When a valid WebID is entered and the button is clicked (or the 'Enter' key is pressed), the `AuthenticateEvents.CLICKED_LOGOUT` event should be fired. + +While the WebID is being validated, the `ProgressComponent` should be displayed at the top of the page. Validation of the entered WebID should start when the input field was not changed for 250ms. + +The alert visible in the Figma mockup will be implemented later. + + +==== Translations + +The following translations are to be written in the 'nl-NL.json' file. No other languages should be supported. A file named 'nl-BE.json' already exists in the project, rename it (and any other nl-BE references) and extend with the following: + +[options="header"] + +|====================================== + +| Key | Translation + +| `nde.features.authenticate.error.invalid-webid.invalid-url` +| Gelieve een geldig WebID in te voeren. Een WebID start met https://. + +| `nde.features.authenticate.error.invalid-webid.no-profile` +| Er werd geen actieve profielpagina gevonden voor deze WebID. + +| `nde.features.authenticate.error.invalid-webid.no-oidc-registration` +| De opgegeven WebID is nog niet gelinkt aan een OIDC-provider. Meer info hierover vind je bij de FAQ. + +| `nde.features.authenticate.error.invalid-webid.invalid-oidc-registration` +| De opgegeven WebID is gelinkt aan een foutieve OIDC-provider. Meer info hierover vind je bij de FAQ. + +| `nde.features.authenticate.pages.login.title` +| Collectiebeheersysteem + +| `nde.features.authenticate.pages.login.search-placeholder` +| E.g. https://profile.janjanssens.nl/ + +|======================================