diff --git a/src/components/dagshub/data-engine/metadataKeyValue/CustomTextField.tsx b/src/components/dagshub/data-engine/metadataKeyValue/CustomTextField.tsx index 5e001bc..143d01f 100644 --- a/src/components/dagshub/data-engine/metadataKeyValue/CustomTextField.tsx +++ b/src/components/dagshub/data-engine/metadataKeyValue/CustomTextField.tsx @@ -1,28 +1,32 @@ -import React, { useState, useRef, useEffect } from 'react'; +import React, {useEffect, useRef, useState} from 'react'; import IconButton from '@mui/material/IconButton'; import EditIcon from '@mui/icons-material/Edit'; import Box from '@mui/material/Box'; import CancelIcon from '@mui/icons-material/Cancel'; import StyledTextField from './StyledTextField'; import './style.scss'; -import { Tooltip } from '@mui/material'; +import {ErroredTooltip, TooltipVariant} from "../../../elements/tooltipV2/ErroredTooltip"; function CustomTextField({ readOnly, value, - onSaveHandler, placeholder, helperText, shouldHighlightIfEmpty, autoFocus, + isErrored, + onInputChange, + onInputSave, }: { readOnly: boolean; value?: string; - onSaveHandler: (newVal?: string) => void; placeholder?: string; helperText?: string; shouldHighlightIfEmpty?: boolean; autoFocus?: boolean; + isErrored?: boolean; + onInputChange?: (newVal?: string) => void; + onInputSave?: (newVal?: string) => void; }) { const [currentValue, setCurrentValue] = useState(value); const [isEditing, setEditing] = useState(false); @@ -67,17 +71,22 @@ function CustomTextField({ const saveChangesHandler = () => { setCurrentValue(editedValue); - onSaveHandler(editedValue); + !!onInputSave && onInputSave(editedValue); setHovered(false); setEditing(false); textFieldRef.current?.blur(); }; const handleKeyDown = (event: any) => { + if(event.key === 'ArrowRight' || event.key === 'ArrowLeft'){ + event.stopPropagation(); + } if (isEditing && event.key === 'Enter') { + event.stopPropagation(); saveChangesHandler(); } if (isEditing && event.key === 'Escape') { + event.stopPropagation(); handleCancelClick(); } }; @@ -107,7 +116,7 @@ function CustomTextField({ }, [currentValue, shouldHighlightIfEmpty]); return ( - + { @@ -153,13 +162,16 @@ function CustomTextField({ onChange={(e: any) => { setEditing(true); setEditedValue(e.target.value); + !!onInputChange && onInputChange(e.target.value); }} onKeyDown={handleKeyDown} value={getValue()} placeholder={placeholder} + isErrored={isErrored} + errorColor={"rgba(252, 165, 165, 1)"} /> - + ); } diff --git a/src/components/dagshub/data-engine/metadataKeyValue/MetadataKeyValueList.tsx b/src/components/dagshub/data-engine/metadataKeyValue/MetadataKeyValueList.tsx index cb2d68e..d8d83c0 100644 --- a/src/components/dagshub/data-engine/metadataKeyValue/MetadataKeyValueList.tsx +++ b/src/components/dagshub/data-engine/metadataKeyValue/MetadataKeyValueList.tsx @@ -30,7 +30,8 @@ export interface MetadataKeyValueListProps { editingEnabled: boolean; deletionEnabled: boolean; onDeleteHandler?: (keyName: string) => void; - onChangeHandler?: (metadataList: NewMetadataField[]) => void; + onSaveHandler?: (metadataList: NewMetadataField[]) => void; + validateValueByType?: (valueType: MetadataType, value: string) => boolean; } export function MetadataKeyValueList({ @@ -39,7 +40,8 @@ export function MetadataKeyValueList({ editingEnabled, deletionEnabled, onDeleteHandler, - onChangeHandler, + onSaveHandler, + validateValueByType, }: MetadataKeyValueListProps) { //Todo: // - Not sure what to do with the multiple field. (If I need to use it as part of the validations in the future, and also what value should I set for newly created fields). @@ -66,10 +68,12 @@ export function MetadataKeyValueList({ } }, [metadataList]); + //Todo: create a function to save changes + // if (onSaveHandler) { + // onSaveHandler({ ...temporaryMetadataList }); + // } + useEffect(() => { - if (onChangeHandler) { - onChangeHandler({ ...temporaryMetadataList }); - } if (shouldScrollToBottom && metadataFieldsSection.current) { // scroll to button only if + button was clicked (metadataFieldsSection.current as HTMLDivElement).scrollTop = ( @@ -172,7 +176,7 @@ export function MetadataKeyValueList({ const checkIfPairIsRemovable = (metadataField: { key?: string; value?: string; - valueType?: string; + valueType?: MetadataType; multiple?: boolean; isAutoGenerated?: boolean; isNewlyCreated?: boolean; @@ -228,6 +232,7 @@ export function MetadataKeyValueList({ : permanentlyDeleteMetadataFieldByIndex } autoFocusKey={metadataField.isNewlyCreated && autoFocusNewlyCreatedFieldKey} + validateValueByType={validateValueByType} /> ))} diff --git a/src/components/dagshub/data-engine/metadataKeyValue/MetadataKeyValuePair.tsx b/src/components/dagshub/data-engine/metadataKeyValue/MetadataKeyValuePair.tsx index f89731c..16f5bf0 100644 --- a/src/components/dagshub/data-engine/metadataKeyValue/MetadataKeyValuePair.tsx +++ b/src/components/dagshub/data-engine/metadataKeyValue/MetadataKeyValuePair.tsx @@ -12,7 +12,7 @@ export interface MetadataKeyValuePairProps { index: number; keyName?: string; value?: string; - valueType?: string; + valueType?: MetadataType; isEditable: boolean; description?: string; isNewlyCreated?: boolean; @@ -23,6 +23,7 @@ export interface MetadataKeyValuePairProps { deleteFieldPermanently?: (index: number) => void; shouldHighlightEmptyFields?: boolean; autoFocusKey?: boolean; + validateValueByType?: (valueType: MetadataType, value: string) => boolean; } export function MetadataKeyValuePair({ @@ -40,7 +41,17 @@ export function MetadataKeyValuePair({ deleteFieldPermanently, shouldHighlightEmptyFields, autoFocusKey, + validateValueByType, }: MetadataKeyValuePairProps) { + + const [isErrored, setIsErrored] = React.useState(false); + + useEffect(()=>{ + if(!!validateValueByType && !!valueType){ + setIsErrored(!validateValueByType(valueType, value as string)) + } + },[valueType]) + const valueTypes: { id: MetadataType; label: string }[] = [ { id: 'INTEGER', @@ -90,7 +101,7 @@ export function MetadataKeyValuePair({ { + onInputSave={(newVal) => { if (saveKeyNameLocally) { saveKeyNameLocally(index, newVal); } @@ -109,10 +120,11 @@ export function MetadataKeyValuePair({ gap: '8px', flexShrink: 1, minWidth: '65%', + height:"100%", }} > {isNewlyCreated && ( - +
{ if (saveValueTypeLocally) { @@ -131,18 +143,26 @@ export function MetadataKeyValuePair({ disableClearable shouldHighlightIfEmpty={shouldHighlightEmptyFields} /> - +
)} { + onInputSave={(newVal) => { + if (!!validateValueByType && !!valueType){ + setIsErrored(!validateValueByType(valueType, newVal as string)) + } if (saveValueLocally) { saveValueLocally(index, newVal); } }} + onInputChange={(newVal) => { + if (!!validateValueByType && !!valueType){ + setIsErrored(!validateValueByType(valueType, newVal as string)) + }}} placeholder={isNewlyCreated || !value ? 'Add value' : 'Typing...'} shouldHighlightIfEmpty={shouldHighlightEmptyFields} + isErrored={isErrored}//TODO: add validation /> {isEditable && isRemovable && ( React.ReactNode; sidebarRenderers?: React.ReactNode; + validateValueByType?: (valueType: MetadataType, value: string) => boolean; }) { + const SIDEBAR_WIDTH = 350; //I decided on this number const ARROWS_SECTION_HEIGHT = 52; @@ -70,7 +73,8 @@ export function SingleFileViewDataSection({ metadataList={itemData.metadataList} editingEnabled={!!enableMetadataEditing} deletionEnabled={!!enableMetadataDeletion} - onChangeHandler={metadataOnChangeHandler} + onSaveHandler={metadataOnChangeHandler} + validateValueByType={validateValueByType} />
@@ -225,7 +229,8 @@ export function SingleFileViewDataSection({ metadataList={itemData.metadataList} editingEnabled={!!enableMetadataEditing} deletionEnabled={!!enableMetadataDeletion} - onChangeHandler={metadataOnChangeHandler} + onSaveHandler={metadataOnChangeHandler} + validateValueByType={validateValueByType} /> {sidebarRenderers} diff --git a/src/components/dagshub/data-engine/singleFileViewModal/SingleFileViewModal.tsx b/src/components/dagshub/data-engine/singleFileViewModal/SingleFileViewModal.tsx index dd7c6d8..815d700 100644 --- a/src/components/dagshub/data-engine/singleFileViewModal/SingleFileViewModal.tsx +++ b/src/components/dagshub/data-engine/singleFileViewModal/SingleFileViewModal.tsx @@ -1,7 +1,7 @@ import { Box } from '@mui/system'; import React, { useEffect, useRef, useState } from 'react'; import { useMediaQuery } from '@mui/material'; -import { GenericModal, MetadataField, NewMetadataField, RGB } from '../../index'; +import {GenericModal, MetadataField, MetadataType, NewMetadataField, RGB} from '../../index'; import './style.scss'; import TopButtonsSection from './TopButtonsSection'; import { SingleFileViewDataSection } from './SingleFileViewDataSection'; @@ -48,6 +48,7 @@ export interface singleFileViewModalProps { enableFileDownloading?: boolean; visualizerRenderer: (props: VisualizerProps) => React.ReactNode; sidebarRenderers?: React.ReactNode; + validateValueByType?: (valueType: MetadataType, value: string) => boolean; } export function SingleFileViewModal({ @@ -65,6 +66,7 @@ export function SingleFileViewModal({ enableFileDownloading, visualizerRenderer, sidebarRenderers, + validateValueByType, }: singleFileViewModalProps) { const [showMetadataOverlay, setShowMetadataOverlay] = useState(false); const breakpoint = useMediaQuery('(max-width: 800px)'); @@ -145,6 +147,7 @@ export function SingleFileViewModal({ enableMetadataDeletion={enableMetadataDeletion} visualizerRenderer={visualizerRenderer} sidebarRenderers={sidebarRenderers} + validateValueByType={validateValueByType} /> , diff --git a/src/components/elements/tooltipV2/ErroredTooltip.tsx b/src/components/elements/tooltipV2/ErroredTooltip.tsx new file mode 100644 index 0000000..eba0d6e --- /dev/null +++ b/src/components/elements/tooltipV2/ErroredTooltip.tsx @@ -0,0 +1,38 @@ +import {Tooltip, tooltipClasses, TooltipProps} from "@mui/material"; +import React, {ReactElement} from "react"; +import {styled} from "@mui/material/styles"; + +const StyledTooltip = styled(({className, isEmpty, ...props}: {isEmpty?:boolean}&TooltipProps) => ( + +))(({theme, isEmpty}) => (!isEmpty ?{ + [`& .${tooltipClasses.tooltip}`]: { + backgroundColor: 'rgba(254, 226, 226, 1)', + color: 'rgba(23, 45, 50, 1)', + fontSize: theme.typography.pxToRem(12), + fontWeight: 400, + border: '1px solid rgba(254, 202, 202, 1)', + }, +}:{[`& .${tooltipClasses.tooltip}`]: { + backgroundColor: 'transparent', + border: '0px', + },})); + +export const enum TooltipVariant { Default = 'default', Error = 'error'}; + +export const ErroredTooltip = ({ + title, + tooltipVariant= TooltipVariant.Default, + children, + placement= 'top', + disableInteractive= true, + ...restProps + }: { tooltipVariant?: TooltipVariant, children: ReactElement} & TooltipProps) => { + return ( + <> + + {children} + + + ) +} + diff --git a/src/stories/dagshub/data-engine/singlefileViewModal/SingleFileViewModal.stories.tsx b/src/stories/dagshub/data-engine/singlefileViewModal/SingleFileViewModal.stories.tsx index c3aa250..9143dea 100644 --- a/src/stories/dagshub/data-engine/singlefileViewModal/SingleFileViewModal.stories.tsx +++ b/src/stories/dagshub/data-engine/singlefileViewModal/SingleFileViewModal.stories.tsx @@ -4,7 +4,7 @@ import SingleFileViewModal, { ItemData, singleFileViewModalProps, } from '../../../../components/dagshub/data-engine/singleFileViewModal/SingleFileViewModal'; -import { NewMetadataField } from '../../../../components'; +import {MetadataType, NewMetadataField} from '../../../../components'; import { Button } from '@mui/material'; import React from 'react'; import { SingleFileViewFileRenderer } from '../../../../components/dagshub/data-engine/singleFileViewModal/SingleFileViewFileRenderer'; @@ -45,7 +45,7 @@ const itemDataMockList: ItemData[] = [ { key: 'is_video', value: 'true', valueType: 'BOOLEAN' }, { key: 'length', value: '16 seconds', valueType: 'STRING' }, { key: 'description', value: 'this is a video about earth space', valueType: 'STRING' }, - { key: 'categories', value: 'earth, space, live, human', valueType: 'INTEGER' }, + { key: 'categories', value: 'earth, space, live, human', valueType: 'STRING' }, ], hasPrevious: true, hasNext: true, @@ -66,7 +66,7 @@ const itemDataMockList: ItemData[] = [ { key: 'is_video', value: 'true', valueType: 'BOOLEAN' }, { key: 'length', value: '16 seconds', valueType: 'STRING' }, { key: 'description', value: 'this is a video about earth space', valueType: 'STRING' }, - { key: 'categories', value: 'earth, space, live, human', valueType: 'INTEGER' }, + { key: 'categories', value: 'earth, space, live, human', valueType: 'STRING' }, ], hasPrevious: true, hasNext: true, @@ -146,7 +146,7 @@ const itemDataMockList: ItemData[] = [ { key: 'is_video', value: 'true', valueType: 'BOOLEAN' }, { key: 'length', value: '16 seconds', valueType: 'STRING' }, { key: 'description', value: 'this is a video about earth space', valueType: 'STRING' }, - { key: 'categories', value: 'earth, space, live, human', valueType: 'INTEGER' }, + { key: 'categories', value: 'earth, space, live, human', valueType: 'STRING' }, ], hasPrevious: true, hasNext: false, @@ -210,6 +210,34 @@ singlefileViewModalWithEditingEnabled.args = { metadataOnChangeHandler: (metadataList: NewMetadataField[]) => { // console.log(metadataList) }, + validateValueByType: ( + valueType: MetadataType, + value: string, + ): boolean => { + if(!value){ + return true; //Accept empty value, it will be handled separately + } + try { + switch (valueType) { + case 'BOOLEAN': + return value === 'true' || value === 'false'; + case 'INTEGER': + const integerRegex = /^([-+]?(0|[1-9][0-9]*))$/; + return !isNaN(parseInt(value)) && integerRegex.test(value); + case 'FLOAT': + const floatRegex = /^([-+]?(0\.[0-9]+|0|[1-9][0-9]*(\.[0-9]+)?))$/; + return !isNaN(parseFloat(value)) && floatRegex.test(value); + case 'STRING': + return true; + case 'BLOB': + return true; + default: + return false; + } + } catch (e) { + return false; + } + } }; export const singlefileViewModalWithSelectAllEnabled: StoryFn = diff --git a/src/stories/elements/customAccordion/customAccordion.stories.tsx b/src/stories/elements/customAccordion/customAccordion.stories.tsx index 3e911ff..4ab0b2f 100644 --- a/src/stories/elements/customAccordion/customAccordion.stories.tsx +++ b/src/stories/elements/customAccordion/customAccordion.stories.tsx @@ -27,7 +27,7 @@ customAccordion.args = { ]} editingEnabled={true} deletionEnabled={false} - onChangeHandler={() => {}} + onSaveHandler={() => {}} /> ), };