feat: change column visibility on add (#1174)
* feat: change column visibility on add * refactor: extract views business logic from table
This commit is contained in:
@ -1,137 +0,0 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import { getOperationName } from '@apollo/client/utilities';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { IconButton } from '@/ui/button/components/IconButton';
|
||||
import { DropdownMenuHeader } from '@/ui/dropdown/components/DropdownMenuHeader';
|
||||
import { DropdownMenuItem } from '@/ui/dropdown/components/DropdownMenuItem';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/dropdown/components/DropdownMenuItemsContainer';
|
||||
import { DropdownMenuSeparator } from '@/ui/dropdown/components/DropdownMenuSeparator';
|
||||
import {
|
||||
ViewFieldDefinition,
|
||||
ViewFieldMetadata,
|
||||
} from '@/ui/editable-field/types/ViewField';
|
||||
import DropdownButton from '@/ui/filter-n-sort/components/DropdownButton';
|
||||
import { FiltersHotkeyScope } from '@/ui/filter-n-sort/types/FiltersHotkeyScope';
|
||||
import { IconChevronLeft, IconMinus, IconPlus, IconTag } from '@/ui/icon';
|
||||
import {
|
||||
hiddenViewFieldsState,
|
||||
visibleViewFieldsState,
|
||||
} from '@/ui/table/states/viewFieldsState';
|
||||
import { useUpdateViewFieldMutation } from '~/generated/graphql';
|
||||
|
||||
import { GET_VIEW_FIELDS } from '../queries/select';
|
||||
|
||||
import { OptionsDropdownSection } from './OptionsDropdownSection';
|
||||
|
||||
type OptionsDropdownButtonProps = {
|
||||
HotkeyScope: FiltersHotkeyScope;
|
||||
};
|
||||
|
||||
enum Option {
|
||||
Properties = 'Properties',
|
||||
}
|
||||
|
||||
export const OptionsDropdownButton = ({
|
||||
HotkeyScope,
|
||||
}: OptionsDropdownButtonProps) => {
|
||||
const theme = useTheme();
|
||||
const [isUnfolded, setIsUnfolded] = useState(false);
|
||||
const [selectedOption, setSelectedOption] = useState<Option | undefined>(
|
||||
undefined,
|
||||
);
|
||||
|
||||
const visibleFields = useRecoilValue(visibleViewFieldsState);
|
||||
const hiddenFields = useRecoilValue(hiddenViewFieldsState);
|
||||
|
||||
const [updateViewFieldMutation] = useUpdateViewFieldMutation();
|
||||
|
||||
const handleViewFieldVisibilityChange = useCallback(
|
||||
(viewFieldId: string, nextIsVisible: boolean) => {
|
||||
updateViewFieldMutation({
|
||||
variables: {
|
||||
data: { isVisible: nextIsVisible },
|
||||
where: { id: viewFieldId },
|
||||
},
|
||||
refetchQueries: [getOperationName(GET_VIEW_FIELDS) ?? ''],
|
||||
});
|
||||
},
|
||||
[updateViewFieldMutation],
|
||||
);
|
||||
|
||||
const renderFieldActions = useCallback(
|
||||
(viewField: ViewFieldDefinition<ViewFieldMetadata>) =>
|
||||
// Do not allow hiding last visible column
|
||||
!viewField.isVisible || visibleFields.length > 1 ? (
|
||||
<IconButton
|
||||
icon={
|
||||
viewField.isVisible ? (
|
||||
<IconMinus size={theme.icon.size.sm} />
|
||||
) : (
|
||||
<IconPlus size={theme.icon.size.sm} />
|
||||
)
|
||||
}
|
||||
onClick={() =>
|
||||
handleViewFieldVisibilityChange(viewField.id, !viewField.isVisible)
|
||||
}
|
||||
/>
|
||||
) : undefined,
|
||||
[handleViewFieldVisibilityChange, theme.icon.size.sm, visibleFields.length],
|
||||
);
|
||||
|
||||
const resetSelectedOption = useCallback(() => {
|
||||
setSelectedOption(undefined);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<DropdownButton
|
||||
label="Options"
|
||||
isActive={false}
|
||||
isUnfolded={isUnfolded}
|
||||
onIsUnfoldedChange={setIsUnfolded}
|
||||
HotkeyScope={HotkeyScope}
|
||||
>
|
||||
{!selectedOption && (
|
||||
<>
|
||||
<DropdownMenuHeader>View settings</DropdownMenuHeader>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItemsContainer>
|
||||
<DropdownMenuItem
|
||||
onClick={() => setSelectedOption(Option.Properties)}
|
||||
>
|
||||
<IconTag size={theme.icon.size.md} />
|
||||
Properties
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuItemsContainer>
|
||||
</>
|
||||
)}
|
||||
{selectedOption === Option.Properties && (
|
||||
<>
|
||||
<DropdownMenuHeader
|
||||
startIcon={<IconChevronLeft size={theme.icon.size.md} />}
|
||||
onClick={resetSelectedOption}
|
||||
>
|
||||
Properties
|
||||
</DropdownMenuHeader>
|
||||
<DropdownMenuSeparator />
|
||||
<OptionsDropdownSection
|
||||
renderActions={renderFieldActions}
|
||||
title="Visible"
|
||||
viewFields={visibleFields}
|
||||
/>
|
||||
{hiddenFields.length > 0 && (
|
||||
<>
|
||||
<DropdownMenuSeparator />
|
||||
<OptionsDropdownSection
|
||||
renderActions={renderFieldActions}
|
||||
title="Hidden"
|
||||
viewFields={hiddenFields}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</DropdownButton>
|
||||
);
|
||||
};
|
||||
@ -1,46 +0,0 @@
|
||||
import { cloneElement } from 'react';
|
||||
import { useTheme } from '@emotion/react';
|
||||
|
||||
import {
|
||||
DropdownMenuItem,
|
||||
DropdownMenuItemProps,
|
||||
} from '@/ui/dropdown/components/DropdownMenuItem';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/dropdown/components/DropdownMenuItemsContainer';
|
||||
import { DropdownMenuSubheader } from '@/ui/dropdown/components/DropdownMenuSubheader';
|
||||
import {
|
||||
ViewFieldDefinition,
|
||||
ViewFieldMetadata,
|
||||
} from '@/ui/editable-field/types/ViewField';
|
||||
|
||||
type OptionsDropdownSectionProps = {
|
||||
renderActions: (
|
||||
viewField: ViewFieldDefinition<ViewFieldMetadata>,
|
||||
) => DropdownMenuItemProps['actions'];
|
||||
title: string;
|
||||
viewFields: ViewFieldDefinition<ViewFieldMetadata>[];
|
||||
};
|
||||
|
||||
export const OptionsDropdownSection = ({
|
||||
renderActions,
|
||||
title,
|
||||
viewFields,
|
||||
}: OptionsDropdownSectionProps) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<>
|
||||
<DropdownMenuSubheader>{title}</DropdownMenuSubheader>
|
||||
<DropdownMenuItemsContainer>
|
||||
{viewFields.map((viewField) => (
|
||||
<DropdownMenuItem actions={renderActions(viewField)}>
|
||||
{viewField.columnIcon &&
|
||||
cloneElement(viewField.columnIcon, {
|
||||
size: theme.icon.size.md,
|
||||
})}
|
||||
{viewField.columnLabel}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuItemsContainer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
146
front/src/modules/views/hooks/useTableViewFields.ts
Normal file
146
front/src/modules/views/hooks/useTableViewFields.ts
Normal file
@ -0,0 +1,146 @@
|
||||
import { useCallback } from 'react';
|
||||
import { getOperationName } from '@apollo/client/utilities';
|
||||
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
||||
|
||||
import {
|
||||
ViewFieldDefinition,
|
||||
ViewFieldMetadata,
|
||||
ViewFieldTextMetadata,
|
||||
} from '@/ui/editable-field/types/ViewField';
|
||||
import {
|
||||
tableColumnsByIdState,
|
||||
tableColumnsState,
|
||||
} from '@/ui/table/states/tableColumnsState';
|
||||
import { currentViewIdState } from '@/views/states/currentViewIdState';
|
||||
import {
|
||||
SortOrder,
|
||||
useCreateViewFieldsMutation,
|
||||
useGetViewFieldsQuery,
|
||||
useUpdateViewFieldMutation,
|
||||
} from '~/generated/graphql';
|
||||
|
||||
import { GET_VIEW_FIELDS } from '../queries/select';
|
||||
|
||||
const DEFAULT_VIEW_FIELD_METADATA: ViewFieldTextMetadata = {
|
||||
type: 'text',
|
||||
placeHolder: '',
|
||||
fieldName: '',
|
||||
};
|
||||
|
||||
export const toViewFieldInput = (
|
||||
objectName: 'company' | 'person',
|
||||
viewFieldDefinition: ViewFieldDefinition<ViewFieldMetadata>,
|
||||
) => ({
|
||||
fieldName: viewFieldDefinition.columnLabel,
|
||||
index: viewFieldDefinition.columnOrder,
|
||||
isVisible: viewFieldDefinition.isVisible ?? true,
|
||||
objectName,
|
||||
sizeInPx: viewFieldDefinition.columnSize,
|
||||
});
|
||||
|
||||
export const useTableViewFields = ({
|
||||
objectName,
|
||||
viewFieldDefinitions,
|
||||
}: {
|
||||
objectName: 'company' | 'person';
|
||||
viewFieldDefinitions: ViewFieldDefinition<ViewFieldMetadata>[];
|
||||
}) => {
|
||||
const currentViewId = useRecoilValue(currentViewIdState);
|
||||
const setColumns = useSetRecoilState(tableColumnsState);
|
||||
const columnsById = useRecoilValue(tableColumnsByIdState);
|
||||
|
||||
const [createViewFieldsMutation] = useCreateViewFieldsMutation();
|
||||
const [updateViewFieldMutation] = useUpdateViewFieldMutation();
|
||||
|
||||
const createViewFields = useCallback(
|
||||
(columns: ViewFieldDefinition<ViewFieldMetadata>[]) => {
|
||||
if (!columns.length) return;
|
||||
|
||||
return createViewFieldsMutation({
|
||||
variables: {
|
||||
data: columns.map((column) => ({
|
||||
...toViewFieldInput(objectName, column),
|
||||
viewId: currentViewId,
|
||||
})),
|
||||
},
|
||||
refetchQueries: [getOperationName(GET_VIEW_FIELDS) ?? ''],
|
||||
});
|
||||
},
|
||||
[createViewFieldsMutation, currentViewId, objectName],
|
||||
);
|
||||
|
||||
const updateViewFields = useCallback(
|
||||
(columns: ViewFieldDefinition<ViewFieldMetadata>[]) => {
|
||||
if (!columns.length) return;
|
||||
|
||||
return Promise.all(
|
||||
columns.map((column) =>
|
||||
updateViewFieldMutation({
|
||||
variables: {
|
||||
data: {
|
||||
isVisible: column.isVisible,
|
||||
sizeInPx: column.columnSize,
|
||||
},
|
||||
where: { id: column.id },
|
||||
},
|
||||
refetchQueries: [getOperationName(GET_VIEW_FIELDS) ?? ''],
|
||||
}),
|
||||
),
|
||||
);
|
||||
},
|
||||
[updateViewFieldMutation],
|
||||
);
|
||||
|
||||
useGetViewFieldsQuery({
|
||||
variables: {
|
||||
orderBy: { index: SortOrder.Asc },
|
||||
where: {
|
||||
objectName: { equals: objectName },
|
||||
viewId: { equals: currentViewId ?? null },
|
||||
},
|
||||
},
|
||||
onCompleted: (data) => {
|
||||
if (data.viewFields.length) {
|
||||
setColumns(
|
||||
data.viewFields.map<ViewFieldDefinition<ViewFieldMetadata>>(
|
||||
(viewField) => ({
|
||||
...(viewFieldDefinitions.find(
|
||||
({ columnLabel }) => viewField.fieldName === columnLabel,
|
||||
) || { metadata: DEFAULT_VIEW_FIELD_METADATA }),
|
||||
id: viewField.id,
|
||||
columnLabel: viewField.fieldName,
|
||||
columnOrder: viewField.index,
|
||||
columnSize: viewField.sizeInPx,
|
||||
isVisible: viewField.isVisible,
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Populate if empty
|
||||
createViewFields(viewFieldDefinitions);
|
||||
},
|
||||
});
|
||||
|
||||
const handleColumnsChange = useCallback(
|
||||
async (nextColumns: ViewFieldDefinition<ViewFieldMetadata>[]) => {
|
||||
const viewFieldsToCreate = nextColumns.filter(
|
||||
(nextColumn) => !columnsById[nextColumn.id],
|
||||
);
|
||||
await createViewFields(viewFieldsToCreate);
|
||||
|
||||
const viewFieldsToUpdate = nextColumns.filter(
|
||||
(nextColumn) =>
|
||||
columnsById[nextColumn.id] &&
|
||||
(columnsById[nextColumn.id].columnSize !== nextColumn.columnSize ||
|
||||
columnsById[nextColumn.id].isVisible !== nextColumn.isVisible),
|
||||
);
|
||||
await updateViewFields(viewFieldsToUpdate);
|
||||
},
|
||||
[columnsById, createViewFields, updateViewFields],
|
||||
);
|
||||
|
||||
return { handleColumnsChange };
|
||||
};
|
||||
@ -1,17 +1,5 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const CREATE_VIEW_FIELD = gql`
|
||||
mutation CreateViewField($data: ViewFieldCreateInput!) {
|
||||
createOneViewField(data: $data) {
|
||||
id
|
||||
fieldName
|
||||
isVisible
|
||||
sizeInPx
|
||||
index
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const CREATE_VIEW_FIELDS = gql`
|
||||
mutation CreateViewFields($data: [ViewFieldCreateManyInput!]!) {
|
||||
createManyViewField(data: $data) {
|
||||
|
||||
Reference in New Issue
Block a user