From db639f67336a21a18de76e48f797773103e0535e Mon Sep 17 00:00:00 2001 From: Anya Wallace Date: Tue, 26 Nov 2024 16:54:17 -0800 Subject: [PATCH 01/15] customize input field depending on data type --- .../ChoiceGroup/ChoiceGroup.module.css | 2 +- .../ExistingAnnotationPathway.tsx | 6 +- .../EditMetadata/MetadataDetails.module.css | 39 +++++++++ .../EditMetadata/MetadataDetails.tsx | 79 ++++++++++++++++--- .../EditMetadata/NewAnnotationPathway.tsx | 9 ++- .../core/components/EditMetadata/index.tsx | 1 + 6 files changed, 121 insertions(+), 15 deletions(-) diff --git a/packages/core/components/ChoiceGroup/ChoiceGroup.module.css b/packages/core/components/ChoiceGroup/ChoiceGroup.module.css index 8cdd99e0..93c4111d 100644 --- a/packages/core/components/ChoiceGroup/ChoiceGroup.module.css +++ b/packages/core/components/ChoiceGroup/ChoiceGroup.module.css @@ -8,7 +8,7 @@ } .choice-group label > span { - font-size: var(--l-paragraph-size); + font-size: var(--s-paragraph-size); margin-right: 5px; margin-top: 1px; padding-left: 24px !important; diff --git a/packages/core/components/EditMetadata/ExistingAnnotationPathway.tsx b/packages/core/components/EditMetadata/ExistingAnnotationPathway.tsx index fe663851..8211b937 100644 --- a/packages/core/components/EditMetadata/ExistingAnnotationPathway.tsx +++ b/packages/core/components/EditMetadata/ExistingAnnotationPathway.tsx @@ -5,13 +5,14 @@ import * as React from "react"; import MetadataDetails, { ValueCountItem } from "./MetadataDetails"; import { PrimaryButton, SecondaryButton } from "../Buttons"; import ComboBox from "../ComboBox"; +import { AnnotationType } from "../../entity/AnnotationFormatter"; import styles from "./EditMetadata.module.css"; interface ExistingAnnotationProps { onDismiss: () => void; annotationValueMap: Map | undefined; - annotationOptions: IComboBoxOption[]; + annotationOptions: { key: string; text: string; data: string }[]; selectedFileCount: number; } @@ -23,6 +24,7 @@ export default function ExistingAnnotationPathway(props: ExistingAnnotationProps const [newValues, setNewValues] = React.useState(); const [valueCount, setValueCount] = React.useState(); const [selectedAnnotation, setSelectedAnnotation] = React.useState(); + const [annotationType, setAnnotationType] = React.useState(); const onSelectMetadataField = ( option: IComboBoxOption | undefined, @@ -61,6 +63,7 @@ export default function ExistingAnnotationPathway(props: ExistingAnnotationProps ]; } setSelectedAnnotation(selectedFieldName); + setAnnotationType(option?.data); setValueCount(valueMap); }; @@ -84,6 +87,7 @@ export default function ExistingAnnotationPathway(props: ExistingAnnotationProps setNewValues(value)} items={valueCount || []} + fieldType={annotationType} /> )}
diff --git a/packages/core/components/EditMetadata/MetadataDetails.module.css b/packages/core/components/EditMetadata/MetadataDetails.module.css index 35a8a2c6..7d92dd77 100644 --- a/packages/core/components/EditMetadata/MetadataDetails.module.css +++ b/packages/core/components/EditMetadata/MetadataDetails.module.css @@ -62,3 +62,42 @@ .stack-item-right { width: 275px; } + +.read-only-placeholder { + margin-right: 20px; + font-style: italic; + color: var(--primary-text-color); +} + +.input-field input { + background-color: var(--secondary-background-color); + border-radius: var(--small-border-radius); + border: 1px solid var(--border-color); + color: var(--secondary-text-color); + outline: none; + padding: 6px; + font-size: var(--s-paragraph-size); + width: 100%; + min-width: fit-content; +} + +.input-field:active > input, .input-field > input:active, +.input-field:focus > input, .input-field > input:focus, +.input-field:focus-within > input, .input-field > input:focus-within { + border: 1px solid var(--aqua); +} + +.date-range-text-field div { + background-color: var(--secondary-background-color); + border: none; + border-radius: var(--small-border-radius); + color: var(--primary-text-color); +} + +.date-range-text-field div::after { + border: 1px solid var(--aqua) +} + +.date-range-text-field > div { + border: 1px solid var(--border-color) +} diff --git a/packages/core/components/EditMetadata/MetadataDetails.tsx b/packages/core/components/EditMetadata/MetadataDetails.tsx index c3c30c57..3b863e31 100644 --- a/packages/core/components/EditMetadata/MetadataDetails.tsx +++ b/packages/core/components/EditMetadata/MetadataDetails.tsx @@ -1,4 +1,5 @@ import { + DatePicker, DetailsList, IColumn, Icon, @@ -11,6 +12,9 @@ import { } from "@fluentui/react"; import * as React from "react"; +import ChoiceGroup from "../ChoiceGroup"; +import { AnnotationType } from "../../entity/AnnotationFormatter"; + import rootStyles from "./EditMetadata.module.css"; import styles from "./MetadataDetails.module.css"; @@ -20,6 +24,7 @@ export interface ValueCountItem { } interface DetailsListProps { + fieldType?: AnnotationType; items: ValueCountItem[]; onChange: (value: string | undefined) => void; newValues?: string; @@ -55,6 +60,68 @@ export default function EditMetadataDetailsList(props: DetailsListProps) { return fieldContent; } + const inputField = () => { + switch (props.fieldType) { + case AnnotationType.DATE: + case AnnotationType.DATETIME: + return ( + + ); + case AnnotationType.NUMBER: + return ( +
+ +
+ ); + case AnnotationType.BOOLEAN: + return ( + { + console.info("placeholder"); + }} + options={[ + { + key: "true", + text: "True", + }, + { + key: "false", + text: "False", + }, + ]} + /> + ); + case AnnotationType.DURATION: + case AnnotationType.DROPDOWN: + case AnnotationType.STRING: + default: + return ( + + e.currentTarget.value && props.onChange(e.currentTarget.value) + } + placeholder="Value(s)" + defaultValue={props.newValues} + /> + ); + } + }; + return (
@@ -98,16 +165,8 @@ export default function EditMetadataDetailsList(props: DetailsListProps) { - {/* TODO: Display different entry types depending on datatype of annotation */} - - e.currentTarget.value && props.onChange(e.currentTarget.value) - } - placeholder="Value(s)" - defaultValue={props.newValues} - /> +

Replace with

+ {inputField()}
diff --git a/packages/core/components/EditMetadata/NewAnnotationPathway.tsx b/packages/core/components/EditMetadata/NewAnnotationPathway.tsx index 50dfde50..c2d63f54 100644 --- a/packages/core/components/EditMetadata/NewAnnotationPathway.tsx +++ b/packages/core/components/EditMetadata/NewAnnotationPathway.tsx @@ -30,7 +30,7 @@ export default function NewAnnotationPathway(props: NewAnnotationProps) { const [step, setStep] = React.useState(EditStep.CREATE_FIELD); const [newValues, setNewValues] = React.useState(); const [newFieldName, setNewFieldName] = React.useState(""); - const [newFieldDataType, setNewFieldDataType] = React.useState(); + const [newFieldDataType, setNewFieldDataType] = React.useState(); const [newDropdownOption, setNewDropdownOption] = React.useState(""); const [dropdownOptions, setDropdownOptions] = React.useState([]); @@ -89,7 +89,9 @@ export default function NewAnnotationPathway(props: NewAnnotationProps) { }; })} useComboBoxAsMenuWidth - onChange={(option) => setNewFieldDataType(option?.text || "")} + onChange={(option) => + setNewFieldDataType((option?.text as AnnotationType) || "") + } /> {newFieldDataType === AnnotationType.DROPDOWN && ( <> @@ -130,13 +132,14 @@ export default function NewAnnotationPathway(props: NewAnnotationProps) { )} {step === EditStep.EDIT_FILES && ( setNewValues(value)} + fieldType={newFieldDataType} items={[ { value: undefined, fileCount: props.selectedFileCount, } as ValueCountItem, ]} + onChange={(value) => setNewValues(value)} /> )}
diff --git a/packages/core/components/EditMetadata/index.tsx b/packages/core/components/EditMetadata/index.tsx index 1bfdfe7a..5e424f3e 100644 --- a/packages/core/components/EditMetadata/index.tsx +++ b/packages/core/components/EditMetadata/index.tsx @@ -34,6 +34,7 @@ export default function EditMetadataForm(props: EditMetadataProps) { return { key: annotation.name, text: annotation.displayName, + data: annotation.type, }; }); const [editPathway, setEditPathway] = React.useState( From 30cf176ab811650be51b5cdd4106b1d24b40c91f Mon Sep 17 00:00:00 2001 From: Anya Wallace Date: Tue, 3 Dec 2024 17:45:17 -0800 Subject: [PATCH 02/15] create new components for duration and number inputs --- .../DurationForm/DurationForm.module.css | 15 ++++ .../core/components/DurationForm/index.tsx | 73 +++++++++++++++++++ .../EditMetadata/MetadataDetails.tsx | 34 ++++++--- .../EditMetadata/NewAnnotationPathway.tsx | 10 ++- .../NumberRangePicker/NumberField.module.css | 35 +++++++++ .../NumberRangePicker/NumberField.tsx | 39 ++++++++++ .../NumberRangePicker.module.css | 32 -------- .../components/NumberRangePicker/index.tsx | 47 +++++------- .../AnnotationFormatter/duration-formatter.ts | 15 +++- 9 files changed, 223 insertions(+), 77 deletions(-) create mode 100644 packages/core/components/DurationForm/DurationForm.module.css create mode 100644 packages/core/components/DurationForm/index.tsx create mode 100644 packages/core/components/NumberRangePicker/NumberField.module.css create mode 100644 packages/core/components/NumberRangePicker/NumberField.tsx diff --git a/packages/core/components/DurationForm/DurationForm.module.css b/packages/core/components/DurationForm/DurationForm.module.css new file mode 100644 index 00000000..0fdb6830 --- /dev/null +++ b/packages/core/components/DurationForm/DurationForm.module.css @@ -0,0 +1,15 @@ +.input-wrapper { + display: flex; +} + +.input-field { + margin-right: 3px; + max-height: fit-content; + display: flex; +} + +.input-field > input { + padding: 6px; + font-size: var(--s-paragraph-size); + max-width: 70px; +} diff --git a/packages/core/components/DurationForm/index.tsx b/packages/core/components/DurationForm/index.tsx new file mode 100644 index 00000000..dc440c2a --- /dev/null +++ b/packages/core/components/DurationForm/index.tsx @@ -0,0 +1,73 @@ +import * as React from "react"; + +import NumberField from "../NumberRangePicker/NumberField"; +import annotationFormatterFactory, { AnnotationType } from "../../entity/AnnotationFormatter"; + +import styles from "./DurationForm.module.css"; + +interface DurationFormProps { + className?: string; + defaultValue?: number; + onChange: (totalDuration: number) => void; + title?: string; +} + +/** + * This component renders a simple form for entering durations + */ +export default function DurationForm(props: DurationFormProps) { + const { onChange } = props; + const [days, setDurationDays] = React.useState(""); + const [hours, setDurationHours] = React.useState(""); + const [minutes, setDurationMinutes] = React.useState(""); + const [seconds, setDurationSeconds] = React.useState(""); + const durationFormatter = annotationFormatterFactory(AnnotationType.DURATION); + + React.useEffect(() => { + const durationString = `${Number(days) || 0}D ${Number(hours) || 0}H ${ + Number(minutes) || 0 + }M ${Number(seconds) || 0}S`; + const totalDurationInMs = Number(durationFormatter.valueOf(durationString)); + onChange(totalDurationInMs); + }, [days, hours, minutes, seconds, durationFormatter, onChange]); + + return ( +
+

{props?.title}

+
+ setDurationDays(event?.target?.value || "")} + placeholder="Days..." + defaultValue={days} + /> + setDurationHours(event?.target?.value || "")} + placeholder="Hrs..." + defaultValue={hours} + /> + setDurationMinutes(event?.target?.value || "")} + placeholder="Mins..." + defaultValue={minutes} + /> + setDurationSeconds(event?.target?.value || "")} + placeholder="Secs..." + defaultValue={seconds} + /> +
+
+ ); +} diff --git a/packages/core/components/EditMetadata/MetadataDetails.tsx b/packages/core/components/EditMetadata/MetadataDetails.tsx index 3b863e31..779a63cc 100644 --- a/packages/core/components/EditMetadata/MetadataDetails.tsx +++ b/packages/core/components/EditMetadata/MetadataDetails.tsx @@ -13,7 +13,9 @@ import { import * as React from "react"; import ChoiceGroup from "../ChoiceGroup"; -import { AnnotationType } from "../../entity/AnnotationFormatter"; +import DurationForm from "../DurationForm"; +import NumberField from "../NumberRangePicker/NumberField"; +import annotationFormatterFactory, { AnnotationType } from "../../entity/AnnotationFormatter"; import rootStyles from "./EditMetadata.module.css"; import styles from "./MetadataDetails.module.css"; @@ -46,6 +48,9 @@ export default function EditMetadataDetailsList(props: DetailsListProps) { } return <>; }; + const annotationFormatter = annotationFormatterFactory( + props.fieldType || AnnotationType.STRING + ); function renderItemColumn( item: ValueCountItem, @@ -53,8 +58,10 @@ export default function EditMetadataDetailsList(props: DetailsListProps) { column: IColumn | undefined ) { const fieldContent = item[column?.fieldName as keyof ValueCountItem] as string; - if (!fieldContent) return "[No value] (blank)"; - if (column?.fieldName === "fileCount") { + if (column?.fieldName === "value") { + if (!fieldContent) return "[No value] (blank)"; + else return annotationFormatter.displayValue(fieldContent); + } else if (column?.fieldName === "fileCount") { return
{fieldContent}
; } return fieldContent; @@ -76,14 +83,14 @@ export default function EditMetadataDetailsList(props: DetailsListProps) { ); case AnnotationType.NUMBER: return ( -
- -
+ { + console.info("placeholder for linting"); + }} + /> ); case AnnotationType.BOOLEAN: return ( @@ -106,6 +113,11 @@ export default function EditMetadataDetailsList(props: DetailsListProps) { /> ); case AnnotationType.DURATION: + return ( + console.info(totalDuration)} + /> + ); case AnnotationType.DROPDOWN: case AnnotationType.STRING: default: diff --git a/packages/core/components/EditMetadata/NewAnnotationPathway.tsx b/packages/core/components/EditMetadata/NewAnnotationPathway.tsx index c2d63f54..14dd24d7 100644 --- a/packages/core/components/EditMetadata/NewAnnotationPathway.tsx +++ b/packages/core/components/EditMetadata/NewAnnotationPathway.tsx @@ -79,18 +79,20 @@ export default function NewAnnotationPathway(props: NewAnnotationProps) { <> { + const text = + type === AnnotationType.BOOLEAN ? "Boolean (true/false)" : type; return { - key: `datatype-${type}`, - text: type, + key: type, + text, }; })} useComboBoxAsMenuWidth onChange={(option) => - setNewFieldDataType((option?.text as AnnotationType) || "") + setNewFieldDataType((option?.key as AnnotationType) || "") } /> {newFieldDataType === AnnotationType.DROPDOWN && ( diff --git a/packages/core/components/NumberRangePicker/NumberField.module.css b/packages/core/components/NumberRangePicker/NumberField.module.css new file mode 100644 index 00000000..b273b626 --- /dev/null +++ b/packages/core/components/NumberRangePicker/NumberField.module.css @@ -0,0 +1,35 @@ +.input-field { + flex-grow: 1; +} + +.input-field input { + background-color: var(--secondary-background-color); + border-radius: var(--small-border-radius); + border: 1px solid var(--border-color); + color: var(--secondary-text-color); + outline: none; + padding: 6px; + font-size: var(--s-paragraph-size); + width: 100%; + min-width: fit-content; +} + +.input-field > input::placeholder { + color: var(--secondary-text-color) !important; + font-style: italic; +} + +.input-field:active > input, +.input-field > input:active, +.input-field:focus > input, +.input-field > input:focus, +.input-field:focus-within > input, +.input-field > input:focus-within { + border: 1px solid var(--aqua); +} + +.input-field input::-webkit-outer-spin-button, +.input-field input::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; /* <-- Margins are still present when hidden */ +} diff --git a/packages/core/components/NumberRangePicker/NumberField.tsx b/packages/core/components/NumberRangePicker/NumberField.tsx new file mode 100644 index 00000000..2a263b0f --- /dev/null +++ b/packages/core/components/NumberRangePicker/NumberField.tsx @@ -0,0 +1,39 @@ +import classNames from "classnames"; +import * as React from "react"; + +import styles from "./NumberField.module.css"; + +interface NumberFieldProps { + className?: string; + id: string; + onChange: (event?: React.ChangeEvent) => void; + defaultValue?: string | number; + label?: string; + min?: number; + max?: number; + placeholder?: string; +} + +/** + * A simple wrapper to provide a consistently styled numerical input field. + * FluentUI does not have an equivalent that fulfills our UX requirements, + * so we instead use the basic html input with styling applied + */ +export default function NumberField(props: NumberFieldProps) { + return ( +
+ {props.label && } + +
+ ); +} diff --git a/packages/core/components/NumberRangePicker/NumberRangePicker.module.css b/packages/core/components/NumberRangePicker/NumberRangePicker.module.css index 790cd5d9..a67b9c16 100644 --- a/packages/core/components/NumberRangePicker/NumberRangePicker.module.css +++ b/packages/core/components/NumberRangePicker/NumberRangePicker.module.css @@ -70,32 +70,6 @@ display: flex; } -.input-field { - flex-grow: 1; -} - -.input-field { - flex-grow: 1; -} - -.input-field input { - background-color: var(--secondary-background-color); - border-radius: var(--small-border-radius); - border: 1px solid var(--border-color); - color: var(--secondary-text-color); - outline: none; - padding: 6px; - font-size: var(--s-paragraph-size); - width: 100%; - min-width: fit-content; -} - -.input-field:active > input, .input-field > input:active, -.input-field:focus > input, .input-field > input:focus, -.input-field:focus-within > input, .input-field > input:focus-within { - border: 1px solid var(--aqua); -} - .range-seperator { align-items: flex-end; display: flex; @@ -116,9 +90,3 @@ .container label,input{ display: block; } - -.container input::-webkit-outer-spin-button, -.container input::-webkit-inner-spin-button { - -webkit-appearance: none; - margin: 0; /* <-- Apparently some margin are still there even though it's hidden */ -} diff --git a/packages/core/components/NumberRangePicker/index.tsx b/packages/core/components/NumberRangePicker/index.tsx index 40da2ff0..7b63cdd6 100644 --- a/packages/core/components/NumberRangePicker/index.tsx +++ b/packages/core/components/NumberRangePicker/index.tsx @@ -8,6 +8,7 @@ import { extractValuesFromRangeOperatorFilterString } from "../../entity/Annotat import { AnnotationValue } from "../../services/AnnotationService"; import styles from "./NumberRangePicker.module.css"; +import NumberField from "./NumberField"; export interface ListItem { displayValue: AnnotationValue; @@ -106,37 +107,27 @@ export default function NumberRangePicker(props: NumberRangePickerProps) {

{units ? `${props.title} (in ${units})` : props.title}

-
- - -
+
-
- - -
+
Date: Wed, 4 Dec 2024 09:35:18 -0800 Subject: [PATCH 03/15] add combobox for dropdown type --- packages/core/components/ComboBox/index.tsx | 3 +-- .../EditMetadata/ExistingAnnotationPathway.tsx | 1 - .../core/components/EditMetadata/MetadataDetails.tsx | 12 ++++++++++++ .../components/EditMetadata/NewAnnotationPathway.tsx | 2 +- .../components/NumberRangePicker/NumberField.tsx | 6 +++--- 5 files changed, 17 insertions(+), 7 deletions(-) diff --git a/packages/core/components/ComboBox/index.tsx b/packages/core/components/ComboBox/index.tsx index 4937b4f5..2ec2aa7d 100644 --- a/packages/core/components/ComboBox/index.tsx +++ b/packages/core/components/ComboBox/index.tsx @@ -12,7 +12,6 @@ interface Props { multiSelect?: boolean; options: IComboBoxOption[]; placeholder: string; - useComboBoxAsMenuWidth?: boolean; onChange?: (option: IComboBoxOption | undefined, value?: string | undefined) => void; } @@ -67,7 +66,7 @@ export default function BaseComboBox(props: Props) { comboBoxOptionStyles={{ rootChecked: styles.comboBoxItemChecked, }} - useComboBoxAsMenuWidth={props?.useComboBoxAsMenuWidth} + useComboBoxAsMenuWidth /> ); } diff --git a/packages/core/components/EditMetadata/ExistingAnnotationPathway.tsx b/packages/core/components/EditMetadata/ExistingAnnotationPathway.tsx index 8211b937..182c85d0 100644 --- a/packages/core/components/EditMetadata/ExistingAnnotationPathway.tsx +++ b/packages/core/components/EditMetadata/ExistingAnnotationPathway.tsx @@ -80,7 +80,6 @@ export default function ExistingAnnotationPathway(props: ExistingAnnotationProps placeholder="Select a field" selectedKey={selectedAnnotation} options={props.annotationOptions} - useComboBoxAsMenuWidth onChange={onSelectMetadataField} /> {!!selectedAnnotation && ( diff --git a/packages/core/components/EditMetadata/MetadataDetails.tsx b/packages/core/components/EditMetadata/MetadataDetails.tsx index 779a63cc..a3a86103 100644 --- a/packages/core/components/EditMetadata/MetadataDetails.tsx +++ b/packages/core/components/EditMetadata/MetadataDetails.tsx @@ -2,6 +2,7 @@ import { DatePicker, DetailsList, IColumn, + IComboBoxOption, Icon, IDetailsRowProps, IRenderFunction, @@ -13,6 +14,7 @@ import { import * as React from "react"; import ChoiceGroup from "../ChoiceGroup"; +import ComboBox from "../ComboBox"; import DurationForm from "../DurationForm"; import NumberField from "../NumberRangePicker/NumberField"; import annotationFormatterFactory, { AnnotationType } from "../../entity/AnnotationFormatter"; @@ -26,6 +28,7 @@ export interface ValueCountItem { } interface DetailsListProps { + dropdownOptions?: IComboBoxOption[]; fieldType?: AnnotationType; items: ValueCountItem[]; onChange: (value: string | undefined) => void; @@ -119,6 +122,15 @@ export default function EditMetadataDetailsList(props: DetailsListProps) { /> ); case AnnotationType.DROPDOWN: + if (props?.dropdownOptions) { + return ( + + ); + } case AnnotationType.STRING: default: return ( diff --git a/packages/core/components/EditMetadata/NewAnnotationPathway.tsx b/packages/core/components/EditMetadata/NewAnnotationPathway.tsx index 14dd24d7..8f40b3dd 100644 --- a/packages/core/components/EditMetadata/NewAnnotationPathway.tsx +++ b/packages/core/components/EditMetadata/NewAnnotationPathway.tsx @@ -90,7 +90,6 @@ export default function NewAnnotationPathway(props: NewAnnotationProps) { text, }; })} - useComboBoxAsMenuWidth onChange={(option) => setNewFieldDataType((option?.key as AnnotationType) || "") } @@ -141,6 +140,7 @@ export default function NewAnnotationPathway(props: NewAnnotationProps) { fileCount: props.selectedFileCount, } as ValueCountItem, ]} + dropdownOptions={dropdownOptions} onChange={(value) => setNewValues(value)} /> )} diff --git a/packages/core/components/NumberRangePicker/NumberField.tsx b/packages/core/components/NumberRangePicker/NumberField.tsx index 2a263b0f..59ef050b 100644 --- a/packages/core/components/NumberRangePicker/NumberField.tsx +++ b/packages/core/components/NumberRangePicker/NumberField.tsx @@ -5,12 +5,12 @@ import styles from "./NumberField.module.css"; interface NumberFieldProps { className?: string; - id: string; - onChange: (event?: React.ChangeEvent) => void; defaultValue?: string | number; + id: string; label?: string; - min?: number; max?: number; + min?: number; + onChange: (event?: React.ChangeEvent) => void; placeholder?: string; } From 214cbce86637ee2e3b508707ef829456d7e9de7f Mon Sep 17 00:00:00 2001 From: Anya Wallace Date: Thu, 5 Dec 2024 10:15:48 -0800 Subject: [PATCH 04/15] respond to UX feedback for fields --- .../DurationForm/DurationForm.module.css | 1 - .../core/components/DurationForm/index.tsx | 16 ++++----- .../EditMetadata/EditMetadata.module.css | 2 +- .../EditMetadata/MetadataDetails.module.css | 25 ++++++++++++- .../EditMetadata/MetadataDetails.tsx | 35 +++++++++---------- .../components/NumberRangePicker/index.tsx | 2 +- 6 files changed, 50 insertions(+), 31 deletions(-) diff --git a/packages/core/components/DurationForm/DurationForm.module.css b/packages/core/components/DurationForm/DurationForm.module.css index 0fdb6830..37011ca0 100644 --- a/packages/core/components/DurationForm/DurationForm.module.css +++ b/packages/core/components/DurationForm/DurationForm.module.css @@ -5,7 +5,6 @@ .input-field { margin-right: 3px; max-height: fit-content; - display: flex; } .input-field > input { diff --git a/packages/core/components/DurationForm/index.tsx b/packages/core/components/DurationForm/index.tsx index dc440c2a..037adbc9 100644 --- a/packages/core/components/DurationForm/index.tsx +++ b/packages/core/components/DurationForm/index.tsx @@ -17,10 +17,10 @@ interface DurationFormProps { */ export default function DurationForm(props: DurationFormProps) { const { onChange } = props; - const [days, setDurationDays] = React.useState(""); - const [hours, setDurationHours] = React.useState(""); - const [minutes, setDurationMinutes] = React.useState(""); - const [seconds, setDurationSeconds] = React.useState(""); + const [days, setDurationDays] = React.useState("0"); + const [hours, setDurationHours] = React.useState("0"); + const [minutes, setDurationMinutes] = React.useState("0"); + const [seconds, setDurationSeconds] = React.useState("0"); const durationFormatter = annotationFormatterFactory(AnnotationType.DURATION); React.useEffect(() => { @@ -39,32 +39,32 @@ export default function DurationForm(props: DurationFormProps) { aria-label="Days" className={styles.inputField} id="durationDays" + label="Days" onChange={(event) => setDurationDays(event?.target?.value || "")} - placeholder="Days..." defaultValue={days} /> setDurationHours(event?.target?.value || "")} - placeholder="Hrs..." defaultValue={hours} /> setDurationMinutes(event?.target?.value || "")} - placeholder="Mins..." defaultValue={minutes} /> setDurationSeconds(event?.target?.value || "")} - placeholder="Secs..." defaultValue={seconds} />
diff --git a/packages/core/components/EditMetadata/EditMetadata.module.css b/packages/core/components/EditMetadata/EditMetadata.module.css index 4c4c0c93..f0fd5275 100644 --- a/packages/core/components/EditMetadata/EditMetadata.module.css +++ b/packages/core/components/EditMetadata/EditMetadata.module.css @@ -81,7 +81,7 @@ .text-field { padding-bottom: var(--margin); - max-width: 300px;; + width: 300px;; } .text-field > div > label, .text-field > div > label::after { diff --git a/packages/core/components/EditMetadata/MetadataDetails.module.css b/packages/core/components/EditMetadata/MetadataDetails.module.css index 7d92dd77..6b11d6a5 100644 --- a/packages/core/components/EditMetadata/MetadataDetails.module.css +++ b/packages/core/components/EditMetadata/MetadataDetails.module.css @@ -43,7 +43,12 @@ } .table-title { - padding-bottom: 5px + padding-bottom: 5px; +} + +.values-title { + padding-bottom: 5px; + padding-left: 35px; } .stack { @@ -87,6 +92,16 @@ border: 1px solid var(--aqua); } +.input-wrapper { + margin-top: 4px; + display: flex; + align-items: center; +} + +.date-range-root { + width: 300px; +} + .date-range-text-field div { background-color: var(--secondary-background-color); border: none; @@ -101,3 +116,11 @@ .date-range-text-field > div { border: 1px solid var(--border-color) } + +.no-padding { + padding-bottom: 0 !important; +} + +.forward-icon { + padding-right: 20px; +} diff --git a/packages/core/components/EditMetadata/MetadataDetails.tsx b/packages/core/components/EditMetadata/MetadataDetails.tsx index a3a86103..0dc9b1fe 100644 --- a/packages/core/components/EditMetadata/MetadataDetails.tsx +++ b/packages/core/components/EditMetadata/MetadataDetails.tsx @@ -11,6 +11,7 @@ import { StackItem, TextField, } from "@fluentui/react"; +import classNames from "classnames"; import * as React from "react"; import ChoiceGroup from "../ChoiceGroup"; @@ -82,6 +83,7 @@ export default function EditMetadataDetailsList(props: DetailsListProps) { textField: styles.dateRangeTextField, }} placeholder={"Select a date"} + onSelectDate={(date) => props.onChange(date?.toISOString())} /> ); case AnnotationType.NUMBER: @@ -90,9 +92,7 @@ export default function EditMetadataDetailsList(props: DetailsListProps) { aria-label="Input a numerical value" id="numInput" placeholder="Enter value..." - onChange={() => { - console.info("placeholder for linting"); - }} + onChange={(ev) => props.onChange(ev?.target?.value)} /> ); case AnnotationType.BOOLEAN: @@ -100,9 +100,7 @@ export default function EditMetadataDetailsList(props: DetailsListProps) { { - console.info("placeholder"); - }} + onChange={(_, opt?) => props.onChange(opt?.key)} options={[ { key: "true", @@ -117,14 +115,13 @@ export default function EditMetadataDetailsList(props: DetailsListProps) { ); case AnnotationType.DURATION: return ( - console.info(totalDuration)} - /> + props.onChange(duration.toString())} /> ); case AnnotationType.DROPDOWN: if (props?.dropdownOptions) { return ( - e.currentTarget.value && props.onChange(e.currentTarget.value) - } + className={classNames(rootStyles.textField, styles.noPadding)} + onChange={(e) => props.onChange(e?.currentTarget?.value)} placeholder="Value(s)" - defaultValue={props.newValues} + defaultValue={props.newValues?.toString()} /> ); } @@ -185,12 +180,14 @@ export default function EditMetadataDetailsList(props: DetailsListProps) { onRenderItemColumn={renderItemColumn} /> - - - -

Replace with

- {inputField()} +

Replace with

+ { +
+ + {inputField()} +
+ }
diff --git a/packages/core/components/NumberRangePicker/index.tsx b/packages/core/components/NumberRangePicker/index.tsx index 7b63cdd6..e8dcc3bc 100644 --- a/packages/core/components/NumberRangePicker/index.tsx +++ b/packages/core/components/NumberRangePicker/index.tsx @@ -2,13 +2,13 @@ import { Icon, Spinner, SpinnerSize } from "@fluentui/react"; import classNames from "classnames"; import * as React from "react"; +import NumberField from "./NumberField"; import { PrimaryButton, TertiaryButton } from "../Buttons"; import FileFilter from "../../entity/FileFilter"; import { extractValuesFromRangeOperatorFilterString } from "../../entity/AnnotationFormatter/number-formatter"; import { AnnotationValue } from "../../services/AnnotationService"; import styles from "./NumberRangePicker.module.css"; -import NumberField from "./NumberField"; export interface ListItem { displayValue: AnnotationValue; From b11a1fd490b258768b16ffeb671b909382504670 Mon Sep 17 00:00:00 2001 From: Anya Wallace Date: Thu, 5 Dec 2024 10:25:45 -0800 Subject: [PATCH 05/15] update labels to match designs --- .../components/EditMetadata/ExistingAnnotationPathway.tsx | 2 +- packages/core/components/EditMetadata/MetadataDetails.tsx | 8 ++++---- .../core/components/EditMetadata/NewAnnotationPathway.tsx | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/core/components/EditMetadata/ExistingAnnotationPathway.tsx b/packages/core/components/EditMetadata/ExistingAnnotationPathway.tsx index 182c85d0..e04194df 100644 --- a/packages/core/components/EditMetadata/ExistingAnnotationPathway.tsx +++ b/packages/core/components/EditMetadata/ExistingAnnotationPathway.tsx @@ -77,7 +77,7 @@ export default function ExistingAnnotationPathway(props: ExistingAnnotationProps props.onChange(date?.toISOString())} /> ); @@ -91,7 +91,7 @@ export default function EditMetadataDetailsList(props: DetailsListProps) { props.onChange(ev?.target?.value)} /> ); @@ -124,7 +124,7 @@ export default function EditMetadataDetailsList(props: DetailsListProps) { className={rootStyles.comboBox} options={props?.dropdownOptions || []} label="" - placeholder="values" + placeholder="Select value(s)..." /> ); } @@ -134,7 +134,7 @@ export default function EditMetadataDetailsList(props: DetailsListProps) { props.onChange(e?.currentTarget?.value)} - placeholder="Value(s)" + placeholder="Value(s)..." defaultValue={props.newValues?.toString()} /> ); diff --git a/packages/core/components/EditMetadata/NewAnnotationPathway.tsx b/packages/core/components/EditMetadata/NewAnnotationPathway.tsx index 8f40b3dd..12b05a72 100644 --- a/packages/core/components/EditMetadata/NewAnnotationPathway.tsx +++ b/packages/core/components/EditMetadata/NewAnnotationPathway.tsx @@ -81,7 +81,7 @@ export default function NewAnnotationPathway(props: NewAnnotationProps) { className={styles.comboBox} selectedKey={newFieldDataType || undefined} label="Data type" - placeholder="Select a data type" + placeholder="Select a data type..." options={Object.values(AnnotationType).map((type) => { const text = type === AnnotationType.BOOLEAN ? "Boolean (true/false)" : type; From 678723364e69ba6e05c1ce5d3d8f70a6cafa12d4 Mon Sep 17 00:00:00 2001 From: Anya Wallace Date: Fri, 6 Dec 2024 10:35:25 -0800 Subject: [PATCH 06/15] minor component fixes --- packages/core/components/ComboBox/index.tsx | 2 +- packages/core/components/Modal/EditMetadata/index.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/components/ComboBox/index.tsx b/packages/core/components/ComboBox/index.tsx index 2ec2aa7d..32892f5e 100644 --- a/packages/core/components/ComboBox/index.tsx +++ b/packages/core/components/ComboBox/index.tsx @@ -66,7 +66,7 @@ export default function BaseComboBox(props: Props) { comboBoxOptionStyles={{ rootChecked: styles.comboBoxItemChecked, }} - useComboBoxAsMenuWidth + useComboBoxAsMenuWidth={true} /> ); } diff --git a/packages/core/components/Modal/EditMetadata/index.tsx b/packages/core/components/Modal/EditMetadata/index.tsx index 0ee57910..504a5f93 100644 --- a/packages/core/components/Modal/EditMetadata/index.tsx +++ b/packages/core/components/Modal/EditMetadata/index.tsx @@ -61,7 +61,7 @@ export default function EditMetadata({ onDismiss }: ModalProps) { title={ showWarning ? "Warning! Edits in progress." - : `Edit Metadata ${filesSelectedCountString}` + : `Edit metadata ${filesSelectedCountString}` } /> ); From 5d6832eaf9cc8223cb1aa1d3c40f0b50d96175eb Mon Sep 17 00:00:00 2001 From: Anya Wallace Date: Fri, 6 Dec 2024 10:45:45 -0800 Subject: [PATCH 07/15] right align number inputs --- .../core/components/NumberRangePicker/NumberField.module.css | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/core/components/NumberRangePicker/NumberField.module.css b/packages/core/components/NumberRangePicker/NumberField.module.css index b273b626..3eaa0ea2 100644 --- a/packages/core/components/NumberRangePicker/NumberField.module.css +++ b/packages/core/components/NumberRangePicker/NumberField.module.css @@ -12,11 +12,13 @@ font-size: var(--s-paragraph-size); width: 100%; min-width: fit-content; + text-align: right; } .input-field > input::placeholder { color: var(--secondary-text-color) !important; font-style: italic; + text-align: left; } .input-field:active > input, From f13faeedee6923d96594c819f6e94b7b1b1377a4 Mon Sep 17 00:00:00 2001 From: Anya Wallace Date: Fri, 6 Dec 2024 11:39:18 -0800 Subject: [PATCH 08/15] address styling feedback --- .../core/components/EditMetadata/MetadataDetails.module.css | 4 ++++ packages/core/components/EditMetadata/MetadataDetails.tsx | 4 ++-- .../core/components/NumberRangePicker/NumberField.module.css | 4 ++++ 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/core/components/EditMetadata/MetadataDetails.module.css b/packages/core/components/EditMetadata/MetadataDetails.module.css index 6b11d6a5..92124c87 100644 --- a/packages/core/components/EditMetadata/MetadataDetails.module.css +++ b/packages/core/components/EditMetadata/MetadataDetails.module.css @@ -117,6 +117,10 @@ border: 1px solid var(--border-color) } +.date-range-text-field i { + color: var(--primary-text-color) +} + .no-padding { padding-bottom: 0 !important; } diff --git a/packages/core/components/EditMetadata/MetadataDetails.tsx b/packages/core/components/EditMetadata/MetadataDetails.tsx index e5730f54..1faffecc 100644 --- a/packages/core/components/EditMetadata/MetadataDetails.tsx +++ b/packages/core/components/EditMetadata/MetadataDetails.tsx @@ -91,7 +91,7 @@ export default function EditMetadataDetailsList(props: DetailsListProps) { props.onChange(ev?.target?.value)} /> ); @@ -134,7 +134,7 @@ export default function EditMetadataDetailsList(props: DetailsListProps) { props.onChange(e?.currentTarget?.value)} - placeholder="Value(s)..." + placeholder="Type in value(s)..." defaultValue={props.newValues?.toString()} /> ); diff --git a/packages/core/components/NumberRangePicker/NumberField.module.css b/packages/core/components/NumberRangePicker/NumberField.module.css index 3eaa0ea2..129aded2 100644 --- a/packages/core/components/NumberRangePicker/NumberField.module.css +++ b/packages/core/components/NumberRangePicker/NumberField.module.css @@ -35,3 +35,7 @@ -webkit-appearance: none; margin: 0; /* <-- Margins are still present when hidden */ } + +.input-field > label { + line-height: 1.5 +} From b6aba6156ff18793006187aae32e186cbc6d98c7 Mon Sep 17 00:00:00 2001 From: Anya Wallace Date: Fri, 6 Dec 2024 16:32:46 -0800 Subject: [PATCH 09/15] add option to prevent draggable modals --- packages/core/components/Modal/BaseModal/index.tsx | 4 +++- packages/core/components/Modal/EditMetadata/index.tsx | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/core/components/Modal/BaseModal/index.tsx b/packages/core/components/Modal/BaseModal/index.tsx index 6c429786..1892add4 100644 --- a/packages/core/components/Modal/BaseModal/index.tsx +++ b/packages/core/components/Modal/BaseModal/index.tsx @@ -10,6 +10,7 @@ interface BaseModalProps { footer?: React.ReactNode; onDismiss?: () => void; title?: string; + isStatic?: boolean; // Not draggable } const DRAG_OPTIONS: IDragOptions = { @@ -31,7 +32,7 @@ export default function BaseModal(props: BaseModalProps) { isOpen onDismiss={onDismiss} containerClassName={styles.container} - dragOptions={DRAG_OPTIONS} + dragOptions={props?.isStatic ? undefined : DRAG_OPTIONS} scrollableContentClassName={styles.scrollableContainer} titleAriaId={titleId} overlay={{ className: styles.overlay }} @@ -53,4 +54,5 @@ export default function BaseModal(props: BaseModalProps) { BaseModal.defaultProps = { footer: null, onDismiss: noop, + isStatic: false, }; diff --git a/packages/core/components/Modal/EditMetadata/index.tsx b/packages/core/components/Modal/EditMetadata/index.tsx index 504a5f93..c9b97025 100644 --- a/packages/core/components/Modal/EditMetadata/index.tsx +++ b/packages/core/components/Modal/EditMetadata/index.tsx @@ -57,6 +57,7 @@ export default function EditMetadata({ onDismiss }: ModalProps) {
} + isStatic onDismiss={onDismissWithWarning} title={ showWarning From 384ed3284b6827b2bb5becf8295925e49d1c018c Mon Sep 17 00:00:00 2001 From: Anya Wallace Date: Tue, 10 Dec 2024 14:32:03 -0800 Subject: [PATCH 10/15] add duration restrictions and file count check --- packages/core/components/DurationForm/index.tsx | 7 +++++++ packages/core/components/EditMetadata/MetadataDetails.tsx | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/core/components/DurationForm/index.tsx b/packages/core/components/DurationForm/index.tsx index 037adbc9..b8ea1a6f 100644 --- a/packages/core/components/DurationForm/index.tsx +++ b/packages/core/components/DurationForm/index.tsx @@ -42,6 +42,7 @@ export default function DurationForm(props: DurationFormProps) { label="Days" onChange={(event) => setDurationDays(event?.target?.value || "")} defaultValue={days} + min={0} /> setDurationHours(event?.target?.value || "")} defaultValue={hours} + min={0} + max={24} /> setDurationMinutes(event?.target?.value || "")} defaultValue={minutes} + min={0} + max={60} /> setDurationSeconds(event?.target?.value || "")} defaultValue={seconds} + min={0} + max={60} />
diff --git a/packages/core/components/EditMetadata/MetadataDetails.tsx b/packages/core/components/EditMetadata/MetadataDetails.tsx index 1faffecc..0dee09a8 100644 --- a/packages/core/components/EditMetadata/MetadataDetails.tsx +++ b/packages/core/components/EditMetadata/MetadataDetails.tsx @@ -66,7 +66,7 @@ export default function EditMetadataDetailsList(props: DetailsListProps) { if (!fieldContent) return "[No value] (blank)"; else return annotationFormatter.displayValue(fieldContent); } else if (column?.fieldName === "fileCount") { - return
{fieldContent}
; + return
{fieldContent || 0}
; } return fieldContent; } From 0ffab9ab2ec96840dc7421579a7555a3d340de8e Mon Sep 17 00:00:00 2001 From: Anya Wallace Date: Mon, 16 Dec 2024 12:03:41 -0800 Subject: [PATCH 11/15] add validation for in-bounds number inputs --- packages/core/components/DurationForm/index.tsx | 6 +++--- .../EditMetadata/MetadataDetails.module.css | 2 +- .../NumberRangePicker/NumberField.module.css | 2 +- .../components/NumberRangePicker/NumberField.tsx | 16 +++++++++++++--- 4 files changed, 18 insertions(+), 8 deletions(-) diff --git a/packages/core/components/DurationForm/index.tsx b/packages/core/components/DurationForm/index.tsx index b8ea1a6f..5235bb54 100644 --- a/packages/core/components/DurationForm/index.tsx +++ b/packages/core/components/DurationForm/index.tsx @@ -52,7 +52,7 @@ export default function DurationForm(props: DurationFormProps) { onChange={(event) => setDurationHours(event?.target?.value || "")} defaultValue={hours} min={0} - max={24} + max={23} /> setDurationMinutes(event?.target?.value || "")} defaultValue={minutes} min={0} - max={60} + max={59} /> setDurationSeconds(event?.target?.value || "")} defaultValue={seconds} min={0} - max={60} + max={59} /> diff --git a/packages/core/components/EditMetadata/MetadataDetails.module.css b/packages/core/components/EditMetadata/MetadataDetails.module.css index 92124c87..85941bfa 100644 --- a/packages/core/components/EditMetadata/MetadataDetails.module.css +++ b/packages/core/components/EditMetadata/MetadataDetails.module.css @@ -122,7 +122,7 @@ } .no-padding { - padding-bottom: 0 !important; + padding-bottom: 0; } .forward-icon { diff --git a/packages/core/components/NumberRangePicker/NumberField.module.css b/packages/core/components/NumberRangePicker/NumberField.module.css index 129aded2..ae8a8859 100644 --- a/packages/core/components/NumberRangePicker/NumberField.module.css +++ b/packages/core/components/NumberRangePicker/NumberField.module.css @@ -16,7 +16,7 @@ } .input-field > input::placeholder { - color: var(--secondary-text-color) !important; + color: var(--secondary-text-color); font-style: italic; text-align: left; } diff --git a/packages/core/components/NumberRangePicker/NumberField.tsx b/packages/core/components/NumberRangePicker/NumberField.tsx index 59ef050b..b4266860 100644 --- a/packages/core/components/NumberRangePicker/NumberField.tsx +++ b/packages/core/components/NumberRangePicker/NumberField.tsx @@ -8,8 +8,8 @@ interface NumberFieldProps { defaultValue?: string | number; id: string; label?: string; - max?: number; - min?: number; + max?: number; // inclusive + min?: number; // inclusive onChange: (event?: React.ChangeEvent) => void; placeholder?: string; } @@ -20,6 +20,16 @@ interface NumberFieldProps { * so we instead use the basic html input with styling applied */ export default function NumberField(props: NumberFieldProps) { + function validateInput(event?: React.ChangeEvent) { + if ( + event?.target?.value && + ((props?.max && Number(event.target.value) > props?.max) || + (props?.min && Number(event.target.value) < props?.min)) + ) { + return event.target.setCustomValidity("Value out of bounds"); + } + props.onChange(event); + } return (
{props.label && } @@ -29,7 +39,7 @@ export default function NumberField(props: NumberFieldProps) { type="number" value={props.defaultValue} step="any" - onChange={props.onChange} + onChange={validateInput} placeholder={props?.placeholder} min={props?.min} max={props?.max} From 43ddfecb5be16ecd64ee2f338fd01c18f9a48267 Mon Sep 17 00:00:00 2001 From: Anya Wallace Date: Mon, 16 Dec 2024 17:11:03 -0800 Subject: [PATCH 12/15] fix duration regex and add unit tests --- .../AnnotationFormatter/duration-formatter.ts | 18 ++++++++------- .../test/formatters.test.ts | 22 +++++++++++++++++++ 2 files changed, 32 insertions(+), 8 deletions(-) diff --git a/packages/core/entity/AnnotationFormatter/duration-formatter.ts b/packages/core/entity/AnnotationFormatter/duration-formatter.ts index a0d5ea99..ae479dd9 100644 --- a/packages/core/entity/AnnotationFormatter/duration-formatter.ts +++ b/packages/core/entity/AnnotationFormatter/duration-formatter.ts @@ -26,14 +26,16 @@ export default { }, valueOf(value: any) { - const RANGE_OPERATOR_REGEX = /([0-9]+)D ([0-9]+)H ([0-9]+)M ([0-9]*\.?[0-9]+)S/g; - const exec = RANGE_OPERATOR_REGEX.exec(value); - // Check if value is a pre-formatted duration string - if (exec) { - const daysInMs = Number(exec[1]) * msInADay; - const hrsInMs = Number(exec[2]) * msInAnHour; - const minsInMs = Number(exec[3]) * msInAMinute; - const secsInMs = Number(exec[4]) * msInASecond; + // Check for pre-formatted duration strings: must have at least one of #D, #H, #M, or #S in that order + const regexMatch = value.match( + /^(([0-9]+)D)?\s?(([0-9]+)H)?\s?(([0-9]+)M)?\s?(([0-9]*\.?[0-9]+)S)?$/ + ); + if (regexMatch) { + // Capture group order is [full string, aD, a, bH, b, cM, c, dS, d] + const daysInMs = (Number(regexMatch[2]) || 0) * msInADay; + const hrsInMs = (Number(regexMatch[4]) || 0) * msInAnHour; + const minsInMs = (Number(regexMatch[6]) || 0) * msInAMinute; + const secsInMs = (Number(regexMatch[8]) || 0) * msInASecond; return daysInMs + hrsInMs + minsInMs + secsInMs; } return Number(value); diff --git a/packages/core/entity/AnnotationFormatter/test/formatters.test.ts b/packages/core/entity/AnnotationFormatter/test/formatters.test.ts index 3753df36..757a177c 100644 --- a/packages/core/entity/AnnotationFormatter/test/formatters.test.ts +++ b/packages/core/entity/AnnotationFormatter/test/formatters.test.ts @@ -143,5 +143,27 @@ describe("Annotation formatters", () => { it("formats a duration with less than a second", () => { expect(durationFormatter.displayValue(125)).to.equal("0.125S"); }); + + it("extracts time in milliseconds from formatted duration strings", () => { + expect(durationFormatter.valueOf("1D 23H 45M 6.78S")).to.equal(171906780); + }); + + it("extracts time in milliseconds from formatted strings with only days", () => { + expect(durationFormatter.valueOf("100D")).to.equal(8.64e9); + }); + + it("extracts time in milliseconds from formatted strings with only some units", () => { + expect(durationFormatter.valueOf("12H 34S")).to.equal(43234000); + }); + + it("extracts time in milliseconds from non-formatted numerical-valued strings", () => { + expect(durationFormatter.valueOf("9876")).to.equal(9876); + }); + + it("returns NaN when string letters or order don't match duration pattern", () => { + expect(isNaN(durationFormatter.valueOf("4A"))).to.be.true; + expect(isNaN(durationFormatter.valueOf("4D 3M 2H 12S"))).to.be.true; + expect(isNaN(durationFormatter.valueOf("DHMS"))).to.be.true; + }); }); }); From c47206a6a2da544ace67f6da1c5dcc3d2c0d1678 Mon Sep 17 00:00:00 2001 From: Anya Wallace Date: Wed, 18 Dec 2024 15:50:02 -0800 Subject: [PATCH 13/15] fix double semicolon Co-authored-by: Sean LeRoy <41307451+SeanLeRoy@users.noreply.github.com> --- packages/core/components/EditMetadata/EditMetadata.module.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/components/EditMetadata/EditMetadata.module.css b/packages/core/components/EditMetadata/EditMetadata.module.css index f0fd5275..68512067 100644 --- a/packages/core/components/EditMetadata/EditMetadata.module.css +++ b/packages/core/components/EditMetadata/EditMetadata.module.css @@ -81,7 +81,7 @@ .text-field { padding-bottom: var(--margin); - width: 300px;; + width: 300px; } .text-field > div > label, .text-field > div > label::after { From 83709f3c40b0fa9f10a0af33ca82fdf9c2537452 Mon Sep 17 00:00:00 2001 From: Anya Wallace Date: Wed, 18 Dec 2024 15:53:14 -0800 Subject: [PATCH 14/15] remove bool assignment Co-authored-by: Sean LeRoy <41307451+SeanLeRoy@users.noreply.github.com> --- packages/core/components/ComboBox/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/components/ComboBox/index.tsx b/packages/core/components/ComboBox/index.tsx index 32892f5e..2ec2aa7d 100644 --- a/packages/core/components/ComboBox/index.tsx +++ b/packages/core/components/ComboBox/index.tsx @@ -66,7 +66,7 @@ export default function BaseComboBox(props: Props) { comboBoxOptionStyles={{ rootChecked: styles.comboBoxItemChecked, }} - useComboBoxAsMenuWidth={true} + useComboBoxAsMenuWidth /> ); } From 9328a44ecfedb6b82b1fedfbbaf73679dbb37633 Mon Sep 17 00:00:00 2001 From: Anya Wallace Date: Fri, 20 Dec 2024 18:59:02 -0800 Subject: [PATCH 15/15] add time selection for DateTime types --- .../components/ComboBox/ComboBox.module.css | 7 ++- .../DateRangePicker.module.css | 10 --- .../DateRangePicker/DateTimePicker.module.css | 47 ++++++++++++++ .../DateRangePicker/DateTimePicker.tsx | 63 +++++++++++++++++++ .../core/components/DateRangePicker/index.tsx | 27 +++----- .../test/DateRangePicker.test.tsx | 10 +-- .../core/components/DurationForm/index.tsx | 1 - .../EditMetadata/MetadataDetails.module.css | 25 +------- .../EditMetadata/MetadataDetails.tsx | 16 ++--- .../Modal/BaseModal/BaseModal.module.css | 2 +- 10 files changed, 139 insertions(+), 69 deletions(-) create mode 100644 packages/core/components/DateRangePicker/DateTimePicker.module.css create mode 100644 packages/core/components/DateRangePicker/DateTimePicker.tsx diff --git a/packages/core/components/ComboBox/ComboBox.module.css b/packages/core/components/ComboBox/ComboBox.module.css index 97821031..c358be94 100644 --- a/packages/core/components/ComboBox/ComboBox.module.css +++ b/packages/core/components/ComboBox/ComboBox.module.css @@ -65,7 +65,8 @@ bottom: 5px; } -.combo-box-item :is(input, button, label){ +.combo-box-item :is(input, button, label), +.options-container :is(input, button, label) { color: var(--primary-text-color); } @@ -93,7 +94,9 @@ .combo-box-item button:not(:disabled):hover, .combo-box-item > div:hover, -.combo-box-item > div:hover :is(input, label) { +.combo-box-item > div:hover :is(input, label), +.options-container button:not(:disabled):hover, +.options-container > div:hover :is(input, label){ background-color: var(--highlight-background-color); color: var(--highlight-text-color); } diff --git a/packages/core/components/DateRangePicker/DateRangePicker.module.css b/packages/core/components/DateRangePicker/DateRangePicker.module.css index a2048290..801b9265 100644 --- a/packages/core/components/DateRangePicker/DateRangePicker.module.css +++ b/packages/core/components/DateRangePicker/DateRangePicker.module.css @@ -31,10 +31,6 @@ margin: 0 10px 8px; } -.date-range-root { - flex-grow: 1; -} - .text-field div { background-color: var(--secondary-background-color); border: none; @@ -54,9 +50,3 @@ margin: 0; padding-bottom: var(--margin); } - -.read-only-placeholder { - margin-right: 20px; - font-style: italic; - color: var(--primary-text-color); -} diff --git a/packages/core/components/DateRangePicker/DateTimePicker.module.css b/packages/core/components/DateRangePicker/DateTimePicker.module.css new file mode 100644 index 00000000..39ec47a7 --- /dev/null +++ b/packages/core/components/DateRangePicker/DateTimePicker.module.css @@ -0,0 +1,47 @@ +.display-block { + display: block; +} + +.dateTimeWrapper { + display: flex; +} + +.date-range-root { + width: 300px; + min-width: 145px; +} + +.date-range-text-field div, .time-picker { + background-color: var(--secondary-background-color); + border: none; + border-radius: var(--small-border-radius); + color: var(--primary-text-color); +} + +.date-range-text-field div::after, .time-picker:focus-visible { + border: 1px solid var(--aqua); + outline: none; +} + +.date-range-text-field > div, .time-picker { + border: 1px solid var(--border-color) +} + +.date-range-text-field i, .time-picker i { + color: var(--primary-text-color); +} + +.read-only-placeholder { + margin-right: 20px; + font-style: italic; + font-size: var(--s-paragraph-size) !important; /* override FluentUI button callout font size*/ + color: var(--primary-text-color); +} + +.time-picker { + height: 34px; + margin: 0 0 5px 5px; + color-scheme: dark; + width: 150px; + padding: 0 5px; +} \ No newline at end of file diff --git a/packages/core/components/DateRangePicker/DateTimePicker.tsx b/packages/core/components/DateRangePicker/DateTimePicker.tsx new file mode 100644 index 00000000..7eb1600e --- /dev/null +++ b/packages/core/components/DateRangePicker/DateTimePicker.tsx @@ -0,0 +1,63 @@ +import { DatePicker } from "@fluentui/react"; +import * as React from "react"; + +import styles from "./DateTimePicker.module.css"; + +interface DateTimePickerProps { + className?: string; + placeholder: string; + defaultDate?: Date; + onSelectDate: (date: Date | null | undefined) => void; + showTimeSelection?: boolean; // Show time input; default false +} + +/** + * DatePicker that can also function as DateTimePicker + * by including showTimeSelection prop + */ +export default function DateTimePicker(props: DateTimePickerProps) { + const { onSelectDate } = props; + const [date, setDate] = React.useState(props?.defaultDate); + const [time, setTime] = React.useState(""); + + React.useEffect(() => { + if (!date && !time) return; + // Prioritize the date from datePicked, otherwise set to today + const combinedDateTime: Date = date || new Date(); + if (time) { + combinedDateTime.setHours(Number(time.split(":")[0])); + combinedDateTime.setMinutes(Number(time.split(":")[1])); + combinedDateTime.setSeconds(Number(time.split(":")[2])); + } + onSelectDate(combinedDateTime); + }, [date, time, onSelectDate]); + + return ( + <> + setDate(date || undefined)} + value={date} + /> + {props?.showTimeSelection && ( + setTime(ev.target.value)} + value={time} + /> + )} + + ); +} + +DateTimePicker.defaultProps = { + showTimeSelection: false, +}; diff --git a/packages/core/components/DateRangePicker/index.tsx b/packages/core/components/DateRangePicker/index.tsx index dad0ce4b..0e0be77b 100644 --- a/packages/core/components/DateRangePicker/index.tsx +++ b/packages/core/components/DateRangePicker/index.tsx @@ -1,4 +1,4 @@ -import { DatePicker, Icon } from "@fluentui/react"; +import { Icon } from "@fluentui/react"; import * as React from "react"; import { TertiaryButton } from "../Buttons"; @@ -6,6 +6,7 @@ import FileFilter from "../../entity/FileFilter"; import { extractDatesFromRangeOperatorFilterString } from "../../entity/AnnotationFormatter/date-time-formatter"; import styles from "./DateRangePicker.module.css"; +import DateTimePicker from "./DateTimePicker"; interface DateRangePickerProps { className?: string; @@ -69,30 +70,18 @@ export default function DateRangePicker(props: DateRangePickerProps) {

{props.title}

- (v ? onDateRangeSelection(v, null) : onReset())} - value={extractDateFromDateString(startDate?.toISOString())} + defaultDate={extractDateFromDateString(startDate?.toISOString())} />
- (v ? onDateRangeSelection(null, v) : onReset())} - value={extractDateFromDateString(endDate?.toISOString())} + defaultDate={extractDateFromDateString(endDate?.toISOString())} /> ", () => { it("renders inputs for start and end dates with selectable date pickers", () => { // Arrange const onSearch = sinon.spy(); - const { getAllByLabelText, getAllByRole, getByLabelText, getByRole } = render( + const { getAllByText, getAllByRole, getByRole, getByText } = render( ); // Should render both input fields expect(getAllByRole("combobox").length).to.equal(2); - expect(getAllByLabelText(/start/).length).to.equal(1); - expect(getAllByLabelText(/end/).length).to.equal(1); + expect(getAllByText(/Start/).length).to.equal(1); + expect(getAllByText(/End/).length).to.equal(1); // Select a start date expect(onSearch.called).to.equal(false); - fireEvent.click(getByLabelText(/start/)); + fireEvent.click(getByText(/Start/)); fireEvent.click(getByRole("button", { name: /^18,\s/ })); expect(onSearch.called).to.equal(true); @@ -31,7 +31,7 @@ describe("", () => { // Select an end date expect(onSearch.called).to.equal(false); - fireEvent.click(getByLabelText(/end/)); + fireEvent.click(getByText(/End/)); fireEvent.click(getByRole("button", { name: /^20,\s/ })); expect(onSearch.called).to.equal(true); }); diff --git a/packages/core/components/DurationForm/index.tsx b/packages/core/components/DurationForm/index.tsx index 5235bb54..f2e2eefe 100644 --- a/packages/core/components/DurationForm/index.tsx +++ b/packages/core/components/DurationForm/index.tsx @@ -7,7 +7,6 @@ import styles from "./DurationForm.module.css"; interface DurationFormProps { className?: string; - defaultValue?: number; onChange: (totalDuration: number) => void; title?: string; } diff --git a/packages/core/components/EditMetadata/MetadataDetails.module.css b/packages/core/components/EditMetadata/MetadataDetails.module.css index 85941bfa..9a0bb6b2 100644 --- a/packages/core/components/EditMetadata/MetadataDetails.module.css +++ b/packages/core/components/EditMetadata/MetadataDetails.module.css @@ -65,7 +65,7 @@ } .stack-item-right { - width: 275px; + width: 300px; } .read-only-placeholder { @@ -98,29 +98,6 @@ align-items: center; } -.date-range-root { - width: 300px; -} - -.date-range-text-field div { - background-color: var(--secondary-background-color); - border: none; - border-radius: var(--small-border-radius); - color: var(--primary-text-color); -} - -.date-range-text-field div::after { - border: 1px solid var(--aqua) -} - -.date-range-text-field > div { - border: 1px solid var(--border-color) -} - -.date-range-text-field i { - color: var(--primary-text-color) -} - .no-padding { padding-bottom: 0; } diff --git a/packages/core/components/EditMetadata/MetadataDetails.tsx b/packages/core/components/EditMetadata/MetadataDetails.tsx index 0dee09a8..d83a09df 100644 --- a/packages/core/components/EditMetadata/MetadataDetails.tsx +++ b/packages/core/components/EditMetadata/MetadataDetails.tsx @@ -1,5 +1,4 @@ import { - DatePicker, DetailsList, IColumn, IComboBoxOption, @@ -16,6 +15,7 @@ import * as React from "react"; import ChoiceGroup from "../ChoiceGroup"; import ComboBox from "../ComboBox"; +import DateTimePicker from "../DateRangePicker/DateTimePicker"; import DurationForm from "../DurationForm"; import NumberField from "../NumberRangePicker/NumberField"; import annotationFormatterFactory, { AnnotationType } from "../../entity/AnnotationFormatter"; @@ -74,16 +74,18 @@ export default function EditMetadataDetailsList(props: DetailsListProps) { const inputField = () => { switch (props.fieldType) { case AnnotationType.DATE: + return ( + props.onChange(date?.toISOString())} + /> + ); case AnnotationType.DATETIME: return ( - props.onChange(date?.toISOString())} + showTimeSelection /> ); case AnnotationType.NUMBER: diff --git a/packages/core/components/Modal/BaseModal/BaseModal.module.css b/packages/core/components/Modal/BaseModal/BaseModal.module.css index 72f9d064..1ef0b70e 100644 --- a/packages/core/components/Modal/BaseModal/BaseModal.module.css +++ b/packages/core/components/Modal/BaseModal/BaseModal.module.css @@ -4,7 +4,7 @@ padding: 2em 2em 2em 2em; min-width: 300px; /* per guidance from fluentui docs: https://developer.microsoft.com/en-us/fluentui#/controls/web/modal */ min-height: 175px; /* per guidance from fluentui docs: https://developer.microsoft.com/en-us/fluentui#/controls/web/modal */ - max-width: 40%; + max-width: 45%; overflow: hidden; /* flex parent */