feat: allow adding available pre-defined table columns to views (#1371)

* feat: allow adding available pre-defined table columns to views

Closes #1360

* fix: allow creating views with the same name for the same table

* refactor: code review

- rename things
- move handleColumnVisibilityChange to useTableColumns hook
This commit is contained in:
Thaïs 2023-08-30 11:33:21 +02:00 committed by GitHub
parent 9df4b475d8
commit 4aae22ab34
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 148 additions and 82 deletions

View File

@ -2805,13 +2805,6 @@ export type ViewWhereInput = {
export type ViewWhereUniqueInput = {
id?: InputMaybe<Scalars['String']>;
workspaceId_type_objectId_name?: InputMaybe<ViewWorkspaceIdTypeObjectIdNameCompoundUniqueInput>;
};
export type ViewWorkspaceIdTypeObjectIdNameCompoundUniqueInput = {
name: Scalars['String'];
objectId: Scalars['String'];
type: ViewType;
};
export type Workspace = {

View File

@ -1,4 +1,4 @@
import { cloneElement, ComponentProps, useRef } from 'react';
import { cloneElement, type ComponentProps, useCallback, useRef } from 'react';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
@ -6,19 +6,22 @@ import { IconButton } from '@/ui/button/components/IconButton';
import { DropdownMenuItem } from '@/ui/dropdown/components/DropdownMenuItem';
import { StyledDropdownMenu } from '@/ui/dropdown/components/StyledDropdownMenu';
import { StyledDropdownMenuItemsContainer } from '@/ui/dropdown/components/StyledDropdownMenuItemsContainer';
import type { ViewFieldMetadata } from '@/ui/editable-field/types/ViewField';
import { IconPlus } from '@/ui/icon';
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue';
import { useTableColumns } from '../hooks/useTableColumns';
import { TableRecoilScopeContext } from '../states/recoil-scope-contexts/TableRecoilScopeContext';
import { hiddenTableColumnsScopedSelector } from '../states/selectors/hiddenTableColumnsScopedSelector';
import type { ColumnDefinition } from '../types/ColumnDefinition';
const StyledColumnMenu = styled(StyledDropdownMenu)`
font-weight: ${({ theme }) => theme.font.weight.regular};
`;
type EntityTableColumnMenuProps = {
onAddColumn: (columnId: string) => void;
onAddColumn?: () => void;
onClickOutside?: () => void;
} & ComponentProps<'div'>;
@ -40,6 +43,16 @@ export const EntityTableColumnMenu = ({
callback: onClickOutside,
});
const { handleColumnVisibilityChange } = useTableColumns();
const handleAddColumn = useCallback(
(column: ColumnDefinition<ViewFieldMetadata>) => {
onAddColumn?.();
handleColumnVisibilityChange(column);
},
[handleColumnVisibilityChange, onAddColumn],
);
return (
<StyledColumnMenu {...props} ref={ref}>
<StyledDropdownMenuItemsContainer>
@ -48,8 +61,9 @@ export const EntityTableColumnMenu = ({
key={column.id}
actions={[
<IconButton
key={`add-${column.id}`}
icon={<IconPlus size={theme.icon.size.sm} />}
onClick={() => onAddColumn(column.id)}
onClick={() => handleAddColumn(column)}
/>,
]}
>

View File

@ -148,19 +148,6 @@ export function EntityTableHeader() {
setIsColumnMenuOpen((previousValue) => !previousValue);
}, []);
const handleAddColumn = useCallback(
(columnId: string) => {
setIsColumnMenuOpen(false);
const nextColumns = columns.map((column) =>
column.id === columnId ? { ...column, isVisible: true } : column,
);
setColumns(nextColumns);
},
[columns, setColumns],
);
return (
<thead data-select-disable>
<tr>
@ -204,7 +191,7 @@ export function EntityTableHeader() {
/>
{isColumnMenuOpen && (
<StyledEntityTableColumnMenu
onAddColumn={handleAddColumn}
onAddColumn={toggleColumnMenu}
onClickOutside={toggleColumnMenu}
/>
)}

View File

@ -0,0 +1,40 @@
import { useCallback } from 'react';
import type { ViewFieldMetadata } from '@/ui/editable-field/types/ViewField';
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue';
import { TableRecoilScopeContext } from '../states/recoil-scope-contexts/TableRecoilScopeContext';
import { tableColumnsByIdScopedSelector } from '../states/selectors/tableColumnsByIdScopedSelector';
import { tableColumnsScopedState } from '../states/tableColumnsScopedState';
import type { ColumnDefinition } from '../types/ColumnDefinition';
export const useTableColumns = () => {
const [tableColumns, setTableColumns] = useRecoilScopedState(
tableColumnsScopedState,
TableRecoilScopeContext,
);
const tableColumnsById = useRecoilScopedValue(
tableColumnsByIdScopedSelector,
TableRecoilScopeContext,
);
const handleColumnVisibilityChange = useCallback(
(column: ColumnDefinition<ViewFieldMetadata>) => {
const nextColumns = tableColumnsById[column.id]
? tableColumns.map((previousColumn) =>
previousColumn.id === column.id
? { ...previousColumn, isVisible: !column.isVisible }
: previousColumn,
)
: [...tableColumns, { ...column, isVisible: true }].sort(
(columnA, columnB) => columnA.order - columnB.order,
);
setTableColumns(nextColumns);
},
[setTableColumns, tableColumns, tableColumnsById],
);
return { handleColumnVisibilityChange };
};

View File

@ -27,9 +27,9 @@ import {
import { tableColumnsScopedState } from '@/ui/table/states/tableColumnsScopedState';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { useContextScopeId } from '@/ui/utilities/recoil-scope/hooks/useContextScopeId';
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue';
import { useTableColumns } from '../../hooks/useTableColumns';
import { TableRecoilScopeContext } from '../../states/recoil-scope-contexts/TableRecoilScopeContext';
import { savedTableColumnsScopedState } from '../../states/savedTableColumnsScopedState';
import { hiddenTableColumnsScopedSelector } from '../../states/selectors/hiddenTableColumnsScopedSelector';
@ -74,10 +74,6 @@ export function TableOptionsDropdownContent({
const [viewEditMode, setViewEditMode] = useRecoilState(
tableViewEditModeState,
);
const [columns, setColumns] = useRecoilScopedState(
tableColumnsScopedState,
TableRecoilScopeContext,
);
const visibleColumns = useRecoilScopedValue(
visibleTableColumnsScopedSelector,
TableRecoilScopeContext,
@ -91,18 +87,7 @@ export function TableOptionsDropdownContent({
TableRecoilScopeContext,
);
const handleColumnVisibilityChange = useCallback(
async (columnId: string, nextIsVisible: boolean) => {
const nextColumns = columns.map((column) =>
column.id === columnId
? { ...column, isVisible: nextIsVisible }
: column,
);
setColumns(nextColumns);
},
[columns, setColumns],
);
const { handleColumnVisibilityChange } = useTableColumns();
const renderFieldActions = useCallback(
(column: ColumnDefinition<ViewFieldMetadata>) =>
@ -110,6 +95,7 @@ export function TableOptionsDropdownContent({
!column.isVisible || visibleColumns.length > 1
? [
<IconButton
key={`action-${column.id}`}
icon={
column.isVisible ? (
<IconMinus size={theme.icon.size.sm} />
@ -117,9 +103,7 @@ export function TableOptionsDropdownContent({
<IconPlus size={theme.icon.size.sm} />
)
}
onClick={() =>
handleColumnVisibilityChange(column.id, !column.isVisible)
}
onClick={() => handleColumnVisibilityChange(column)}
/>,
]
: undefined,

View File

@ -112,12 +112,15 @@ export const TableUpdateViewButtonGroup = ({
}, []);
const handleViewSubmit = useCallback(async () => {
await Promise.resolve(onViewSubmit?.());
if (canPersistColumns) setSavedColumns(currentColumns);
if (canPersistFilters) setSavedFilters(selectedFilters);
if (canPersistSorts) setSavedSorts(selectedSorts);
setSavedColumns(currentColumns);
setSavedFilters(selectedFilters);
setSavedSorts(selectedSorts);
await Promise.resolve(onViewSubmit?.());
}, [
canPersistColumns,
canPersistFilters,
canPersistSorts,
currentColumns,
onViewSubmit,
selectedFilters,

View File

@ -0,0 +1,13 @@
import { atomFamily } from 'recoil';
import type { ViewFieldMetadata } from '@/ui/editable-field/types/ViewField';
import type { ColumnDefinition } from '../types/ColumnDefinition';
export const availableTableColumnsScopedState = atomFamily<
ColumnDefinition<ViewFieldMetadata>[],
string
>({
key: 'availableTableColumnsScopedState',
default: [],
});

View File

@ -1,13 +1,22 @@
import { selectorFamily } from 'recoil';
import { availableTableColumnsScopedState } from '../availableTableColumnsScopedState';
import { tableColumnsScopedState } from '../tableColumnsScopedState';
export const hiddenTableColumnsScopedSelector = selectorFamily({
key: 'hiddenTableColumnsScopedSelector',
get:
(scopeId: string) =>
({ get }) =>
get(tableColumnsScopedState(scopeId)).filter(
(column) => !column.isVisible,
),
({ get }) => {
const columns = get(tableColumnsScopedState(scopeId));
const columnLabels = columns.map(({ label }) => label);
const otherAvailableColumns = get(
availableTableColumnsScopedState(scopeId),
).filter(({ label }) => !columnLabels.includes(label));
return [
...columns.filter((column) => !column.isVisible),
...otherAvailableColumns,
];
},
});

View File

@ -5,6 +5,7 @@ import type {
ViewFieldMetadata,
ViewFieldTextMetadata,
} from '@/ui/editable-field/types/ViewField';
import { availableTableColumnsScopedState } from '@/ui/table/states/availableTableColumnsScopedState';
import { TableRecoilScopeContext } from '@/ui/table/states/recoil-scope-contexts/TableRecoilScopeContext';
import { savedTableColumnsScopedState } from '@/ui/table/states/savedTableColumnsScopedState';
import { savedTableColumnsByIdScopedSelector } from '@/ui/table/states/selectors/savedTableColumnsByIdScopedSelector';
@ -45,19 +46,24 @@ export const useTableViewFields = ({
objectName: 'company' | 'person';
columnDefinitions: ColumnDefinition<ViewFieldMetadata>[];
}) => {
const currentViewId = useRecoilScopedValue(
const currentTableViewId = useRecoilScopedValue(
currentTableViewIdState,
TableRecoilScopeContext,
);
const [columns, setColumns] = useRecoilScopedState(
const [availableTableColumns, setAvailableTableColumns] =
useRecoilScopedState(
availableTableColumnsScopedState,
TableRecoilScopeContext,
);
const [tableColumns, setTableColumns] = useRecoilScopedState(
tableColumnsScopedState,
TableRecoilScopeContext,
);
const setSavedColumns = useSetRecoilState(
savedTableColumnsScopedState(currentViewId),
const setSavedTableColumns = useSetRecoilState(
savedTableColumnsScopedState(currentTableViewId),
);
const savedColumnsById = useRecoilValue(
savedTableColumnsByIdScopedSelector(currentViewId),
const savedTableColumnsById = useRecoilValue(
savedTableColumnsByIdScopedSelector(currentTableViewId),
);
const [createViewFieldsMutation] = useCreateViewFieldsMutation();
@ -66,7 +72,7 @@ export const useTableViewFields = ({
const createViewFields = useCallback(
(
columns: ColumnDefinition<ViewFieldMetadata>[],
viewId = currentViewId,
viewId = currentTableViewId,
) => {
if (!viewId || !columns.length) return;
@ -79,12 +85,12 @@ export const useTableViewFields = ({
},
});
},
[createViewFieldsMutation, currentViewId, objectName],
[createViewFieldsMutation, currentTableViewId, objectName],
);
const updateViewFields = useCallback(
(columns: ColumnDefinition<ViewFieldMetadata>[]) => {
if (!currentViewId || !columns.length) return;
if (!currentTableViewId || !columns.length) return;
return Promise.all(
columns.map((column) =>
@ -100,16 +106,16 @@ export const useTableViewFields = ({
),
);
},
[currentViewId, updateViewFieldMutation],
[currentTableViewId, updateViewFieldMutation],
);
const { refetch } = useGetViewFieldsQuery({
skip: !currentViewId,
skip: !currentTableViewId,
variables: {
orderBy: { index: SortOrder.Asc },
where: {
objectName: { equals: objectName },
viewId: { equals: currentViewId ?? null },
viewId: { equals: currentTableViewId ?? null },
},
},
onCompleted: async (data) => {
@ -132,26 +138,42 @@ export const useTableViewFields = ({
isVisible: viewField.isVisible,
}));
if (!isDeeplyEqual(columns, nextColumns)) {
setSavedColumns(nextColumns);
setColumns(nextColumns);
if (!isDeeplyEqual(tableColumns, nextColumns)) {
setSavedTableColumns(nextColumns);
setTableColumns(nextColumns);
}
if (!availableTableColumns.length) {
setAvailableTableColumns(columnDefinitions);
}
},
});
const persistColumns = useCallback(async () => {
if (!currentViewId) return;
if (!currentTableViewId) return;
const viewFieldsToUpdate = columns.filter(
const viewFieldsToCreate = tableColumns.filter(
(column) => !savedTableColumnsById[column.id],
);
await createViewFields(viewFieldsToCreate);
const viewFieldsToUpdate = tableColumns.filter(
(column) =>
savedColumnsById[column.id] &&
(savedColumnsById[column.id].size !== column.size ||
savedColumnsById[column.id].isVisible !== column.isVisible),
savedTableColumnsById[column.id] &&
(savedTableColumnsById[column.id].size !== column.size ||
savedTableColumnsById[column.id].isVisible !== column.isVisible),
);
await updateViewFields(viewFieldsToUpdate);
return refetch();
}, [columns, currentViewId, refetch, savedColumnsById, updateViewFields]);
}, [
createViewFields,
currentTableViewId,
refetch,
savedTableColumnsById,
tableColumns,
updateViewFields,
]);
return { createViewFields, persistColumns };
};

View File

@ -0,0 +1,2 @@
-- DropIndex
DROP INDEX "views_workspaceId_type_objectId_name_key";

View File

@ -603,7 +603,6 @@ model View {
/// @TypeGraphQL.omit(input: true, output: true)
workspaceId String
@@unique([workspaceId, type, objectId, name])
@@map("views")
}