-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
refactor (frontend): implement InputState in forms and extract (most)…
… 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
Showing
12 changed files
with
398 additions
and
427 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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%; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.