From 00363cb65635d5285254df954d78464c5d2985a3 Mon Sep 17 00:00:00 2001 From: Mathias Oterhals Myklebust Date: Thu, 5 Sep 2024 15:32:44 +0200 Subject: [PATCH] refactor(salariesInput): break down SalariesInput into smaller components --- .../SalaryCsvInput/SalariesInput.tsx | 145 ------------------ .../salariesInput/SalariesInput.tsx | 66 ++++++++ .../components/SalariesFileUpload.tsx | 80 ++++++++++ .../SalariesParseErrorsToastDescription.tsx | 12 +- .../components/SalariesTableEditor.tsx | 46 ++++++ .../components}/SalaryNumberInput.tsx | 2 +- .../salariesInput.module.css | 0 .../utils/parseSalaries.tsx} | 8 +- .../compensations/salariesByLocation.ts | 2 +- 9 files changed, 204 insertions(+), 157 deletions(-) delete mode 100644 studio/components/SalaryCsvInput/SalariesInput.tsx create mode 100644 studio/components/salariesInput/SalariesInput.tsx create mode 100644 studio/components/salariesInput/components/SalariesFileUpload.tsx rename studio/components/{SalaryCsvInput => salariesInput/components}/SalariesParseErrorsToastDescription.tsx (66%) create mode 100644 studio/components/salariesInput/components/SalariesTableEditor.tsx rename studio/components/{SalaryCsvInput => salariesInput/components}/SalaryNumberInput.tsx (92%) rename studio/components/{SalaryCsvInput => salariesInput}/salariesInput.module.css (100%) rename studio/components/{SalaryCsvInput/salariesParseUtils.tsx => salariesInput/utils/parseSalaries.tsx} (91%) diff --git a/studio/components/SalaryCsvInput/SalariesInput.tsx b/studio/components/SalaryCsvInput/SalariesInput.tsx deleted file mode 100644 index c46b47601..000000000 --- a/studio/components/SalaryCsvInput/SalariesInput.tsx +++ /dev/null @@ -1,145 +0,0 @@ -import { set, StringInputProps } from "sanity"; -import { Box, Grid, Inline, Stack, Text, useToast } from "@sanity/ui"; -import { ChangeEvent } from "react"; -import styles from "./salariesInput.module.css"; -import { UploadIcon } from "@sanity/icons"; -import { - salariesAsStoredString, - salariesFromCsvString, - salariesFromStoredString, -} from "./salariesParseUtils"; -import { SalariesParseErrorsToastDescription } from "./SalariesParseErrorsToastDescription"; -import SalaryNumberInput from "./SalaryNumberInput"; - -const UPLOAD_CSV_INPUT_ID = "upload-csv-input"; - -export const SalariesInput = (props: StringInputProps) => { - const toast = useToast(); - - const salaries = - props.value === undefined - ? undefined - : salariesFromStoredString(props.value); - - async function handleFileRead(e: ProgressEvent): Promise { - const fileData = e.target?.result; - if (fileData === null || typeof fileData !== "string") { - toast.push({ - title: "Invalid data", - description: "Verify the file content and try again.", - status: "error", - }); - return; - } - const salariesParseResult = salariesFromCsvString(fileData); - if (!salariesParseResult.ok) { - toast.push({ - title: "Invalid salaries data", - description: ( - - ), - status: "error", - duration: 10000, - }); - return; - } - props.onChange(set(salariesAsStoredString(salariesParseResult.value))); - } - - function handleFileChange(e: ChangeEvent): void { - if (e.target.files !== null && e.target.files.length > 0) { - const file = e.target.files[0]; - if (file.type === "text/csv") { - const reader = new FileReader(); - reader.onload = handleFileRead; - reader.readAsText(file); - } else { - toast.push({ - title: "Invalid salaries file type", - description: "Verify the file extension and try again.", - status: "error", - }); - } - } - // reset to allow subsequent uploads of the same file - e.target.value = ""; - } - - function handleYearSalaryChange(year: string, salary: number): void { - props.onChange( - set( - salariesAsStoredString({ - ...salaries, - [year]: salary, - }), - ), - ); - } - - return ( - - -
- {/* - Using label for hidden input as a custom file upload button, based on: - https://developer.mozilla.org/en-US/docs/Web/API/File_API/Using_files_from_web_applications#using_a_label_element_to_trigger_a_hidden_file_input_element - */} - - - - -
- {salaries && Object.keys(salaries).length && ( - - replaces all values below - - )} -
- {salaries && ( - -
- Examination Year -
-
- Amount -
- {Object.entries(salaries) - .toSorted(([a], [b]) => Number(b) - Number(a)) - .map(([year, salary], index) => ( - <> -
- -
-
- handleYearSalaryChange(year, s)} - /> -
- - ))} -
- )} -
- ); -}; diff --git a/studio/components/salariesInput/SalariesInput.tsx b/studio/components/salariesInput/SalariesInput.tsx new file mode 100644 index 000000000..795e15e56 --- /dev/null +++ b/studio/components/salariesInput/SalariesInput.tsx @@ -0,0 +1,66 @@ +import { set, StringInputProps } from "sanity"; +import { Inline, Stack, Text, useToast } from "@sanity/ui"; +import { + Salaries, + salariesAsStoredString, + salariesFromStoredString, + SalariesParseError, +} from "./utils/parseSalaries"; +import { SalariesParseErrorsToastDescription } from "./components/SalariesParseErrorsToastDescription"; +import SalariesFileUpload from "./components/SalariesFileUpload"; +import SalariesTableEditor from "./components/SalariesTableEditor"; + +export const SalariesInput = (props: StringInputProps) => { + const toast = useToast(); + + const salaries = + props.value === undefined + ? undefined + : salariesFromStoredString(props.value); + + function handleYearSalaryChange(year: string, salary: number): void { + props.onChange( + set( + salariesAsStoredString({ + ...salaries, + [year]: salary, + }), + ), + ); + } + + function handleSalariesChangedFromFile(salariesFromFile: Salaries) { + props.onChange(set(salariesAsStoredString(salariesFromFile))); + } + + function handleSalariesFileParseErrors(errors: SalariesParseError[]) { + toast.push({ + title: "Invalid salaries data", + description: , + status: "error", + duration: 10000, + }); + } + + return ( + + + + {salaries && Object.keys(salaries).length && ( + + replaces all values below + + )} + + {salaries && ( + + )} + + ); +}; diff --git a/studio/components/salariesInput/components/SalariesFileUpload.tsx b/studio/components/salariesInput/components/SalariesFileUpload.tsx new file mode 100644 index 000000000..c1ae6603d --- /dev/null +++ b/studio/components/salariesInput/components/SalariesFileUpload.tsx @@ -0,0 +1,80 @@ +import styles from "../salariesInput.module.css"; +import { Box } from "@sanity/ui"; +import { UploadIcon } from "@sanity/icons"; +import { + Salaries, + salariesFromCsvString, + SalariesParseError, + SalariesParseErrorType, +} from "../utils/parseSalaries"; +import { ChangeEvent } from "react"; + +const UPLOAD_CSV_INPUT_ID = "upload-csv-input"; + +interface SalariesFileUploadProps { + onSalariesChanged: (salaries: Salaries) => void; + onParseErrors: (errors: SalariesParseError[]) => void; +} + +const SalariesFileUpload = ({ + onSalariesChanged, + onParseErrors, +}: SalariesFileUploadProps) => { + async function handleFileRead(e: ProgressEvent): Promise { + const fileData = e.target?.result; + if (fileData === null || typeof fileData !== "string") { + onParseErrors([{ error: SalariesParseErrorType.INVALID_FORMAT }]); + return; + } + const salariesParseResult = salariesFromCsvString(fileData); + if (!salariesParseResult.ok) { + onParseErrors(salariesParseResult.error); + return; + } + onSalariesChanged(salariesParseResult.value); + } + + function handleFileChange(e: ChangeEvent): void { + if (e.target.files !== null && e.target.files.length > 0) { + const file = e.target.files[0]; + if (file.type === "text/csv") { + const reader = new FileReader(); + reader.onload = handleFileRead; + reader.readAsText(file); + } else { + onParseErrors([{ error: SalariesParseErrorType.INVALID_FORMAT }]); + } + } + // reset to allow subsequent uploads of the same file + e.target.value = ""; + } + + return ( +
+ {/* + Using label for hidden input as a custom file upload button, based on: + https://developer.mozilla.org/en-US/docs/Web/API/File_API/Using_files_from_web_applications#using_a_label_element_to_trigger_a_hidden_file_input_element + */} + + + + +
+ ); +}; + +export default SalariesFileUpload; diff --git a/studio/components/SalaryCsvInput/SalariesParseErrorsToastDescription.tsx b/studio/components/salariesInput/components/SalariesParseErrorsToastDescription.tsx similarity index 66% rename from studio/components/SalaryCsvInput/SalariesParseErrorsToastDescription.tsx rename to studio/components/salariesInput/components/SalariesParseErrorsToastDescription.tsx index b1954bb09..f79b34361 100644 --- a/studio/components/SalaryCsvInput/SalariesParseErrorsToastDescription.tsx +++ b/studio/components/salariesInput/components/SalariesParseErrorsToastDescription.tsx @@ -1,18 +1,18 @@ import { SalariesParseError, SalariesParseErrorType, -} from "./salariesParseUtils"; +} from "../utils/parseSalaries"; -function descriptionOfCsvParseError(error: SalariesParseError): string { +function descriptionOfSalariesParseError(error: SalariesParseError): string { switch (error.error) { case SalariesParseErrorType.INVALID_FORMAT: return "Invalid file type. Only CSV files (extension .csv) are allowed."; case SalariesParseErrorType.NO_DATA: - return "File is empty. Verify the file content and try again"; + return "File is empty. Verify the file content and try again."; case SalariesParseErrorType.INVALID_SHAPE: - return `Row ${error.rowNumber} does not match the format '{year},{salary}'`; + return `Row ${error.rowIndex + 1} does not match the format '{year},{salary}'`; case SalariesParseErrorType.INVALID_DATA: - return `Row ${error.rowNumber} contains invalid salary data. Verify that each line has the format '{year},{salary}'.`; + return `Row ${error.rowIndex + 1} contains invalid salary data. Verify that each line has the format '{year},{salary}'.`; } } @@ -26,7 +26,7 @@ export function SalariesParseErrorsToastDescription({ return (
{errors.slice(0, maxLines).map((e, i) => ( -

{descriptionOfCsvParseError(e)}

+

{descriptionOfSalariesParseError(e)}

))} {errors.length > maxLines && (

+ {errors.length - maxLines} more errors

diff --git a/studio/components/salariesInput/components/SalariesTableEditor.tsx b/studio/components/salariesInput/components/SalariesTableEditor.tsx new file mode 100644 index 000000000..f3d3b9caa --- /dev/null +++ b/studio/components/salariesInput/components/SalariesTableEditor.tsx @@ -0,0 +1,46 @@ +import styles from "../salariesInput.module.css"; +import SalaryNumberInput from "./SalaryNumberInput"; +import { Grid } from "@sanity/ui"; +import { Salaries } from "../utils/parseSalaries"; + +interface SalariesTableEditorProps { + salaries: Salaries; + onYearSalaryChange: (year: string, salary: number) => void; +} + +const SalariesTableEditor = ({ + salaries, + onYearSalaryChange, +}: SalariesTableEditorProps) => { + return ( + +
+ Examination Year +
+
+ Amount +
+ {Object.entries(salaries) + .toSorted(([a], [b]) => Number(b) - Number(a)) + .map(([year, salary], index) => ( + <> +
+ +
+
+ onYearSalaryChange(year, s)} + /> +
+ + ))} +
+ ); +}; + +export default SalariesTableEditor; diff --git a/studio/components/SalaryCsvInput/SalaryNumberInput.tsx b/studio/components/salariesInput/components/SalaryNumberInput.tsx similarity index 92% rename from studio/components/SalaryCsvInput/SalaryNumberInput.tsx rename to studio/components/salariesInput/components/SalaryNumberInput.tsx index ca40be69c..586a9e486 100644 --- a/studio/components/SalaryCsvInput/SalaryNumberInput.tsx +++ b/studio/components/salariesInput/components/SalaryNumberInput.tsx @@ -1,5 +1,5 @@ import { HTMLProps, useState, ChangeEvent } from "react"; -import { VALID_SALARY_REGEX } from "./salariesParseUtils"; +import { VALID_SALARY_REGEX } from "../utils/parseSalaries"; type SalaryNumberInputProps = Omit< HTMLProps, diff --git a/studio/components/SalaryCsvInput/salariesInput.module.css b/studio/components/salariesInput/salariesInput.module.css similarity index 100% rename from studio/components/SalaryCsvInput/salariesInput.module.css rename to studio/components/salariesInput/salariesInput.module.css diff --git a/studio/components/SalaryCsvInput/salariesParseUtils.tsx b/studio/components/salariesInput/utils/parseSalaries.tsx similarity index 91% rename from studio/components/SalaryCsvInput/salariesParseUtils.tsx rename to studio/components/salariesInput/utils/parseSalaries.tsx index 9ead5dd72..02840a8f6 100644 --- a/studio/components/SalaryCsvInput/salariesParseUtils.tsx +++ b/studio/components/salariesInput/utils/parseSalaries.tsx @@ -1,4 +1,4 @@ -import { Result, ResultError, ResultOk } from "../../utils/result"; +import { Result, ResultError, ResultOk } from "../../../utils/result"; export interface Salaries { [year: string]: number; @@ -19,7 +19,7 @@ export type SalariesParseError = error: | SalariesParseErrorType.INVALID_SHAPE | SalariesParseErrorType.INVALID_DATA; - rowNumber: number; + rowIndex: number; } | { error: @@ -49,7 +49,7 @@ export function salariesFromCsvString( const values = cleanRow.split(","); if (values.length != 2) { errors.push({ - rowNumber: i, + rowIndex: i, error: SalariesParseErrorType.INVALID_SHAPE, }); continue; @@ -61,7 +61,7 @@ export function salariesFromCsvString( VALID_SALARY_REGEX.test(salary) ) ) { - errors.push({ rowNumber: i, error: SalariesParseErrorType.INVALID_DATA }); + errors.push({ rowIndex: i, error: SalariesParseErrorType.INVALID_DATA }); continue; } salaries[Number(year)] = Number(salary); diff --git a/studio/schemas/objects/compensations/salariesByLocation.ts b/studio/schemas/objects/compensations/salariesByLocation.ts index 7aa9f8efe..296e30ff1 100644 --- a/studio/schemas/objects/compensations/salariesByLocation.ts +++ b/studio/schemas/objects/compensations/salariesByLocation.ts @@ -4,8 +4,8 @@ import { DocumentWithLocation, checkForDuplicateLocations, } from "./utils/validation"; -import { SalariesInput } from "../../../components/SalaryCsvInput/SalariesInput"; import { companyLocationNameID } from "../../documents/companyLocation"; +import { SalariesInput } from "../../../components/salariesInput/SalariesInput"; export const salariesByLocation = defineField({ name: "salaries",