Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Various editor and UI improvements #151

Open
wants to merge 15 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -110,5 +110,23 @@ module.exports = {
}],
},
],
// Removing for..of loops from this rule. Vite already targets modern browsers, so for..of doesn't require
// transpilation. Array.forEach doesn't work at all with async.
// Modified from https://github.com/airbnb/javascript/blob/master/packages/eslint-config-airbnb-base/rules/style.js
"no-restricted-syntax": [
"error",
{
"selector": "ForInStatement",
"message": "for..in loops iterate over the entire prototype chain, which is virtually never what you want. Use Object.{keys,values,entries}, and iterate over the resulting array."
},
{
"selector": "LabeledStatement",
"message": "Labels are a form of GOTO; using them makes code confusing and hard to maintain and understand."
},
{
"selector": "WithStatement",
"message": "`with` is disallowed in strict mode because it makes code impossible to predict and optimize."
}
],
}
};
18 changes: 0 additions & 18 deletions packages/ilmomasiina-backend/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -1,23 +1,5 @@
{
"rules": {
// Removing for..of loops from this rule. We're compiling the server for a new enough ES version that there
// is no performance issue with them server-side, and Array.forEach doesn't work at all with async.
// Modified from https://github.com/airbnb/javascript/blob/master/packages/eslint-config-airbnb-base/rules/style.js
"no-restricted-syntax": [
"error",
{
"selector": "ForInStatement",
"message": "for..in loops iterate over the entire prototype chain, which is virtually never what you want. Use Object.{keys,values,entries}, and iterate over the resulting array."
},
{
"selector": "LabeledStatement",
"message": "Labels are a form of GOTO; using them makes code confusing and hard to maintain and understand."
},
{
"selector": "WithStatement",
"message": "`with` is disallowed in strict mode because it makes code impossible to predict and optimize."
}
],
// Used server-side.
"no-console": "off"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,11 @@ export default function BaseFieldRow({
}: BaseFieldRowProps) {
return (
<Form.Group as={Row} controlId={controlId}>
<Form.Label column sm="3" data-required={required} className={checkAlign ? "pt-0" : ""}>
{label}
</Form.Label>
<Col sm="3" className="ilmo--label-column">
<Form.Label data-required={required} className={`col-form-label ${checkAlign ? "pt-0" : ""}`}>
{label}
</Form.Label>
</Col>
<Col sm="9">
{children}
{error && (
Expand Down
67 changes: 43 additions & 24 deletions packages/ilmomasiina-components/src/components/FieldRow/index.tsx
Original file line number Diff line number Diff line change
@@ -1,46 +1,73 @@
import React, { ComponentType, ReactNode } from "react";
import React, { ComponentPropsWithoutRef, ComponentType, JSX, ReactNode } from "react";

import { Form } from "react-bootstrap";
import { Form, FormControlProps } from "react-bootstrap";
import { useField, UseFieldConfig } from "react-final-form";

import BaseFieldRow, { BaseFieldRowProps } from "../BaseFieldRow";

type Props = Omit<BaseFieldRowProps, "error" | "children"> &
type BaseProps = Omit<BaseFieldRowProps, "error" | "children"> &
Pick<UseFieldConfig<any>, "type"> & {
/** The name of the field in the data. */
name: string;
/** Passed as `controlId` if no `controlId` is separately set. */
id?: string;
/** Overrides the real error message if the field has errors. */
alternateError?: string;
/** Formats a field error. */
formatError?: (error: any) => ReactNode;
/** Passed as `label` to the field component. Intended for checkboxes. */
checkLabel?: ReactNode;
/** The component or element to use as the field. */
as?: ComponentType<any> | string;
/** useField() config. */
config?: UseFieldConfig<any>;
/** If given, this is used as the field. */
children?: ReactNode;
};

// These typings do the best attempt we can do with merged props.
// Ideally, with TypeScript, we should provide a render function, but I'll defer that change
// to when we get rid of react-bootstrap altogether.

// react-bootstrap's typing for Form.Control is extremely broad, so only allow <input> props.
type InputProps = ComponentPropsWithoutRef<"input">;

// Props with neither `children` nor `as`, assume `Form.Control`.
type PropsWithFormControl = BaseProps & {
children?: undefined;
as?: undefined;
} & Omit<FormControlProps & InputProps, keyof BaseProps | "as" | "children">;

// Props with `children` given, no extra props to pass through.
type PropsWithChildren = BaseProps & {
/** If given, this is used as the field. */
children: ReactNode;
as?: undefined;
};

type As = keyof JSX.IntrinsicElements | ComponentType<any>;

// Props with a custom `as` component.
type PropsWithAs<C extends As> = BaseProps & {
/** The component or element to use as the field. */
as: C;
children?: undefined;
} & Omit<ComponentPropsWithoutRef<C>, keyof BaseProps | "as" | "children">;

type Props<C extends As> = PropsWithFormControl | PropsWithChildren | PropsWithAs<C>;

/** react-final-field field row component */
export default function FieldRow<P = unknown>({
export default function FieldRow<C extends As>({
name,
label = "",
help,
required = false,
alternateError,
formatError,
extraFeedback,
checkAlign,
checkLabel,
as: Component = Form.Control,
as,
children,
type,
id,
controlId = id ?? name,
config,
...props
}: Props & P) {
}: Props<C>) {
const {
input,
meta: { error: validationError, submitError, invalid },
Expand All @@ -55,16 +82,8 @@ export default function FieldRow<P = unknown>({
// and calls it "label", but we still want to call the other one "label" for all other types of field. Therefore
// we pass "checkLabel" to the field here.
const overrideProps = checkLabel !== undefined ? { label: checkLabel } : {};
field = (
<Component
required={required}
isInvalid={invalid}
{...props}
id={id}
{...input}
{...overrideProps}
/>
);
const Component = (as ?? Form.Control) as ComponentType<any>;
field = <Component required={required} isInvalid={invalid} {...props} id={id} {...input} {...overrideProps} />;
}

return (
Expand All @@ -73,7 +92,7 @@ export default function FieldRow<P = unknown>({
label={label}
help={help}
required={required}
error={invalid && (alternateError || error)}
error={invalid && (formatError ? formatError(error) : error)}
extraFeedback={extraFeedback}
checkAlign={checkAlign}
>
Expand Down
7 changes: 6 additions & 1 deletion packages/ilmomasiina-components/src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"editSignup.action.cancel": "Cancel",
"editSignup.action.edit": "Update",
"editSignup.action.save": "Save",
"editSignup.action.back": "Back",
"editSignup.delete.action": "Delete signup",
"editSignup.delete.action.confirm": "Paina uudelleen varmistukseksi\u2026",
"editSignup.delete.info1": "Are you sure you want to delete your sign up to <1>{{event}}</1>?",
Expand Down Expand Up @@ -53,8 +54,10 @@
"editSignup.fieldError.invalidEmail": "The email address is invalid.",
"editSignup.fieldError.notANumber": "The answer to this question must be a valid number.",
"editSignup.fieldError.notAnOption": "The answer to this question isn't in the allowed options. Try refreshing the page.",
"editSignup.fieldError": "Kentän vastaus on virheellinen. Kokeile päivittää sivu.",
"editSignup.title.edit": "Edit signup",
"editSignup.title.signup": "Sign up",
"editSignup.title.preview": "Preview signup form",
"errors.returnToEvents": "Return to event list",
"errors.contactAdmin": "If the problem persists, please report it to the administrators.",
"errors.default.title": "Whoops, something went wrong",
Expand All @@ -78,7 +81,8 @@
"signupState.notOpened.short": "Opens on {{date}}.",
"signupState.open": "Signup open until {{date}}.",
"signupState.open.short": "Until {{date}}.",
"signupState.closed": "The signup for this event has closed.",
"signupState.closed": "The signup for this event has closed at {{date}}.",
"signupState.closed.short": "Closed on {{date}}.",
"singleEvent.editEvent": "Edit",
"singleEvent.info.category": "Category:",
"singleEvent.info.date": "Date:",
Expand All @@ -98,6 +102,7 @@
"singleEvent.returnToEvents": "Back",
"singleEvent.signupButton.singleQuota": "Sign up now",
"singleEvent.signupButton": "Sign up: {{quota}}",
"singleEvent.signupButton.preview": "Preview signup form",
"singleEvent.signupButtons.title": "Sign up",
"singleEvent.signups.emptyQuota": "No signups.",
"singleEvent.signups.nameHidden": "Hidden",
Expand Down
7 changes: 6 additions & 1 deletion packages/ilmomasiina-components/src/locales/fi.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"editSignup.action.cancel": "Peruuta",
"editSignup.action.edit": "Päivitä",
"editSignup.action.save": "Tallenna",
"editSignup.action.back": "Takaisin",
"editSignup.delete.action": "Poista ilmoittautuminen",
"editSignup.delete.action.confirm": "Paina uudelleen varmistukseksi\u2026",
"editSignup.delete.info1": "Oletko varma, että haluat poistaa ilmoittautumisesi tapahtumaan <1>{{event}}</1>?",
Expand Down Expand Up @@ -53,8 +54,10 @@
"editSignup.fieldError.invalidEmail": "Sähköpostiosoite on virheellinen.",
"editSignup.fieldError.notANumber": "Kentän vastauksen tulee olla numero.",
"editSignup.fieldError.notAnOption": "Kentän vastaus ei ole sallituissa vaihtoehdoissa. Kokeile päivittää sivu.",
"editSignup.fieldError": "Kentän vastaus on virheellinen. Kokeile päivittää sivu.",
"editSignup.title.edit": "Muokkaa ilmoittautumista",
"editSignup.title.signup": "Ilmoittaudu",
"editSignup.title.preview": "Esikatsele ilmolomaketta",
"errors.returnToEvents": "Palaa tapahtumalistaukseen",
"errors.contactAdmin": "Jos ongelma jatkuu, ota yhteyttä ylläpitäjiin.",
"errors.default.title": "Hups, jotain meni pieleen",
Expand All @@ -78,7 +81,8 @@
"signupState.notOpened.short": "Alkaa {{date}}.",
"signupState.open": "Ilmoittautuminen auki {{date}} asti.",
"signupState.open.short": "Auki {{date}} asti.",
"signupState.closed": "Ilmoittautuminen on päättynyt.",
"signupState.closed": "Ilmoittautuminen päättyi {{date}}.",
"signupState.closed.short": "Päättyi {{date}}.",
"singleEvent.editEvent": "Muokkaa",
"singleEvent.info.category": "Kategoria:",
"singleEvent.info.date": "Ajankohta:",
Expand All @@ -98,6 +102,7 @@
"singleEvent.returnToEvents": "Takaisin",
"singleEvent.signupButton.singleQuota": "Ilmoittaudu nyt",
"singleEvent.signupButton": "Ilmoittaudu: {{quota}}",
"singleEvent.signupButton.preview": "Esikatsele ilmolomaketta",
"singleEvent.signupButtons.title": "Ilmoittautuminen",
"singleEvent.signups.emptyQuota": "Ei ilmoittautumisia.",
"singleEvent.signups.nameHidden": "Piilotettu",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ export interface EditSignupProps {
editToken: string;
}

export { useStateContext as useEditSignupContext } from "./state";
export { useStateContext as useEditSignupContext, Provider as EditSignupContextProvider } from "./state";
export type { State as EditSignupState } from "./state";
export { useDeleteSignup, useUpdateSignup } from "./actions";

export function useEditSignupState({ id, editToken }: EditSignupProps) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export type State = Partial<SignupForEditResponse> & {
error?: ApiError;
editToken: string;
registrationClosed: boolean;
preview?: { setPreviewingForm: (form: boolean) => void };
};

export const { Provider, useStateContext, createThunk } = createStateContext<State>();
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,14 @@ type State = {
signupsByQuota?: QuotaSignups[];
pending: boolean;
error?: ApiError;
preview?: { setPreviewingForm: (form: boolean) => void };
};

const { Provider, useStateContext } = createStateContext<State>();
export { useStateContext as useSingleEventContext };
export { beginSignup } from "./actions";
export type { State as SingleEventState };
export { Provider as SingleEventContextProvider };

export function useSingleEventState({ slug }: SingleEventProps) {
const fetchEvent = useAbortablePromise((signal) => apiFetch<UserEventResponse>(`events/${slug}`, { signal }), [slug]);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
import React from "react";

import { Form } from "react-bootstrap";
import { useFormState } from "react-final-form";
import { useTranslation } from "react-i18next";

import FieldRow from "../../../components/FieldRow";
import { useEditSignupContext } from "../../../modules/editSignup";
import fieldError from "./fieldError";
import useFieldErrors from "./fieldError";

const CommonFields = () => {
const { event, signup, registrationClosed } = useEditSignupContext();
const { submitErrors } = useFormState({ subscription: { submitErrors: true } });
const isNew = !signup!.confirmed;
const { t } = useTranslation();
const formatError = useFieldErrors();
return (
<>
{event!.nameQuestion && (
Expand All @@ -24,7 +23,7 @@ const CommonFields = () => {
placeholder={t("editSignup.fields.firstName.placeholder")}
required
readOnly={!isNew || registrationClosed}
alternateError={fieldError(t, submitErrors?.firstName)}
formatError={formatError}
/>
<FieldRow
name="lastName"
Expand All @@ -33,7 +32,7 @@ const CommonFields = () => {
placeholder={t("editSignup.fields.lastName.placeholder")}
required
readOnly={!isNew || registrationClosed}
alternateError={fieldError(t, submitErrors?.lastName)}
formatError={formatError}
/>
<FieldRow
name="namePublic"
Expand All @@ -53,7 +52,7 @@ const CommonFields = () => {
placeholder={t("editSignup.fields.email.placeholder")}
required
readOnly={!isNew || registrationClosed}
alternateError={fieldError(t, submitErrors?.email)}
formatError={formatError}
/>
)}
</>
Expand Down
Loading