diff --git a/packages/components/src/dev.html b/packages/components/src/dev.html index 2d3d490096..b32aabbe45 100644 --- a/packages/components/src/dev.html +++ b/packages/components/src/dev.html @@ -34,7 +34,7 @@
- z.B. +
diff --git a/packages/samples/react/.eslintignore b/packages/samples/react/.eslintignore index d38162f5c9..e2b5d24146 100644 --- a/packages/samples/react/.eslintignore +++ b/packages/samples/react/.eslintignore @@ -1 +1,2 @@ -**/assets/** \ No newline at end of file +**/assets/** +**/complex-form/** diff --git a/packages/samples/react/.ts-prunerc.js b/packages/samples/react/.ts-prunerc.js index 9eb0a6acb1..e34042f3d3 100644 --- a/packages/samples/react/.ts-prunerc.js +++ b/packages/samples/react/.ts-prunerc.js @@ -1,4 +1,4 @@ // https://github.com/nadeesha/ts-prune https: module.exports = { - ignore: 'node_modules', + ignore: 'node_modules|complex-form', }; diff --git a/packages/samples/react/package.json b/packages/samples/react/package.json index 46e49ea646..67764f3c45 100644 --- a/packages/samples/react/package.json +++ b/packages/samples/react/package.json @@ -34,6 +34,7 @@ "cpy-cli": "5.0.0", "eslint-plugin-jsx-a11y": "6.7.1", "eslint-plugin-react": "7.33.2", + "formik": "2.4.5", "nightwatch-axe-verbose": "2.2.2", "npm-run-all": "4.1.5", "react": "18.2.0", @@ -43,7 +44,8 @@ "rimraf": "3.0.2", "ts-prune": "0.10.3", "typescript": "5.2.2", - "world_countries_lists": "2.8.2" + "world_countries_lists": "2.8.2", + "yup": "1.3.1" }, "files": [ ".eslintignore", diff --git a/packages/samples/react/src/scenarios/appointment-form/AppointmentForm.tsx b/packages/samples/react/src/scenarios/appointment-form/AppointmentForm.tsx new file mode 100644 index 0000000000..a26c8a676e --- /dev/null +++ b/packages/samples/react/src/scenarios/appointment-form/AppointmentForm.tsx @@ -0,0 +1,127 @@ +import React, { useEffect, useState } from 'react'; +import { KolTabs } from '@public-ui/react'; +import { DistrictForm } from './DistrictForm'; +import { Summary } from './Summary'; +import { PersonalInformationForm } from './PersonalInformationForm'; +import { Formik, FormikHelpers } from 'formik'; +import * as Yup from 'yup'; +import { AvailableAppointmentsForm } from './AvailableAppointmentsForm'; +import { Iso8601 } from '@public-ui/components'; +import { checkAppointmentAvailability } from './appointmentService'; + +// export interface FormProps {} +export interface FormValues { + district: string; + date: Iso8601; + time: Iso8601; + salutation: string; + name: string; + company: string; + email: string; + phone: string; +} + +enum FormSection { + DISTRICT, + AVAILABLE_APPOINTMENTS, + PERSONAL_INFORMATION, + SUMMARY, +} + +const formSectionSequence = [FormSection.DISTRICT, FormSection.AVAILABLE_APPOINTMENTS, FormSection.PERSONAL_INFORMATION, FormSection.SUMMARY] as const; + +const initialValues: FormValues = { + district: '', + date: '' as Iso8601, + time: '' as Iso8601, + salutation: '', + name: '', + company: '', + email: '', + phone: '', +}; + +const districtSchema = { + district: Yup.string().required('Bitte Stadtteil wählen.'), +}; +const personalInformationSchema = { + salutation: Yup.string().required('Bitte Anrede auswählen.'), + name: Yup.string().required('Bitte Name eingeben.'), + company: Yup.string().when('salutation', { + is: (salutation: string) => salutation === 'Firma', + then: (schema) => schema.required('Bitte Firmenname angeben.'), + }), + email: Yup.string().required('Bitte E-Mail-Adresse eingeben.'), +}; +const availableAppointmentsSchema = { + date: Yup.string().required('Bitte Datum eingeben.'), + time: Yup.string().when('date', { + is: (date: string) => Boolean(date), // only validate time when date is already set + then: (schema) => schema.test('checkTimeAvailability', 'Termin leider nicht mehr verfügbar.', checkAppointmentAvailability), + }), +}; + +export function AppointmentForm() { + const [activeFormSection, setActiveFormSection] = useState(FormSection.DISTRICT); + const [selectedTab, setSelectedTab] = useState(activeFormSection); + + const validationSchema = Yup.object().shape({ + ...(activeFormSection === FormSection.DISTRICT ? districtSchema : {}), + ...(activeFormSection === FormSection.AVAILABLE_APPOINTMENTS ? availableAppointmentsSchema : {}), + ...(activeFormSection === FormSection.PERSONAL_INFORMATION ? personalInformationSchema : {}), + }); + + useEffect(() => { + setSelectedTab(activeFormSection); + }, [activeFormSection]); + + const handleSubmit = async (_values: FormValues, formik: FormikHelpers) => { + console.log(_values, formik); + const currentSectionIndex = formSectionSequence.indexOf(activeFormSection); + const nextSection = formSectionSequence[currentSectionIndex + 1]; + if (nextSection !== undefined) { + await formik.setTouched({}); + setTimeout(() => setActiveFormSection(nextSection), 1000); + } + }; + + return ( + initialValues={initialValues} validationSchema={validationSchema} onSubmit={handleSubmit}> + setActiveFormSection(selectedTab) }} + > +
+ +
+
+ +
+
+ +
+
+ +
+
+ + ); +} diff --git a/packages/samples/react/src/scenarios/appointment-form/AvailableAppointmentsForm.tsx b/packages/samples/react/src/scenarios/appointment-form/AvailableAppointmentsForm.tsx new file mode 100644 index 0000000000..a87bccc8b6 --- /dev/null +++ b/packages/samples/react/src/scenarios/appointment-form/AvailableAppointmentsForm.tsx @@ -0,0 +1,118 @@ +import React, { useEffect, useState } from 'react'; +import { KolButton, KolForm, KolHeading, KolInputDate, KolInputRadio, KolSpin } from '@public-ui/react'; +import { FormValues } from './AppointmentForm'; +import { ErrorList } from './ErrorList'; +import { Field, FieldProps, useFormikContext } from 'formik'; +import { fetchAvailableTimes } from './appointmentService'; +import { Option } from '@public-ui/components/src'; + +export function AvailableAppointmentsForm() { + const form = useFormikContext(); + + const [sectionSubmitted, setSectionSubmitted] = useState(false); + const [availableTimes, setAvailableTimes] = useState[] | null>(null); + + useEffect(() => { + let ignoreResponse = false; + setAvailableTimes(null); + + if (form.values.date) { + fetchAvailableTimes().then( + (times) => { + if (!ignoreResponse) { + setAvailableTimes(times); + void form.setFieldValue('time', times[0].value); + void form.setFieldTouched('time'); + } + }, + () => {}, + ); // ignore errors + } + return () => { + ignoreResponse = true; + }; + }, [form.values.date]); + + return ( +
+ + + {sectionSubmitted && Object.keys(form.errors).length ? ( +
+ +
+ ) : null} + + { + void form.submitForm(); + setSectionSubmitted(true); + }, + }} + > + + {({ field }: FieldProps) => ( + { + if (event.target) { + void form.setFieldValue('date', value, true); + } + }, + onBlur: () => { + void form.setFieldTouched('date', true); + }, + }} + /> + )} + + + {form.values.date && ( +
+ {availableTimes ? ( + <> + + {({ field }: FieldProps) => ( + { + if (event.target) { + void form.setFieldTouched('time', true); + void form.setFieldValue('time', value, true); + } + }, + }} + /> + )} + +

+ Aus Testzwecken sind nur die Termine zu jeder halben Stunde verfügbar. +

+ + ) : ( + + )} +
+ )} + + + {form.values.date && form.isValidating ? : ''} +
+
+ ); +} diff --git a/packages/samples/react/src/scenarios/appointment-form/DistrictForm.tsx b/packages/samples/react/src/scenarios/appointment-form/DistrictForm.tsx new file mode 100644 index 0000000000..2f8fe84e0f --- /dev/null +++ b/packages/samples/react/src/scenarios/appointment-form/DistrictForm.tsx @@ -0,0 +1,80 @@ +import React, { useState } from 'react'; +import { KolButton, KolForm, KolHeading, KolSelect } from '@public-ui/react'; +import { Field, FieldProps, useFormikContext } from 'formik'; +import { FormValues } from './AppointmentForm'; +import { ErrorList } from './ErrorList'; + +const LOCATION_OPTIONS = [ + { + value: 'Aplerbeck', + label: 'Aplerbeck', + }, + { + value: 'Brackel', + label: 'Brackel', + }, + { + value: 'Dorstfeld', + label: 'Dorstfeld', + }, + { + value: 'Innenstadt Ost', + label: 'Innenstadt Ost', + }, + { + value: 'Innenstadt West', + label: 'Innenstadt West', + }, +]; + +export function DistrictForm() { + const form = useFormikContext(); + const [sectionSubmitted, setSectionSubmitted] = useState(false); + + return ( +
+ + + {sectionSubmitted && Object.keys(form.errors).length ? ( +
+ +
+ ) : null} + + { + void form.submitForm(); + setSectionSubmitted(true); + }, + }} + > + + {({ field }: FieldProps) => ( + { + // Select und Radio setzen den Wert immer initial. + if (event.target) { + const [value] = values as [FormValues['district']]; + void form.setFieldTouched('district', true); + void form.setFieldValue('district', value, true); + } + }, + }} + /> + )} + + + + +
+ ); +} diff --git a/packages/samples/react/src/scenarios/appointment-form/ErrorList.tsx b/packages/samples/react/src/scenarios/appointment-form/ErrorList.tsx new file mode 100644 index 0000000000..a10d1804aa --- /dev/null +++ b/packages/samples/react/src/scenarios/appointment-form/ErrorList.tsx @@ -0,0 +1,35 @@ +import React from 'react'; +import { KolAlert, KolLink } from '@public-ui/react'; + +type ErrorListPropType = { + errors: Record; +}; + +export function ErrorList({ errors }: ErrorListPropType) { + const handleLinkClick = (event: Event) => { + const href = (event.target as HTMLAnchorElement | undefined)?.href; + if (href) { + const hrefUrl = new URL(href); + + const targetElement = document.querySelector(hrefUrl.hash); + if (targetElement && typeof targetElement.focus === 'function') { + targetElement.focus(); + } + } + }; + + return ( + + Bitte korrigieren Sie folgende Fehler: + + + ); +} diff --git a/packages/samples/react/src/scenarios/appointment-form/PersonalInformationForm.tsx b/packages/samples/react/src/scenarios/appointment-form/PersonalInformationForm.tsx new file mode 100644 index 0000000000..28cd06d633 --- /dev/null +++ b/packages/samples/react/src/scenarios/appointment-form/PersonalInformationForm.tsx @@ -0,0 +1,157 @@ +import React, { useState } from 'react'; +import { KolButton, KolForm, KolHeading, KolInputEmail, KolInputText, KolSelect } from '@public-ui/react'; +import { Field, FieldProps, useFormikContext } from 'formik'; +import { FormValues } from './AppointmentForm'; + +const SALUTATION_OPTIONS = [ + { + value: 'Firma', + label: 'Firma', + }, + { + value: 'Frau', + label: 'Frau', + }, + { + value: 'Herr', + label: 'Herr', + }, + { + value: 'Hallo', + label: 'Hallo', + }, +]; + +export function PersonalInformationForm() { + const form = useFormikContext(); + const [sectionSubmitted, setSectionSubmitted] = useState(false); + + return ( +
+ +
    {sectionSubmitted && Object.entries(form.errors).map(([field, error]) =>
  • {error}
  • )}
+ { + void form.submitForm(); + setSectionSubmitted(true); + }, + }} + > + + {({ field }: FieldProps) => ( + { + if (event.target) { + const [value] = values as [FormValues['salutation']]; + void form.setFieldTouched('salutation', true); + void form.setFieldValue('salutation', value, true); + } + }, + }} + /> + )} + + + {form.values.salutation === 'Firma' && ( + + {({ field }: FieldProps) => ( +
+ { + if (event.target) { + void form.setFieldTouched('company', true); + void form.setFieldValue('company', value, true); + } + }, + }} + /> +
+ )} +
+ )} + + + {({ field }: FieldProps) => ( +
+ { + if (event.target) { + void form.setFieldTouched('name', true); + void form.setFieldValue('name', value, true); + } + }, + }} + /> +
+ )} +
+ + + {({ field }: FieldProps) => ( +
+ { + if (event.target) { + void form.setFieldTouched('email', true); + void form.setFieldValue('email', value, true); + } + }, + }} + /> +
+ )} +
+ + + {({ field }: FieldProps) => ( +
+ { + if (event.target) { + void form.setFieldTouched('phone', true); + void form.setFieldValue('phone', value, true); + } + }, + }} + /> +
+ )} +
+ + +
+
+ ); +} diff --git a/packages/samples/react/src/scenarios/appointment-form/Summary.tsx b/packages/samples/react/src/scenarios/appointment-form/Summary.tsx new file mode 100644 index 0000000000..2358b803a6 --- /dev/null +++ b/packages/samples/react/src/scenarios/appointment-form/Summary.tsx @@ -0,0 +1,55 @@ +import React from 'react'; +import { useFormikContext } from 'formik'; +import { KolHeading } from '@public-ui/react'; +import { FormValues } from './AppointmentForm'; + +const ValueFallback = () => Nicht angegeben; +const ValueWithFallback = ({ value }: { value: string }) => (value ? value : ); + +export function Summary() { + const { values } = useFormikContext(); + + return ( + <> + + +
+
Stadtteil
+
+ +
+
Termin
+
{values.date && values.time ? `${values.date} ${values.time} Uhr` : }
+ + {values.salutation === 'Firma' ? ( + <> +
Firma
+
+ +
+ + ) : ( + <> +
Anrede
+
+ +
+ + )} + +
Name
+
+ +
+
E-Mail
+
+ +
+
Telefon
+
+ +
+
+ + ); +} diff --git a/packages/samples/react/src/scenarios/appointment-form/appointmentService.ts b/packages/samples/react/src/scenarios/appointment-form/appointmentService.ts new file mode 100644 index 0000000000..afd412c3ac --- /dev/null +++ b/packages/samples/react/src/scenarios/appointment-form/appointmentService.ts @@ -0,0 +1,37 @@ +import { Option } from '@public-ui/components'; + +const getRandomIntInclusive = (min: number, max: number) => { + min = Math.ceil(min); + max = Math.floor(max); + return Math.floor(Math.random() * (max - min + 1) + min); +}; + +const padHours = (hours: number): string => `${hours < 10 ? '0' : ''}${hours}`; + +const getRandomTimes = () => { + const earliest = 8; + const latest = 17; + const amount = getRandomIntInclusive(2, 9); + + const times = new Set(); + while (times.size !== amount) { + times.add(getRandomIntInclusive(earliest, latest)); + } + return [...times].sort((timeA, timeB) => timeA - timeB).flatMap((hours) => [`${padHours(hours)}:00`, `${padHours(hours)}:30`]); +}; + +const sleep = (timeout: number) => { + return new Promise((resolve) => setTimeout(resolve, timeout)); +}; +export const fetchAvailableTimes = async (): Promise[]> => { + await sleep(1000); + return getRandomTimes().map((time) => ({ + label: time, + value: time, + })); +}; + +export const checkAppointmentAvailability = async (time?: string): Promise => { + await sleep(500); + return time?.endsWith(':30') ?? false; +}; diff --git a/packages/samples/react/src/scenarios/complex-form/common/form/component.tsx b/packages/samples/react/src/scenarios/complex-form/common/form/component.tsx new file mode 100644 index 0000000000..683b0fa991 --- /dev/null +++ b/packages/samples/react/src/scenarios/complex-form/common/form/component.tsx @@ -0,0 +1,25 @@ +import React, { FC, ReactNode } from 'react'; + +import { KolForm, KolLinkGroup } from '@public-ui/react'; + +type Props = { + submitted: boolean; + children: ReactNode; + onSubmit: (event: Event) => void; +}; + +export const FromComponent: FC = (props) => ( + <> + {props.submitted === true && ( + + )} + + {props.children} + + +); diff --git a/packages/samples/react/src/scenarios/complex-form/common/form/types.ts b/packages/samples/react/src/scenarios/complex-form/common/form/types.ts new file mode 100644 index 0000000000..61ae356df1 --- /dev/null +++ b/packages/samples/react/src/scenarios/complex-form/common/form/types.ts @@ -0,0 +1,13 @@ +export type Fehler = { + _label: string; + _selector: string; +}; + +export type FormProps = { + onSubmitted: (event: Event) => void; +}; + +export type FormState = { + loader: boolean; + touched: boolean; +}; diff --git a/packages/samples/react/src/scenarios/complex-form/component.tsx b/packages/samples/react/src/scenarios/complex-form/component.tsx new file mode 100644 index 0000000000..df98b4858a --- /dev/null +++ b/packages/samples/react/src/scenarios/complex-form/component.tsx @@ -0,0 +1,163 @@ +import React, { FC } from 'react'; + +import { KolHeading, KolProgress, KolTable, KolTabs } from '@public-ui/react'; + +import { TerminKopfdatenComponent } from './kopfdaten/component'; +import { TerminLocationComponent } from './location/component'; +import { TerminScheduleComponent } from './schedule/component'; + +type Zeiten = { + stadtteil: string; + zeiten: string; + montag: string; + dienstag: string; + mittwoch: string; + donnerstag: string; + freitag: string; +}; + +export const TerminComponent: FC = () => ( + <> +
+ Terminreservierung + Termine für Einwohnermelde- (incl. Pass- und Ausweisangelegenheiten) und Kraftfahrzeugangelegenheiten +
+

Derzeit kann generell nur mit vorheriger Terminvereinbarung bei den Bürgerdiensten vorgesprochen werden.

+

+ Die Termine für Einwohnermelde- und Kraftfahrzeugangelegenheiten werden täglich ab 07:00 Uhr für den gleichen Tag, für den gleichen Tag 7 Tage und für + den gleichen Tag 14 Tage später freigegeben. So können Sie jeden Tag spontan Termine für den gleichen Tag und planbar Termine für eine Woche oder zwei + Wochen später erhalten. +

+

+ Sofern online keine Termine innerhalb der nächsten 14 Tage mehr verfügbar sind, führt auch eine darüber hinaus gehende telefonische Kontaktaufnahme + leider zu keinem anderen Ergebnis. In diesem Fall versuchen Sie es bitte am nächsten Morgen erneut. +

+

+ Bitte achten Sie darauf, den richtigen Kalender für Ihr Anliegen auszuwählen. Hinweis: Bitte geben Sie bei Ihrer Terminvereinbarung zur lückenlosen + Kontaktverfolgung immer Ihre korrekte Telefonnummer und E-Mail Adresse an. Wir behalten uns vor, gebuchte Termine mit falschen Angaben zu löschen. +

+
+
+
+ 14:00 - 16:00', + dienstag: '08:00 - 12:00
14:00 - 15:00', + mittwoch: '08:00 - 12:00
14:00 - 15:00', + donnerstag: '08:00 - 12:00
14:00 - 18:00', + freitag: '08:00 - 12:00', + }, + { + stadtteil: 'Dorstfeld', + montag: '09:00 - 12:00
14:00 - 16:00', + dienstag: '09:00 - 12:00
14:00 - 15:00', + mittwoch: '09:00 - 12:00
14:00 - 15:00', + donnerstag: '09:00 - 12:00
14:00 - 18:00', + freitag: '09:00 - 12:00', + }, + { + stadtteil: 'Aplerbeck', + montag: '08:00 - 12:00
14:00 - 16:00', + dienstag: '08:00 - 12:00
14:00 - 15:00', + mittwoch: '08:00 - 12:00
14:00 - 15:00', + donnerstag: '08:00 - 12:00
14:00 - 18:00', + freitag: '08:00 - 12:00', + }, + { + stadtteil: 'Innenstadt Ost', + montag: '07:00 - 12:00
14:00 - 16:00', + dienstag: '07:00 - 12:00
14:00 - 15:00', + mittwoch: '07:00 - 12:00
14:00 - 15:00', + donnerstag: '07:00 - 12:00
14:00 - 18:00', + freitag: '07:00 - 12:00
13:00 - 16:00', + }, + { + stadtteil: 'Innenstadt West', + montag: '07:00 - 12:00
14:00 - 16:00', + dienstag: '07:00 - 12:00
14:00 - 15:00', + mittwoch: '07:00 - 12:00
14:00 - 15:00', + donnerstag: '07:00 - 12:00
14:00 - 18:00', + freitag: '07:00 - 12:00
13:00 - 16:00', + /*render: (el, data) => { + el.innerHTML = ``; + },*/ + }, + ] as Zeiten[] + } + _headers={{ + horizontal: [ + [ + { label: '', asTd: true }, + { label: 'Tag', colSpan: 5 }, + ], + [ + { + label: 'Stadtteil', + key: 'stadtteil', + textAlign: 'left', + sort: (data: Zeiten[]) => { + return data.sort((first, second) => { + if (first.stadtteil < second.stadtteil) { + return -1; + } + if (first.stadtteil > second.stadtteil) { + return 1; + } + return 0; + }); + }, + }, + { label: 'Montag', key: 'montag', textAlign: 'center' }, + { label: 'Dienstag', key: 'dienstag', textAlign: 'center' }, + { label: 'Mittwoch', key: 'mittwoch', textAlign: 'center' }, + { label: 'Donnerstag', key: 'donnerstag', textAlign: 'center' }, + { label: 'Freitag', key: 'freitag', textAlign: 'center' }, + ], + ], + }} + _minWidth="50em" + style={{ + display: 'inline-grid', + width: '100%', + }} + >
+ +
+ Wählen Sie einen Stadtteil aus + {}} /> +
+
+ Wählen Sie einen Termin aus + {}} /> +
+
+ Geben Sie Ihre Kontaktdaten ein + {}} /> +
+
+
+
+
+ Fortschritt + +
+ +); diff --git a/packages/samples/react/src/scenarios/complex-form/kopfdaten/component.tsx b/packages/samples/react/src/scenarios/complex-form/kopfdaten/component.tsx new file mode 100644 index 0000000000..d67468cc9e --- /dev/null +++ b/packages/samples/react/src/scenarios/complex-form/kopfdaten/component.tsx @@ -0,0 +1,50 @@ +import React, { FC } from 'react'; + +import { KolAbbr, KolButton, KolInputEmail, KolInputRadio, KolInputText, KolSpin } from '@public-ui/react'; +import { FormProps } from '../common/form/types'; +import { FromComponent } from '../common/form/component'; + +export const TerminKopfdatenComponent: FC = (props) => ( + {}}> +
+ + + + +
+ + + PLZ + + + +
+ + +
+ + +
+
+
+); diff --git a/packages/samples/react/src/scenarios/complex-form/location/component.tsx b/packages/samples/react/src/scenarios/complex-form/location/component.tsx new file mode 100644 index 0000000000..4436638727 --- /dev/null +++ b/packages/samples/react/src/scenarios/complex-form/location/component.tsx @@ -0,0 +1,16 @@ +import React, { FC } from 'react'; + +import { KolButton, KolSelect, KolSpin } from '@public-ui/react'; +import { LOCATION_OPTIONS } from './location.form'; +import { FormProps } from '../common/form/types'; +import { FromComponent } from '../common/form/component'; + +export const TerminLocationComponent: FC = () => ( + {}}> +
+ +
+ + +
+); diff --git a/packages/samples/react/src/scenarios/complex-form/location/location.form.ts b/packages/samples/react/src/scenarios/complex-form/location/location.form.ts new file mode 100644 index 0000000000..c4fdd89bd8 --- /dev/null +++ b/packages/samples/react/src/scenarios/complex-form/location/location.form.ts @@ -0,0 +1,22 @@ +export const LOCATION_OPTIONS = [ + { + value: 'Aplerbeck', + label: 'Aplerbeck', + }, + { + value: 'Brackel', + label: 'Brackel', + }, + { + value: 'Dorstfeld', + label: 'Dorstfeld', + }, + { + value: 'Innenstadt Ost', + label: 'Innenstadt Ost', + }, + { + value: 'Innenstadt West', + label: 'Innenstadt West', + }, +]; diff --git a/packages/samples/react/src/scenarios/complex-form/schedule/component.tsx b/packages/samples/react/src/scenarios/complex-form/schedule/component.tsx new file mode 100644 index 0000000000..e76d3ff1fa --- /dev/null +++ b/packages/samples/react/src/scenarios/complex-form/schedule/component.tsx @@ -0,0 +1,16 @@ +import React, { FC } from 'react'; + +import { KolButton, KolInputDate, KolSpin } from '@public-ui/react'; +import { FromComponent } from '../common/form/component'; +import { FormProps } from '../common/form/types'; + +export const TerminScheduleComponent: FC = (props) => ( + {}}> +
+ + +
+ + +
+); diff --git a/packages/samples/react/src/scenarios/complex-form/schedule/schedule.form.ts b/packages/samples/react/src/scenarios/complex-form/schedule/schedule.form.ts new file mode 100644 index 0000000000..6476b791f5 --- /dev/null +++ b/packages/samples/react/src/scenarios/complex-form/schedule/schedule.form.ts @@ -0,0 +1,34 @@ +import { FormControl, InputControl, RequiredValidator, ValidationHandler } from '@leanup/form'; + +export interface Schedule { + schedule: string; + time: string; +} + +export class ScheduleForm extends FormControl { + public constructor() { + super('schedule'); + + this.addControl( + new InputControl('schedule', { + label: 'Datum', + mandatory: true, + }), + ); + + this.addControl( + new InputControl('time', { + label: 'Uhrzeit', + mandatory: true, + }), + ); + + const validationHandler = new ValidationHandler(); + validationHandler.validators.add([new RequiredValidator('Bitte wählen Sie ein Datum aus.')]); + this.getInput('schedule')?.setValidationHandler(validationHandler); + + const timeHandler = new ValidationHandler(); + timeHandler.validators.add([new RequiredValidator('Bitte wählen Sie eine Uhrzeit aus.')]); + this.getInput('time')?.setValidationHandler(timeHandler); + } +} diff --git a/packages/samples/react/src/scenarios/routes.ts b/packages/samples/react/src/scenarios/routes.ts new file mode 100644 index 0000000000..9b3ba6cc40 --- /dev/null +++ b/packages/samples/react/src/scenarios/routes.ts @@ -0,0 +1,10 @@ +import { Routes } from '../shares/types'; +import { TerminComponent } from './complex-form/component'; +import { AppointmentForm } from './appointment-form/AppointmentForm'; + +export const SCENARIO_ROUTES: Routes = { + scenarios: { + 'complex-form': TerminComponent, + 'appointment-form': AppointmentForm, + }, +}; diff --git a/packages/samples/react/src/shares/routes.ts b/packages/samples/react/src/shares/routes.ts index 31f6f82ccc..4c8d0cebba 100644 --- a/packages/samples/react/src/shares/routes.ts +++ b/packages/samples/react/src/shares/routes.ts @@ -13,6 +13,7 @@ import { FORM_ROUTES } from '../components/form/routes'; import { HANDOUT_ROUTES } from '../components/handout/routes'; import { HEADING_ROUTES } from '../components/heading/routes'; import { ICON_ROUTES } from '../components/icon/routes'; +import { IMAGE_ROUTES } from '../components/image/routes'; import { INDENTED_ROUTES } from '../components/indented-text/routes'; import { INPUT_CHECKBOX_ROUTES } from '../components/input-checkbox/routes'; import { INPUT_COLOR_ROUTES } from '../components/input-color/routes'; @@ -24,28 +25,28 @@ import { INPUT_PASSWORD_ROUTES } from '../components/input-password/routes'; import { INPUT_RADIO_ROUTES } from '../components/input-radio/routes'; import { INPUT_RANGE_ROUTES } from '../components/input-range/routes'; import { INPUT_TEXT_ROUTES } from '../components/input-text/routes'; +import { KOLIBRI_ROUTES } from '../components/kolibri/routes'; import { LINK_BUTTON_ROUTES } from '../components/link-button/routes'; import { LINK_GROUP_ROUTES } from '../components/link-group/routes'; import { LINK_ROUTES } from '../components/link/routes'; +import { LOGO_ROUTES } from '../components/logo/routes'; +import { MODAL_ROUTES } from '../components/modal/routes'; import { NAV_ROUTES } from '../components/nav/routes'; import { PAGINATION_ROUTES } from '../components/pagination/routes'; import { POPOVER_ROUTES } from '../components/popover/routes'; import { PROGRESS_ROUTES } from '../components/progress/routes'; +import { QUOTE_ROUTES } from '../components/quote/routes'; import { SELECT_ROUTES } from '../components/select/routes'; import { SKIP_NAV_ROUTES } from '../components/skip-nav/routes'; import { SPIN_ROUTES } from '../components/spin/routes'; import { SPLIT_BUTTON_ROUTES } from '../components/split-button/routes'; import { TABLE_ROUTES } from '../components/table/routes'; +import { TABS_ROUTES } from '../components/tabs/routes'; import { TEXTAREA_ROUTES } from '../components/textarea/routes'; import { TOAST_ROUTES } from '../components/toast/routes'; import { VERSION_ROUTES } from '../components/version/routes'; +import { SCENARIO_ROUTES } from '../scenarios/routes'; import { Routes } from './types'; -import { IMAGE_ROUTES } from '../components/image/routes'; -import { KOLIBRI_ROUTES } from '../components/kolibri/routes'; -import { LOGO_ROUTES } from '../components/logo/routes'; -import { MODAL_ROUTES } from '../components/modal/routes'; -import { QUOTE_ROUTES } from '../components/quote/routes'; -import { TABS_ROUTES } from '../components/tabs/routes'; export const ROUTES: Routes = { ...HANDOUT_ROUTES, @@ -96,4 +97,5 @@ export const ROUTES: Routes = { ...TEXTAREA_ROUTES, ...TOAST_ROUTES, ...VERSION_ROUTES, + ...SCENARIO_ROUTES, }; diff --git a/packages/tools/kolibri-cli/src/migrate/index.ts b/packages/tools/kolibri-cli/src/migrate/index.ts index ecb206d335..ccc7175f5b 100644 --- a/packages/tools/kolibri-cli/src/migrate/index.ts +++ b/packages/tools/kolibri-cli/src/migrate/index.ts @@ -96,8 +96,8 @@ Source folder to migrate: ${baseDir} /** * Creates a replacer function for the package.json file. - * @param version The version to replace - * @returns The replacer function + * @param {string} version Version to set + * @returns {string} The replacer function */ function createVersionReplacer(version: string) { return (...args: string[]) => { @@ -110,8 +110,8 @@ Source folder to migrate: ${baseDir} /** * Sets the version of the @public-ui/* packages in the package.json file. - * @param version Version to set - * @param cb Callback function + * @param {string} version Version to set + * @param {function} cb Callback function */ function setVersionOfPublicUiPackages(version: string, cb: () => void) { let packageJson = getContentOfProjectPkgJson(); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index afec25f88c..f6e1d629cb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -72,7 +72,7 @@ importers: specifier: 11.2.14 version: 11.2.14(rxjs@6.5.5)(zone.js@0.11.8) '@public-ui/components': - specifier: 1.7.0 + specifier: 1.7.1 version: link:../../../components '@types/minimatch': specifier: 5.1.2 @@ -117,7 +117,7 @@ importers: specifier: 12.2.17 version: 12.2.17(rxjs@7.6.0)(zone.js@0.11.8) '@public-ui/components': - specifier: 1.7.0 + specifier: 1.7.1 version: link:../../../components '@types/minimatch': specifier: 5.1.2 @@ -162,7 +162,7 @@ importers: specifier: 13.4.0 version: 13.4.0(rxjs@7.6.0)(zone.js@0.11.8) '@public-ui/components': - specifier: 1.7.0 + specifier: 1.7.1 version: link:../../../components '@types/minimatch': specifier: 5.1.2 @@ -207,7 +207,7 @@ importers: specifier: 14.3.0 version: 14.3.0(rxjs@7.6.0)(zone.js@0.12.0) '@public-ui/components': - specifier: 1.7.0 + specifier: 1.7.1 version: link:../../../components '@types/minimatch': specifier: 5.1.2 @@ -252,7 +252,7 @@ importers: specifier: 15.2.9 version: 15.2.9(rxjs@7.8.1)(zone.js@0.12.0) '@public-ui/components': - specifier: 1.7.0 + specifier: 1.7.1 version: link:../../../components '@types/minimatch': specifier: 5.1.2 @@ -297,7 +297,7 @@ importers: specifier: 16.2.7 version: 16.2.7(rxjs@7.8.1)(zone.js@0.13.3) '@public-ui/components': - specifier: 1.7.0 + specifier: 1.7.1 version: link:../../../components '@types/minimatch': specifier: 5.1.2 @@ -333,7 +333,7 @@ importers: packages/adapters/hydrate: devDependencies: '@public-ui/components': - specifier: 1.7.0 + specifier: 1.7.1 version: link:../../components rimraf: specifier: 3.0.2 @@ -342,14 +342,14 @@ importers: packages/adapters/preact: dependencies: '@public-ui/react': - specifier: 1.7.0 + specifier: 1.7.1 version: link:../react preact: specifier: '>=10.11.3' version: 10.11.3 devDependencies: '@public-ui/components': - specifier: 1.7.0 + specifier: 1.7.1 version: link:../../components react: specifier: 18.2.0 @@ -367,7 +367,7 @@ importers: packages/adapters/react: devDependencies: '@public-ui/components': - specifier: 1.7.0 + specifier: 1.7.1 version: link:../../components '@types/minimatch': specifier: 5.1.2 @@ -403,7 +403,7 @@ importers: packages/adapters/react-standalone: dependencies: '@public-ui/components': - specifier: 1.7.0 + specifier: 1.7.1 version: link:../../components react: specifier: '>=16.14.0' @@ -413,7 +413,7 @@ importers: version: 18.2.0(react@18.2.0) devDependencies: '@public-ui/react': - specifier: 1.7.0 + specifier: 1.7.1 version: link:../react cpy-cli: specifier: 5.0.0 @@ -425,7 +425,7 @@ importers: packages/adapters/solid: devDependencies: '@public-ui/components': - specifier: 1.7.0 + specifier: 1.7.1 version: link:../../components '@types/minimatch': specifier: 5.1.2 @@ -455,7 +455,7 @@ importers: specifier: 7.23.0 version: 7.23.0 '@public-ui/components': - specifier: 1.7.0 + specifier: 1.7.1 version: link:../../components '@types/minimatch': specifier: 5.1.2 @@ -488,7 +488,7 @@ importers: specifier: 1.5.3 version: 1.5.3 '@public-ui/schema': - specifier: 1.7.0 + specifier: 1.7.1 version: link:../schema i18next: specifier: 23.5.1 @@ -700,13 +700,13 @@ importers: specifier: 0.0.3 version: 0.0.3(@public-ui/components@packages+components) '@public-ui/components': - specifier: 1.7.0 + specifier: 1.7.1 version: link:../components '@public-ui/solid': - specifier: 1.7.0 + specifier: 1.7.1 version: link:../adapters/solid '@public-ui/themes': - specifier: 1.7.0 + specifier: 1.7.1 version: link:../themes monaco-editor: specifier: 0.43.0 @@ -800,13 +800,13 @@ importers: specifier: 0.0.3 version: 0.0.3(@public-ui/components@packages+components) '@public-ui/components': - specifier: 1.7.0 + specifier: 1.7.1 version: link:../../components '@public-ui/react': - specifier: 1.7.0 + specifier: 1.7.1 version: link:../../adapters/react '@public-ui/themes': - specifier: 1.7.0 + specifier: 1.7.1 version: link:../../themes '@types/node': specifier: 20.8.0 @@ -838,6 +838,9 @@ importers: eslint-plugin-react: specifier: 7.33.2 version: 7.33.2(eslint@8.50.0) + formik: + specifier: 2.4.5 + version: 2.4.5(react@18.2.0) nightwatch-axe-verbose: specifier: 2.2.2 version: 2.2.2 @@ -868,6 +871,9 @@ importers: world_countries_lists: specifier: 2.8.2 version: 2.8.2 + yup: + specifier: 1.3.1 + version: 1.3.1 packages/schema: dependencies: @@ -900,11 +906,11 @@ importers: packages/themes: dependencies: '@public-ui/components': - specifier: 1.7.0 + specifier: 1.7.1 version: link:../components devDependencies: '@public-ui/schema': - specifier: 1.7.0 + specifier: 1.7.1 version: link:../schema '@types/node': specifier: ts5.1 @@ -934,14 +940,14 @@ importers: packages/themes/bmf: dependencies: '@public-ui/components': - specifier: 1.7.0 + specifier: 1.7.1 version: link:../../components devDependencies: '@public-ui/schema': - specifier: 1.7.0 + specifier: 1.7.1 version: link:../../schema '@public-ui/visual-tests': - specifier: 1.7.0 + specifier: 1.7.1 version: link:../../tools/visual-tests '@types/node': specifier: ts5.2 @@ -953,14 +959,14 @@ importers: packages/themes/bzst: dependencies: '@public-ui/components': - specifier: 1.7.0 + specifier: 1.7.1 version: link:../../components devDependencies: '@public-ui/schema': - specifier: 1.7.0 + specifier: 1.7.1 version: link:../../schema '@public-ui/visual-tests': - specifier: 1.7.0 + specifier: 1.7.1 version: link:../../tools/visual-tests '@types/node': specifier: ts5.2 @@ -972,14 +978,14 @@ importers: packages/themes/default: dependencies: '@public-ui/components': - specifier: 1.7.0 + specifier: 1.7.1 version: link:../../components devDependencies: '@public-ui/schema': - specifier: 1.7.0 + specifier: 1.7.1 version: link:../../schema '@public-ui/visual-tests': - specifier: 1.7.0 + specifier: 1.7.1 version: link:../../tools/visual-tests '@types/node': specifier: ts5.2 @@ -991,14 +997,14 @@ importers: packages/themes/ecl: dependencies: '@public-ui/components': - specifier: 1.7.0 + specifier: 1.7.1 version: link:../../components devDependencies: '@public-ui/schema': - specifier: 1.7.0 + specifier: 1.7.1 version: link:../../schema '@public-ui/visual-tests': - specifier: 1.7.0 + specifier: 1.7.1 version: link:../../tools/visual-tests '@types/node': specifier: ts5.2 @@ -1013,14 +1019,14 @@ importers: packages/themes/itzbund: dependencies: '@public-ui/components': - specifier: 1.7.0 + specifier: 1.7.1 version: link:../../components devDependencies: '@public-ui/schema': - specifier: 1.7.0 + specifier: 1.7.1 version: link:../../schema '@public-ui/visual-tests': - specifier: 1.7.0 + specifier: 1.7.1 version: link:../../tools/visual-tests '@types/node': specifier: ts5.2 @@ -1032,14 +1038,14 @@ importers: packages/themes/mfm: dependencies: '@public-ui/components': - specifier: 1.7.0 + specifier: 1.7.1 version: link:../../components devDependencies: '@public-ui/schema': - specifier: 1.7.0 + specifier: 1.7.1 version: link:../../schema '@public-ui/visual-tests': - specifier: 1.7.0 + specifier: 1.7.1 version: link:../../tools/visual-tests '@types/node': specifier: ts5.2 @@ -1051,14 +1057,14 @@ importers: packages/themes/zoll: dependencies: '@public-ui/components': - specifier: 1.7.0 + specifier: 1.7.1 version: link:../../components devDependencies: '@public-ui/schema': - specifier: 1.7.0 + specifier: 1.7.1 version: link:../../schema '@public-ui/visual-tests': - specifier: 1.7.0 + specifier: 1.7.1 version: link:../../tools/visual-tests '@types/node': specifier: ts5.2 @@ -1095,7 +1101,7 @@ importers: version: 7.5.4 devDependencies: '@public-ui/components': - specifier: 1.7.0 + specifier: 1.7.1 version: link:../../components '@types/gradient-string': specifier: 1.1.3 @@ -1164,7 +1170,7 @@ importers: specifier: 1.38.1 version: 1.38.1 '@public-ui/sample-react': - specifier: 1.7.0 + specifier: 1.7.1 version: link:../../samples/react portfinder: specifier: 1.0.32 @@ -7539,6 +7545,13 @@ packages: '@types/tinycolor2': 1.4.3 dev: true + /@types/hoist-non-react-statics@3.3.3: + resolution: {integrity: sha512-Wny3a2UXn5FEA1l7gc6BbpoV5mD1XijZqgkp4TRgDCDL5r3B5ieOFGUX5h3n78Tr1MEG7BfvoM8qeztdvNU0fw==} + dependencies: + '@types/react': 18.2.23 + hoist-non-react-statics: 3.3.2 + dev: false + /@types/http-cache-semantics@4.0.2: resolution: {integrity: sha512-FD+nQWA2zJjh4L9+pFXqWOi0Hs1ryBCfI+985NjluQ1p8EYtoLvjLOKidXBtZ4/IcxDX4o8/E8qDS3540tNliw==} dev: true @@ -10696,6 +10709,11 @@ packages: /deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + /deepmerge@2.2.1: + resolution: {integrity: sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA==} + engines: {node: '>=0.10.0'} + dev: false + /deepmerge@4.3.1: resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} engines: {node: '>=0.10.0'} @@ -12337,6 +12355,22 @@ packages: combined-stream: 1.0.8 mime-types: 2.1.35 + /formik@2.4.5(react@18.2.0): + resolution: {integrity: sha512-Gxlht0TD3vVdzMDHwkiNZqJ7Mvg77xQNfmBRrNtvzcHZs72TJppSTDKHpImCMJZwcWPBJ8jSQQ95GJzXFf1nAQ==} + peerDependencies: + react: '>=16.8.0' + dependencies: + '@types/hoist-non-react-statics': 3.3.3 + deepmerge: 2.2.1 + hoist-non-react-statics: 3.3.2 + lodash: 4.17.21 + lodash-es: 4.17.21 + react: 18.2.0 + react-fast-compare: 2.0.4 + tiny-warning: 1.0.3 + tslib: 2.6.2 + dev: false + /forwarded@0.2.0: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} engines: {node: '>= 0.6'} @@ -13036,6 +13070,12 @@ packages: resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} hasBin: true + /hoist-non-react-statics@3.3.2: + resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} + dependencies: + react-is: 16.13.1 + dev: false + /homedir-polyfill@1.0.3: resolution: {integrity: sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==} engines: {node: '>=0.10.0'} @@ -15276,6 +15316,10 @@ packages: engines: {node: '>= 10'} dev: true + /lodash-es@4.17.21: + resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==} + dev: false + /lodash._arraycopy@3.0.0: resolution: {integrity: sha512-RHShTDnPKP7aWxlvXKiDT6IX2jCs6YZLCtNhOru/OX2Q/tzX295vVBK5oX1ECtN+2r86S0Ogy8ykP1sgCZAN0A==} @@ -17930,6 +17974,10 @@ packages: object-assign: 4.1.1 react-is: 16.13.1 + /property-expr@2.0.6: + resolution: {integrity: sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA==} + dev: false + /proto-list@1.2.4: resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==} dev: true @@ -18242,6 +18290,10 @@ packages: resolution: {integrity: sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg==} dev: true + /react-fast-compare@2.0.4: + resolution: {integrity: sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw==} + dev: false + /react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} @@ -20065,6 +20117,14 @@ packages: next-tick: 1.1.0 dev: false + /tiny-case@1.0.3: + resolution: {integrity: sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==} + dev: false + + /tiny-warning@1.0.3: + resolution: {integrity: sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==} + dev: false + /tinycolor2@1.6.0: resolution: {integrity: sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==} dev: false @@ -20151,6 +20211,10 @@ packages: resolution: {integrity: sha512-VSsyNPPW74RpHwR8Fc21uubwHY7wMDeJLys2IX5zJNih+OnAnaifKHo+1LHT7DAdloQ7apeaaWg8l7qnf/TnEg==} dev: true + /toposort@2.0.2: + resolution: {integrity: sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==} + dev: false + /touch@3.1.0: resolution: {integrity: sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==} hasBin: true @@ -21908,6 +21972,15 @@ packages: engines: {node: '>=12.20'} dev: true + /yup@1.3.1: + resolution: {integrity: sha512-2stNyEF96SnPUxzRL99kt1bEHWytnvC2stwmTTqjoFXZRf63JtYK2pQt2AJvWcQvkrAzr/pcXvc6c5vrqsBzDg==} + dependencies: + property-expr: 2.0.6 + tiny-case: 1.0.3 + toposort: 2.0.2 + type-fest: 2.19.0 + dev: false + /zod-validation-error@1.5.0(zod@3.22.2): resolution: {integrity: sha512-/7eFkAI4qV0tcxMBB/3+d2c1P6jzzZYdYSlBuAklzMuCrJu5bzJfHS0yVAS87dRHVlhftd6RFJDIvv03JgkSbw==} engines: {node: '>=16.0.0'}