ToolJet/frontend/src/Editor/Components/Table/Table.jsx

1121 lines
38 KiB
React
Raw Normal View History

/* eslint-disable no-unused-vars */
/* eslint-disable react-hooks/exhaustive-deps */
import React, { useMemo, useState, useEffect, useCallback } from 'react';
2021-04-30 06:31:32 +00:00
import {
useTable,
useFilters,
useSortBy,
useGlobalFilter,
useAsyncDebounce,
usePagination,
useBlockLayout,
useResizeColumns,
useRowSelect,
2021-04-30 06:31:32 +00:00
} from 'react-table';
import cx from 'classnames';
import { resolveReferences, resolveWidgetFieldValue, validateWidget } from '@/_helpers/utils';
import SelectSearch, { fuzzySearch } from 'react-select-search';
2021-04-30 06:31:32 +00:00
import { useExportData } from 'react-table-plugins';
import Papa from 'papaparse';
import { Pagination } from './Pagination';
2021-05-09 07:26:54 +00:00
import { CustomSelect } from './CustomSelect';
import { Tags } from './Tags';
import { Radio } from './Radio';
import { Toggle } from './Toggle';
import { Datepicker } from './Datepicker';
import { GlobalFilter } from './GlobalFilter';
var _ = require('lodash');
2021-04-30 06:31:32 +00:00
export function Table({
id,
width,
height,
component,
onComponentClick,
currentState = { components: {} },
onEvent,
paramUpdated,
changeCanDrag,
onComponentOptionChanged,
onComponentOptionsChanged,
darkMode,
fireEvent,
setExposedVariable,
registerAction,
2021-04-30 06:31:32 +00:00
}) {
const color =
component.definition.styles.textColor.value !== '#000'
? component.definition.styles.textColor.value
: darkMode && '#fff';
2021-04-30 06:31:32 +00:00
const actions = component.definition.properties.actions || { value: [] };
const serverSidePaginationProperty = component.definition.properties.serverSidePagination;
const serverSidePagination = serverSidePaginationProperty
? resolveWidgetFieldValue(serverSidePaginationProperty.value, currentState)
: false;
2021-04-30 06:31:32 +00:00
const serverSideSearchProperty = component.definition.properties.serverSideSearch;
const serverSideSearch = serverSideSearchProperty
? resolveWidgetFieldValue(serverSideSearchProperty.value, currentState)
: false;
const displaySearchBoxProperty = component.definition.properties.displaySearchBox;
const displaySearchBox = displaySearchBoxProperty
? resolveWidgetFieldValue(displaySearchBoxProperty.value, currentState)
: true;
const showDownloadButtonProperty = component.definition.properties.showDownloadButton?.value;
const showDownloadButton = resolveWidgetFieldValue(showDownloadButtonProperty, currentState) ?? true; // default is true for backward compatibility
const showFilterButtonProperty = component.definition.properties.showFilterButton?.value;
const showFilterButton = resolveWidgetFieldValue(showFilterButtonProperty, currentState) ?? true; // default is true for backward compatibility
const showBulkUpdateActionsProperty = component.definition.properties.showBulkUpdateActions?.value;
const showBulkUpdateActions = resolveWidgetFieldValue(showBulkUpdateActionsProperty, currentState) ?? true; // default is true for backward compatibility
const showBulkSelectorProperty = component.definition.properties.showBulkSelector?.value;
const showBulkSelector = resolveWidgetFieldValue(showBulkSelectorProperty, currentState) ?? false; // default is false for backward compatibility
const highlightSelectedRowProperty = component.definition.properties.highlightSelectedRow?.value;
const highlightSelectedRow = resolveWidgetFieldValue(highlightSelectedRowProperty, currentState) ?? false; // default is false for backward compatibility
const clientSidePaginationProperty = component.definition.properties.clientSidePagination?.value;
const clientSidePagination =
resolveWidgetFieldValue(clientSidePaginationProperty, currentState) ?? !serverSidePagination; // default is true for backward compatibility
2021-08-28 16:10:45 +00:00
const tableTypeProperty = component.definition.styles.tableType;
let tableType = tableTypeProperty ? tableTypeProperty.value : 'table-bordered';
tableType = tableType === '' ? 'table-bordered' : tableType;
const cellSizeType = component.definition.styles.cellSize?.value;
const borderRadius = component.definition.styles.borderRadius?.value;
const widgetVisibility = component.definition.styles?.visibility?.value ?? true;
const disabledState = component.definition.styles?.disabledState?.value ?? false;
const parsedDisabledState =
typeof disabledState !== 'boolean' ? resolveWidgetFieldValue(disabledState, currentState) : disabledState;
2021-08-30 11:43:05 +00:00
let parsedWidgetVisibility = widgetVisibility;
2021-08-30 11:43:05 +00:00
try {
parsedWidgetVisibility = resolveReferences(parsedWidgetVisibility, currentState, []);
} catch (err) {
console.log(err);
}
2021-08-30 11:43:05 +00:00
2021-04-30 06:31:32 +00:00
const [loadingState, setLoadingState] = useState(false);
useEffect(() => {
const loadingStateProperty = component.definition.properties.loadingState;
if (loadingStateProperty && currentState) {
const newState = resolveReferences(loadingStateProperty.value, currentState, false);
2021-04-30 06:31:32 +00:00
setLoadingState(newState);
}
2021-04-30 06:31:32 +00:00
}, [currentState]);
const [componentState, setcomponentState] = useState(currentState.components[component.component] || {});
useEffect(() => {
setcomponentState(currentState.components[component.name] || {});
}, [currentState.components[component.name]]);
const [isFiltersVisible, setFiltersVisibility] = useState(false);
const [filters, setFilters] = useState([]);
function showFilters() {
setFiltersVisibility(true);
}
function hideFilters() {
setFiltersVisibility(false);
}
function filterColumnChanged(index, value) {
const newFilters = filters;
newFilters[index].id = value;
2021-04-30 06:31:32 +00:00
setFilters(newFilters);
setAllFilters(newFilters.filter((filter) => filter.id !== ''));
2021-04-30 06:31:32 +00:00
}
function filterOperationChanged(index, value) {
const newFilters = filters;
newFilters[index].value = {
...newFilters[index].value,
operation: value,
2021-04-30 06:31:32 +00:00
};
setFilters(newFilters);
setAllFilters(newFilters.filter((filter) => filter.id !== ''));
2021-04-30 06:31:32 +00:00
}
function filterValueChanged(index, value) {
const newFilters = filters;
newFilters[index].value = {
...newFilters[index].value,
value: value,
2021-04-30 06:31:32 +00:00
};
setFilters(newFilters);
setAllFilters(newFilters.filter((filter) => filter.id !== ''));
2021-04-30 06:31:32 +00:00
}
function addFilter() {
setFilters([...filters, { id: '', value: { operation: 'contains', value: '' } }]);
}
function removeFilter(index) {
let newFilters = filters;
newFilters.splice(index, 1);
setFilters(newFilters);
setAllFilters(newFilters.filter((filter) => filter.id !== ''));
2021-04-30 06:31:32 +00:00
}
function clearFilters() {
setFilters([]);
setAllFilters([]);
}
const defaultColumn = React.useMemo(
() => ({
minWidth: 60,
width: 268,
2021-04-30 06:31:32 +00:00
}),
[]
);
const columnSizes = component.definition.properties.columnSizes || {};
function handleCellValueChange(index, key, value, rowData) {
const changeSet = componentState.changeSet;
const dataUpdates = componentState.dataUpdates || [];
let obj = changeSet ? changeSet[index] || {} : {};
obj = _.set(obj, key, value);
let newChangeset = {
...changeSet,
[index]: {
...obj,
},
2021-04-30 06:31:32 +00:00
};
obj = _.set(rowData, key, value);
let newDataUpdates = {
...dataUpdates,
[index]: { ...obj },
};
2021-04-30 06:31:32 +00:00
return onComponentOptionsChanged(component, [
2021-04-30 06:31:32 +00:00
['dataUpdates', newDataUpdates],
['changeSet', newChangeset],
2021-04-30 06:31:32 +00:00
]);
}
function getExportFileBlob({ columns, data }) {
2021-04-30 06:31:32 +00:00
const headerNames = columns.map((col) => col.exportValue);
const csvString = Papa.unparse({ fields: headerNames, data });
return new Blob([csvString], { type: 'text/csv' });
}
function onPageIndexChanged(page) {
onComponentOptionChanged(component, 'pageIndex', page).then(() => {
onEvent('onPageChanged', { component, data: {} });
});
}
2021-04-30 06:31:32 +00:00
function handleChangesSaved() {
Object.keys(changeSet).forEach((key) => {
2021-04-30 06:31:32 +00:00
tableData[key] = {
..._.merge(tableData[key], changeSet[key]),
2021-04-30 06:31:32 +00:00
};
});
onComponentOptionChanged(component, 'changeSet', {});
onComponentOptionChanged(component, 'dataUpdates', []);
}
function handleChangesDiscarded() {
onComponentOptionChanged(component, 'changeSet', {});
onComponentOptionChanged(component, 'dataUpdates', []);
}
function customFilter(rows, columnIds, filterValue) {
try {
if (filterValue.operation === 'equals') {
return rows.filter((row) => row.values[columnIds[0]] === filterValue.value);
}
if (filterValue.operation === 'ne') {
return rows.filter((row) => row.values[columnIds[0]] !== filterValue.value);
}
2021-04-30 06:31:32 +00:00
2021-05-09 12:03:03 +00:00
if (filterValue.operation === 'matches') {
return rows.filter((row) =>
row.values[columnIds[0]].toString().toLowerCase().includes(filterValue.value.toLowerCase())
);
2021-05-09 12:03:03 +00:00
}
if (filterValue.operation === 'nl') {
return rows.filter((row) =>
!row.values[columnIds[0]].toString().toLowerCase().includes(filterValue.value.toLowerCase())
);
}
2021-05-09 12:03:03 +00:00
2021-04-30 06:31:32 +00:00
if (filterValue.operation === 'gt') {
return rows.filter((row) => row.values[columnIds[0]] > filterValue.value);
}
if (filterValue.operation === 'lt') {
return rows.filter((row) => row.values[columnIds[0]] < filterValue.value);
}
if (filterValue.operation === 'gte') {
return rows.filter((row) => row.values[columnIds[0]] >= filterValue.value);
}
if (filterValue.operation === 'lte') {
return rows.filter((row) => row.values[columnIds[0]] <= filterValue.value);
}
let value = filterValue.value;
if (typeof value === 'string') {
value = value.toLowerCase();
}
return rows.filter((row) => {
let rowValue = row.values[columnIds[0]];
if (typeof rowValue === 'string') {
rowValue = rowValue.toLowerCase();
}
return rowValue.includes(value);
});
} catch {
return rows;
}
}
2021-04-04 06:26:46 +00:00
2021-04-30 06:31:32 +00:00
const changeSet = componentState ? componentState.changeSet : {};
2021-04-09 04:56:17 +00:00
const computeFontColor = useCallback(() => {
if (color !== undefined) {
return color;
} else {
return darkMode ? '#ffffff' : '#000000';
}
}, [color, darkMode]);
2021-04-30 06:31:32 +00:00
const columnData = component.definition.properties.columns.value.map((column) => {
const columnSize = columnSizes[column.id] || columnSizes[column.name];
2021-04-30 06:31:32 +00:00
const columnType = column.columnType;
2021-04-09 04:56:17 +00:00
2021-04-30 06:31:32 +00:00
const columnOptions = {};
if (
columnType === 'dropdown' ||
columnType === 'multiselect' ||
columnType === 'badge' ||
columnType === 'badges' ||
columnType === 'radio'
) {
const values = resolveReferences(column.values, currentState) || [];
const labels = resolveReferences(column.labels, currentState, []) || [];
2021-04-30 06:31:32 +00:00
if (Array.isArray(labels)) {
columnOptions.selectOptions = labels.map((label, index) => {
return { name: label, value: values[index] };
2021-04-30 06:31:32 +00:00
});
}
2021-04-03 05:25:41 +00:00
}
if (columnType === 'datepicker') {
column.isTimeChecked = column.isTimeChecked ? column.isTimeChecked : false;
column.dateFormat = column.dateFormat ? column.dateFormat : 'DD/MM/YYYY';
2021-12-03 08:22:14 +00:00
column.parseDateFormat = column.parseDateFormat ?? column.dateFormat; //backwards compatibility
}
2021-04-03 05:25:41 +00:00
const width = columnSize || defaultColumn.width;
2021-04-30 06:31:32 +00:00
return {
id: column.id,
2021-04-30 06:31:32 +00:00
Header: column.name,
accessor: column.key || column.name,
filter: customFilter,
width: width,
columnOptions,
columnType,
isEditable: column.isEditable,
2021-04-30 06:31:32 +00:00
Cell: function (cell) {
const rowChangeSet = changeSet ? changeSet[cell.row.index] : null;
const cellValue = rowChangeSet ? rowChangeSet[column.name] || cell.value : cell.value;
switch (columnType) {
case 'string':
case undefined:
case 'default': {
2021-11-17 11:37:06 +00:00
const textColor = resolveReferences(column.textColor, currentState, '', { cellValue });
const cellStyles = {
color: textColor ?? '',
};
if (column.isEditable) {
const validationData = validateWidget({
validationObject: {
regex: {
value: column.regex,
},
minLength: {
value: column.minLength,
},
maxLength: {
value: column.maxLength,
},
customRule: {
value: column.customRule,
},
},
widgetValue: cellValue,
currentState,
customResolveObjects: { cellValue },
});
const { isValid, validationError } = validationData;
const cellStyles = {
color: textColor ?? '',
};
return (
<div>
<input
type="text"
style={{ ...cellStyles, maxWidth: width, minWidth: width - 10 }}
onKeyDown={(e) => {
if (e.key === 'Enter') {
if (e.target.defaultValue !== e.target.value) {
handleCellValueChange(
cell.row.index,
column.key || column.name,
e.target.value,
cell.row.original
);
}
}
}}
onBlur={(e) => {
if (e.target.defaultValue !== e.target.value) {
handleCellValueChange(
cell.row.index,
column.key || column.name,
e.target.value,
cell.row.original
);
}
}}
className={`form-control-plaintext form-control-plaintext-sm ${!isValid ? 'is-invalid' : ''}`}
defaultValue={cellValue}
/>
<div className="invalid-feedback">{validationError}</div>
</div>
);
}
return <span style={cellStyles}>{cellValue}</span>;
}
case 'text': {
return (
<textarea
rows="1"
className="form-control-plaintext text-container text-muted"
readOnly={!column.isEditable}
style={{ maxWidth: width, minWidth: width - 10 }}
onBlur={(e) => {
if (column.isEditable) {
handleCellValueChange(cell.row.index, column.key || column.name, e.target.value, cell.row.original);
}
}}
onChange={(e) => {
if (column.isEditable) {
handleCellValueChange(cell.row.index, column.key || column.name, e.target.value, cell.row.original);
}
}}
value={cellValue}
defaultValue={cellValue}
></textarea>
);
}
case 'dropdown': {
const validationData = validateWidget({
validationObject: {
regex: {
value: column.regex,
},
minLength: {
value: column.minLength,
},
maxLength: {
value: column.maxLength,
},
customRule: {
value: column.customRule,
},
},
widgetValue: cellValue,
currentState,
customResolveObjects: { cellValue },
});
const { isValid, validationError } = validationData;
2021-04-30 06:31:32 +00:00
return (
<div>
<SelectSearch
options={columnOptions.selectOptions}
value={cellValue}
search={true}
onChange={(value) => {
handleCellValueChange(cell.row.index, column.key || column.name, value, cell.row.original);
}}
filterOptions={fuzzySearch}
placeholder="Select.."
/>
<div className={`invalid-feedback ${isValid ? '' : 'd-flex'}`}>{validationError}</div>
</div>
);
}
case 'multiselect': {
return (
<div>
<SelectSearch
printOptions="on-focus"
multiple
search={true}
placeholder="Select.."
options={columnOptions.selectOptions}
value={cellValue}
onChange={(value) => {
handleCellValueChange(cell.row.index, column.key || column.name, value, cell.row.original);
}}
/>
</div>
);
}
case 'badge':
case 'badges': {
return (
<div>
<CustomSelect
options={columnOptions.selectOptions}
value={cellValue}
multiple={columnType === 'badges'}
onChange={(value) => {
handleCellValueChange(cell.row.index, column.key || column.name, value, cell.row.original);
}}
/>
</div>
);
}
case 'tags': {
return (
<div>
<Tags
value={cellValue}
onChange={(value) => {
handleCellValueChange(cell.row.index, column.key || column.name, value, cell.row.original);
}}
/>
</div>
);
}
case 'radio': {
return (
<div>
<Radio
options={columnOptions.selectOptions}
value={cellValue}
readOnly={!column.isEditable}
onChange={(value) => {
handleCellValueChange(cell.row.index, column.key || column.name, value, cell.row.original);
}}
/>
</div>
);
}
case 'toggle': {
return (
<div>
<Toggle
value={cellValue}
readOnly={!column.isEditable}
activeColor={column.activeColor}
onChange={(value) => {
handleCellValueChange(cell.row.index, column.key || column.name, value, cell.row.original).then(
() => {
fireEvent('OnTableToggleCellChanged', {
column: column,
rowId: cell.row.id,
row: cell.row.original,
});
}
);
}}
/>
</div>
);
}
case 'datepicker': {
return (
<div>
<Datepicker
2021-12-03 08:22:14 +00:00
dateDisplayFormat={column.dateFormat}
isTimeChecked={column.isTimeChecked}
value={cellValue}
readOnly={column.isEditable}
2021-12-03 08:22:14 +00:00
parseDateFormat={column.parseDateFormat}
onChange={(value) => {
handleCellValueChange(cell.row.index, column.key || column.name, value, cell.row.original);
}}
/>
</div>
2021-04-30 06:31:32 +00:00
);
}
}
return cellValue || '';
},
2021-04-30 06:31:32 +00:00
};
});
let tableData = [];
if (currentState) {
tableData = resolveReferences(component.definition.properties.data.value, currentState, []);
2021-04-30 06:31:32 +00:00
if (!Array.isArray(tableData)) tableData = [];
console.log('resolved param', tableData);
}
tableData = tableData || [];
const leftActions = () => actions.value.filter((action) => action.position === 'left');
const rightActions = () => actions.value.filter((action) => [undefined, 'right'].includes(action.position));
const leftActionsCellData =
leftActions().length > 0
? [
{
id: 'leftActions',
Header: 'Actions',
accessor: 'edit',
width: columnSizes.leftActions || defaultColumn.width,
Cell: (cell) => {
return leftActions().map((action) => (
<button
key={action.name}
className="btn btn-sm m-1 btn-light"
style={{
background: action.backgroundColor,
color: action.textColor,
borderRadius: component.definition.styles.actionButtonRadius?.value
? parseFloat(component.definition.styles.actionButtonRadius?.value)
: 0,
}}
onClick={(e) => {
e.stopPropagation();
onEvent('onTableActionButtonClicked', {
component,
data: cell.row.original,
rowId: cell.row.id,
action,
});
}}
>
{action.buttonText}
</button>
));
},
},
]
: [];
const rightActionsCellData =
rightActions().length > 0
? [
{
id: 'rightActions',
Header: 'Actions',
accessor: 'edit',
width: columnSizes.rightActions || defaultColumn.width,
Cell: (cell) => {
return rightActions().map((action) => (
<button
key={action.name}
className="btn btn-sm m-1 btn-light"
style={{
background: action.backgroundColor,
color: action.textColor,
borderRadius: component.definition.styles.actionButtonRadius?.value
? parseFloat(component.definition.styles.actionButtonRadius?.value)
: 0,
}}
onClick={(e) => {
e.stopPropagation();
onEvent('onTableActionButtonClicked', {
component,
data: cell.row.original,
rowId: cell.row.id,
action,
});
}}
>
{action.buttonText}
</button>
));
},
},
]
: [];
const IndeterminateCheckbox = React.forwardRef(({ indeterminate, ...rest }, ref) => {
const defaultRef = React.useRef();
const resolvedRef = ref || defaultRef;
React.useEffect(() => {
resolvedRef.current.indeterminate = indeterminate;
}, [resolvedRef, indeterminate]);
return (
<>
<input
type="checkbox"
ref={resolvedRef}
style={{
width: 15,
height: 15,
marginTop: 8,
marginLeft: 10,
}}
onClick={(event) => event.stopPropagation()}
{...rest}
/>
</>
);
});
const optionsData = columnData.map((column) => column.columnOptions?.selectOptions);
2021-04-30 06:31:32 +00:00
const columns = useMemo(
() => [...leftActionsCellData, ...columnData, ...rightActionsCellData],
[
JSON.stringify(columnData),
leftActionsCellData.length,
rightActionsCellData.length,
componentState.changeSet,
JSON.stringify(optionsData),
JSON.stringify(component.definition.properties.columns),
showBulkSelector,
] // Hack: need to fix
2021-04-30 06:31:32 +00:00
);
const data = useMemo(
() => tableData,
[tableData.length, componentState.changeSet, component.definition.properties.data.value]
);
2021-04-30 06:31:32 +00:00
const computedStyles = {
// width: `${width}px`,
2021-04-30 06:31:32 +00:00
};
const {
getTableProps,
getTableBodyProps,
headerGroups,
page,
canPreviousPage,
canNextPage,
pageOptions,
gotoPage,
pageCount,
nextPage,
previousPage,
setPageSize,
state,
rows,
2021-04-30 06:31:32 +00:00
prepareRow,
setAllFilters,
preGlobalFilteredRows,
setGlobalFilter,
state: { pageIndex, pageSize },
exportData,
selectedFlatRows,
2021-04-30 06:31:32 +00:00
} = useTable(
{
autoResetPage: false,
2021-04-30 06:31:32 +00:00
columns,
data,
defaultColumn,
initialState: { pageIndex: 0, pageSize: -1 },
pageCount: -1,
manualPagination: false,
getExportFileBlob,
2021-04-09 04:56:17 +00:00
},
2021-04-30 06:31:32 +00:00
useFilters,
useGlobalFilter,
useSortBy,
usePagination,
useBlockLayout,
useResizeColumns,
useExportData,
useRowSelect,
(hooks) => {
showBulkSelector &&
hooks.visibleColumns.push((columns) => [
{
id: 'selection',
Header: ({ getToggleAllPageRowsSelectedProps }) => (
<div className="d-flex flex-column align-items-center">
<IndeterminateCheckbox {...getToggleAllPageRowsSelectedProps()} />
</div>
),
Cell: ({ row }) => (
<div className="d-flex flex-column align-items-center">
<IndeterminateCheckbox {...row.getToggleRowSelectedProps()} />
</div>
),
width: 1,
columnType: 'selector',
},
...columns,
]);
}
2021-04-30 06:31:32 +00:00
);
const registerSetPageAction = () => {
registerAction('setPage', (targetPageIndex) => {
setPaginationInternalPageIndex(targetPageIndex);
setExposedVariable('pageIndex', targetPageIndex);
if (!serverSidePagination && clientSidePagination) gotoPage(targetPageIndex - 1);
});
};
useEffect(registerSetPageAction, []);
useEffect(registerSetPageAction, [serverSidePagination, clientSidePagination]);
useEffect(() => {
const selectedRowsOriginalData = selectedFlatRows.map((row) => row.original);
onComponentOptionChanged(component, 'selectedRows', selectedRowsOriginalData);
}, [selectedFlatRows.length]);
React.useEffect(() => {
if (serverSidePagination || !clientSidePagination) {
setPageSize(rows?.length || 10);
}
if (!serverSidePagination && clientSidePagination) {
setPageSize(10);
}
}, [clientSidePagination, serverSidePagination, rows]);
useEffect(() => {
const pageData = page.map((row) => row.original);
const currentData = rows.map((row) => row.original);
onComponentOptionsChanged(component, [
['currentPageData', pageData],
['currentData', currentData],
]);
}, [tableData.length, componentState.changeSet]);
2021-04-30 06:31:32 +00:00
useEffect(() => {
if (!state.columnResizing.isResizingColumn) {
changeCanDrag(true);
paramUpdated(id, 'columnSizes', { ...columnSizes, ...state.columnResizing.columnWidths });
2021-04-30 06:31:32 +00:00
} else {
changeCanDrag(false);
}
}, [state.columnResizing.isResizingColumn]);
2021-04-30 06:31:32 +00:00
const [paginationInternalPageIndex, setPaginationInternalPageIndex] = useState(pageIndex ?? 1);
useEffect(() => {
if (pageCount <= pageIndex) gotoPage(pageCount - 1);
}, [pageCount]);
2021-04-30 06:31:32 +00:00
return (
<div
data-disabled={parsedDisabledState}
className="card jet-table"
style={{
width: `100%`,
height: `${height}px`,
display: parsedWidgetVisibility ? '' : 'none',
overflow: 'hidden',
borderRadius: Number.parseFloat(borderRadius),
}}
onClick={(event) => {
event.stopPropagation();
Feature: Collaboration ( realtime comments for canvas ) 🔥 (#810) * feat: initial commit for collaboration feature * add dnd to comments * add positions endpoint * feat: encapsulate all http common logic in http-client * segregate sections and transfer responsibility of state * feat: use-spring to add fade effect :zap: * fix: open in right * fix: left-right position css * add footer for message * integrate getcomment endpoint * use fromnow for date ago * add dnd * - Add data trasfer object for comment - Add class-validator package to check the response type from client - Add comment repository class for persistance layer - Add comment service with std. http methods - Update controller with all http methods - Update comment module - Fix http-client bug when error is thrown * fix http client bug when error is thrown * feat: add entity thread * feat: add migrations for thread and comment * update entitites * add tid to migration * filter comments by tid(thread_id) * fix: comment migration, add missing column comment * feat: integrate in ui * feat: split comments based on app_id * fix: dnd to correct position * package json engines * engines update * update npm * npm 6 to 7 * fix: add user initials to thread * fix: add firtname lastname to the comments * - Return user object when save thread called - Hide password field from user response - Fix created_at date typo - Instead of fetch all threads on new thread added, add the response to array of existing threads * feat: update ui components * change icon on comments view * ui fixes * fix: close icon close the popover * temp: comment select: false * use currentUser from localStorage * fix: on click outside if comment is open, dont hit addThread * fix: auth token issue in http-client * on drag hide the comment if open * add jwt auth * spec: add test for comment & thread * cleanup: remove console.log * feat: add comment actions * feat: add edit, delete, resolve options * feat: add mentions component * feat: add nestjs websockets * temp * websocket: establish client-server communication * ws: add message listner to comments module in ui * feat: add broadcast method to broadcast new events to all clients :bomb: * ws: cleanup :call_me_hand: * fix: remove max height from comment actions * feat: add user mentions, emoji support * fix: add static list of users - temp * update and delete iterations * - Rename comment, thread to comments, threads - Add conditional actions - Show edit, delete only if he is comment owner - Show resolve only if he is thread owner * reset engines * move svgr webpack to deps * fix: ui issues * remove log stmt * refactor: move resolved icon to comment-header * feat: allow comments to be added on top of widgets * feat: add keyboard shortcut * scroll to bottom on comment add * ui fixes * feat: add react toast for notification display * feat: add comment badge * fix: ws connection * fix: ws * remove rvrse * feat: add comment sidebar * feat: add comment right sidebar * fix: add missing foreign key elements * - upgrade typeorm to 0.2.38 - comment sidebar ui - added filter ui * feat: on click of right sidebar notificaiton open the comment box * reset engines * fix: add organization id to the comment and thread module * fix: add current version id * add currentversion id * disable comments if no id present * temp:checking for heroku deploy * fetch app on edit and deploy version * rename current_version_id to app_versions_id * ui fixes * show mentioned user in blue color * add ui changes * add authorization for create thread * change color to blue on click of comment, add auth for other endpoints of thread * update threads, notifications using socket * add auth for comments * remove events spec file * fix duplicate key error * fix notificaitons updation on edit, delete, resolve buttons clicked * update notifications for edit * feature toggle changes for frontend * add check for comments server * add emoji mart package for emoji * add reply count in comment sidebar * subtract 1 from count in comment sidebar * change empty text when no comments available
2021-11-01 07:28:03 +00:00
onComponentClick(id, component, event);
}}
2021-04-30 06:31:32 +00:00
>
{/* Show top bar unless search box is disabled and server pagination is enabled */}
{displaySearchBox && (
<div className="card-body border-bottom py-3 jet-data-table-header">
<div className="d-flex">
<div className="ms-auto text-muted">
<GlobalFilter
preGlobalFilteredRows={preGlobalFilteredRows}
globalFilter={state.globalFilter}
useAsyncDebounce={useAsyncDebounce}
setGlobalFilter={setGlobalFilter}
onComponentOptionChanged={onComponentOptionChanged}
component={component}
serverSideSearch={serverSideSearch}
onEvent={onEvent}
/>
</div>
2021-04-30 06:31:32 +00:00
</div>
</div>
)}
2021-04-30 06:31:32 +00:00
<div className="table-responsive jet-data-table">
2021-08-28 16:10:45 +00:00
<table {...getTableProps()} className={`table table-vcenter table-nowrap ${tableType}`} style={computedStyles}>
2021-04-30 06:31:32 +00:00
<thead>
{headerGroups.map((headerGroup, index) => (
<tr key={index} {...headerGroup.getHeaderGroupProps()} tabIndex="0" className="tr">
{headerGroup.headers.map((column, index) => (
2021-04-30 06:31:32 +00:00
<th
key={index}
2021-04-30 06:31:32 +00:00
{...column.getHeaderProps(column.getSortByToggleProps())}
className={column.isSorted ? (column.isSortedDesc ? 'sort-desc th' : 'sort-asc th') : 'th'}
>
{column.render('Header')}
<div
draggable="true"
{...column.getResizerProps()}
className={`resizer ${column.isResizing ? 'isResizing' : ''}`}
/>
</th>
))}
</tr>
))}
</thead>
2021-05-26 12:31:33 +00:00
{!loadingState && page.length === 0 && (
<center className="w-100">
<div className="py-5"> no data </div>
</center>
)}
2021-05-26 12:31:33 +00:00
2021-04-30 06:31:32 +00:00
{!loadingState && (
<tbody {...getTableBodyProps()} style={{ color: computeFontColor() }}>
2021-04-30 06:31:32 +00:00
{console.log('page', page)}
{page.map((row, index) => {
2021-04-30 06:31:32 +00:00
prepareRow(row);
return (
<tr
key={index}
className={`table-row ${
highlightSelectedRow && row.id === componentState.selectedRowId ? 'selected' : ''
}`}
2021-04-30 06:31:32 +00:00
{...row.getRowProps()}
onClick={(e) => {
e.stopPropagation();
onEvent('onRowClicked', { component, data: row.original, rowId: row.id });
2021-04-30 06:31:32 +00:00
}}
>
{row.cells.map((cell, index) => {
2021-04-30 06:31:32 +00:00
let cellProps = cell.getCellProps();
if (componentState.changeSet) {
if (componentState.changeSet[cell.row.index]) {
const currentColumn = columnData.find((column) => column.id === cell.column.id);
if (
_.get(componentState.changeSet[cell.row.index], currentColumn?.accessor, undefined) !==
undefined
) {
2021-04-30 06:31:32 +00:00
console.log('componentState.changeSet', componentState.changeSet);
cellProps.style.backgroundColor = darkMode ? '#1c252f' : '#ffffde';
cellProps.style['--tblr-table-accent-bg'] = darkMode ? '#1c252f' : '#ffffde';
2021-04-30 06:31:32 +00:00
}
}
}
return (
// Does not require key as its already being passed by react-table via cellProps
// eslint-disable-next-line react/jsx-key
<td
className={cx({
'has-actions': cell.column.id === 'rightActions' || cell.column.id === 'leftActions',
'has-text': cell.column.columnType === 'text' || cell.column.isEditable,
'has-dropdown': cell.column.columnType === 'dropdown',
'has-multiselect': cell.column.columnType === 'multiselect',
'has-datepicker': cell.column.columnType === 'datepicker',
'align-items-center flex-column': cell.column.columnType === 'selector',
[cellSizeType]: true,
})}
{...cellProps}
>
<div className="td-container">{cell.render('Cell')}</div>
</td>
);
2021-04-30 06:31:32 +00:00
})}
</tr>
);
})}
</tbody>
)}
</table>
{loadingState === true && (
<div style={{ width: '100%' }} className="p-2">
2021-07-03 18:07:51 +00:00
<center>
<div className="spinner-border mt-5" role="status"></div>
</center>
2021-04-30 06:31:32 +00:00
</div>
)}
</div>
{(clientSidePagination ||
serverSidePagination ||
Object.keys(componentState.changeSet || {}).length > 0 ||
showFilterButton ||
showDownloadButton) && (
<div className="card-footer d-flex align-items-center jet-table-footer">
<div className="table-footer row">
<div className="col">
{(clientSidePagination || serverSidePagination) && (
<Pagination
lastActivePageIndex={pageIndex}
serverSide={serverSidePagination}
autoGotoPage={gotoPage}
autoCanNextPage={canNextPage}
autoPageCount={pageCount}
autoPageOptions={pageOptions}
onPageIndexChanged={onPageIndexChanged}
pageIndex={paginationInternalPageIndex}
setPageIndex={setPaginationInternalPageIndex}
/>
)}
</div>
{showBulkUpdateActions && Object.keys(componentState.changeSet || {}).length > 0 && (
<div className="col">
<button
className={`btn btn-primary btn-sm ${componentState.isSavingChanges ? 'btn-loading' : ''}`}
onClick={() =>
onEvent('onBulkUpdate', { component }).then(() => {
handleChangesSaved();
})
}
>
Save Changes
</button>
<button className="btn btn-light btn-sm mx-2" onClick={() => handleChangesDiscarded()}>
Discard changes
</button>
</div>
)}
<div className="col-auto">
{showFilterButton && (
<span data-tip="Filter data" className="btn btn-light btn-sm p-1 mx-2" onClick={() => showFilters()}>
<img src="/assets/images/icons/filter.svg" width="13" height="13" />
{filters.length > 0 && (
<a className="badge bg-azure" style={{ width: '4px', height: '4px', marginTop: '5px' }}></a>
)}
</span>
)}
{showDownloadButton && (
<span
data-tip="Download as CSV"
className="btn btn-light btn-sm p-1"
onClick={() => exportData('csv', true)}
>
<img src="/assets/images/icons/download.svg" width="13" height="13" />
</span>
)}
</div>
2021-04-30 06:31:32 +00:00
</div>
</div>
)}
2021-04-30 06:31:32 +00:00
{isFiltersVisible && (
<div className="table-filters card">
<div className="card-header row">
2021-04-30 06:31:32 +00:00
<div className="col">
<h4 className="font-weight-normal">Filters</h4>
2021-04-30 06:31:32 +00:00
</div>
<div className="col-auto">
<button onClick={() => hideFilters()} className="btn btn-light btn-sm">
x
</button>
</div>
</div>
<div className="card-body">
{filters.map((filter, index) => (
<div className="row mb-2" key={index}>
<div className="col p-2" style={{ maxWidth: '70px' }}>
<small>{index > 0 ? 'and' : 'where'}</small>
</div>
<div className="col">
<SelectSearch
options={columnData.map((column) => {
2021-08-23 13:24:28 +00:00
return { name: column.Header, value: column.id };
2021-04-30 06:31:32 +00:00
})}
value={filter.id}
search={true}
onChange={(value) => {
filterColumnChanged(index, value);
}}
filterOptions={fuzzySearch}
placeholder="Select.."
/>
</div>
<div className="col" style={{ maxWidth: '180px' }}>
<SelectSearch
options={[
{ name: 'contains', value: 'contains' },
2021-05-09 12:03:03 +00:00
{ name: 'matches', value: 'matches' },
{ name: 'does not match', value: 'nl' },
2021-04-30 06:31:32 +00:00
{ name: 'equals', value: 'equals' },
{ name: 'does not equal', value: 'ne' },
2021-04-30 06:31:32 +00:00
{ name: 'greater than', value: 'gt' },
{ name: 'less than', value: 'lt' },
{ name: 'greater than or equals', value: 'gte' },
{ name: 'less than or equals', value: 'lte' },
2021-04-30 06:31:32 +00:00
]}
value={filter.value.operation}
search={true}
onChange={(value) => {
filterOperationChanged(index, value);
}}
filterOptions={fuzzySearch}
placeholder="Select.."
/>
</div>
<div className="col">
<input
type="text"
value={filter.value.value}
placeholder="value"
className="form-control"
onChange={(e) => filterValueChanged(index, e.target.value)}
/>
</div>
<div className="col-auto">
<button
onClick={() => removeFilter(index)}
className={`btn ${darkMode ? 'btn-dark' : 'btn-light'} btn-sm p-2 text-danger font-weight-bold`}
>
2021-04-30 06:31:32 +00:00
x
</button>
</div>
</div>
))}
{filters.length === 0 && (
2021-04-30 06:31:32 +00:00
<div>
<center>
<span>no filters yet.</span>
2021-04-30 06:31:32 +00:00
</center>
</div>
)}
</div>
<div className="card-footer">
<button onClick={addFilter} className="btn btn-light btn-sm">
2021-04-30 06:31:32 +00:00
+ add filter
</button>
<button onClick={() => clearFilters()} className="btn btn-light btn-sm mx-2">
2021-04-30 06:31:32 +00:00
clear filters
</button>
</div>
</div>
)}
</div>
);
2021-04-12 13:35:48 +00:00
}