Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/metadata editing/restrict annotations #414

Draft
wants to merge 10 commits into
base: feature/metadata-editing/develop
Choose a base branch
from
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,6 @@
margin-bottom: 5px;
}

.column-data > div > div {
border-color: var(--primary-text-color) var(--primary-background-color) var(--primary-background-color);
}

.label {
font-weight: lighter;
margin: 0;
Expand Down
12 changes: 3 additions & 9 deletions packages/core/components/AggregateInfoBox/index.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import filesize from "filesize";
import { Spinner, SpinnerSize } from "@fluentui/react";
import * as React from "react";
import { useSelector } from "react-redux";

import LoadingIcon from "../Icons/LoadingIcon";
import FileSelection from "../../entity/FileSelection";
import { interaction, selection } from "../../state";

Expand Down Expand Up @@ -85,10 +85,7 @@ export default function AggregateInfoBox() {
{!isLoading && aggregateData ? (
aggregateData.count
) : (
<Spinner
size={SpinnerSize.small}
data-testid="aggregate-info-box-spinner"
/>
<LoadingIcon data-testid="aggregate-info-box-spinner" />
)}
</div>
<h6 className={styles.label}>
Expand All @@ -101,10 +98,7 @@ export default function AggregateInfoBox() {
{!isLoading && aggregateData ? (
aggregateData.size
) : (
<Spinner
size={SpinnerSize.small}
data-testid="aggregate-info-box-spinner"
/>
<LoadingIcon data-testid="aggregate-info-box-spinner" />
)}
</div>
<h6 className={styles.label}>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@
.loading-container > div > div {
border-color: var(--primary-text-color) var(--primary-background-color) var(--primary-background-color);
}

.loading-container, .picker {
background-color: var(--primary-background-color);
color: var(--primary-text-color);
Expand Down
5 changes: 3 additions & 2 deletions packages/core/components/AnnotationFilterForm/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { IChoiceGroupOption, Spinner, SpinnerSize } from "@fluentui/react";
import { IChoiceGroupOption } from "@fluentui/react";
import classNames from "classnames";
import { isNil } from "lodash";
import * as React from "react";
Expand All @@ -8,6 +8,7 @@ import useAnnotationValues from "./useAnnotationValues";
import SearchBoxForm from "./SearchBoxForm";
import ChoiceGroup from "../ChoiceGroup";
import DateRangePicker from "../DateRangePicker";
import LoadingIcon from "../Icons/LoadingIcon";
import ListPicker from "../ListPicker";
import { ListItem } from "../ListPicker/ListRow";
import NumberRangePicker from "../NumberRangePicker";
Expand Down Expand Up @@ -136,7 +137,7 @@ export default function AnnotationFilterForm(props: AnnotationFilterFormProps) {
if (isLoading) {
return (
<div className={styles.loadingContainer}>
<Spinner size={SpinnerSize.small} />
<LoadingIcon />
</div>
);
}
Expand Down
1 change: 0 additions & 1 deletion packages/core/components/ComboBox/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@ export default function BaseComboBox(props: Props) {
multiSelect={props?.multiSelect}
options={options}
onChange={(_ev, option, _ind, value) => props.onChange?.(option, value)}
onItemClick={(_, option) => props.onChange?.(option)}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was causing duplicate callbacks doubling our network calls

onRenderItem={(props, defaultRender) => onRenderItem(props, defaultRender)}
scrollSelectedToTop
styles={{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import classNames from "classnames";
import { Spinner, SpinnerSize } from "@fluentui/react";
import * as React from "react";
import { useSelector } from "react-redux";
import Tippy from "@tippyjs/react";
import "tippy.js/dist/tippy.css"; // side-effect

import LoadingIcon from "../Icons/LoadingIcon";
import SvgIcon from "../../components/SvgIcon";
import { selection } from "../../state";
import FileSet from "../../entity/FileSet";
Expand Down Expand Up @@ -111,7 +111,7 @@ export default React.memo(function DirectoryTreeNodeHeader(props: DirectoryTreeN
/>
<h4 className={styles.directoryName}>{title}</h4>
{selectionCountBadge}
{loading && <Spinner size={SpinnerSize.small} />}
{loading && <LoadingIcon />}
{!loading && error && (
<Tippy content={error.message}>
<SvgIcon
Expand Down
96 changes: 73 additions & 23 deletions packages/core/components/EditMetadata/ExistingAnnotationPathway.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
import { IComboBoxOption } from "@fluentui/react";
import classNames from "classnames";
import { uniqueId } from "lodash";
import * as React from "react";
import { useDispatch } from "react-redux";
import { useDispatch, useSelector } from "react-redux";

import MetadataDetails, { ValueCountItem } from "./MetadataDetails";
import useAnnotationValueByNameMap from "./useAnnotationValueByNameMap";
import { PrimaryButton, SecondaryButton } from "../Buttons";
import ComboBox from "../ComboBox";
import { AnnotationType } from "../../entity/AnnotationFormatter";
import { interaction } from "../../state";
import { interaction, metadata } from "../../state";

import styles from "./EditMetadata.module.css";

interface ExistingAnnotationProps {
onDismiss: () => void;
annotationValueMap: Map<string, any> | undefined;
annotationOptions: { key: string; text: string; data: string }[];
selectedFileCount: number;
}

Expand All @@ -25,27 +25,37 @@ interface ExistingAnnotationProps {
export default function ExistingAnnotationPathway(props: ExistingAnnotationProps) {
const dispatch = useDispatch();
const [newValues, setNewValues] = React.useState<string>();
const [valueCount, setValueCount] = React.useState<ValueCountItem[]>();
const [selectedAnnotation, setSelectedAnnotation] = React.useState<string | undefined>();
const [annotationType, setAnnotationType] = React.useState<AnnotationType | undefined>();
const [valueCounts, setValueCounts] = React.useState<ValueCountItem[]>();
const [selectedAnnotation, setSelectedAnnotation] = React.useState<string>();
const [annotationType, setAnnotationType] = React.useState<AnnotationType>();
const [dropdownOptions, setDropdownOptions] = React.useState<string[]>();

const annotationValueByNameMap = useAnnotationValueByNameMap();
const annotationService = useSelector(interaction.selectors.getAnnotationService);

const annotationOptions = useSelector(metadata.selectors.getEdittableAnnotations).map(
(annotation) => ({
key: annotation.name,
text: annotation.displayName,
data: annotation.type,
})
);

const onSelectMetadataField = (
option: IComboBoxOption | undefined,
value: string | undefined
) => {
let valueMap: ValueCountItem[] = [];
// FluentUI's combobox doesn't always register the entered value as an option,
// so we need to be able to check both
const selectedFieldName = option?.text || value;
if (
!selectedFieldName ||
!props.annotationOptions.some((opt) => opt.key === selectedFieldName)
)
if (!selectedFieldName || !annotationOptions.some((opt) => opt.key === selectedFieldName))
return;

let valueMap: ValueCountItem[] = [];
// Track how many values we've seen, since some files may not have a value for this field
let totalValueCount = 0;
if (props?.annotationValueMap?.has(selectedFieldName)) {
const fieldValueToOccurenceMap = props.annotationValueMap.get(selectedFieldName);
const fieldValueToOccurenceMap = annotationValueByNameMap?.get(selectedFieldName);
if (fieldValueToOccurenceMap) {
valueMap = Object.keys(fieldValueToOccurenceMap).map((fieldName) => {
totalValueCount += fieldValueToOccurenceMap[fieldName];
return {
Expand All @@ -65,16 +75,54 @@ export default function ExistingAnnotationPathway(props: ExistingAnnotationProps
...valueMap,
];
}
setSelectedAnnotation(selectedFieldName);
setAnnotationType(option?.data);
setValueCount(valueMap);

// String type annotations might be dropdown or lookup
// If it's a dropdown, we need to fetch the dropdown options
if (option?.data === AnnotationType.STRING) {
annotationService
.fetchAnnotationDetails(selectedFieldName)
.then((response) => {
setSelectedAnnotation(selectedFieldName);
setAnnotationType(response.type);
setValueCounts(valueMap);
setDropdownOptions(response.dropdownOptions);
})
.catch((err) => {
const errMsg = `Failed to grab details for metadata field "${selectedFieldName}". Error: ${err.message}`;
dispatch(interaction.actions.processError(uniqueId(), errMsg));
});
} else {
setSelectedAnnotation(selectedFieldName);
setAnnotationType(option?.data);
setValueCounts(valueMap);
setDropdownOptions(undefined);
}
};

function onSubmit() {
if (selectedAnnotation && newValues?.trim()) {
dispatch(interaction.actions.editFiles({ [selectedAnnotation]: [newValues.trim()] }));
const trimmedValues = newValues?.trim();
const newValuesAsArray = newValues?.split(",").map((value) => value.trim());
if (selectedAnnotation && trimmedValues && newValuesAsArray?.length) {
annotationService
.validateAnnotationValues(selectedAnnotation, newValuesAsArray)
.then((isValid: boolean) => {
if (isValid) {
dispatch(
interaction.actions.editFiles({ [selectedAnnotation]: [trimmedValues] })
);
props.onDismiss();
} else {
const errMsg = "Invalid value for selected metadata field";
dispatch(interaction.actions.processError(uniqueId(), errMsg));
}
})
.catch((err) => {
const errMsg = `Failed trying to validate metadata field, unsure why. Details: ${err.message}`;
dispatch(interaction.actions.processError(uniqueId(), errMsg));
});
} else {
props.onDismiss();
}
props.onDismiss();
}

return (
Expand All @@ -84,19 +132,21 @@ export default function ExistingAnnotationPathway(props: ExistingAnnotationProps
label="Select a metadata field"
placeholder="Select a field..."
selectedKey={selectedAnnotation}
options={props.annotationOptions}
options={annotationOptions}
onChange={onSelectMetadataField}
disabled={!annotationOptions.length}
/>
{!!selectedAnnotation && (
<MetadataDetails
dropdownOptions={dropdownOptions}
onChange={(value) => setNewValues(value)}
items={valueCount || []}
items={valueCounts || []}
fieldType={annotationType}
/>
)}
<div className={classNames(styles.footer, styles.footerAlignRight)}>
<SecondaryButton title="" text="CANCEL" onClick={props.onDismiss} />
{valueCount && (
{valueCounts && (
<PrimaryButton
className={styles.primaryButton}
disabled={!newValues?.trim()}
Expand Down
32 changes: 19 additions & 13 deletions packages/core/components/EditMetadata/MetadataDetails.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import {
DetailsList,
IColumn,
IComboBoxOption,
Icon,
IDetailsRowProps,
IRenderFunction,
Expand All @@ -17,6 +16,7 @@ import ChoiceGroup from "../ChoiceGroup";
import ComboBox from "../ComboBox";
import DateTimePicker from "../DateRangePicker/DateTimePicker";
import DurationForm from "../DurationForm";
import LoadingIcon from "../Icons/LoadingIcon";
import NumberField from "../NumberRangePicker/NumberField";
import annotationFormatterFactory, { AnnotationType } from "../../entity/AnnotationFormatter";

Expand All @@ -29,7 +29,7 @@ export interface ValueCountItem {
}

interface DetailsListProps {
dropdownOptions?: IComboBoxOption[];
dropdownOptions?: string[];
fieldType?: AnnotationType;
items: ValueCountItem[];
onChange: (value: string | undefined) => void;
Expand All @@ -41,8 +41,9 @@ interface DetailsListProps {
* and provides an field for user to input new values.
* Used by both the new & existing annotation pathways
*/
export default function EditMetadataDetailsList(props: DetailsListProps) {
export default function MetadataDetails(props: DetailsListProps) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To match the filename

const { items } = props;
const isLoading = !items.length;
const renderRow = (
rowProps: IDetailsRowProps | undefined,
defaultRender: IRenderFunction<IDetailsRowProps> | undefined
Expand Down Expand Up @@ -119,17 +120,21 @@ export default function EditMetadataDetailsList(props: DetailsListProps) {
return (
<DurationForm onChange={(duration) => props.onChange(duration.toString())} />
);
case AnnotationType.LOOKUP:
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was missing this typing

case AnnotationType.DROPDOWN:
if (props?.dropdownOptions) {
return (
<ComboBox
className={rootStyles.comboBox}
options={props?.dropdownOptions || []}
label=""
placeholder="Select value(s)..."
/>
);
}
return (
<ComboBox
className={rootStyles.comboBox}
options={(props?.dropdownOptions || []).map((opt) => ({
key: opt,
text: opt,
}))}
disabled={!props.dropdownOptions?.length}
label=""
placeholder="Select value(s)..."
onChange={(opt) => props.onChange(opt?.text)}
/>
);
case AnnotationType.STRING:
default:
return (
Expand Down Expand Up @@ -181,6 +186,7 @@ export default function EditMetadataDetailsList(props: DetailsListProps) {
onRenderRow={(props, defaultRender) => renderRow(props, defaultRender)}
onRenderItemColumn={renderItemColumn}
/>
{isLoading && <LoadingIcon invertColor />}
</StackItem>
<StackItem grow className={styles.stackItemRight}>
<h4 className={styles.valuesTitle}>Replace with</h4>
Expand Down
Loading
Loading