diff --git a/packages/dm-core-plugins/src/data-grid/ColumnHeader/ColumnHeader.tsx b/packages/dm-core-plugins/src/data-grid/ColumnHeader/ColumnHeader.tsx deleted file mode 100644 index 7d1003ee8..000000000 --- a/packages/dm-core-plugins/src/data-grid/ColumnHeader/ColumnHeader.tsx +++ /dev/null @@ -1,108 +0,0 @@ -import React, { useState } from 'react' -import { Icon, Menu, Typography } from '@equinor/eds-core-react' -import * as Styled from '../styles' -import * as utils from '../utils' -import { add, delete_to_trash } from '@equinor/eds-icons' -import { TAttribute } from '@development-framework/dm-core' - -type ColumnHeaderProps = { - attributeType: string - column: any - columns: number[] - columnsAreSetDimension: boolean - data: any[] - deleteColumn: (index: number) => void - index: number - multi: boolean - selectedColumn: number | undefined - setColumns: React.Dispatch> - setData: (data: any[]) => void - setSelectedColumn: React.Dispatch> -} - -export function ColumnHeader(props: ColumnHeaderProps) { - const { - attributeType, - column, - columns, - data, - index, - multi, - selectedColumn, - setColumns, - setData, - setSelectedColumn, - } = props - const [isMenuOpen, setIsMenuOpen] = useState(false) - const [menuButtonAnchor, setMenuButtonAnchor] = - useState(null) - - function changeSelectedColumn(index: number) { - if (index === selectedColumn) { - setSelectedColumn(undefined) - return - } - setSelectedColumn(index) - } - - function handleColumnRightClick(event: any) { - event.preventDefault() - setSelectedColumn(index) - setIsMenuOpen(true) - } - - function addColumn(placement: 'before' | 'after') { - const newIndex = placement === 'before' ? index : index + 1 - const newColumns = utils.createArrayFromNumber(columns.length + 1) - const fillValue = utils.getFillValue(attributeType) - const updatedData = data.map((item) => { - item.splice(newIndex, 0, fillValue) - return item - }) - setData(updatedData) - setColumns(newColumns) - setSelectedColumn(newIndex) - } - - function onContextMenuClose() { - setIsMenuOpen(false) - setSelectedColumn(undefined) - } - - return ( - - (multi ? changeSelectedColumn(index) : null)} - onContextMenu={handleColumnRightClick} - ref={setMenuButtonAnchor} - > - {utils.columnLabels[column - 1]} - - - {props.columnsAreSetDimension || !multi ? ( - - Columns are pre-defined and cannot be deleted or added. - - ) : ( - <> - props.deleteColumn(index)}> - Delete column - - addColumn('before')}> - Add 1 column left - - addColumn('after')}> - Add 1 column right - - - )} - - - ) -} diff --git a/packages/dm-core-plugins/src/data-grid/DataCell/DataCell.tsx b/packages/dm-core-plugins/src/data-grid/DataCell/DataCell.tsx index f8bd4e663..dcf682e71 100644 --- a/packages/dm-core-plugins/src/data-grid/DataCell/DataCell.tsx +++ b/packages/dm-core-plugins/src/data-grid/DataCell/DataCell.tsx @@ -1,19 +1,22 @@ import React, { ChangeEvent } from 'react' import * as Styled from '../styles' import { Checkbox } from '@equinor/eds-core-react' +import { DataGridConfig } from '../types' type DataCellProps = { - selected: boolean - rowIndex: number + attributeType: string cellIndex?: number - value: string | number | boolean + config: DataGridConfig data: any[] + rowIndex: number + selected: boolean setData: (data: any[]) => void - attributeType: string + value: string | number | boolean } export function DataCell(props: DataCellProps) { - const { attributeType, data, setData, rowIndex, value, cellIndex } = props + const { attributeType, data, setData, rowIndex, value, cellIndex, config } = + props function parseValue(event: ChangeEvent) { const { value, checked } = event.target @@ -47,12 +50,14 @@ export function DataCell(props: DataCellProps) { updateValue(event, rowIndex, cellIndex)} + readOnly={!config.editable} /> ) : ( updateValue(event, rowIndex, cellIndex)} attributeType={attributeType} + readOnly={!config.editable} /> )} diff --git a/packages/dm-core-plugins/src/data-grid/DataGrid.tsx b/packages/dm-core-plugins/src/data-grid/DataGrid.tsx index 6131bdd8f..15f31d16b 100644 --- a/packages/dm-core-plugins/src/data-grid/DataGrid.tsx +++ b/packages/dm-core-plugins/src/data-grid/DataGrid.tsx @@ -1,25 +1,19 @@ import React, { useState, useMemo, useEffect } from 'react' -import { Stack, TAttribute } from '@development-framework/dm-core' -import { Button, EdsProvider, Icon } from '@equinor/eds-core-react' +import { Stack } from '@development-framework/dm-core' +import { EdsProvider, Icon, Typography } from '@equinor/eds-core-react' import { add, chevron_down, chevron_up, minimize } from '@equinor/eds-icons' import * as Styled from './styles' import * as utils from './utils' import { DataCell } from './DataCell/DataCell' import { DataGridPagination } from './DataGridPagination/DataGridPagination' -import { ColumnHeader } from './ColumnHeader/ColumnHeader' -import { VerticalHeader } from './VerticalHeader/VerticalHeader' - -type DataGridProps = { - attributeType: string - dimensions?: string - data: any[] - setData: (data: any[]) => void - initialRowsPerPage?: number -} +import { HeaderCell } from './HeaderCell/HeaderCell' +import { DataGridConfig, DataGridProps, defaultConfig } from './types' export function DataGrid(props: DataGridProps) { - const { data, attributeType, dimensions, setData } = props - const [columns, setColumns] = useState([]) + const { data, attributeType, dimensions, setData, config: userConfig } = props + const config: DataGridConfig = { ...defaultConfig, ...userConfig } + const [columnLabels, setColumnLabels] = useState([]) + const [rowLabels, setRowLabels] = useState([]) const [selectedRow, setSelectedRow] = useState(undefined) const [selectedColumn, setSelectedColumn] = useState( undefined @@ -27,35 +21,49 @@ export function DataGrid(props: DataGridProps) { const [paginationPage, setPaginationPage] = useState(0) const [rowsPerPage, setRowsPerPage] = useState(props.initialRowsPerPage || 25) - const dataGridId = useMemo(() => crypto.randomUUID(), []) - const multi: boolean = dimensions?.includes(',') || false + const dataGridId: string = useMemo(() => crypto.randomUUID(), []) const fillValue = utils.getFillValue(attributeType) const paginatedRows = data.slice( paginationPage * rowsPerPage, paginationPage * rowsPerPage + rowsPerPage ) - const [definedColumns, definedRows] = dimensions?.split(',') || ['*', '*'] - const rowsAreSetDimension = multi - ? definedRows !== '*' - : definedColumns !== '*' + const [ + rowsAreEditable, + columnsAreEditable, + addButtonFunctionality, + addButtonIsEnabled, + isMultiDimensional, + columnDimensions, + isSortEnabled, + ] = utils.getFunctionalityVariables(config, dimensions) useEffect(() => { - const columnsArray = multi - ? definedColumns === '*' - ? utils.createArrayFromNumber(data.length > 0 ? data[0].length : []) - : utils.createArrayFromNumber(parseInt(definedColumns, 10)) - : [1] + const columnLabels = isMultiDimensional + ? columnDimensions === '*' + ? utils.createLabels( + config.columnLabels, + data.length > 0 ? data[0].length : 0 + ) + : utils.createLabels( + config.columnLabels, + parseInt(columnDimensions, 10) + ) + : ['1'] + const rowLabels = utils.createLabels(config.rowLabels, data?.length) - setColumns(columnsArray) + setRowLabels(rowLabels) + setColumnLabels(columnLabels) }, []) function addRow(newIndex?: number) { const newRow = - columns.length > 1 - ? Array.from({ length: columns.length }).fill(fillValue) + columnLabels.length > 1 + ? Array.from({ length: columnLabels.length }).fill(fillValue) : fillValue const dataCopy = [...data] - dataCopy.splice(newIndex || columns.length, 0, newRow) + dataCopy.splice(newIndex || data.length, 0, newRow) + const newLabels = utils.createLabels(config.rowLabels, data.length + 1) + setRowLabels(newLabels) setData(dataCopy) } @@ -77,110 +85,152 @@ export function DataGrid(props: DataGridProps) { } } + function addColumn(newIndex: number) { + const newColumns = utils.createLabels( + config.columnLabels, + columnLabels.length + 1 + ) + const fillValue = utils.getFillValue(attributeType) + const updatedData = data.map((item) => { + item.splice(newIndex, 0, fillValue) + return item + }) + setData(updatedData) + setColumnLabels(newColumns) + setSelectedColumn(newIndex) + } + function deleteColumn(index: number) { - const newColumns = utils.createArrayFromNumber(columns.length - 1) + const newColumns = utils.createLabels( + config.columnLabels, + columnLabels.length - 1 + ) const updatedData = data.map((item) => { item.splice(index, 1) return item }) - setColumns(newColumns) + setColumnLabels(newColumns) setData(updatedData) setSelectedColumn(undefined) } return ( + + {config.title && {config.title}} + {config.description && {config.description}} + - - - - # - - {columns.map((column, index) => ( - - ))} - - {paginatedRows.map((item, rowIndex) => { - const calculatedIndex = paginationPage * rowsPerPage + rowIndex - return ( - - - {multi ? ( - item.map((cellValue: any, cellIndex: number) => ( - + {config.showColumns && ( + + + {config.showRows && #} + {columnLabels.map((column, index) => ( + + ))} + + + )} + + {paginatedRows.map((item, rowIndex) => { + const calculatedIndex = paginationPage * rowsPerPage + rowIndex + return ( + + {config.showRows && ( + + )} + {isMultiDimensional ? ( + item?.map((cellValue: any, cellIndex: number) => ( + + )) + ) : ( + - )) - ) : ( - - )} - - ) - })} + )} + + ) + })} + - {!rowsAreSetDimension && ( + {addButtonIsEnabled && ( addRow()} + onClick={() => + addButtonFunctionality === 'addRow' + ? addRow() + : addColumn(columnLabels.length) + } > )} {selectedRow !== undefined && ( <> - {definedRows === '*' && ( + {rowsAreEditable && ( )} - moveRow('up')}> - - - moveRow('down')}> - - + {isSortEnabled && ( + <> + moveRow('up')}> + + + moveRow('down')}> + + + + )} )} diff --git a/packages/dm-core-plugins/src/data-grid/DataGridPlugin.tsx b/packages/dm-core-plugins/src/data-grid/DataGridPlugin.tsx index 4a13b17d7..3f587319e 100644 --- a/packages/dm-core-plugins/src/data-grid/DataGridPlugin.tsx +++ b/packages/dm-core-plugins/src/data-grid/DataGridPlugin.tsx @@ -8,11 +8,13 @@ import { useDMSS, useDocument, } from '@development-framework/dm-core' -import { DataGrid } from './DataGrid' import { Button } from '@equinor/eds-core-react' +import { DataGridConfig, defaultConfig } from './types' +import { DataGrid } from './DataGrid' export function DataGridPlugin(props: IUIPlugin) { - const { idReference, config, type } = props + const { idReference, config: userConfig, type } = props + const config: DataGridConfig = { ...defaultConfig, ...userConfig } const dmssAPI = useDMSS() const [data, setData] = useState() const [loading, setLoading] = useState(false) @@ -22,14 +24,24 @@ export function DataGridPlugin(props: IUIPlugin) { idReference, 1 ) - const { fieldName, rowsPerPage } = config + const { fieldNames, rowsPerPage } = config + const multiplePrimitives = fieldNames?.length > 1 const attribute = blueprint?.attributes?.find( - (atts: TAttribute) => atts.name === fieldName + (atts: TAttribute) => atts.name === fieldNames[0] + ) + const attributes = fieldNames.map((field) => + blueprint?.attributes.find((att: TAttribute) => att.name === field) ) useEffect(() => { if (isLoading || !document) return - setData(document?.[fieldName] || []) + if (multiplePrimitives) { + const mergedData: string[] = [] + fieldNames.forEach((field) => mergedData.push(document[field])) + setData(mergedData) + return + } + setData(document?.[fieldNames[0]] || []) }, [document, isLoading]) function onChange(data: any[]) { @@ -40,7 +52,13 @@ export function DataGridPlugin(props: IUIPlugin) { async function saveDocument() { setLoading(true) try { - const payload = { ...document, [fieldName]: data } + let newData = { [fieldNames[0]]: data } + if (multiplePrimitives) { + newData = Object.fromEntries( + (data || []).map((value, index) => [fieldNames[index], value]) + ) + } + const payload = { ...document, ...newData } await dmssAPI.documentUpdate({ idAddress: idReference, data: JSON.stringify(payload), @@ -58,15 +76,22 @@ export function DataGridPlugin(props: IUIPlugin) { return !data ? null : ( - + {config.editable && ( + + )} ) } diff --git a/packages/dm-core-plugins/src/data-grid/HeaderCell/HeaderCell.tsx b/packages/dm-core-plugins/src/data-grid/HeaderCell/HeaderCell.tsx new file mode 100644 index 000000000..9a68f0632 --- /dev/null +++ b/packages/dm-core-plugins/src/data-grid/HeaderCell/HeaderCell.tsx @@ -0,0 +1,89 @@ +import React, { useState } from 'react' +import { Icon, Menu, Typography } from '@equinor/eds-core-react' +import * as Styled from '../styles' +import { add as addIcon, delete_to_trash } from '@equinor/eds-icons' + +type HeaderCellProps = { + add: (newIndex: number) => void + editable: boolean + delete: (index: number) => void + index: number + label: string + selected: number | undefined + setSelected: React.Dispatch> + type: 'column' | 'row' +} + +export function HeaderCell(props: HeaderCellProps) { + const { add, index, label, selected, setSelected, type } = props + const [isMenuOpen, setIsMenuOpen] = useState(false) + const [menuButtonAnchor, setMenuButtonAnchor] = + useState(null) + + function changeSelected(index: number) { + if (index === selected) { + setSelected(undefined) + return + } + setSelected(index) + } + + function handleColumnRightClick(event: any) { + event.preventDefault() + setSelected(index) + setIsMenuOpen(true) + } + + function onContextMenuClose() { + setIsMenuOpen(false) + setSelected(undefined) + } + + function setSelectedOnKeyDown(event: any) { + if (event.key === 'Enter' || event.key === ' ') { + event.preventDefault() + changeSelected(index) + } + } + + return ( + changeSelected(index)} + onContextMenu={handleColumnRightClick} + //@ts-ignore + ref={setMenuButtonAnchor} + selected={index === selected} + tabIndex={0} + onKeyDown={setSelectedOnKeyDown} + > + {label} + + {!props.editable ? ( + + {type === 'column' ? 'Columns' : 'Rows'} are pre-defined and cannot + be deleted or added. + + ) : ( + <> + props.delete(index)}> + Delete {type} + + add(index - 1)}> + Add 1 {type}{' '} + {type === 'column' ? 'left' : 'above'} + + add(index)}> + Add 1 {type}{' '} + {type === 'column' ? 'left' : 'below'} + + + )} + + + ) +} diff --git a/packages/dm-core-plugins/src/data-grid/VerticalHeader/VerticalHeader.tsx b/packages/dm-core-plugins/src/data-grid/VerticalHeader/VerticalHeader.tsx deleted file mode 100644 index 9402d7d59..000000000 --- a/packages/dm-core-plugins/src/data-grid/VerticalHeader/VerticalHeader.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import React, { useState } from 'react' -import * as Styled from '../styles' -import { Icon, Menu, Typography } from '@equinor/eds-core-react' -import { add, delete_to_trash } from '@equinor/eds-icons' - -type VerticalHeaderProps = { - selectedRow: number | undefined - setSelectedRow: React.Dispatch> - index: number - addRow: (newIndex?: number) => void - deleteRow: () => void - rowsAreSetDimension: boolean -} - -export function VerticalHeader(props: VerticalHeaderProps) { - const { selectedRow, setSelectedRow, index, addRow } = props - const [isMenuOpen, setIsMenuOpen] = useState(false) - const [menuButtonAnchor, setMenuButtonAnchor] = - useState(null) - - function changeSelectedRow(index: number) { - if (index === selectedRow) { - setSelectedRow(undefined) - return - } - setSelectedRow(index) - } - - function handleColumnRightClick(event: any) { - event.preventDefault() - setSelectedRow(index) - setIsMenuOpen(true) - } - - function onContextMenuClose() { - setIsMenuOpen(false) - setSelectedRow(undefined) - } - - return ( - - changeSelectedRow(index)} - onContextMenu={handleColumnRightClick} - ref={setMenuButtonAnchor} - > - {index + 1} - - - {props.rowsAreSetDimension ? ( - - Number of rows are pre-defined and cannot be deleted or added. - - ) : ( - <> - - Delete row - - addRow(index)}> - Add 1 row above - - addRow(index + 1)}> - Add 1 row below - - - )} - - - ) -} diff --git a/packages/dm-core-plugins/src/data-grid/styles.ts b/packages/dm-core-plugins/src/data-grid/styles.ts index ba98a8693..e1b3e22b9 100644 --- a/packages/dm-core-plugins/src/data-grid/styles.ts +++ b/packages/dm-core-plugins/src/data-grid/styles.ts @@ -1,46 +1,90 @@ -import { Button } from '@equinor/eds-core-react' import { tokens } from '@equinor/eds-tokens' import styled, { css } from 'styled-components' -export const DataGrid = styled.div` +export const DataGrid = styled.table<{ flip?: boolean }>` width: 100%; - display: table; - justify-content: stretch ; - align-items: center; + max-width: 100%; + vertical-align: top; + overflow-x: auto; + white-space: nowrap; + border-collapse: collapse; + border-spacing: 0; border-bottom: 1px solid ${tokens.colors.ui.background__medium.rgba}; - > div { - &:nth-child(odd) { - background: ${tokens.colors.ui.background__light.rgba}; + tbody { + > tr { + &:nth-child(odd) { + td { + background-color: ${tokens.colors.ui.background__light.rgba}; + } + } } } + + ${({ flip }) => + flip && + css` + display: flex; + overflow: hidden; + thead { + display: flex; + flex-shrink: 0; + min-width: min-content; + th { + border-bottom: 0; + position: relative; + } + } + tbody { + display: flex; + position: relative; + overflow-x: auto; + overflow-y: hidden; + } + tr { + display: flex; + flex-direction: column; + min-width: min-content; + flex-shrink: 0; + } + th, td { + display: block; + } + `} ` -export const Row = styled.div<{ header?: boolean; selected?: boolean }>` - width: 100%; - display: table-row; - justify-content: stretch ; - align-items: stretch; - font-weight: ${({ header }) => (header ? 'bold' : 'normal')}; +export const Row = styled.tr` + position: relative; + th { + border: 1px solid ${tokens.colors.ui.background__medium.rgba}; + border-bottom: 0; + background-clip: padding-box; + padding: 0; + position: relative; + } +` + +export const Head = styled.thead` + font-weight: bold; + th { + border-bottom: 2px solid ${tokens.colors.ui.background__medium.rgba}; + text-align: center; + } ` -export const Cell = styled.div<{ - header?: boolean +type IStyledCell = { selected?: boolean attributeType?: string -}>` +} +export const Cell = styled.td` border: 1px solid ${tokens.colors.ui.background__medium.rgba}; - border-bottom: none; - display: table-cell; + border-bottom: 0; position: relative; - ${({ header }) => - header && - css` - border-bottom: 2px solid ${tokens.colors.ui.background__medium.rgba}; - `} + background-clip: padding-box; + padding: 0; ${({ selected }) => selected && css` - background: ${tokens.colors.interactive.primary__selected_highlight.rgba}; + background-color: ${tokens.colors.interactive.primary__selected_highlight.rgba}!important; `} ${({ attributeType }) => attributeType === 'boolean' && @@ -49,12 +93,27 @@ export const Cell = styled.div<{ `} ` +export const Header = styled.th<{ selected?: boolean }>` + background: ${({ selected }) => + selected ? 'rgba(0, 0, 0, 0.2)' : 'transparent'}; + cursor: pointer; + &:hover { + background: rgba(0, 0, 0, 0.2); + } + svg { + fill: #666; + } +` + export const RowButton = styled.button<{ selected?: boolean }>` - height: 100%; - width: 100%; - position: absolute; background: ${({ selected }) => selected ? 'rgba(0, 0, 0, 0.2)' : 'transparent'}; + width: 100%; + height: 100%; + padding: 0; + position: absolute; + top: 0; + left: 0; &:hover { background: rgba(0, 0, 0, 0.2); } @@ -99,13 +158,6 @@ export const ActionRowButton = styled.button` } ` -export const SaveButton = styled(Button)` - padding: 0.5rem; - height: 1.5rem; - font-size: 0.75rem; - line-height: 0; -` - export const Select = styled.select` background: transparent; font-size: 0.75rem; diff --git a/packages/dm-core-plugins/src/data-grid/utils.ts b/packages/dm-core-plugins/src/data-grid/utils.ts index 34d2a8ca3..39044cc91 100644 --- a/packages/dm-core-plugins/src/data-grid/utils.ts +++ b/packages/dm-core-plugins/src/data-grid/utils.ts @@ -1,6 +1,50 @@ -export const columnLabels = Array.from({ length: 26 }, (_, i) => - String.fromCharCode(i + 65) -) +import { DataGridConfig, PredefinedLabels } from './types' + +const predefinedLabels: PredefinedLabels[] = ['...ABC', '...ZYX', '...123'] + +function getPredefinedLabels(type: PredefinedLabels, length: number) { + if (type === '...ABC') { + const labels_ABC = Array.from({ length }, (_, i) => + String.fromCharCode(i + 65) + ) + return labels_ABC + } + if (type === '...ZYX') { + const labels_ZYX = Array.from({ length }, (_, i) => + String.fromCharCode(90 - i) + ) + return labels_ZYX + } + if (type === '...123') { + const labels_123 = Array.from({ length }, (_, i) => `${i + 1}`) + return labels_123 + } + return [] +} + +export function createLabels(labels: string[], length: number): string[] { + const predefinedColumns = labels.filter((label) => + predefinedLabels.includes(label as PredefinedLabels) + ) + if (predefinedColumns?.length > 0) { + const predefinedLabelsNeeded = + length - (labels?.length - predefinedColumns?.length) + let mappedLabels: string[] = [] + labels.forEach((label) => { + if (predefinedLabels.includes(label as PredefinedLabels)) { + const createdLabels = getPredefinedLabels( + label as PredefinedLabels, + predefinedLabelsNeeded + ) + mappedLabels = [...mappedLabels, ...createdLabels] + } else { + mappedLabels.push(label) + } + }) + return mappedLabels as string[] + } + return labels +} export const createArrayFromNumber = (number: number) => Array.from({ length: number }, (_, i) => i + 1) @@ -15,3 +59,48 @@ export function arrayMove(arr: any[], fromIndex: number, toIndex: number) { export const getFillValue = (type: string) => type === 'boolean' ? false : type === 'number' ? 0 : '' + +export function getFunctionalityVariables( + config: DataGridConfig, + dimensions: string | undefined +): [boolean, boolean, string, boolean, boolean, string, boolean] { + const { + editable, + adjustableColumns, + adjustableRows, + fieldNames, + printDirection, + } = config + + const [columnDimensions, rowDimensions] = dimensions?.split(',') || ['*', '*'] + const isMultiPrimitive = fieldNames.length > 1 + const isMultiDimensional: boolean = dimensions?.includes(',') || false + const addButtonFunctionality = + printDirection === 'horizontal' ? 'addRow' : 'addColumn' + const isSortEnabled = + !isMultiPrimitive && + config.adjustableRows && + columnDimensions === '*' && + config.editable && + config.movableRows + const rowsAreEditable = editable && adjustableRows && rowDimensions === '*' + const columnsAreEditable = + editable && + adjustableColumns && + columnDimensions === '*' && + isMultiDimensional + const addButtonIsEnabled = + (isMultiPrimitive && printDirection === 'vertical') || + (rowsAreEditable && printDirection === 'horizontal') || + (columnsAreEditable && printDirection === 'vertical') + + return [ + rowsAreEditable, + columnsAreEditable, + addButtonFunctionality, + addButtonIsEnabled, + isMultiDimensional, + columnDimensions, + isSortEnabled, + ] +}