Skip to content

Commit

Permalink
refactor (frontend): implement InputState in forms and extract (most)…
Browse files Browse the repository at this point in the history
… forms to new /src/components/forms dir

the new input length limit i've implemented with InputState broke tests relating to excessive input length.
i'm extracting the forms in order to do form-specific integration testing, which'll allow the E2E tests to be greatly simplified. WIP.
also, disabled some failing tests temporarily pending the test refactors
  • Loading branch information
EvAvKein committed May 24, 2024
1 parent d44b7b3 commit e70af67
Show file tree
Hide file tree
Showing 12 changed files with 398 additions and 427 deletions.
8 changes: 8 additions & 0 deletions frontend/src/components/forms/agentForm.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#agentFormWrapper > * + * {
margin-top: 1rem;
}

#agentFormWrapper > div,
#agentFormWrapper > details > div {
font-size: 1.5rem;
}
59 changes: 59 additions & 0 deletions frontend/src/components/forms/agentForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import {type recursiveRecord} from "../../../../shared/helpers/dataRecord";
import {type Agent} from "../../../../shared/objects/org";
import {type InputState} from "../../helpers/inputState";
import {LabelledInput, SearchableInput} from "../inputs";
import {timezones} from "../../../../shared/objects/timezones";
import styles from "./agentForm.module.css";

type formState = recursiveRecord<Omit<Agent, "id" | "orgId" | "internals">, InputState>;
type props = {
formState: formState;
setFormState: (formState: formState) => void;
};
export function AgentForm({formState, setFormState}: props) {
return (
<section id={styles.agentFormWrapper}>
<LabelledInput
label={"Name"}
id={"agentNameInput"}
data-testId={"agentNameInput"}
aria-invalid={!formState.name.valid}
autoFocus={true}
collapsedLabel={true}
value={formState.name.value}
handler={(value) => (formState.name.set(value), setFormState({...formState}))}
/>
<LabelledInput
label={"Department"}
id={"agentDepartmentInput"}
data-testId={"agentDepartmentInput"}
aria-invalid={!formState.department.valid}
collapsedLabel={true}
value={formState.department.value}
handler={(value) => (formState.department.set(value), setFormState({...formState}))}
/>
<LabelledInput
label={"Country Code (Phone)"}
id={"agentCountryCodeInput"}
data-testId={"agentCountryCodeInput"}
aria-invalid={!formState.countryCode.valid}
type={"number"}
collapsedLabel={true}
value={formState.countryCode.value}
handler={(value) => (formState.countryCode.set(value), setFormState({...formState}))}
/>
<SearchableInput
label={"Timezone"}
id={"agentTimezoneInput"}
data-testId={"agentTimezoneInput"}
aria-invalid={!formState.timezone.valid}
type={"search"}
labelled={true}
collapsedLabel={true}
value={formState.timezone.value}
options={timezones}
handler={(value) => (formState.timezone.set(value), setFormState({...formState}))}
/>
</section>
);
}
20 changes: 20 additions & 0 deletions frontend/src/components/forms/inviteCreation.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#inviteCreationWrapper {
font-size: 1.2rem;
}

#inviteCreationWrapper > * + * {
margin-top: 0.5em;
}

#inviteCreationWrapper #recipientPhoneWrapper {
display: flex;
align-items: center;
}

#inviteCreationWrapper #recipientPhoneWrapper > :first-child {
flex-basis: 27.5%;
}

#inviteCreationWrapper #recipientPhoneWrapper > :nth-child(2) {
flex-basis: 72.5%;
}
84 changes: 84 additions & 0 deletions frontend/src/components/forms/inviteCreation.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import {recursiveRecord} from "../../../../shared/helpers/dataRecord";
import {InviteCreationRequest} from "../../../../shared/objects/inv";
import {InputState} from "../../helpers/inputState";
import {LabelledInput} from "../inputs";
import styles from "./inviteCreation.module.css";

type formData = recursiveRecord<InviteCreationRequest, InputState>;
type props = {
formState: formData;
setFormState: (formState: formData) => void;
};
export function InviteCreation({formState, setFormState}: props) {
return (
<section id={styles.inviteCreationWrapper}>
<LabelledInput
label={"Recipient name"}
id={"recipientName"}
data-testid={"newRecipientName"}
autoFocus={true}
aria-invalid={!formState.recipient.name.valid}
value={formState.recipient.name.value}
handler={(value) => (formState.recipient.name.set(value), setFormState({...formState}))}
/>
<section id={styles.recipientPhoneWrapper}>
<LabelledInput
label={"Prefix"}
id={"countryCode"}
data-testid={"newCountryCode"}
type={"number"}
aria-invalid={!formState.recipient.phone.countryCode.valid}
value={formState.recipient.phone.countryCode.value}
handler={(value) => (formState.recipient.phone.countryCode.set(value), setFormState({...formState}))}
/>
|
<LabelledInput
label={"Phone number"}
id={"phoneNumber"}
data-testid={"newPhoneNumber"}
type={"number"} // avoiding "tel" because it allows non-number characters
aria-invalid={!formState.recipient.phone.number.valid}
value={formState.recipient.phone.number.value}
handler={(value) => (formState.recipient.phone.number.set(value), setFormState({...formState}))}
/>
</section>
<LabelledInput
label={"Call duration (minutes)"}
id={"callDuration"}
data-testid={"newCallDuration"}
type={"number"}
aria-invalid={!formState.secCallDuration.valid}
value={formState.secCallDuration.value}
handler={(value) => (formState.secCallDuration.set(value), setFormState({...formState}))}
/>
<LabelledInput
label={"Invite Expiry"}
id={"expiry"}
data-testid={"newExpiry"}
type={"datetime-local"}
aria-invalid={!formState.expiry.valid}
value={formState.expiry.value.replace(":00Z", "")}
handler={
(value) => (formState.expiry.set(value + ":00Z"), setFormState({...formState})) // adding ":00Z" to make it a valid ISO string for backend's Zod validation
}
/>
<LabelledInput
label={"Notes for recipient"}
id={"notesForRecipient"}
data-testid={"newNotesForRecipient"}
aria-invalid={!formState.notes.forRecipient!.valid}
value={formState.notes.forRecipient!.value}
handler={(value) => (formState.notes.forRecipient!.set(value), setFormState({...formState}))}
/>
<LabelledInput
label={"Notes for organization"}
id={"notesForOrg"}
data-testid={"newNotesForOrg"}
type={"textarea"}
aria-invalid={!formState.notes.forOrg!.valid}
value={formState.notes.forOrg!.value}
handler={(value) => (formState.notes.forOrg!.set(value), setFormState({...formState}))}
/>
</section>
);
}
47 changes: 47 additions & 0 deletions frontend/src/components/forms/orgForm.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
#orgFormWrapper > * + * {
margin-top: 1rem;
}

#orgFormWrapper > div,
#orgFormWrapper > details > div {
font-size: 1.5rem;
}

.orgCustomCssSection {
padding: 0.5rem 0.75rem;
border: 0.1rem dashed var(--textColor);
}

.orgCustomCssSection summary {
display: revert;
margin: 0;
}

#orgCustomCssOverridesLabel {
cursor: pointer;
margin-block: 0.5rem;
}
#cssEditor {
font-size: unset;
color: unset;
background-color: unset;
}
#cssEditor:focus-within {
outline: 0.1em solid var(--midColor);
}
#cssEditor > * {
outline: none;
}

#orgCustomCssLinks {
margin-top: 0.5rem;
font-size: 0.75rem;
}
#orgCustomCssLinks a {
text-decoration: underline;
}
#orgCustomCssOverridesLabel,
#orgCustomCssLinks {
display: flex;
justify-content: space-between;
}
89 changes: 89 additions & 0 deletions frontend/src/components/forms/orgForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import {useEffect, useRef, useState} from "react";
import {basicSetup, EditorView} from "codemirror";
import {css as codemirrorCss} from "@codemirror/lang-css";
import {LabelledInput} from "../inputs";
import codeMirrorStyles from "../../helpers/codemirrorTheme.module.css?raw";
import {type Org} from "../../../../shared/objects/org";
import {type recursiveRecord} from "../../../../shared/helpers/dataRecord";
import {type InputState} from "../../helpers/inputState";
import coreStyles from "../../core.module.css";
import styles from "./orgForm.module.css";

type formState = recursiveRecord<Omit<Org, "id" | "internals">, InputState>;

interface props {
formState: formState;
setFormState: (formState: formState) => void;
}
export function OrgForm({formState, setFormState}: props) {
const cssEditorRef = useRef<HTMLDivElement | null>(null);
const [cssEditor] = useState(
new EditorView({
extensions: [basicSetup, codemirrorCss()],
}),
);
useEffect(() => {
cssEditorRef.current?.appendChild(cssEditor.dom);
}, []);

return (
<section id={styles.orgFormWrapper}>
<LabelledInput
label={"Name"}
id={"orgNameInput"}
data-testId={"orgNameInput"}
aria-invalid={!formState.name.valid}
autoFocus={true}
collapsedLabel={true}
value={formState.name.value}
handler={(value) => (formState.name.set(value), setFormState({...formState}))}
/>
<LabelledInput
label={"Color"}
id={"orgColorInput"}
data-testId={"orgColorInput"}
aria-invalid={!formState.color.valid}
type={"color"}
collapsedLabel={true}
value={"#" + formState.color.value}
handler={(value) => (formState.color.set(value.slice(1)), setFormState({...formState}))}
/>
<details className={styles.orgCustomCssSection}>
<summary className={coreStyles.contentButton}>Custom invitation style (CSS)</summary>
<label id={styles.orgCustomCssOverridesLabel} className={coreStyles.contentButton}>
<span>Override default styles:</span>
<input
data-testid="orgCustomCssOverridesCheckbox"
type="checkbox"
value={formState.customInvCssOverrides.value}
onInput={(event) => formState.customInvCssOverrides.set(String(event.currentTarget.checked))}
/>
</label>
<style>{codeMirrorStyles}</style>
<div
ref={cssEditorRef}
id={styles.cssEditor}
onInput={() => (formState.customInvCss.set(cssEditor.state.doc.toString()), setFormState({...formState}))}
/>
<div id={styles.orgCustomCssLinks}>
<a
className={coreStyles.contentButton}
target={"_blank"}
rel={"noreferrer"}
href="https://github.com/EvAvKein/Dialplan/blob/main/frontend/src/components/inviteCard.tsx"
>
Markup (TSX)
</a>
<a
className={coreStyles.contentButton}
target={"_blank"}
rel={"noreferrer"}
href="https://github.com/EvAvKein/Dialplan/blob/main/frontend/src/components/inviteCard.css"
>
Default styles
</a>
</div>
</details>
</section>
);
}
18 changes: 0 additions & 18 deletions frontend/src/pages/dashboard/invites.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,6 @@
}

#inviteForm {
font-size: 1.2rem;
color: var(--textColor);
background-color: var(--backgroundColor);
box-sizing: border-box;
Expand All @@ -61,23 +60,6 @@
border: 0.1rem solid var(--midColor);
}

#inviteForm > * + * {
margin-top: 0.5em;
}

#inviteForm #recipientPhoneWrapper {
display: flex;
align-items: center;
}

#inviteForm #recipientPhoneWrapper > :first-child {
flex-basis: 27.5%;
}

#inviteForm #recipientPhoneWrapper > :nth-child(2) {
flex-basis: 72.5%;
}

#inviteForm button {
display: block;
margin-left: auto;
Expand Down
Loading

0 comments on commit e70af67

Please sign in to comment.