From 2a51f53371f9fbebfe5bf59729bb958621fc6b11 Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH Date: Sun, 16 Jul 2023 11:05:24 +0200 Subject: [PATCH 1/9] f-3945/ref: header filters components --- .../MyDataHeaderFilters/DateFieldSelector.tsx | 54 +++++ .../MyDataHeaderFilters/DateSelector.tsx | 212 +++++++++++++++++ .../MyDataHeaderFilters/IssuerSelector.tsx | 29 +++ .../MyDataHeaderFilters/SearchInput.tsx | 38 ++++ .../MyDataHeaderFilters/TypeSelector.tsx | 215 ++++++++++++++++++ .../MyDataHeaderFilters/index.tsx | 27 +++ 6 files changed, 575 insertions(+) create mode 100644 src/shared/molecules/MyDataHeader/MyDataHeaderFilters/DateFieldSelector.tsx create mode 100644 src/shared/molecules/MyDataHeader/MyDataHeaderFilters/DateSelector.tsx create mode 100644 src/shared/molecules/MyDataHeader/MyDataHeaderFilters/IssuerSelector.tsx create mode 100644 src/shared/molecules/MyDataHeader/MyDataHeaderFilters/SearchInput.tsx create mode 100644 src/shared/molecules/MyDataHeader/MyDataHeaderFilters/TypeSelector.tsx create mode 100644 src/shared/molecules/MyDataHeader/MyDataHeaderFilters/index.tsx diff --git a/src/shared/molecules/MyDataHeader/MyDataHeaderFilters/DateFieldSelector.tsx b/src/shared/molecules/MyDataHeader/MyDataHeaderFilters/DateFieldSelector.tsx new file mode 100644 index 000000000..408ae6591 --- /dev/null +++ b/src/shared/molecules/MyDataHeader/MyDataHeaderFilters/DateFieldSelector.tsx @@ -0,0 +1,54 @@ +import { RightOutlined } from '@ant-design/icons'; +import { Button, Dropdown, Menu } from 'antd'; +import { + TDateField, + THandleMenuSelect, + THeaderFilterProps, +} from 'shared/canvas/MyData/types'; + +type TDateFieldSelectorProps = Pick< + THeaderFilterProps, + 'dateField' | 'setFilterOptions' +>; + +const dateFieldName = { + createdAt: 'Creation Date', + updatedAt: 'Update Date', +}; + +const DateFieldSelector = ({ + dateField, + setFilterOptions, +}: TDateFieldSelectorProps) => { + const handleDateFieldChange: THandleMenuSelect = ({ key }) => + setFilterOptions({ dateField: key as TDateField }); + + const DateFieldMenu = ( + + {dateFieldName.createdAt} + {dateFieldName.updatedAt} + + ); + return ( + + + + ); +}; + +export default DateFieldSelector; diff --git a/src/shared/molecules/MyDataHeader/MyDataHeaderFilters/DateSelector.tsx b/src/shared/molecules/MyDataHeader/MyDataHeaderFilters/DateSelector.tsx new file mode 100644 index 000000000..321e559f5 --- /dev/null +++ b/src/shared/molecules/MyDataHeader/MyDataHeaderFilters/DateSelector.tsx @@ -0,0 +1,212 @@ +import { CalendarOutlined } from '@ant-design/icons'; +import { useNexusContext } from '@bbp/react-nexus'; +import { Button, Dropdown, Input, Radio, RadioChangeEvent } from 'antd'; +import { capitalize } from 'lodash'; +import * as moment from 'moment'; +import { Fragment, useReducer, useRef, useState } from 'react'; +import { + DATE_PATTERN, + TCurrentDate, + TDateOptions, + THeaderFilterProps, +} from '../../../canvas/MyData/types'; +import DateSeparated from '../../../components/DateSeparatedInputs/DateSeparated'; +import useClickOutside from '../../../hooks/useClickOutside'; +import useMeasure from '../../../hooks/useMeasure'; + +type TDateSelectorProps = Pick< + THeaderFilterProps, + 'dateField' | 'setFilterOptions' +>; +const DateSelector = ({ dateField, setFilterOptions }: TDateSelectorProps) => { + const popoverRef = useRef(null); + const nexus = useNexusContext(); + const [ + { dateEnd, dateFilterType, singleDate, dateStart }, + updateCurrentDates, + ] = useReducer( + (previous: TCurrentDate, next: Partial) => ({ + ...previous, + ...next, + }), + { + singleDate: undefined, + dateEnd: undefined, + dateStart: undefined, + dateFilterType: undefined, + } + ); + const selectedDate = + dateFilterType === 'range' && dateStart !== '' && dateEnd !== '' + ? `${moment(dateStart).format(DATE_PATTERN)} → ${moment(dateEnd).format( + DATE_PATTERN + )}` + : singleDate + ? `${capitalize(dateFilterType)} ${moment(singleDate).format( + DATE_PATTERN + )}` + : undefined; + const [dateInputRef, { width: datePopWidth }] = useMeasure< + HTMLInputElement + >(); + + const onDatePopoverVisibleChange = () => + setOpenDateFilterContainer(state => !state); + const [dateFilterContainer, setOpenDateFilterContainer] = useState( + false + ); + const updateDate = (type: TDateOptions, date: string) => + updateCurrentDates({ + [type]: date, + }); + + const onChangeDateType = (e: RadioChangeEvent) => { + updateCurrentDates({ + dateFilterType: e.target.value, + singleDate: '', + dateStart: '', + dateEnd: '', + }); + }; + const notValidForm = + !dateFilterType || + !dateField || + (dateFilterType === 'range' && (!dateStart || !dateEnd)) || + (dateFilterType === 'range' && + dateStart && + dateEnd && + moment(dateEnd).isBefore(dateStart, 'days')) || + (dateFilterType !== 'range' && !singleDate); + const handleSubmitDates: React.FormEventHandler = e => { + e.preventDefault(); + setFilterOptions({ + dateFilterType, + singleDate, + dateStart, + dateEnd, + }); + }; + const DatePickerContainer = ( + +
+ + + Before + + + After + + + Range + + + {dateFilterType === 'range' ? ( + + From + { + if ( + dateEnd && + moment(dateEnd).isValid() && + moment(dateEnd).isBefore(dateStart, 'days') + ) { + return updateCurrentDates({ + dateStart: dateEnd, + dateEnd: value, + }); + } + return updateDate('dateStart', value); + }} + /> + To + { + if ( + dateStart && + moment(dateStart).isValid() && + moment(dateStart).isAfter(dateEnd, 'days') + ) { + return updateCurrentDates({ + dateStart: value, + dateEnd: dateStart, + }); + } + return updateDate('dateEnd', value); + }} + /> + + ) : ( + updateDate('singleDate', value)} + /> + )} + + +
+ ); + useClickOutside(popoverRef, onDatePopoverVisibleChange); + return ( + + {dateFilterContainer && ( +
+ {DatePickerContainer} +
+ )} + + } + overlayStyle={{ width: datePopWidth }} + > + } + onClick={() => setOpenDateFilterContainer(state => !state)} + onChange={(e: React.ChangeEvent) => { + if (e.type === 'click') { + updateCurrentDates({ + dateFilterType: undefined, + singleDate: undefined, + dateStart: undefined, + dateEnd: undefined, + }); + setFilterOptions({ + dateFilterType: undefined, + singleDate: undefined, + dateStart: undefined, + dateEnd: undefined, + }); + } + }} + /> +
+ ); +}; + +export default DateSelector; diff --git a/src/shared/molecules/MyDataHeader/MyDataHeaderFilters/IssuerSelector.tsx b/src/shared/molecules/MyDataHeader/MyDataHeaderFilters/IssuerSelector.tsx new file mode 100644 index 000000000..6675d8c5c --- /dev/null +++ b/src/shared/molecules/MyDataHeader/MyDataHeaderFilters/IssuerSelector.tsx @@ -0,0 +1,29 @@ +import { Radio, RadioChangeEvent } from 'antd'; +import { THeaderFilterProps } from 'shared/canvas/MyData/types'; + +type TIssuerSelectorProps = Pick< + THeaderFilterProps, + 'issuer' | 'setFilterOptions' +>; + +const IssuerSelector = ({ issuer, setFilterOptions }: TIssuerSelectorProps) => { + const onIssuerChange = (e: RadioChangeEvent) => + setFilterOptions({ issuer: e.target.value }); + + return ( + + + Created by me + + + Last updated by me + + + ); +}; + +export default IssuerSelector; diff --git a/src/shared/molecules/MyDataHeader/MyDataHeaderFilters/SearchInput.tsx b/src/shared/molecules/MyDataHeader/MyDataHeaderFilters/SearchInput.tsx new file mode 100644 index 000000000..f5de2a75d --- /dev/null +++ b/src/shared/molecules/MyDataHeader/MyDataHeaderFilters/SearchInput.tsx @@ -0,0 +1,38 @@ +import { Checkbox, Input } from 'antd'; +import { CheckboxChangeEvent } from 'antd/lib/checkbox'; +import { THeaderFilterProps } from 'shared/canvas/MyData/types'; + +type TSearchInputProps = Pick< + THeaderFilterProps, + 'locate' | 'query' | 'setFilterOptions' +>; +const SearchInput = ({ + query, + locate, + setFilterOptions, +}: TSearchInputProps) => { + const onSearchLocateChange = (e: CheckboxChangeEvent) => + setFilterOptions({ locate: e.target.checked }); + const handleQueryChange: React.ChangeEventHandler = event => + setFilterOptions({ query: event.target.value }); + return ( +
+ +
+ + By resource id or self + +
+
+ ); +}; + +export default SearchInput; diff --git a/src/shared/molecules/MyDataHeader/MyDataHeaderFilters/TypeSelector.tsx b/src/shared/molecules/MyDataHeader/MyDataHeaderFilters/TypeSelector.tsx new file mode 100644 index 000000000..562d0c3c3 --- /dev/null +++ b/src/shared/molecules/MyDataHeader/MyDataHeaderFilters/TypeSelector.tsx @@ -0,0 +1,215 @@ +import React, { useCallback, useRef, useState } from 'react'; +import { NexusClient } from '@bbp/nexus-sdk'; +import { useNexusContext } from '@bbp/react-nexus'; +import { Checkbox, Col, Dropdown, Input, Row, Select } from 'antd'; +import { concat, isString, startCase } from 'lodash'; +import { useQuery } from 'react-query'; +import { + THeaderProps, + TType, + TTypeAggregationsResult, + TTypesAggregatedBucket, +} from '../../../canvas/MyData/types'; +import useMeasure from '../../../hooks/useMeasure'; +import isValidUrl from '../../../../utils/validUrl'; +import { prettifyNumber } from '../../../../utils/formatNumber'; + +const getTypesByAggregation = async ({ + nexus, + orgLabel, + projectLabel, +}: { + nexus: NexusClient; + orgLabel?: string; + projectLabel?: string; +}) => { + return await nexus.Resource.list(orgLabel, projectLabel, { + aggregations: true, + }); +}; + +const useTypesAggregation = ({ + nexus, + selectCallback, +}: { + nexus: NexusClient; + selectCallback: (data: any) => TType[]; +}) => { + return useQuery({ + refetchOnWindowFocus: false, + queryKey: ['types-aggregation-results'], + queryFn: () => getTypesByAggregation({ nexus }), + select: selectCallback, + }); +}; +const typesOptionsBuilder = (typeBucket: TTypesAggregatedBucket): TType => { + const typeKey = typeBucket.key; + const typeLabel = + isString(typeKey) && isValidUrl(typeKey) + ? typeKey.split('/').pop() + : typeKey; + + return { + key: typeKey, + value: typeKey, + label: startCase(typeLabel), + docCount: typeBucket.doc_count, + }; +}; + +const TypeRowRenderer = ({ + type, + checked, + onCheck, +}: { + checked: boolean; + type: TType; + onCheck(e: React.MouseEvent, type: TType): void; +}) => { + return ( + + + {type.label} + + + onCheck(e, type)} checked={checked} /> + + + ); +}; +const TypeSelector = ({ + types, + setFilterOptions, +}: Pick) => { + const nexus = useNexusContext(); + const originTypes = useRef([]); + const [typeSearchValue, updateSearchType] = useState(''); + const [typesOptionsArray, setTypesOptionsArray] = useState([]); + + const selectCallback = useCallback((data: any) => { + const options = ( + ((data as unknown) as TTypeAggregationsResult).aggregations.types + ?.buckets ?? ([] as TTypesAggregatedBucket[]) + ).map(item => typesOptionsBuilder(item)); + originTypes.current = options; + return options; + }, []); + + const { data: typeOptions } = useTypesAggregation({ + nexus, + selectCallback, + }); + + const onSearchTypeChange = ({ + target: { value }, + type, + }: React.ChangeEvent) => { + updateSearchType(value); + if (value === '' || type === 'click') { + setTypesOptionsArray(originTypes.current); + } else { + setTypesOptionsArray( + originTypes.current.filter(item => + item.label.toLowerCase().includes(value.toLowerCase()) + ) ?? [] + ); + } + }; + const onDeselectTypesChange = (value: any) => + setFilterOptions({ + types: types?.filter(item => item.value !== value), + }); + const onClearTypesChange = () => + setFilterOptions({ + types: [], + }); + + const handleOnCheckType = ( + e: React.MouseEvent, + type: TType + ) => { + e.preventDefault(); + e.stopPropagation(); + setFilterOptions({ + types: types?.find(item => item.key === type.key) + ? types.filter(item => item.key !== type.key) + : types + ? concat(types, type) + : [type], + }); + }; + + const [typeInputRef, { width }] = useMeasure(); + const renderedTypes = typeSearchValue ? typesOptionsArray : typeOptions ?? []; + return ( + +
+ + { +
{`${prettifyNumber( + renderedTypes.length + )} types`}
+ } +
+
+ {renderedTypes.length ? ( + renderedTypes.map((type: TType) => { + return ( + item.key === type.key) + )} + onCheck={handleOnCheckType} + /> + ); + }) + ) : ( +
+ No types found +
+ )} +
+ + } + > + } - onClick={() => setOpenDateFilterContainer(state => !state)} - onChange={(e: React.ChangeEvent) => { - if (e.type === 'click') { - updateCurrentDates({ - dateFilterType: undefined, - singleDate: undefined, - dateStart: undefined, - dateEnd: undefined, - }); - setFilterOptions({ - dateFilterType: undefined, - singleDate: undefined, - dateStart: undefined, - dateEnd: undefined, - }); - } - }} - /> -
- {/* -
- -
{typesDataSources.length} types total
-
-
- {typesDataSources && - typesDataSources.map((tp: TType) => { - return ( - - {startCase(tp.label)} - - handleOnCheckType(e, tp)} - checked={dataType.includes(tp.key)} - /> - - - ); - })} -
- - } - > - startCase(item.split('/').pop()))} - /> -
*/} -
- -
- - By resource id or self - -
-
+ {text} + {total ? `${prettifyNumber(total)} ${label}` : ''} ); }; const MyDataHeader: React.FC = ({ total, - dataType, + types, dateField, query, setFilterOptions, @@ -439,13 +28,11 @@ const MyDataHeader: React.FC = ({ - <Filters + <MyDataHeaderFilters {...{ - dataType, + types, dateField, query, locate, diff --git a/src/shared/molecules/MyDataHeader/styles.less b/src/shared/molecules/MyDataHeader/styles.less index 987c40a3f..4c60abf39 100644 --- a/src/shared/molecules/MyDataHeader/styles.less +++ b/src/shared/molecules/MyDataHeader/styles.less @@ -2,9 +2,9 @@ .my-data-table-header { display: flex; - align-items: center; + align-items: flex-start; justify-content: space-between; - flex-wrap: wrap; + flex-direction: column; &-title { user-select: none; @@ -33,10 +33,11 @@ } &-actions { + width: 100%; margin: 0; display: grid; gap: 15px; - grid-template-columns: max-content max-content 300px 1fr; + grid-template-columns: max-content max-content 300px 300px 1fr; align-items: center; justify-content: center; align-content: center; @@ -137,8 +138,22 @@ .ant-select-selection-placeholder { color: @fusion-daybreak-10 !important; } + .ant-select-selection-overflow { + flex-wrap: nowrap; + overflow: scroll; + // style the scrollbar of this ant-select-selection-overflow + &::-webkit-scrollbar { + width: 2px; + height: 2px; + } + &::-webkit-scrollbar-thumb { + background: @fusion-daybreak-10; + } + } +} +.my-data-type-picker-popup { + display: none; } - .my-data-type-filter { &-popover { background-color: white !important; @@ -194,6 +209,17 @@ -ms-overflow-style: none; scrollbar-width: none; + .no-types-content { + display: flex; + align-items: center; + justify-content: center; + height: 100%; + span { + user-select: none; + font-size: 12px; + color: #8c8c8c; + } + } } } @@ -228,6 +254,14 @@ outline: none; box-shadow: none; } + .ant-input-affix-wrapper { + border: none; + background-color: transparent; + &-focused { + box-shadow: none; + outline: none; + } + } } .my-data-type-picker { From 004d91dcbd8a9049ce0e6546263d14b95db44756 Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH <b_meddah@esi.dz> Date: Sun, 16 Jul 2023 11:08:52 +0200 Subject: [PATCH 5/9] f-3945/ref: my-data --- src/pages/MyDataPage/MyDataPage.tsx | 6 +-- src/shared/canvas/MyData/MyData.tsx | 59 ++++++---------------------- src/shared/canvas/MyData/styles.less | 6 ++- 3 files changed, 16 insertions(+), 55 deletions(-) diff --git a/src/pages/MyDataPage/MyDataPage.tsx b/src/pages/MyDataPage/MyDataPage.tsx index 20957ed7e..d7defc808 100644 --- a/src/pages/MyDataPage/MyDataPage.tsx +++ b/src/pages/MyDataPage/MyDataPage.tsx @@ -1,11 +1,7 @@ import { MyData } from '../../shared/canvas'; const MyDataPage = () => { - return ( - <div className="my-data-view view-container" style={{ padding: '0 1em' }}> - <MyData /> - </div> - ); + return <MyData />; }; export default MyDataPage; diff --git a/src/shared/canvas/MyData/MyData.tsx b/src/shared/canvas/MyData/MyData.tsx index ef4123a1a..5ca77d179 100644 --- a/src/shared/canvas/MyData/MyData.tsx +++ b/src/shared/canvas/MyData/MyData.tsx @@ -1,56 +1,16 @@ import * as React from 'react'; -import * as moment from 'moment'; import { useQuery } from 'react-query'; import { useSelector } from 'react-redux'; import { useNexusContext } from '@bbp/react-nexus'; import { notification } from 'antd'; -import { isObject, isString } from 'lodash'; +import { get, isObject, isString } from 'lodash'; import { MyDataHeader, MyDataTable } from '../../molecules'; import { RootState } from '../../store/reducers'; -import { TDateFilterType, TFilterOptions } from './types'; +import { TFilterOptions } from './types'; +import { makeDatetimePattern } from './utils'; import './styles.less'; -const makeDatetimePattern = ({ - dateFilterType, - singleDate, - dateStart, - dateEnd, -}: { - dateFilterType?: TDateFilterType; - singleDate?: string; - dateStart?: string; - dateEnd?: string; -}) => { - switch (dateFilterType) { - case 'after': { - if (!!singleDate && moment(singleDate).isValid()) { - return `${singleDate}..*`; - } - return undefined; - } - case 'before': { - if (!!singleDate && moment(singleDate).isValid()) { - return `*..${singleDate}`; - } - return undefined; - } - case 'range': { - if ( - !!dateStart && - !!dateEnd && - moment(dateStart).isValid() && - moment(dateEnd).isValid() && - moment(dateStart).isBefore(moment(dateEnd), 'days') - ) { - return `${dateStart}..${dateEnd}`; - } - return undefined; - } - default: - return undefined; - } -}; const HomeMyData: React.FC<{}> = () => { const nexus = useNexusContext(); const identities = useSelector( @@ -59,7 +19,6 @@ const HomeMyData: React.FC<{}> = () => { const issuerUri = identities?.find(item => item['@type'] === 'User')?.['@id']; const [ { - dataType, dateField, query, dateFilterType, @@ -71,6 +30,7 @@ const HomeMyData: React.FC<{}> = () => { sort, locate, issuer, + types, }, setFilterOptions, ] = React.useReducer( @@ -84,13 +44,13 @@ const HomeMyData: React.FC<{}> = () => { singleDate: undefined, dateStart: undefined, dateEnd: undefined, - dataType: [], query: '', offset: 0, size: 50, sort: ['-_createdAt', '@id'], locate: false, issuer: 'createdBy', + types: [], } ); @@ -116,6 +76,7 @@ const HomeMyData: React.FC<{}> = () => { ? `${dateField}-${dateFilterType}-${dateFilterRange}` : undefined; const order = sort.join('-'); + const resourceTypes = types?.map(item => get(item, 'value')); const { data: resources, isLoading } = useQuery({ queryKey: [ 'my-data-resources', @@ -127,6 +88,7 @@ const HomeMyData: React.FC<{}> = () => { issuer, date, order, + types: resourceTypes, }, ], retry: false, @@ -150,7 +112,8 @@ const HomeMyData: React.FC<{}> = () => { [dateField]: dateFilterRange, } : {}), - // type: dataType, + // @ts-ignore + type: resourceTypes, }), onError: error => { notification.error({ @@ -170,10 +133,10 @@ const HomeMyData: React.FC<{}> = () => { }); const total = resources?._total; return ( - <div className="home-mydata"> + <div className="my-data-view view-container"> <MyDataHeader {...{ - dataType, + types, dateField, query, dateFilterRange, diff --git a/src/shared/canvas/MyData/styles.less b/src/shared/canvas/MyData/styles.less index 5b58965aa..1e9186cac 100644 --- a/src/shared/canvas/MyData/styles.less +++ b/src/shared/canvas/MyData/styles.less @@ -1,3 +1,5 @@ -.home-mydata { - margin-top: 52px; +.my-data-view { + margin-top: 52px !important; + padding: 0 1em !important; + flex-direction: column !important; } From df2a518658ca4d47bfdd661c51153ce5b3355ae3 Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH <b_meddah@esi.dz> Date: Mon, 17 Jul 2023 11:03:23 +0200 Subject: [PATCH 6/9] f-3945/update: fix styles --- .../MyDataHeaderFilters/DateFieldSelector.tsx | 1 + .../MyDataHeaderFilters/IssuerSelector.tsx | 1 + .../MyDataHeaderFilters/TypeSelector.tsx | 10 +++------ src/shared/molecules/MyDataHeader/styles.less | 22 ++++++++++++++++--- 4 files changed, 24 insertions(+), 10 deletions(-) diff --git a/src/shared/molecules/MyDataHeader/MyDataHeaderFilters/DateFieldSelector.tsx b/src/shared/molecules/MyDataHeader/MyDataHeaderFilters/DateFieldSelector.tsx index 408ae6591..9660a92cd 100644 --- a/src/shared/molecules/MyDataHeader/MyDataHeaderFilters/DateFieldSelector.tsx +++ b/src/shared/molecules/MyDataHeader/MyDataHeaderFilters/DateFieldSelector.tsx @@ -36,6 +36,7 @@ const DateFieldSelector = ({ ); return ( <Dropdown + className="date-field-selector" placement="bottomLeft" trigger={['click']} overlay={DateFieldMenu} diff --git a/src/shared/molecules/MyDataHeader/MyDataHeaderFilters/IssuerSelector.tsx b/src/shared/molecules/MyDataHeader/MyDataHeaderFilters/IssuerSelector.tsx index 6675d8c5c..53016486e 100644 --- a/src/shared/molecules/MyDataHeader/MyDataHeaderFilters/IssuerSelector.tsx +++ b/src/shared/molecules/MyDataHeader/MyDataHeaderFilters/IssuerSelector.tsx @@ -12,6 +12,7 @@ const IssuerSelector = ({ issuer, setFilterOptions }: TIssuerSelectorProps) => { return ( <Radio.Group + className="issuer-selector" defaultValue={'createdBy'} value={issuer} onChange={onIssuerChange} diff --git a/src/shared/molecules/MyDataHeader/MyDataHeaderFilters/TypeSelector.tsx b/src/shared/molecules/MyDataHeader/MyDataHeaderFilters/TypeSelector.tsx index 562d0c3c3..cb341d71a 100644 --- a/src/shared/molecules/MyDataHeader/MyDataHeaderFilters/TypeSelector.tsx +++ b/src/shared/molecules/MyDataHeader/MyDataHeaderFilters/TypeSelector.tsx @@ -107,7 +107,7 @@ const TypeSelector = ({ selectCallback, }); - const onSearchTypeChange = ({ + const onChangeTypeChange = ({ target: { value }, type, }: React.ChangeEvent<HTMLInputElement>) => { @@ -138,11 +138,7 @@ const TypeSelector = ({ e.preventDefault(); e.stopPropagation(); setFilterOptions({ - types: types?.find(item => item.key === type.key) - ? types.filter(item => item.key !== type.key) - : types - ? concat(types, type) - : [type], + types: [type], }); }; @@ -161,7 +157,7 @@ const TypeSelector = ({ className="my-data-type-filter-search" placeholder="Search for type" value={typeSearchValue} - onChange={onSearchTypeChange} + onChange={onChangeTypeChange} /> { <div className="count">{`${prettifyNumber( diff --git a/src/shared/molecules/MyDataHeader/styles.less b/src/shared/molecules/MyDataHeader/styles.less index 4c60abf39..17d103eea 100644 --- a/src/shared/molecules/MyDataHeader/styles.less +++ b/src/shared/molecules/MyDataHeader/styles.less @@ -35,13 +35,29 @@ &-actions { width: 100%; margin: 0; - display: grid; + display: flex; + flex-flow: row wrap; + // grid-template-columns: max-content max-content 300px 300px 1fr; gap: 15px; - grid-template-columns: max-content max-content 300px 300px 1fr; align-items: center; - justify-content: center; + justify-content: flex-start; align-content: center; } + .issuer-selector, + .date-field-selector { + width: 100%; + max-width: max-content; + } + .my-data-date-picker, + .my-data-type-picker { + width: 100%; + max-width: 300px; + } + .my-data-search-container { + flex: 0 1 25%; + width: 100%; + min-width: 250px; + } } .radio-filter { &.ant-radio-wrapper-checked { From ba8e169c6abb569b7ea7ffeb6422d40cd5758615 Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH <b_meddah@esi.dz> Date: Tue, 18 Jul 2023 09:35:26 +0200 Subject: [PATCH 7/9] f-3945/fix: the type selection/deslection --- .../MyDataHeader/MyDataHeaderFilters/TypeSelector.tsx | 2 +- src/shared/molecules/MyDataTable/MyDataTable.tsx | 11 ++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/shared/molecules/MyDataHeader/MyDataHeaderFilters/TypeSelector.tsx b/src/shared/molecules/MyDataHeader/MyDataHeaderFilters/TypeSelector.tsx index cb341d71a..b1a7f3263 100644 --- a/src/shared/molecules/MyDataHeader/MyDataHeaderFilters/TypeSelector.tsx +++ b/src/shared/molecules/MyDataHeader/MyDataHeaderFilters/TypeSelector.tsx @@ -138,7 +138,7 @@ const TypeSelector = ({ e.preventDefault(); e.stopPropagation(); setFilterOptions({ - types: [type], + types: types?.find(item => item.value === type.value) ? [] : [type], }); }; diff --git a/src/shared/molecules/MyDataTable/MyDataTable.tsx b/src/shared/molecules/MyDataTable/MyDataTable.tsx index d73b27a15..82cf6a1dd 100644 --- a/src/shared/molecules/MyDataTable/MyDataTable.tsx +++ b/src/shared/molecules/MyDataTable/MyDataTable.tsx @@ -4,6 +4,7 @@ import React, { useReducer, useEffect, useState, + CSSProperties, } from 'react'; import { Button, Empty, Table, Tag, Tooltip, notification } from 'antd'; import { @@ -155,7 +156,7 @@ type TSorterProps = { onSortDescend(): void; onSortAscend(): void; }; - +const columnWhiteSpaceWrap = { whiteSpace: 'nowrap' } as CSSProperties; const Sorter = ({ onSortDescend, onSortAscend, order, name }: TSorterProps) => { if (!order) { return ( @@ -239,7 +240,7 @@ const MyDataTable: React.FC<TProps> = ({ return ( <Tooltip title={resourceId}> <Button - style={{ padding: 0 }} + style={{ padding: 0, ...columnWhiteSpaceWrap }} type="link" onClick={() => goToResource(org, project, resourceId)} > @@ -262,7 +263,7 @@ const MyDataTable: React.FC<TProps> = ({ : 'asc' : undefined; return ( - <div> + <div style={columnWhiteSpaceWrap}> organization / project {(!query || query.trim() === '') && ( <Sorter @@ -328,7 +329,7 @@ const MyDataTable: React.FC<TProps> = ({ : 'asc' : undefined; return ( - <div> + <div style={columnWhiteSpaceWrap}> updated date {(!query || query.trim() === '') && ( <Sorter @@ -356,7 +357,7 @@ const MyDataTable: React.FC<TProps> = ({ : 'asc' : undefined; return ( - <div> + <div style={columnWhiteSpaceWrap}> created date {(!query || query.trim() === '') && ( <Sorter From b536113372de1e1e328ef1dd04080818eb205405 Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH <b_meddah@esi.dz> Date: Tue, 18 Jul 2023 11:58:14 +0200 Subject: [PATCH 8/9] f-3945/update: change the design --- src/shared/canvas/MyData/types.ts | 9 +- .../molecules/MyDataHeader/MyDataHeader.tsx | 30 +-- .../MyDataHeaderFilters/IssuerSelector.tsx | 9 +- .../MyDataHeaderFilters/PageTitle.tsx | 13 ++ .../MyDataHeaderFilters/SearchInput.tsx | 8 +- .../MyDataHeaderFilters/TypeSelector.tsx | 2 +- .../MyDataHeaderFilters/index.tsx | 43 +++- src/shared/molecules/MyDataHeader/styles.less | 209 ++++++++++++------ 8 files changed, 220 insertions(+), 103 deletions(-) create mode 100644 src/shared/molecules/MyDataHeader/MyDataHeaderFilters/PageTitle.tsx diff --git a/src/shared/canvas/MyData/types.ts b/src/shared/canvas/MyData/types.ts index 2c0ebf5df..1d118c3ca 100644 --- a/src/shared/canvas/MyData/types.ts +++ b/src/shared/canvas/MyData/types.ts @@ -31,7 +31,14 @@ export type TTitleProps = { label: string; total?: number; }; -export type THeaderFilterProps = Omit<THeaderProps, 'total' | 'sort'>; +export type THeaderFilterProps = Pick< + THeaderProps, + 'types' | 'dateField' | 'setFilterOptions' +>; +export type THeaderTitleProps = Pick< + THeaderProps, + 'total' | 'query' | 'locate' | 'issuer' | 'setFilterOptions' +>; export type THandleMenuSelect = MenuProps['onClick']; export type TTypeDateItem = { key: string; diff --git a/src/shared/molecules/MyDataHeader/MyDataHeader.tsx b/src/shared/molecules/MyDataHeader/MyDataHeader.tsx index 9eeea6b3f..a44bb5086 100644 --- a/src/shared/molecules/MyDataHeader/MyDataHeader.tsx +++ b/src/shared/molecules/MyDataHeader/MyDataHeader.tsx @@ -1,19 +1,8 @@ import * as React from 'react'; -import * as pluralize from 'pluralize'; -import { THeaderProps, TTitleProps } from '../../../shared/canvas/MyData/types'; -import MyDataHeaderFilters from './MyDataHeaderFilters'; -import { prettifyNumber } from '../../../utils/formatNumber'; +import { THeaderProps } from '../../../shared/canvas/MyData/types'; +import { MyDataHeaderTitle, MyDataHeaderFilters } from './MyDataHeaderFilters'; import './styles.less'; -const Title = ({ text, label, total }: TTitleProps) => { - return ( - <div className="my-data-table-header-title"> - <span>{text}</span> - <span>{total ? `${prettifyNumber(total)} ${label}` : ''}</span> - </div> - ); -}; - const MyDataHeader: React.FC<THeaderProps> = ({ total, types, @@ -24,12 +13,17 @@ const MyDataHeader: React.FC<THeaderProps> = ({ issuer, }) => { return ( - <div className="my-data-table-header"> - <Title - text="My data" - label={pluralize('Dataset', Number(total))} - total={total} + <div className="my-data-header"> + <MyDataHeaderTitle + {...{ + total, + query, + setFilterOptions, + locate, + issuer, + }} /> + <div className="divider" /> <MyDataHeaderFilters {...{ types, diff --git a/src/shared/molecules/MyDataHeader/MyDataHeaderFilters/IssuerSelector.tsx b/src/shared/molecules/MyDataHeader/MyDataHeaderFilters/IssuerSelector.tsx index 53016486e..6a6b0fd39 100644 --- a/src/shared/molecules/MyDataHeader/MyDataHeaderFilters/IssuerSelector.tsx +++ b/src/shared/molecules/MyDataHeader/MyDataHeaderFilters/IssuerSelector.tsx @@ -1,10 +1,7 @@ import { Radio, RadioChangeEvent } from 'antd'; -import { THeaderFilterProps } from 'shared/canvas/MyData/types'; +import { THeaderProps } from 'shared/canvas/MyData/types'; -type TIssuerSelectorProps = Pick< - THeaderFilterProps, - 'issuer' | 'setFilterOptions' ->; +type TIssuerSelectorProps = Pick<THeaderProps, 'issuer' | 'setFilterOptions'>; const IssuerSelector = ({ issuer, setFilterOptions }: TIssuerSelectorProps) => { const onIssuerChange = (e: RadioChangeEvent) => @@ -12,7 +9,7 @@ const IssuerSelector = ({ issuer, setFilterOptions }: TIssuerSelectorProps) => { return ( <Radio.Group - className="issuer-selector" + className="my-data-header-title_issuer_selector" defaultValue={'createdBy'} value={issuer} onChange={onIssuerChange} diff --git a/src/shared/molecules/MyDataHeader/MyDataHeaderFilters/PageTitle.tsx b/src/shared/molecules/MyDataHeader/MyDataHeaderFilters/PageTitle.tsx new file mode 100644 index 000000000..8f9ae7015 --- /dev/null +++ b/src/shared/molecules/MyDataHeader/MyDataHeaderFilters/PageTitle.tsx @@ -0,0 +1,13 @@ +import { TTitleProps } from 'shared/canvas/MyData/types'; +import { prettifyNumber } from '../../../../utils/formatNumber'; + +const PageTitle = ({ text, label, total }: TTitleProps) => { + return ( + <div className="my-data-header-title_heading"> + <span>{text}</span> + <span>{total ? `${prettifyNumber(total)} ${label}` : ''}</span> + </div> + ); +}; + +export default PageTitle; diff --git a/src/shared/molecules/MyDataHeader/MyDataHeaderFilters/SearchInput.tsx b/src/shared/molecules/MyDataHeader/MyDataHeaderFilters/SearchInput.tsx index f5de2a75d..71cb13378 100644 --- a/src/shared/molecules/MyDataHeader/MyDataHeaderFilters/SearchInput.tsx +++ b/src/shared/molecules/MyDataHeader/MyDataHeaderFilters/SearchInput.tsx @@ -1,9 +1,9 @@ import { Checkbox, Input } from 'antd'; import { CheckboxChangeEvent } from 'antd/lib/checkbox'; -import { THeaderFilterProps } from 'shared/canvas/MyData/types'; +import { THeaderProps } from 'shared/canvas/MyData/types'; type TSearchInputProps = Pick< - THeaderFilterProps, + THeaderProps, 'locate' | 'query' | 'setFilterOptions' >; const SearchInput = ({ @@ -16,11 +16,11 @@ const SearchInput = ({ const handleQueryChange: React.ChangeEventHandler<HTMLInputElement> = event => setFilterOptions({ query: event.target.value }); return ( - <div className="my-data-search-container"> + <div className="my-data-header-title_search"> <Input.Search allowClear className="my-data-search" - placeholder="Search dataset" + placeholder="Search for data" bordered={false} value={query} onChange={handleQueryChange} diff --git a/src/shared/molecules/MyDataHeader/MyDataHeaderFilters/TypeSelector.tsx b/src/shared/molecules/MyDataHeader/MyDataHeaderFilters/TypeSelector.tsx index b1a7f3263..056fe04f3 100644 --- a/src/shared/molecules/MyDataHeader/MyDataHeaderFilters/TypeSelector.tsx +++ b/src/shared/molecules/MyDataHeader/MyDataHeaderFilters/TypeSelector.tsx @@ -2,7 +2,7 @@ import React, { useCallback, useRef, useState } from 'react'; import { NexusClient } from '@bbp/nexus-sdk'; import { useNexusContext } from '@bbp/react-nexus'; import { Checkbox, Col, Dropdown, Input, Row, Select } from 'antd'; -import { concat, isString, startCase } from 'lodash'; +import { isString, startCase } from 'lodash'; import { useQuery } from 'react-query'; import { THeaderProps, diff --git a/src/shared/molecules/MyDataHeader/MyDataHeaderFilters/index.tsx b/src/shared/molecules/MyDataHeader/MyDataHeaderFilters/index.tsx index 30c50ffb3..e165e4f7d 100644 --- a/src/shared/molecules/MyDataHeader/MyDataHeaderFilters/index.tsx +++ b/src/shared/molecules/MyDataHeader/MyDataHeaderFilters/index.tsx @@ -1,27 +1,52 @@ +import * as pluralize from 'pluralize'; import TypeSelector from './TypeSelector'; import DateSelector from './DateSelector'; -import IssuerSelector from './IssuerSelector'; import DateFieldSelector from './DateFieldSelector'; +import PageTitle from './PageTitle'; +import IssuerSelector from './IssuerSelector'; import SearchInput from './SearchInput'; -import { THeaderFilterProps } from '../../../canvas/MyData/types'; +import { + THeaderFilterProps, + THeaderTitleProps, +} from '../../../canvas/MyData/types'; -const MyDataHeaderFilters = ({ - types, - dateField, +const MyDataHeaderTitle = ({ + total, query, + setFilterOptions, locate, issuer, +}: THeaderTitleProps) => { + return ( + <div className="my-data-header-title"> + <div className="left"> + <PageTitle + text="My data" + label={pluralize('Dataset', Number(total))} + total={total} + /> + <IssuerSelector {...{ issuer, setFilterOptions }} /> + </div> + <div className="right"> + <SearchInput {...{ query, locate, setFilterOptions }} /> + </div> + </div> + ); +}; + +const MyDataHeaderFilters = ({ + types, + dateField, setFilterOptions, }: THeaderFilterProps) => { return ( - <div className="my-data-table-header-actions"> - <IssuerSelector {...{ issuer, setFilterOptions }} /> + <div className="my-data-header-actions"> + <span className="filter-heading">Filter: </span> <DateFieldSelector {...{ dateField, setFilterOptions }} /> <DateSelector {...{ dateField, setFilterOptions }} /> <TypeSelector {...{ types, setFilterOptions }} /> - <SearchInput {...{ query, locate, setFilterOptions }} /> </div> ); }; -export default MyDataHeaderFilters; +export { MyDataHeaderFilters, MyDataHeaderTitle }; diff --git a/src/shared/molecules/MyDataHeader/styles.less b/src/shared/molecules/MyDataHeader/styles.less index 17d103eea..35ddfee7f 100644 --- a/src/shared/molecules/MyDataHeader/styles.less +++ b/src/shared/molecules/MyDataHeader/styles.less @@ -1,64 +1,159 @@ @import '../../lib.less'; -.my-data-table-header { +.my-data-header { display: flex; align-items: flex-start; justify-content: space-between; - flex-direction: column; + flex-flow: column nowrap; + margin-top: 15px; + .divider { + width: 100%; + height: 1px; + background-color: #e8e8e8; + margin: 30px 0 18px; + } +} + +.my-data-header-title { + width: 100%; + display: flex; + align-items: center; + justify-content: space-between; + + .left { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + } + + .right { + display: flex; + align-items: flex-end; + justify-content: space-between; + gap: 10px; + } +} + +.my-data-header-title_heading { + user-select: none; + margin-right: 20px; + + span:first-child { + font-weight: 700; + font-size: 30px; + line-height: 110%; + color: @fusion-main-color; + } + + span:last-child { + font-weight: 400; + font-size: 14px; + line-height: 22px; + letter-spacing: 0.01em; + color: #8c8c8c; + padding-left: 10px; + } +} + +.my-data-header-title_issuer_selector { + width: 100%; + max-width: max-content; + + .ant-radio + span { + color: @fusion-main-color; + } +} + +.my-data-header-title_search { + width: 100%; + min-width: 250px; + display: flex; + flex-direction: row; + align-items: flex-end; + gap: 10px; + + // margin-top: 52px; + .filter-options { + display: flex; + align-items: center; + justify-content: space-evenly; + column-gap: 10px; + width: 100%; - &-title { + .ant-checkbox-inner { + border-radius: 4px !important; + border-color: @fusion-main-color !important; + } + } + + .locate-text, + .spread-text { user-select: none; - margin: 20px 0 10px; - margin-right: 20px; + color: @fusion-main-color; + } - span:first-child { - font-family: 'Titillium Web'; - font-style: normal; - font-weight: 700; - font-size: 28px; - line-height: 110%; - color: @fusion-main-color; + .my-data-search { + border-bottom: 1px solid @fusion-main-color !important; + min-width: 350px; + width: 100%; + + .ant-btn { + border: none; } - span:last-child { - font-family: 'Titillium Web'; - font-style: normal; - font-weight: 400; - font-size: 14px; - line-height: 22px; - letter-spacing: 0.01em; - color: #8c8c8c; - padding-left: 10px; + .anticon.anticon-search { + color: @fusion-main-color; } } +} - &-actions { - width: 100%; - margin: 0; - display: flex; - flex-flow: row wrap; - // grid-template-columns: max-content max-content 300px 300px 1fr; - gap: 15px; - align-items: center; - justify-content: flex-start; - align-content: center; +.my-data-header-actions { + width: 100%; + margin: 0; + display: flex; + flex-flow: row wrap; + gap: 15px; + align-items: center; + justify-content: flex-start; + align-content: center; + + .filter-heading { + font-weight: 400; + font-size: 14px; + line-height: 110%; + color: @fusion-neutral-7; } - .issuer-selector, + .date-field-selector { width: 100%; max-width: max-content; + + span { + color: @fusion-main-color; + } } - .my-data-date-picker, - .my-data-type-picker { + + .my-data-date-picker { width: 100%; max-width: 300px; + + input::placeholder, + input::-webkit-input-placeholder { + color: @fusion-main-color !important; + } } - .my-data-search-container { - flex: 0 1 25%; + + .my-data-type-picker { width: 100%; - min-width: 250px; + max-width: 300px; + + .ant-select-selection-placeholder { + color: @fusion-main-color !important; + } } } + .radio-filter { &.ant-radio-wrapper-checked { span:nth-child(2) { @@ -66,6 +161,7 @@ } } } + .date-type-selector { display: flex !important; align-items: center; @@ -73,6 +169,7 @@ gap: 15px; margin-bottom: 15px !important; } + .my-data-date { &-container { position: relative; @@ -107,10 +204,12 @@ align-items: center; justify-content: center; flex-direction: column; + p { font-size: 10px; color: red; } + .range-born { font-weight: 200; color: #40a9ff; @@ -154,22 +253,27 @@ .ant-select-selection-placeholder { color: @fusion-daybreak-10 !important; } + .ant-select-selection-overflow { flex-wrap: nowrap; overflow: scroll; + // style the scrollbar of this ant-select-selection-overflow &::-webkit-scrollbar { width: 2px; height: 2px; } + &::-webkit-scrollbar-thumb { background: @fusion-daybreak-10; } } } + .my-data-type-picker-popup { display: none; } + .my-data-type-filter { &-popover { background-color: white !important; @@ -177,33 +281,6 @@ } } -.my-data-search-container { - display: flex; - flex-direction: column; - align-items: flex-start; - column-gap: 5px; - // margin-top: 52px; - .filter-options { - display: flex; - align-items: center; - justify-content: space-evenly; - column-gap: 10px; - } - .locate-text, - .spread-text { - color: #8c8c8c; - } -} - -.my-data-search { - border-bottom: 1px solid @fusion-neutral-5 !important; - min-width: 350px; - width: 100%; - .ant-btn { - border: none; - } -} - .my-data-type-filter { &-overlay { background-color: white; @@ -225,11 +302,13 @@ -ms-overflow-style: none; scrollbar-width: none; + .no-types-content { display: flex; align-items: center; justify-content: center; height: 100%; + span { user-select: none; font-size: 12px; @@ -270,9 +349,11 @@ outline: none; box-shadow: none; } + .ant-input-affix-wrapper { border: none; background-color: transparent; + &-focused { box-shadow: none; outline: none; From 48563b219da566b5b2d0030134234fa97dd4ddce Mon Sep 17 00:00:00 2001 From: Bilal MEDDAH <b_meddah@esi.dz> Date: Tue, 18 Jul 2023 13:00:48 +0200 Subject: [PATCH 9/9] f-3945/update: clean selectCallback fn type --- .../MyDataHeader/MyDataHeaderFilters/TypeSelector.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/shared/molecules/MyDataHeader/MyDataHeaderFilters/TypeSelector.tsx b/src/shared/molecules/MyDataHeader/MyDataHeaderFilters/TypeSelector.tsx index 056fe04f3..918579c40 100644 --- a/src/shared/molecules/MyDataHeader/MyDataHeaderFilters/TypeSelector.tsx +++ b/src/shared/molecules/MyDataHeader/MyDataHeaderFilters/TypeSelector.tsx @@ -93,10 +93,9 @@ const TypeSelector = ({ const [typeSearchValue, updateSearchType] = useState(''); const [typesOptionsArray, setTypesOptionsArray] = useState<TType[]>([]); - const selectCallback = useCallback((data: any) => { + const selectCallback = useCallback((data: TTypeAggregationsResult) => { const options = ( - ((data as unknown) as TTypeAggregationsResult).aggregations.types - ?.buckets ?? ([] as TTypesAggregatedBucket[]) + data.aggregations.types?.buckets ?? ([] as TTypesAggregatedBucket[]) ).map<TType>(item => typesOptionsBuilder(item)); originTypes.current = options; return options;