-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add DynamicDataTable component
- Loading branch information
1 parent
0dcdfe2
commit 8b1f089
Showing
8 changed files
with
650 additions
and
0 deletions.
There are no files selected for viewing
27 changes: 27 additions & 0 deletions
27
src/Shared/Components/DynamicDataTable/DynamicDataTable.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
/* | ||
* Copyright (c) 2024. Devtron Inc. | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
import { DynamicDataTableHeader } from './DynamicDataTableHeader' | ||
import { DynamicDataTableRow } from './DynamicDataTableRow' | ||
import { DynamicDataTableProps } from './types' | ||
import './styles.scss' | ||
|
||
export const DynamicDataTable = <K extends string>(props: DynamicDataTableProps<K>) => ( | ||
<div className="w-100"> | ||
<DynamicDataTableHeader {...props} /> | ||
<DynamicDataTableRow {...props} /> | ||
</div> | ||
) |
81 changes: 81 additions & 0 deletions
81
src/Shared/Components/DynamicDataTable/DynamicDataTableHeader.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
import { ReactComponent as ICArrowDown } from '@Icons/ic-sort-arrow-down.svg' | ||
import { ReactComponent as ICAdd } from '@Icons/ic-add.svg' | ||
import { ComponentSizeType } from '@Shared/constants' | ||
import { SortingOrder } from '@Common/Constants' | ||
|
||
import { Button, ButtonVariantType } from '../Button' | ||
import { getHeaderGridTemplateColumn } from './utils' | ||
import { DynamicDataTableHeaderType, DynamicDataTableHeaderProps } from './types' | ||
|
||
export const DynamicDataTableHeader = <K extends string>({ | ||
headers, | ||
rows, | ||
sortingConfig, | ||
onRowAdd, | ||
readOnly, | ||
isAdditionNotAllowed, | ||
headerComponent = null, | ||
}: DynamicDataTableHeaderProps<K>) => { | ||
// CONSTANTS | ||
const firstHeaderKey = headers[0].key | ||
const lastHeaderKey = headers[headers.length - 1].key | ||
/** Boolean determining if table actions are disabled. */ | ||
const isActionDisabled = readOnly || isAdditionNotAllowed | ||
/** Boolean determining if table has rows. */ | ||
const hasRows = (!readOnly && !isAdditionNotAllowed) || !!rows.length | ||
/** style: grid-template-columns */ | ||
const headerGridTemplateColumn = getHeaderGridTemplateColumn(headers) | ||
|
||
// RENDERERS | ||
const renderHeaderCell = ({ key, label, isSortable }: DynamicDataTableHeaderType<K>) => ( | ||
<div | ||
key={`${key}-header`} | ||
className={`bcn-50 py-6 px-8 flexbox dc__content-space dc__align-items-center ${key === firstHeaderKey ? `${hasRows || !isActionDisabled ? 'dc__top-left-radius' : 'dc__left-radius-4'}` : ''} ${key === lastHeaderKey ? `${hasRows || !isActionDisabled ? 'dc__top-right-radius-4' : 'dc__right-radius-4'}` : ''}`} | ||
> | ||
{isSortable ? ( | ||
<button | ||
type="button" | ||
className="cn-7 fs-12 lh-20-imp fw-6 flexbox dc__align-items-center dc__gap-2 dc__transparent" | ||
onClick={sortingConfig?.handleSorting} | ||
> | ||
{label} | ||
<ICArrowDown | ||
className="icon-dim-16 dc__no-shrink scn-7 rotate cursor" | ||
style={{ | ||
['--rotateBy' as string]: sortingConfig?.sortOrder === SortingOrder.ASC ? '0deg' : '180deg', | ||
}} | ||
/> | ||
</button> | ||
) : ( | ||
<div | ||
className={`cn-7 fs-12 lh-20 fw-6 flexbox dc__align-items-center dc__content-space dc__gap-2 ${hasRows ? 'dc__top-left-radius' : 'dc__left-radius-4'}`} | ||
> | ||
{label} | ||
</div> | ||
)} | ||
{key === firstHeaderKey && ( | ||
<Button | ||
dataTestId="data-table-add-row-button" | ||
ariaLabel="Add" | ||
disabled={isActionDisabled} | ||
onClick={onRowAdd} | ||
icon={<ICAdd />} | ||
variant={ButtonVariantType.borderLess} | ||
size={ComponentSizeType.xs} | ||
/> | ||
)} | ||
{key === lastHeaderKey && headerComponent} | ||
</div> | ||
) | ||
|
||
return ( | ||
<div className={`bcn-2 p-1 ${hasRows ? 'dc__top-radius-4' : 'br-4'}`}> | ||
<div | ||
className="dynamic-data-table two-columns w-100 bcn-1 br-4" | ||
style={{ gridTemplateColumns: headerGridTemplateColumn }} | ||
> | ||
<div className="dynamic-data-table__row">{headers.map((header) => renderHeaderCell(header))}</div> | ||
</div> | ||
</div> | ||
) | ||
} |
232 changes: 232 additions & 0 deletions
232
src/Shared/Components/DynamicDataTable/DynamicDataTableRow.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,232 @@ | ||
import { createRef, Fragment, RefObject, useEffect, useRef } from 'react' | ||
// eslint-disable-next-line import/no-extraneous-dependencies | ||
import { followCursor } from 'tippy.js' | ||
|
||
import { ReactComponent as ICClose } from '@Icons/ic-close.svg' | ||
import { ReactComponent as ICCross } from '@Icons/ic-cross.svg' | ||
import { ComponentSizeType, DEFAULT_SECRET_PLACEHOLDER } from '@Shared/constants' | ||
import { Tooltip } from '@Common/Tooltip' | ||
|
||
import { Button, ButtonStyleType, ButtonVariantType } from '../Button' | ||
import { | ||
getSelectPickerOptionByValue, | ||
SelectPicker, | ||
SelectPickerOptionType, | ||
SelectPickerVariantType, | ||
} from '../SelectPicker' | ||
import { MultipleResizableTextArea } from '../MultipleResizableTextArea' | ||
import { getRowGridTemplateColumn } from './utils' | ||
import { DynamicDataTableRowType, DynamicDataTableRowProps } from './types' | ||
|
||
export const DynamicDataTableRow = <K extends string>({ | ||
rows, | ||
headers, | ||
maskValue, | ||
readOnly, | ||
isAdditionNotAllowed, | ||
validationSchema, | ||
showError, | ||
errorMessages = [], | ||
actionButton = null, | ||
actionButtonWidth = '', | ||
onRowEdit, | ||
onRowDelete, | ||
leadingCellIcon, | ||
trailingCellIcon, | ||
}: DynamicDataTableRowProps<K>) => { | ||
// CONSTANTS | ||
const isFirstRowEmpty = headers.every(({ key }) => !rows[0]?.data[key].value) | ||
/** Boolean determining if table has rows. */ | ||
const hasRows = (!readOnly && !isAdditionNotAllowed) || !!rows.length | ||
const disableDeleteRow = rows.length === 1 && isFirstRowEmpty | ||
/** style: grid-template-columns */ | ||
const rowGridTemplateColumn = getRowGridTemplateColumn(headers, actionButtonWidth, readOnly) | ||
|
||
const cellRef = useRef<Record<string | number, Record<K, RefObject<HTMLTextAreaElement>>>>() | ||
if (!cellRef.current) { | ||
cellRef.current = rows.reduce( | ||
(acc, curr) => ({ | ||
...acc, | ||
[curr.id]: headers.reduce((headerAcc, { key }) => ({ ...headerAcc, [key]: createRef() }), {}), | ||
}), | ||
{}, | ||
) | ||
} | ||
|
||
useEffect(() => { | ||
if (rows) { | ||
const rowIds = rows.map(({ id }) => id) | ||
|
||
const updatedCellRef = rowIds.reduce((acc, curr) => { | ||
if (cellRef.current[curr]) { | ||
acc[curr] = cellRef.current[curr] | ||
} else { | ||
acc[curr] = headers.reduce((headerAcc, { key }) => ({ ...headerAcc, [key]: createRef() }), {}) | ||
} | ||
return acc | ||
}, {}) | ||
|
||
cellRef.current = updatedCellRef | ||
} | ||
}, [rows]) | ||
|
||
// METHODS | ||
const onChange = | ||
(row: DynamicDataTableRowType<K>, key: K) => | ||
(e: React.ChangeEvent<HTMLTextAreaElement> | SelectPickerOptionType<string>) => { | ||
let value = '' | ||
switch (row.data[key].type) { | ||
case 'dropdown': | ||
value = (e as SelectPickerOptionType<string>).value | ||
break | ||
default: | ||
value = (e as React.ChangeEvent<HTMLTextAreaElement>).target.value | ||
} | ||
|
||
onRowEdit(row, key, value) | ||
} | ||
|
||
const onDelete = (row: DynamicDataTableRowType<K>) => () => { | ||
onRowDelete(row) | ||
} | ||
|
||
// RENDERERS | ||
const renderCellContent = (row: DynamicDataTableRowType<K>, key: K) => { | ||
switch (row.data[key].type) { | ||
case 'dropdown': | ||
return ( | ||
<div className="p-8 w-100 h-100 flex top dc__align-self-start"> | ||
<SelectPicker<string, false> | ||
{...row.data[key].props} | ||
inputId={`data-table-${row.id}-${key}-cell`} | ||
variant={SelectPickerVariantType.BORDER_LESS} | ||
value={getSelectPickerOptionByValue(row.data[key].props?.options, row.data[key].value)} | ||
onChange={onChange(row, key)} | ||
isDisabled={readOnly || row.data[key].disabled} | ||
fullWidth | ||
/> | ||
</div> | ||
) | ||
default: | ||
return ( | ||
<MultipleResizableTextArea | ||
{...row.data[key].props} | ||
className={`dynamic-data-table__cell-input placeholder-cn5 p-8 cn-9 fs-13 lh-20 dc__no-border-radius ${readOnly || row.data[key].disabled ? 'cursor-not-allowed' : ''}`} | ||
minHeight={20} | ||
maxHeight={160} | ||
value={row.data[key].value} | ||
onChange={onChange(row, key)} | ||
disabled={readOnly || row.data[key].disabled} | ||
refVar={cellRef?.current?.[row.id]?.[key]} | ||
dependentRefs={cellRef?.current?.[row.id]} | ||
disableOnBlurResizeToMinHeight | ||
/> | ||
) | ||
} | ||
} | ||
|
||
const renderAsterisk = (row: DynamicDataTableRowType<K>, key: K) => | ||
row.data[key].required && <span className="mt-10 px-6 w-20 cr-5 fs-16 lh-20 dc__align-self-start">*</span> | ||
|
||
const renderCellIcon = (row: DynamicDataTableRowType<K>, key: K, isLeadingIcon?: boolean) => { | ||
const iconConfig = isLeadingIcon ? leadingCellIcon : trailingCellIcon | ||
if (!iconConfig?.[key]) { | ||
return null | ||
} | ||
|
||
return ( | ||
<div | ||
className={`flex dc__align-self-start ${row.data[key].type !== 'text' ? `py-8 ${isLeadingIcon ? 'pl-8' : 'pr-8'}` : ''}`} | ||
> | ||
{iconConfig[key](row.id)} | ||
</div> | ||
) | ||
} | ||
|
||
const renderErrorMessage = (errorMessage: string) => ( | ||
<div key={errorMessage} className="flexbox align-items-center dc__gap-4"> | ||
<ICClose className="icon-dim-16 fcr-5 dc__align-self-start dc__no-shrink" /> | ||
<p className="fs-12 lh-16 cn-7 m-0">{errorMessage}</p> | ||
</div> | ||
) | ||
|
||
const renderErrorMessages = ( | ||
value: Parameters<typeof validationSchema>[0], | ||
key: Parameters<typeof validationSchema>[1], | ||
rowId: DynamicDataTableRowType<K>['id'], | ||
) => { | ||
const showErrorMessages = showError && !validationSchema(value, key, rowId) | ||
if (!showErrorMessages) { | ||
return null | ||
} | ||
|
||
return ( | ||
<div className="dynamic-data-table__error bcn-0 dc__border br-4 py-7 px-8 flexbox-col dc__gap-4"> | ||
{errorMessages.map((error) => renderErrorMessage(error))} | ||
</div> | ||
) | ||
} | ||
|
||
return hasRows ? ( | ||
<div className="bcn-2 px-1 pb-1 dc__bottom-radius-4"> | ||
{!!rows.length && ( | ||
<div | ||
className={`dynamic-data-table w-100 bcn-1 dc__bottom-radius-4 ${!readOnly ? 'three-columns' : 'two-columns'}`} | ||
style={{ | ||
gridTemplateColumns: rowGridTemplateColumn, | ||
}} | ||
> | ||
{rows.map((row) => ( | ||
<div key={row.id} className="dynamic-data-table__row"> | ||
{headers.map(({ key }) => ( | ||
<Fragment key={key}> | ||
<Tooltip | ||
alwaysShowTippyOnHover={readOnly || false} | ||
content="Cannot edit in read-only mode" | ||
followCursor="horizontal" | ||
plugins={[followCursor]} | ||
> | ||
<div | ||
className={`dynamic-data-table__cell bcn-0 flexbox dc__align-items-center dc__gap-4 dc__position-rel ${readOnly || row.data[key].disabled ? 'cursor-not-allowed no-hover' : ''} ${showError && !validationSchema(row.data[key].value, key, row.id) ? 'dynamic-data-table__cell--error no-hover' : ''}`} | ||
> | ||
{maskValue?.[key] && row.data[key].value ? ( | ||
<div className="py-8 px-12 h-36 flex">{DEFAULT_SECRET_PLACEHOLDER}</div> | ||
) : ( | ||
<> | ||
{renderCellIcon(row, key, true)} | ||
{renderCellContent(row, key)} | ||
{renderAsterisk(row, key)} | ||
{renderCellIcon(row, key)} | ||
{renderErrorMessages(row.data[key].value, key, row.id)} | ||
</> | ||
)} | ||
</div> | ||
</Tooltip> | ||
</Fragment> | ||
))} | ||
{actionButton && ( | ||
<div className="dynamic-data-table__cell flex top p-8 bcn-0"> | ||
{actionButton(row.id)} | ||
</div> | ||
)} | ||
{!readOnly && ( | ||
<div className="dynamic-data-table__row-delete-btn flex top bcn-0 dc__no-shrink py-6 px-8"> | ||
<Button | ||
dataTestId="data-table-delete-row-button" | ||
ariaLabel="Delete" | ||
onClick={onDelete(row)} | ||
disabled={disableDeleteRow} | ||
icon={<ICCross className="dc__align-self-start" />} | ||
size={ComponentSizeType.xs} | ||
variant={ButtonVariantType.borderLess} | ||
style={ButtonStyleType.negative} | ||
/> | ||
</div> | ||
)} | ||
</div> | ||
))} | ||
</div> | ||
)} | ||
</div> | ||
) : null | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
/* | ||
* Copyright (c) 2024. Devtron Inc. | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
export * from './DynamicDataTable' | ||
export * from './types' |
Oops, something went wrong.