diff --git a/packages/react/src/components/CardEditor/CardEditForm/CardEditFormItems/DataSeriesFormItems/DataSeriesFormContent.jsx b/packages/react/src/components/CardEditor/CardEditForm/CardEditFormItems/DataSeriesFormItems/DataSeriesFormContent.jsx index ae70eea26f..f6a77960a3 100644 --- a/packages/react/src/components/CardEditor/CardEditForm/CardEditFormItems/DataSeriesFormItems/DataSeriesFormContent.jsx +++ b/packages/react/src/components/CardEditor/CardEditForm/CardEditFormItems/DataSeriesFormItems/DataSeriesFormContent.jsx @@ -20,6 +20,9 @@ import { Dropdown } from '../../../../Dropdown'; import DataSeriesFormItemModal from '../DataSeriesFormItemModal'; import { CARD_TYPES, BAR_CHART_TYPES } from '../../../../../constants/LayoutConstants'; import ContentFormItemTitle from '../ContentFormItemTitle'; +import HierarchyDataFormItems, { + isHierarchyDataItem, +} from '../HierarchyDataFormItems/HierarchyDataFormItems'; import BarChartDataSeriesContent from './BarChartDataSeriesContent'; @@ -328,6 +331,32 @@ const DataSeriesFormItem = ({ [cardConfig, dataSection, onChange, setSelectedDataItems, validDataItems] ); + const handleHierarchyDataItemChange = useCallback( + (items) => { + const updatedItems = items.map((item) => ({ + ...item, + // create a unique dataSourceId if it's going into attributes + // if it's going into the groupBy section then just use the dataItem ID + dataSourceId: `${item.dataItemId}_${uuidv4()}`, + })); + + const selectedItems = canMultiSelectDataItems + ? [...dataSection, ...updatedItems] + : [updatedItems[0]]; + + const newCard = handleDataSeriesChange( + selectedItems, + cardConfig, + setEditDataSeries, + undefined, + removedItemsCountRef + ); + setSelectedDataItems(selectedItems.map(({ dataSourceId }) => dataSourceId)); + onChange(newCard); + }, + [canMultiSelectDataItems, cardConfig, dataSection, onChange, setSelectedDataItems] + ); + const handleEditButton = useCallback( async (dataItem, i) => { const dataItemWithMetaData = validDataItems?.find( @@ -387,62 +416,67 @@ const DataSeriesFormItem = ({ [cardConfig, dataSection, onChange, removedDataItems, setSelectedDataItems] ); + const generateListItems = useCallback( + (data, isHierarchy = false) => + data + ?.filter((dataItem) => isHierarchyDataItem(dataItem) === isHierarchy) + .map((dataItem, i) => { + const colorIndex = (i + removedItemsCountRef.current) % DATAITEM_COLORS_OPTIONS.length; + const iconColorOption = dataItem.color || DATAITEM_COLORS_OPTIONS[colorIndex]; + return { + id: dataItem.dataSourceId, + content: { + value: dataItem.label || dataItem.dataItemId, + icon: + cardConfig.type === CARD_TYPES.TIMESERIES || cardConfig.type === CARD_TYPES.BAR ? ( +
+ ) : null, + rowActions: () => [ + + } + title="" + items={hierarchyDataItemListItems} + /> + + ) + ); +}; + +HierarchyDataFormItems.propTypes = propTypes; +HierarchyDataFormItems.defaultProps = defaultProps; + +export default HierarchyDataFormItems; diff --git a/packages/react/src/components/CardEditor/CardEditForm/CardEditFormItems/TableCardFormItems/TableCardFormContent.jsx b/packages/react/src/components/CardEditor/CardEditForm/CardEditFormItems/TableCardFormItems/TableCardFormContent.jsx index 7b8a90d556..5a4ad315e5 100644 --- a/packages/react/src/components/CardEditor/CardEditForm/CardEditFormItems/TableCardFormItems/TableCardFormContent.jsx +++ b/packages/react/src/components/CardEditor/CardEditForm/CardEditFormItems/TableCardFormItems/TableCardFormContent.jsx @@ -20,6 +20,9 @@ import DataSeriesFormItemModal from '../DataSeriesFormItemModal'; import ContentFormItemTitle from '../ContentFormItemTitle'; import { CARD_SIZES, CARD_TYPES } from '../../../../../constants/LayoutConstants'; import { formatDataItemsForDropdown } from '../DataSeriesFormItems/DataSeriesFormContent'; +import HierarchyDataFormItems, { + isHierarchyDataItem, +} from '../HierarchyDataFormItems/HierarchyDataFormItems'; const { iotPrefix } = settings; @@ -209,6 +212,23 @@ const TableCardFormContent = ({ } }; + const handleHierarchyDataItemChange = useCallback( + (items) => { + const updatedItems = items.map((item) => ({ + ...item, + // create a unique dataSourceId if it's going into attributes + // if it's going into the groupBy section then just use the dataItem ID + dataSourceId: `${item.dataItemId}_${uuidv4()}`, + })); + const selectedItems = [...dataSection, ...updatedItems]; + + const newCard = handleDataSeriesChange(selectedItems, cardConfig, null, null); + setSelectedDataItems(selectedItems.map(({ text }) => text)); + onChange(newCard); + }, + [cardConfig, dataSection, onChange, setSelectedDataItems] + ); + // need to handle thresholds from the DataSeriesFormItemModal and convert it to the right format const handleDataItemModalChanges = useCallback( (card) => { @@ -319,40 +339,52 @@ const TableCardFormContent = ({ [cardConfig, onEditDataItem, validDataItems] ); + const generateListItems = useCallback( + (data, isHierarchy = false) => + data + ?.filter((dataItem) => isHierarchyDataItem(dataItem) === isHierarchy) + ?.map((dataItem) => ({ + id: dataItem.dataSourceId, + content: { + value: dataItem.label || dataItem.dataItemId, + icon: null, + rowActions: () => [ +
- {!isEmpty(validDimensions) ? ( + {!isEmpty(validDimensions) && (
@@ -458,7 +490,7 @@ const TableCardFormContent = ({ titleText={mergedI18n.dataItemEditorDimensionTitle} />
- ) : null} + )} + + ); }; diff --git a/packages/react/src/components/DashboardEditor/DashboardEditor.story.jsx b/packages/react/src/components/DashboardEditor/DashboardEditor.story.jsx index ac44ee748f..a1be3a3b5b 100644 --- a/packages/react/src/components/DashboardEditor/DashboardEditor.story.jsx +++ b/packages/react/src/components/DashboardEditor/DashboardEditor.story.jsx @@ -1204,7 +1204,6 @@ export const I18N = () => ( yCoordinateDropdownLabelText: 'yCoordinateDropdownLabelText', selectDataItemsText: 'selectDataItemsText', dataItemText: 'dataItemText', - editText: 'editText', // Hotspot Text Style Tab fields textTypeStyleInfoText: 'textTypeStyleInfoText', fontColorLabelText: 'fontColorLabelText', @@ -1519,3 +1518,89 @@ export const withGetDefaultCard = () => { }; withGetDefaultCard.storyName = 'With get default card'; + +export const withHierarchyDataItems = () => { + const hierarchyDataItems = object('hierarchyDataItems', [ + { + dataItemId: 'speed', + label: `speed:Device1`, + resourceData: { + type: 'DEVICE', + uuid: '12345', + deviceTypeUUId: '7890', + }, + }, + { + dataItemId: 'pressure', + label: `pressure:Asset1`, + resourceData: { + type: 'ASSET', + uuid: '2', + siteUUId: '1', + }, + }, + ]); + const actions = { + ...commonActions, + onAddHierarchyDataItems: (cardConfig, handleHierarchyDataItemChange) => + handleHierarchyDataItemChange(hierarchyDataItems), + dataSeriesFormActions: { + ...commonActions.dataSeriesFormActions, + hasHierarchyDataItemsEnabled: (card) => { + const allowCardTypes = [ + CARD_TYPES.TIMESERIES, + CARD_TYPES.BAR, + CARD_TYPES.VALUE, + CARD_TYPES.TABLE, + CARD_TYPES.IMAGE, + ]; + return allowCardTypes.includes(card.type); + }, + }, + }; + + return ( + mockDataItems} + dataItems={mockDataItems} + availableImages={images} + i18n={{ + headerEditTitleButton: 'Edit title updated', + }} + onAddImage={action('onAddImage')} + onImport={action('onImport')} + onExport={action('onExport')} + onDelete={action('onDelete')} + onCancel={action('onCancel')} + onSubmit={action('onSubmit')} + actions={actions} + onImageDelete={action('onImageDelete')} + onLayoutChange={action('onLayoutChange')} + isSubmitDisabled={boolean('isSubmitDisabled', false)} + isSubmitLoading={boolean('isSubmitLoading', false)} + availableDimensions={{ + deviceid: ['73000', '73001', '73002'], + manufacturer: ['rentech', 'GHI Industries'], + }} + supportedCardTypes={array('supportedCardTypes', [ + 'TIMESERIES', + 'SIMPLE_BAR', + 'GROUPED_BAR', + 'STACKED_BAR', + 'VALUE', + 'IMAGE', + 'TABLE', + 'CUSTOM', + ])} + headerBreadcrumbs={[ + Dashboard library, + Favorites, + ]} + isLoading={boolean('isLoading', false)} + onCardSelect={action('onCardSelect')} + /> + ); +}; + +withHierarchyDataItems.storyName = 'with hierarchy data items'; diff --git a/packages/react/src/components/DashboardEditor/editorUtils.jsx b/packages/react/src/components/DashboardEditor/editorUtils.jsx index d6e3606aad..a620ee99f6 100644 --- a/packages/react/src/components/DashboardEditor/editorUtils.jsx +++ b/packages/react/src/components/DashboardEditor/editorUtils.jsx @@ -692,6 +692,11 @@ export const DashboardEditorActionsPropTypes = PropTypes.shape({ ] */ onEditDataItem: PropTypes.func, + /** Call back function for on click of add hierarchy data items button, returns a selcted dataSource + * onAddHierarchyDataItems(cardProps: card properties, handleHierarchyDataItemChange: callback function to handle hierarchy data items change) + * return: void + */ + onAddHierarchyDataItems: PropTypes.func, /** Form actions for dataSeries modal */ dataSeriesFormActions: PropTypes.shape({ /** callback function to determine aggregation dropdown visibility @@ -709,6 +714,11 @@ export const DashboardEditorActionsPropTypes = PropTypes.shape({ * return {boolean} : true or false */ hasDataFilterDropdown: PropTypes.func, + /** callback function to determine hierarchyDataItems dropdown visibility + * hasHierarchyDataItemsEnabled(cardProps: card properties) + * return {boolean} : true or false + */ + hasHierarchyDataItemsEnabled: PropTypes.func, }), }); @@ -717,9 +727,11 @@ const noop = () => {}; export const defaultDashboardEditorActionsProps = { onEditDataItem: noop, + onAddHierarchyDataItems: noop, dataSeriesFormActions: { hasAggregationsDropDown: noop, hasGrainsDropDown: noop, hasDataFilterDropdown: noop, + hasHierarchyDataItemsEnabled: noop, }, }; diff --git a/packages/react/src/components/HotspotEditorModal/HotspotEditorDataSourceTab/HotspotEditorDataSourceTab.jsx b/packages/react/src/components/HotspotEditorModal/HotspotEditorDataSourceTab/HotspotEditorDataSourceTab.jsx index c362e60264..931859a50e 100644 --- a/packages/react/src/components/HotspotEditorModal/HotspotEditorDataSourceTab/HotspotEditorDataSourceTab.jsx +++ b/packages/react/src/components/HotspotEditorModal/HotspotEditorDataSourceTab/HotspotEditorDataSourceTab.jsx @@ -1,8 +1,10 @@ -import React, { useState, useCallback } from 'react'; +import React, { useState, useCallback, useMemo } from 'react'; import PropTypes from 'prop-types'; import { Edit } from '@carbon/react/icons'; import { MultiSelect } from '@carbon/react'; import { isEmpty } from 'lodash-es'; +import { v4 as uuidv4 } from 'uuid'; +import { MisuseOutline } from '@carbon/icons-react'; import DataSeriesFormItemModal from '../../CardEditor/CardEditForm/CardEditFormItems/DataSeriesFormItemModal'; import List from '../../List/List'; @@ -13,6 +15,9 @@ import { DashboardEditorActionsPropTypes, defaultDashboardEditorActionsProps, } from '../../DashboardEditor/editorUtils'; +import HierarchyDataFormItems, { + isHierarchyDataItem, +} from '../../CardEditor/CardEditForm/CardEditFormItems/HierarchyDataFormItems/HierarchyDataFormItems'; const { iotPrefix } = settings; @@ -47,7 +52,8 @@ const propTypes = { i18n: PropTypes.shape({ selectDataItemsText: PropTypes.string, dataItemText: PropTypes.string, - editText: PropTypes.string, + edit: PropTypes.string, + remove: PropTypes.string, dataItemEditorDataItemTitle: PropTypes.string, dataItemEditorDataItemCustomLabel: PropTypes.string, dataItemEditorDataItemUnit: PropTypes.string, @@ -85,7 +91,8 @@ const defaultProps = { i18n: { selectDataItemsText: 'Select data items', dataItemText: 'Data items', - editText: 'Edit', + edit: 'Edit', + remove: 'Remove', dataItemEditorDataItemTitle: 'Data items', dataItemEditorDataItemCustomLabel: 'Custom label', dataItemEditorDataItemUnit: 'Unit', @@ -102,10 +109,12 @@ const defaultProps = { }; export const formatDataItemsForDropdown = (dataItems) => - dataItems?.map(({ dataSourceId, label }) => ({ - id: dataSourceId, - label, - })); + dataItems + ?.filter((dataItem) => !isHierarchyDataItem(dataItem)) // filter hierarchy data items + ?.map(({ dataSourceId, label }) => ({ + id: dataSourceId, + label, + })); const HotspotEditorDataSourceTab = ({ hotspot, @@ -127,7 +136,6 @@ const HotspotEditorDataSourceTab = ({ const selectedItemsArray = hotspot.content?.attributes || []; const baseClassName = `${iotPrefix}--card-edit-form`; - const initialSelectedItems = formatDataItemsForDropdown(selectedItemsArray); const { onEditDataItem } = actions; const handleSelectionChange = ({ selectedItems }) => { @@ -135,7 +143,7 @@ const HotspotEditorDataSourceTab = ({ // loop through selected Items and find their selectedItemsArray object or the dataItem object with same id selectedItems.forEach((item) => { const containedItem = selectedItemsArray.find( - (selectedItem) => selectedItem.dataItemId === item.id + (selectedItem) => selectedItem.dataSourceId === item.id ); const containedDataItem = dataItems.find( (selectedItem) => selectedItem.dataItemId === item.id @@ -146,17 +154,37 @@ const HotspotEditorDataSourceTab = ({ newArray.push(containedDataItem); } }); + + // Add existing hierarchy data items + newArray.push(...selectedItemsArray.filter((item) => isHierarchyDataItem(item))); + onChange({ attributes: newArray }); }; + const handleHierarchyDataItemChange = useCallback( + (items) => { + const updatedItems = items?.map((item) => ({ + ...item, + // create a unique dataSourceId + dataSourceId: `${item.dataItemId}_${uuidv4()}`, + })); + + const selectedItems = [...selectedItemsArray, ...updatedItems]; + + onChange({ attributes: selectedItems }); + }, + [onChange, selectedItemsArray] + ); + // MultiSelect - // For the initial selection to work the objects in prop "initialSelectedItems" + // For the initial selection to work the objects in prop "selectedItemsArray" // must be identical to the objects in prop "items". It is not enough that the // ids are the same. Therefore, we must adjust the labels in "items" if they have - // been modified in the "initialSelectedItems". + // been modified in the "selectedItemsArray". const multiSelectItems = formatDataItemsForDropdown(dataItems).map((item) => ({ ...item, - label: initialSelectedItems.find((selected) => selected.id === item.id)?.label ?? item.label, + label: + selectedItemsArray.find((selected) => selected.dataSourceId === item.id)?.label ?? item.label, })); const handleEditButton = useCallback( @@ -182,6 +210,64 @@ const HotspotEditorDataSourceTab = ({ [cardConfig, onEditDataItem, dataItems] ); + const handleRemoveButton = useCallback( + (selectedItem) => { + const newArray = selectedItemsArray.filter( + (item) => item.dataSourceId !== selectedItem.dataSourceId + ); + + onChange({ attributes: newArray }); + }, + [selectedItemsArray, onChange] + ); + + const generateListItems = useCallback( + (data, isHierarchy = false) => + data + ?.filter((dataItem) => isHierarchyDataItem(dataItem) === isHierarchy) + ?.map((dataItem) => ({ + id: dataItem.dataSourceId, + content: { + value: dataItem.label, + rowActions: () => [ +