Merge commit 'cd3a32e55503dc1e6b9873d812dd401bf7d51045' into context-menu-vertical
This commit is contained in:
@ -1,16 +1,19 @@
|
||||
import { useRef } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import type {
|
||||
ViewFieldDefinition,
|
||||
ViewFieldMetadata,
|
||||
} from '@/ui/editable-field/types/ViewField';
|
||||
import { SelectedSortType, SortType } from '@/ui/filter-n-sort/types/interface';
|
||||
import { DragSelect } from '@/ui/utilities/drag-select/components/DragSelect';
|
||||
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
||||
|
||||
import { useLeaveTableFocus } from '../hooks/useLeaveTableFocus';
|
||||
import { useMapKeyboardToSoftFocus } from '../hooks/useMapKeyboardToSoftFocus';
|
||||
import { useResetTableRowSelection } from '../hooks/useResetTableRowSelection';
|
||||
import { useSetRowSelectedState } from '../hooks/useSetRowSelectedState';
|
||||
import { EntityUpdateMutationHookContext } from '../states/EntityUpdateMutationHookContext';
|
||||
import { tableRowIdsState } from '../states/tableRowIdsState';
|
||||
import { EntityUpdateMutationContext } from '../states/EntityUpdateMutationHookContext';
|
||||
import { TableHeader } from '../table-header/components/TableHeader';
|
||||
|
||||
import { EntityTableBody } from './EntityTableBody';
|
||||
@ -25,6 +28,8 @@ const StyledTable = styled.table`
|
||||
margin-right: ${({ theme }) => theme.table.horizontalCellMargin};
|
||||
table-layout: fixed;
|
||||
|
||||
width: calc(100% - ${({ theme }) => theme.table.horizontalCellMargin} * 2);
|
||||
|
||||
th {
|
||||
border: 1px solid ${({ theme }) => theme.border.color.light};
|
||||
border-collapse: collapse;
|
||||
@ -90,28 +95,24 @@ type OwnProps<SortField> = {
|
||||
viewName: string;
|
||||
viewIcon?: React.ReactNode;
|
||||
availableSorts?: Array<SortType<SortField>>;
|
||||
onColumnsChange?: (columns: ViewFieldDefinition<ViewFieldMetadata>[]) => void;
|
||||
onSortsUpdate?: (sorts: Array<SelectedSortType<SortField>>) => void;
|
||||
onRowSelectionChange?: (rowSelection: string[]) => void;
|
||||
useUpdateEntityMutation: any;
|
||||
updateEntityMutation: any;
|
||||
};
|
||||
|
||||
export function EntityTable<SortField>({
|
||||
viewName,
|
||||
viewIcon,
|
||||
availableSorts,
|
||||
onColumnsChange,
|
||||
onSortsUpdate,
|
||||
useUpdateEntityMutation,
|
||||
updateEntityMutation,
|
||||
}: OwnProps<SortField>) {
|
||||
const tableBodyRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const rowIds = useRecoilValue(tableRowIdsState);
|
||||
const setRowSelectedState = useSetRowSelectedState();
|
||||
|
||||
function resetSelections() {
|
||||
for (const rowId of rowIds) {
|
||||
setRowSelectedState(rowId, false);
|
||||
}
|
||||
}
|
||||
const resetTableRowSelection = useResetTableRowSelection();
|
||||
|
||||
useMapKeyboardToSoftFocus();
|
||||
|
||||
@ -125,28 +126,29 @@ export function EntityTable<SortField>({
|
||||
});
|
||||
|
||||
return (
|
||||
<EntityUpdateMutationHookContext.Provider value={useUpdateEntityMutation}>
|
||||
<EntityUpdateMutationContext.Provider value={updateEntityMutation}>
|
||||
<StyledTableWithHeader>
|
||||
<StyledTableContainer ref={tableBodyRef}>
|
||||
<TableHeader
|
||||
viewName={viewName}
|
||||
viewIcon={viewIcon}
|
||||
availableSorts={availableSorts}
|
||||
onColumnsChange={onColumnsChange}
|
||||
onSortsUpdate={onSortsUpdate}
|
||||
/>
|
||||
<StyledTableWrapper>
|
||||
<StyledTable>
|
||||
<EntityTableHeader />
|
||||
<EntityTableHeader onColumnsChange={onColumnsChange} />
|
||||
<EntityTableBody />
|
||||
</StyledTable>
|
||||
</StyledTableWrapper>
|
||||
<DragSelect
|
||||
dragSelectable={tableBodyRef}
|
||||
onDragSelectionStart={resetSelections}
|
||||
onDragSelectionStart={resetTableRowSelection}
|
||||
onDragSelectionChange={setRowSelectedState}
|
||||
/>
|
||||
</StyledTableContainer>
|
||||
</StyledTableWithHeader>
|
||||
</EntityUpdateMutationHookContext.Provider>
|
||||
</EntityUpdateMutationContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { cloneElement, ComponentProps, useRef } from 'react';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { IconButton } from '@/ui/button/components/IconButton';
|
||||
import { DropdownMenu } from '@/ui/dropdown/components/DropdownMenu';
|
||||
@ -9,32 +10,27 @@ import { DropdownMenuItemsContainer } from '@/ui/dropdown/components/DropdownMen
|
||||
import { IconPlus } from '@/ui/icon';
|
||||
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
||||
|
||||
import type {
|
||||
ViewFieldDefinition,
|
||||
ViewFieldMetadata,
|
||||
} from '../../editable-field/types/ViewField';
|
||||
import { hiddenTableColumnsState } from '../states/tableColumnsState';
|
||||
|
||||
const StyledColumnMenu = styled(DropdownMenu)`
|
||||
font-weight: ${({ theme }) => theme.font.weight.regular};
|
||||
`;
|
||||
|
||||
type EntityTableColumnMenuProps = {
|
||||
onAddViewField: (
|
||||
viewFieldDefinition: ViewFieldDefinition<ViewFieldMetadata>,
|
||||
) => void;
|
||||
onAddColumn: (columnId: string) => void;
|
||||
onClickOutside?: () => void;
|
||||
viewFieldDefinitions: ViewFieldDefinition<ViewFieldMetadata>[];
|
||||
} & ComponentProps<'div'>;
|
||||
|
||||
export const EntityTableColumnMenu = ({
|
||||
onAddViewField,
|
||||
onAddColumn,
|
||||
onClickOutside = () => undefined,
|
||||
viewFieldDefinitions,
|
||||
...props
|
||||
}: EntityTableColumnMenuProps) => {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const theme = useTheme();
|
||||
|
||||
const hiddenColumns = useRecoilValue(hiddenTableColumnsState);
|
||||
|
||||
useListenClickOutside({
|
||||
refs: [ref],
|
||||
callback: onClickOutside,
|
||||
@ -43,21 +39,21 @@ export const EntityTableColumnMenu = ({
|
||||
return (
|
||||
<StyledColumnMenu {...props} ref={ref}>
|
||||
<DropdownMenuItemsContainer>
|
||||
{viewFieldDefinitions.map((viewFieldDefinition) => (
|
||||
{hiddenColumns.map((column) => (
|
||||
<DropdownMenuItem
|
||||
key={viewFieldDefinition.id}
|
||||
key={column.id}
|
||||
actions={
|
||||
<IconButton
|
||||
icon={<IconPlus size={theme.icon.size.sm} />}
|
||||
onClick={() => onAddViewField(viewFieldDefinition)}
|
||||
onClick={() => onAddColumn(column.id)}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{viewFieldDefinition.columnIcon &&
|
||||
cloneElement(viewFieldDefinition.columnIcon, {
|
||||
{column.columnIcon &&
|
||||
cloneElement(column.columnIcon, {
|
||||
size: theme.icon.size.md,
|
||||
})}
|
||||
{viewFieldDefinition.columnLabel}
|
||||
{column.columnLabel}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuItemsContainer>
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import { getOperationName } from '@apollo/client/utilities';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { useRecoilCallback, useRecoilState, useRecoilValue } from 'recoil';
|
||||
@ -11,21 +10,14 @@ import type {
|
||||
} from '@/ui/editable-field/types/ViewField';
|
||||
import { IconPlus } from '@/ui/icon';
|
||||
import { useTrackPointer } from '@/ui/utilities/pointer-event/hooks/useTrackPointer';
|
||||
import { GET_VIEW_FIELDS } from '@/views/queries/select';
|
||||
import { currentViewIdState } from '@/views/states/currentViewIdState';
|
||||
import {
|
||||
useCreateViewFieldMutation,
|
||||
useUpdateViewFieldMutation,
|
||||
} from '~/generated/graphql';
|
||||
|
||||
import { toViewFieldInput } from '../hooks/useLoadViewFields';
|
||||
import { resizeFieldOffsetState } from '../states/resizeFieldOffsetState';
|
||||
import {
|
||||
addableViewFieldDefinitionsState,
|
||||
columnWidthByViewFieldIdState,
|
||||
viewFieldsState,
|
||||
visibleViewFieldsState,
|
||||
} from '../states/viewFieldsState';
|
||||
hiddenTableColumnsState,
|
||||
tableColumnsByIdState,
|
||||
tableColumnsState,
|
||||
visibleTableColumnsState,
|
||||
} from '../states/tableColumnsState';
|
||||
|
||||
import { ColumnHead } from './ColumnHead';
|
||||
import { EntityTableColumnMenu } from './EntityTableColumnMenu';
|
||||
@ -86,17 +78,18 @@ const StyledEntityTableColumnMenu = styled(EntityTableColumnMenu)`
|
||||
z-index: ${({ theme }) => theme.lastLayerZIndex};
|
||||
`;
|
||||
|
||||
export function EntityTableHeader() {
|
||||
export type EntityTableHeaderProps = {
|
||||
onColumnsChange?: (columns: ViewFieldDefinition<ViewFieldMetadata>[]) => void;
|
||||
};
|
||||
|
||||
export function EntityTableHeader({ onColumnsChange }: EntityTableHeaderProps) {
|
||||
const theme = useTheme();
|
||||
|
||||
const [{ objectName }, setViewFieldsState] = useRecoilState(viewFieldsState);
|
||||
const currentViewId = useRecoilValue(currentViewIdState);
|
||||
const viewFields = useRecoilValue(visibleViewFieldsState);
|
||||
const columnWidths = useRecoilValue(columnWidthByViewFieldIdState);
|
||||
const addableViewFieldDefinitions = useRecoilValue(
|
||||
addableViewFieldDefinitionsState,
|
||||
);
|
||||
const [columns, setColumns] = useRecoilState(tableColumnsState);
|
||||
const [offset, setOffset] = useRecoilState(resizeFieldOffsetState);
|
||||
const columnsById = useRecoilValue(tableColumnsByIdState);
|
||||
const hiddenColumns = useRecoilValue(hiddenTableColumnsState);
|
||||
const visibleColumns = useRecoilValue(visibleTableColumnsState);
|
||||
|
||||
const [initialPointerPositionX, setInitialPointerPositionX] = useState<
|
||||
number | null
|
||||
@ -104,9 +97,6 @@ export function EntityTableHeader() {
|
||||
const [resizedFieldId, setResizedFieldId] = useState<string | null>(null);
|
||||
const [isColumnMenuOpen, setIsColumnMenuOpen] = useState(false);
|
||||
|
||||
const [createViewFieldMutation] = useCreateViewFieldMutation();
|
||||
const [updateViewFieldMutation] = useUpdateViewFieldMutation();
|
||||
|
||||
const handleResizeHandlerStart = useCallback((positionX: number) => {
|
||||
setInitialPointerPositionX(positionX);
|
||||
}, []);
|
||||
@ -126,37 +116,28 @@ export function EntityTableHeader() {
|
||||
|
||||
const nextWidth = Math.round(
|
||||
Math.max(
|
||||
columnWidths[resizedFieldId] +
|
||||
columnsById[resizedFieldId].columnSize +
|
||||
snapshot.getLoadable(resizeFieldOffsetState).valueOrThrow(),
|
||||
COLUMN_MIN_WIDTH,
|
||||
),
|
||||
);
|
||||
|
||||
if (nextWidth !== columnWidths[resizedFieldId]) {
|
||||
// Optimistic update to avoid "bouncing width" visual effect on resize.
|
||||
setViewFieldsState((previousState) => ({
|
||||
...previousState,
|
||||
viewFields: previousState.viewFields.map((viewField) =>
|
||||
viewField.id === resizedFieldId
|
||||
? { ...viewField, columnSize: nextWidth }
|
||||
: viewField,
|
||||
),
|
||||
}));
|
||||
if (nextWidth !== columnsById[resizedFieldId].columnSize) {
|
||||
const nextColumns = columns.map((column) =>
|
||||
column.id === resizedFieldId
|
||||
? { ...column, columnSize: nextWidth }
|
||||
: column,
|
||||
);
|
||||
|
||||
updateViewFieldMutation({
|
||||
variables: {
|
||||
data: { sizeInPx: nextWidth },
|
||||
where: { id: resizedFieldId },
|
||||
},
|
||||
refetchQueries: [getOperationName(GET_VIEW_FIELDS) ?? ''],
|
||||
});
|
||||
setColumns(nextColumns);
|
||||
onColumnsChange?.(nextColumns);
|
||||
}
|
||||
|
||||
set(resizeFieldOffsetState, 0);
|
||||
setInitialPointerPositionX(null);
|
||||
setResizedFieldId(null);
|
||||
},
|
||||
[resizedFieldId, columnWidths, setResizedFieldId],
|
||||
[resizedFieldId, columnsById, setResizedFieldId],
|
||||
);
|
||||
|
||||
useTrackPointer({
|
||||
@ -170,26 +151,18 @@ export function EntityTableHeader() {
|
||||
setIsColumnMenuOpen((previousValue) => !previousValue);
|
||||
}, []);
|
||||
|
||||
const handleAddViewField = useCallback(
|
||||
(viewFieldDefinition: ViewFieldDefinition<ViewFieldMetadata>) => {
|
||||
const handleAddColumn = useCallback(
|
||||
(columnId: string) => {
|
||||
setIsColumnMenuOpen(false);
|
||||
|
||||
if (!objectName) return;
|
||||
const nextColumns = columns.map((column) =>
|
||||
column.id === columnId ? { ...column, isVisible: true } : column,
|
||||
);
|
||||
|
||||
createViewFieldMutation({
|
||||
variables: {
|
||||
data: {
|
||||
...toViewFieldInput(objectName, {
|
||||
...viewFieldDefinition,
|
||||
columnOrder: viewFields.length + 1,
|
||||
}),
|
||||
view: { connect: { id: currentViewId } },
|
||||
},
|
||||
},
|
||||
refetchQueries: [getOperationName(GET_VIEW_FIELDS) ?? ''],
|
||||
});
|
||||
setColumns(nextColumns);
|
||||
onColumnsChange?.(nextColumns);
|
||||
},
|
||||
[createViewFieldMutation, currentViewId, objectName, viewFields.length],
|
||||
[columns, onColumnsChange, setColumns],
|
||||
);
|
||||
|
||||
return (
|
||||
@ -205,31 +178,31 @@ export function EntityTableHeader() {
|
||||
<SelectAllCheckbox />
|
||||
</th>
|
||||
|
||||
{viewFields.map((viewField) => (
|
||||
{visibleColumns.map((column) => (
|
||||
<StyledColumnHeaderCell
|
||||
key={viewField.id}
|
||||
isResizing={resizedFieldId === viewField.id}
|
||||
key={column.id}
|
||||
isResizing={resizedFieldId === column.id}
|
||||
columnWidth={Math.max(
|
||||
columnWidths[viewField.id] +
|
||||
(resizedFieldId === viewField.id ? offset : 0),
|
||||
columnsById[column.id].columnSize +
|
||||
(resizedFieldId === column.id ? offset : 0),
|
||||
COLUMN_MIN_WIDTH,
|
||||
)}
|
||||
>
|
||||
<ColumnHead
|
||||
viewName={viewField.columnLabel}
|
||||
viewIcon={viewField.columnIcon}
|
||||
viewName={column.columnLabel}
|
||||
viewIcon={column.columnIcon}
|
||||
/>
|
||||
<StyledResizeHandler
|
||||
className="cursor-col-resize"
|
||||
role="separator"
|
||||
onPointerDown={() => {
|
||||
setResizedFieldId(viewField.id);
|
||||
setResizedFieldId(column.id);
|
||||
}}
|
||||
/>
|
||||
</StyledColumnHeaderCell>
|
||||
))}
|
||||
<th>
|
||||
{addableViewFieldDefinitions.length > 0 && (
|
||||
{hiddenColumns.length > 0 && (
|
||||
<StyledAddIconButtonWrapper>
|
||||
<StyledAddIconButton
|
||||
size="large"
|
||||
@ -238,9 +211,8 @@ export function EntityTableHeader() {
|
||||
/>
|
||||
{isColumnMenuOpen && (
|
||||
<StyledEntityTableColumnMenu
|
||||
onAddViewField={handleAddViewField}
|
||||
onAddColumn={handleAddColumn}
|
||||
onClickOutside={toggleColumnMenu}
|
||||
viewFieldDefinitions={addableViewFieldDefinitions}
|
||||
/>
|
||||
)}
|
||||
</StyledAddIconButtonWrapper>
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { visibleTableColumnsState } from '../states/tableColumnsState';
|
||||
import { ViewFieldContext } from '../states/ViewFieldContext';
|
||||
import { visibleViewFieldsState } from '../states/viewFieldsState';
|
||||
|
||||
import { CheckboxCell } from './CheckboxCell';
|
||||
import { EntityTableCell } from './EntityTableCell';
|
||||
@ -13,7 +13,7 @@ const StyledRow = styled.tr<{ selected: boolean }>`
|
||||
`;
|
||||
|
||||
export function EntityTableRow({ rowId }: { rowId: string }) {
|
||||
const viewFields = useRecoilValue(visibleViewFieldsState);
|
||||
const columns = useRecoilValue(visibleTableColumnsState);
|
||||
|
||||
return (
|
||||
<StyledRow
|
||||
@ -24,9 +24,9 @@ export function EntityTableRow({ rowId }: { rowId: string }) {
|
||||
<td>
|
||||
<CheckboxCell />
|
||||
</td>
|
||||
{viewFields.map((viewField, columnIndex) => {
|
||||
{columns.map((column, columnIndex) => {
|
||||
return (
|
||||
<ViewFieldContext.Provider value={viewField} key={viewField.id}>
|
||||
<ViewFieldContext.Provider value={column} key={column.id}>
|
||||
<EntityTableCell cellIndex={columnIndex} />
|
||||
</ViewFieldContext.Provider>
|
||||
);
|
||||
|
||||
@ -1,34 +1,21 @@
|
||||
import { defaultOrderBy } from '@/people/queries';
|
||||
import {
|
||||
ViewFieldDefinition,
|
||||
ViewFieldMetadata,
|
||||
} from '@/ui/editable-field/types/ViewField';
|
||||
import { FilterDefinition } from '@/ui/filter-n-sort/types/FilterDefinition';
|
||||
import { useSetEntityTableData } from '@/ui/table/hooks/useSetEntityTableData';
|
||||
|
||||
import { useLoadViewFields } from '../hooks/useLoadViewFields';
|
||||
|
||||
export function GenericEntityTableData({
|
||||
objectName,
|
||||
useGetRequest,
|
||||
getRequestResultKey,
|
||||
orderBy = defaultOrderBy,
|
||||
whereFilters,
|
||||
viewFieldDefinitions,
|
||||
filterDefinitionArray,
|
||||
}: {
|
||||
objectName: 'company' | 'person';
|
||||
useGetRequest: any;
|
||||
getRequestResultKey: string;
|
||||
orderBy?: any;
|
||||
whereFilters?: any;
|
||||
viewFieldDefinitions: ViewFieldDefinition<ViewFieldMetadata>[];
|
||||
filterDefinitionArray: FilterDefinition[];
|
||||
}) {
|
||||
const setEntityTableData = useSetEntityTableData();
|
||||
|
||||
useLoadViewFields({ objectName, viewFieldDefinitions });
|
||||
|
||||
useGetRequest({
|
||||
variables: { orderBy, where: whereFilters },
|
||||
onCompleted: (data: any) => {
|
||||
|
||||
Reference in New Issue
Block a user