Skip to content

Commit

Permalink
refactor(salariesInput): break down SalariesInput into smaller compon…
Browse files Browse the repository at this point in the history
…ents
  • Loading branch information
mathiazom committed Sep 5, 2024
1 parent 81deeef commit 00363cb
Show file tree
Hide file tree
Showing 9 changed files with 204 additions and 157 deletions.
145 changes: 0 additions & 145 deletions studio/components/SalaryCsvInput/SalariesInput.tsx

This file was deleted.

66 changes: 66 additions & 0 deletions studio/components/salariesInput/SalariesInput.tsx
Original file line number Diff line number Diff line change
@@ -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: <SalariesParseErrorsToastDescription errors={errors} />,
status: "error",
duration: 10000,
});
}

return (
<Stack space={4}>
<Inline space={2}>
<SalariesFileUpload
onSalariesChanged={handleSalariesChangedFromFile}
onParseErrors={handleSalariesFileParseErrors}
/>
{salaries && Object.keys(salaries).length && (
<Text muted size={1}>
replaces all values below
</Text>
)}
</Inline>
{salaries && (
<SalariesTableEditor
salaries={salaries}
onYearSalaryChange={handleYearSalaryChange}
/>
)}
</Stack>
);
};
80 changes: 80 additions & 0 deletions studio/components/salariesInput/components/SalariesFileUpload.tsx
Original file line number Diff line number Diff line change
@@ -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<FileReader>): Promise<void> {
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<HTMLInputElement>): 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 (
<div>
{/*
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
*/}
<input
id={UPLOAD_CSV_INPUT_ID}
className={styles.uploadInput}
type={"file"}
onChange={handleFileChange}
accept=".csv"
/>
<Box className={styles.uploadButtonWrapper}>
<button className={styles.uploadButton} tabIndex={-1}>
<label
htmlFor={UPLOAD_CSV_INPUT_ID}
className={styles.uploadButtonContent}
>
<UploadIcon className={styles.uploadButtonIcon} />
<span className={styles.uploadButtonText}>Upload (.csv)</span>
</label>
</button>
</Box>
</div>
);
};

export default SalariesFileUpload;
Original file line number Diff line number Diff line change
@@ -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}'.`;
}
}

Expand All @@ -26,7 +26,7 @@ export function SalariesParseErrorsToastDescription({
return (
<div>
{errors.slice(0, maxLines).map((e, i) => (
<p key={i}>{descriptionOfCsvParseError(e)}</p>
<p key={i}>{descriptionOfSalariesParseError(e)}</p>
))}
{errors.length > maxLines && (
<p>+ {errors.length - maxLines} more errors</p>
Expand Down
46 changes: 46 additions & 0 deletions studio/components/salariesInput/components/SalariesTableEditor.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Grid columns={[2]}>
<div className={styles.tableHeader}>
<span className={styles.tableHeaderLabel}>Examination Year</span>
</div>
<div className={`${styles.tableHeader} ${styles.tableSalaryHeader}`}>
<span className={styles.tableHeaderLabel}>Amount</span>
</div>
{Object.entries(salaries)
.toSorted(([a], [b]) => Number(b) - Number(a))
.map(([year, salary], index) => (
<>
<div key={year} className={styles.tableCell}>
<label htmlFor={`salary-number-input-${year}`}>
<span className={styles.tableYearLabel}>{year}</span>
</label>
</div>
<div key={`${year}-salary`} className={styles.tableCell}>
<SalaryNumberInput
id={`salary-number-input-${year}`}
value={salary}
className={styles.tableSalaryInput}
onChange={(s) => onYearSalaryChange(year, s)}
/>
</div>
</>
))}
</Grid>
);
};

export default SalariesTableEditor;
Original file line number Diff line number Diff line change
@@ -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<HTMLInputElement>,
Expand Down
Loading

0 comments on commit 00363cb

Please sign in to comment.