feat: add table columns (#1056)

* feat: add table columns

Closes #879

* refactor: ComponentProps first
This commit is contained in:
Thaïs
2023-08-04 19:07:31 +02:00
committed by GitHub
parent a8856516bd
commit 417ca3d131
11 changed files with 406 additions and 107 deletions

View File

@ -905,6 +905,7 @@ export type Mutation = {
createOneCompany: Company;
createOnePerson: Person;
createOnePipelineProgress: PipelineProgress;
createOneViewField: ViewField;
deleteCurrentWorkspace: Workspace;
deleteManyActivities: AffectedRows;
deleteManyCompany: AffectedRows;
@ -980,6 +981,11 @@ export type MutationCreateOnePipelineProgressArgs = {
};
export type MutationCreateOneViewFieldArgs = {
data: ViewFieldCreateInput;
};
export type MutationDeleteManyActivitiesArgs = {
where?: InputMaybe<ActivityWhereInput>;
};
@ -2137,6 +2143,15 @@ export type ViewField = {
sizeInPx: Scalars['Int'];
};
export type ViewFieldCreateInput = {
fieldName: Scalars['String'];
id?: InputMaybe<Scalars['String']>;
index: Scalars['Int'];
isVisible: Scalars['Boolean'];
objectName: Scalars['String'];
sizeInPx: Scalars['Int'];
};
export type ViewFieldCreateManyInput = {
fieldName: Scalars['String'];
id?: InputMaybe<Scalars['String']>;
@ -2709,6 +2724,13 @@ export type DeleteUserAccountMutationVariables = Exact<{ [key: string]: never; }
export type DeleteUserAccountMutation = { __typename?: 'Mutation', deleteUserAccount: { __typename?: 'User', id: string } };
export type CreateViewFieldMutationVariables = Exact<{
data: ViewFieldCreateInput;
}>;
export type CreateViewFieldMutation = { __typename?: 'Mutation', createOneViewField: { __typename?: 'ViewField', id: string, fieldName: string, isVisible: boolean, sizeInPx: number, index: number } };
export type CreateViewFieldsMutationVariables = Exact<{
data: Array<ViewFieldCreateManyInput> | ViewFieldCreateManyInput;
}>;
@ -5128,6 +5150,43 @@ export function useDeleteUserAccountMutation(baseOptions?: Apollo.MutationHookOp
export type DeleteUserAccountMutationHookResult = ReturnType<typeof useDeleteUserAccountMutation>;
export type DeleteUserAccountMutationResult = Apollo.MutationResult<DeleteUserAccountMutation>;
export type DeleteUserAccountMutationOptions = Apollo.BaseMutationOptions<DeleteUserAccountMutation, DeleteUserAccountMutationVariables>;
export const CreateViewFieldDocument = gql`
mutation CreateViewField($data: ViewFieldCreateInput!) {
createOneViewField(data: $data) {
id
fieldName
isVisible
sizeInPx
index
}
}
`;
export type CreateViewFieldMutationFn = Apollo.MutationFunction<CreateViewFieldMutation, CreateViewFieldMutationVariables>;
/**
* __useCreateViewFieldMutation__
*
* To run a mutation, you first call `useCreateViewFieldMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useCreateViewFieldMutation` returns a tuple that includes:
* - A mutate function that you can call at any time to execute the mutation
* - An object with fields that represent the current status of the mutation's execution
*
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
*
* @example
* const [createViewFieldMutation, { data, loading, error }] = useCreateViewFieldMutation({
* variables: {
* data: // value for 'data'
* },
* });
*/
export function useCreateViewFieldMutation(baseOptions?: Apollo.MutationHookOptions<CreateViewFieldMutation, CreateViewFieldMutationVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useMutation<CreateViewFieldMutation, CreateViewFieldMutationVariables>(CreateViewFieldDocument, options);
}
export type CreateViewFieldMutationHookResult = ReturnType<typeof useCreateViewFieldMutation>;
export type CreateViewFieldMutationResult = Apollo.MutationResult<CreateViewFieldMutation>;
export type CreateViewFieldMutationOptions = Apollo.BaseMutationOptions<CreateViewFieldMutation, CreateViewFieldMutationVariables>;
export const CreateViewFieldsDocument = gql`
mutation CreateViewFields($data: [ViewFieldCreateManyInput!]!) {
createManyViewField(data: $data) {

View File

@ -13,18 +13,21 @@ export function CompanyTableMockData() {
const setEntityTableDimensions = useSetRecoilState(
entityTableDimensionsState,
);
const setViewFields = useSetRecoilState(viewFieldsState);
const setViewFieldsState = useSetRecoilState(viewFieldsState);
const setEntityTableData = useSetEntityTableData();
setEntityTableData(mockedCompaniesData, []);
useEffect(() => {
setViewFields(companyViewFields);
setViewFieldsState({
objectName: 'company',
viewFields: companyViewFields,
});
setEntityTableDimensions((prevState) => ({
...prevState,
numberOfColumns: companyViewFields.length,
}));
}, [setEntityTableDimensions, setViewFields]);
}, [setEntityTableDimensions, setViewFieldsState]);
return <></>;
}

View File

@ -1,7 +1,8 @@
import React from 'react';
import React, { type ComponentProps } from 'react';
import styled from '@emotion/styled';
import { IconButtonSize, IconButtonVariant } from './IconButton';
import type { IconButtonSize, IconButtonVariant } from './IconButton';
const StyledIconButtonGroupContainer = styled.div`
align-items: flex-start;
background: ${({ theme }) => theme.background.transparent.primary};
@ -11,24 +12,27 @@ const StyledIconButtonGroupContainer = styled.div`
padding: ${({ theme }) => theme.spacing(0.5)};
`;
type IconButtonGroupProps = {
type IconButtonGroupProps = Omit<ComponentProps<'div'>, 'children'> & {
variant: IconButtonVariant;
size: IconButtonSize;
children: React.ReactElement[];
children: React.ReactElement | React.ReactElement[];
};
export function IconButtonGroup({
children,
variant,
size,
...props
}: IconButtonGroupProps) {
return (
<StyledIconButtonGroupContainer>
{React.Children.map(children, (child) =>
React.cloneElement(child, {
...(variant ? { variant } : {}),
...(size ? { size } : {}),
}),
<StyledIconButtonGroupContainer {...props}>
{React.Children.map(
Array.isArray(children) ? children : [children],
(child) =>
React.cloneElement(child, {
...(variant ? { variant } : {}),
...(size ? { size } : {}),
}),
)}
</StyledIconButtonGroupContainer>
);

View File

@ -20,7 +20,6 @@ const StyledTable = styled.table`
margin-left: ${({ theme }) => theme.table.horizontalCellMargin};
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};
@ -37,7 +36,7 @@ const StyledTable = styled.table`
border-right-color: transparent;
}
:last-of-type {
min-width: 0;
min-width: fit-content;
width: 100%;
}
}
@ -58,7 +57,7 @@ const StyledTable = styled.table`
border-right-color: transparent;
}
:last-of-type {
min-width: 0;
min-width: fit-content;
width: 100%;
}
}

View File

@ -0,0 +1,86 @@
import React, { ComponentProps, useRef } from 'react';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { IconButton } from '@/ui/button/components/IconButton';
import { IconButtonGroup } from '@/ui/button/components/IconButtonGroup';
import { DropdownMenu } from '@/ui/dropdown/components/DropdownMenu';
import { DropdownMenuItem } from '@/ui/dropdown/components/DropdownMenuItem';
import { DropdownMenuItemsContainer } from '@/ui/dropdown/components/DropdownMenuItemsContainer';
import { IconPlus } from '@/ui/icon';
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
import type {
ViewFieldDefinition,
ViewFieldMetadata,
} from '../types/ViewField';
const StyledColumnMenu = styled(DropdownMenu)`
font-weight: ${({ theme }) => theme.font.weight.regular};
`;
const StyledIconButtonGroup = styled(IconButtonGroup)`
display: none;
position: absolute;
right: ${({ theme }) => theme.spacing(1)};
`;
const styledIconButtonGroupClassName = 'column-menu-item-icon-button-group';
const StyledColumnMenuItem = styled(DropdownMenuItem)`
position: relative;
&:hover {
.${styledIconButtonGroupClassName} {
display: flex;
}
}
`;
type EntityTableColumnMenuProps = {
onAddViewField: (
viewFieldDefinition: ViewFieldDefinition<ViewFieldMetadata>,
) => void;
onClickOutside?: () => void;
viewFieldDefinitions: ViewFieldDefinition<ViewFieldMetadata>[];
} & ComponentProps<'div'>;
export const EntityTableColumnMenu = ({
onAddViewField,
onClickOutside = () => undefined,
viewFieldDefinitions,
...props
}: EntityTableColumnMenuProps) => {
const ref = useRef<HTMLDivElement>(null);
const theme = useTheme();
useListenClickOutside({
refs: [ref],
callback: onClickOutside,
});
return (
<StyledColumnMenu {...props} ref={ref}>
<DropdownMenuItemsContainer>
{viewFieldDefinitions.map((viewFieldDefinition) => (
<StyledColumnMenuItem key={viewFieldDefinition.id}>
{viewFieldDefinition.columnIcon &&
React.cloneElement(viewFieldDefinition.columnIcon, {
size: theme.icon.size.md,
})}
{viewFieldDefinition.columnLabel}
<StyledIconButtonGroup
className={styledIconButtonGroupClassName}
variant="transparent"
size="small"
>
<IconButton
icon={<IconPlus size={theme.icon.size.sm} />}
onClick={() => onAddViewField(viewFieldDefinition)}
/>
</StyledIconButtonGroup>
</StyledColumnMenuItem>
))}
</DropdownMenuItemsContainer>
</StyledColumnMenu>
);
};

View File

@ -1,25 +1,44 @@
import { useCallback, useMemo, useState } from 'react';
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,
useSetRecoilState,
} from 'recoil';
import { useRecoilCallback, useRecoilState, useRecoilValue } from 'recoil';
import { IconButton } from '@/ui/button/components/IconButton';
import { IconPlus } from '@/ui/icon';
import { useTrackPointer } from '@/ui/utilities/pointer-event/hooks/useTrackPointer';
import { useUpdateViewFieldMutation } from '~/generated/graphql';
import { GET_VIEW_FIELDS } from '@/views/queries/select';
import {
useCreateViewFieldMutation,
useUpdateViewFieldMutation,
} from '~/generated/graphql';
import { toViewFieldInput } from '../hooks/useLoadView';
import { resizeFieldOffsetState } from '../states/resizeFieldOffsetState';
import { viewFieldsState } from '../states/viewFieldsState';
import {
addableViewFieldDefinitionsState,
columnWidthByViewFieldIdState,
viewFieldsState,
} from '../states/viewFieldsState';
import type {
ViewFieldDefinition,
ViewFieldMetadata,
} from '../types/ViewField';
import { ColumnHead } from './ColumnHead';
import { EntityTableColumnMenu } from './EntityTableColumnMenu';
import { SelectAllCheckbox } from './SelectAllCheckbox';
const COLUMN_MIN_WIDTH = 75;
const StyledColumnHeaderCell = styled.th<{ isResizing?: boolean }>`
min-width: ${COLUMN_MIN_WIDTH}px;
const StyledColumnHeaderCell = styled.th<{
columnWidth: number;
isResizing?: boolean;
}>`
${({ columnWidth }) => `
min-width: ${columnWidth}px;
width: ${columnWidth}px;
`}
position: relative;
user-select: none;
${({ isResizing, theme }) => {
@ -49,57 +68,48 @@ const StyledResizeHandler = styled.div`
z-index: 1;
`;
export function EntityTableHeader() {
const viewFields = useRecoilValue(viewFieldsState);
const setViewFields = useSetRecoilState(viewFieldsState);
const StyledAddIconButtonWrapper = styled.div`
display: inline-flex;
position: relative;
`;
const [updateViewFieldMutation] = useUpdateViewFieldMutation();
const columnWidths = useMemo(
() =>
viewFields.reduce<Record<string, number>>(
(result, viewField) => ({
...result,
[viewField.id]: viewField.columnSize,
}),
{},
),
[viewFields],
const StyledAddIconButton = styled(IconButton)`
border-radius: 0;
`;
const StyledEntityTableColumnMenu = styled(EntityTableColumnMenu)`
position: absolute;
right: 0;
top: 100%;
z-index: ${({ theme }) => theme.lastLayerZIndex};
`;
export function EntityTableHeader() {
const theme = useTheme();
const [{ objectName, viewFields }, setViewFieldsState] =
useRecoilState(viewFieldsState);
const columnWidths = useRecoilValue(columnWidthByViewFieldIdState);
const addableViewFieldDefinitions = useRecoilValue(
addableViewFieldDefinitionsState,
);
const [offset, setOffset] = useRecoilState(resizeFieldOffsetState);
const [initialPointerPositionX, setInitialPointerPositionX] = useState<
number | null
>(null);
const [resizedFieldId, setResizedFieldId] = useState<string | null>(null);
const [offset, setOffset] = useRecoilState(resizeFieldOffsetState);
const [isColumnMenuOpen, setIsColumnMenuOpen] = useState(false);
const handleColumnResize = useCallback(
(resizedFieldId: string, width: number) => {
setViewFields((previousViewFields) =>
previousViewFields.map((viewField) =>
viewField.id === resizedFieldId
? { ...viewField, columnSize: width }
: viewField,
),
);
updateViewFieldMutation({
variables: {
data: { sizeInPx: width },
where: { id: resizedFieldId },
},
});
},
[setViewFields, updateViewFieldMutation],
);
const [createViewFieldMutation] = useCreateViewFieldMutation();
const [updateViewFieldMutation] = useUpdateViewFieldMutation();
const handleResizeHandlerStart = useCallback(
(positionX: number, _: number) => {
setInitialPointerPositionX(positionX);
},
[],
);
const handleResizeHandlerStart = useCallback((positionX: number) => {
setInitialPointerPositionX(positionX);
}, []);
const handleResizeHandlerMove = useCallback(
(positionX: number, _positionY: number) => {
(positionX: number) => {
if (!initialPointerPositionX) return;
setOffset(positionX - initialPointerPositionX);
},
@ -108,8 +118,9 @@ export function EntityTableHeader() {
const handleResizeHandlerEnd = useRecoilCallback(
({ snapshot, set }) =>
(_positionX: number, _positionY: number) => {
() => {
if (!resizedFieldId) return;
const nextWidth = Math.round(
Math.max(
columnWidths[resizedFieldId] +
@ -119,13 +130,30 @@ export function EntityTableHeader() {
);
if (nextWidth !== columnWidths[resizedFieldId]) {
handleColumnResize(resizedFieldId, nextWidth);
// 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,
),
}));
updateViewFieldMutation({
variables: {
data: { sizeInPx: nextWidth },
where: { id: resizedFieldId },
},
refetchQueries: [getOperationName(GET_VIEW_FIELDS) ?? ''],
});
}
set(resizeFieldOffsetState, 0);
setInitialPointerPositionX(null);
setResizedFieldId(null);
},
[resizedFieldId, columnWidths, setResizedFieldId, handleColumnResize],
[resizedFieldId, columnWidths, setResizedFieldId],
);
useTrackPointer({
@ -135,6 +163,29 @@ export function EntityTableHeader() {
onMouseUp: handleResizeHandlerEnd,
});
const toggleColumnMenu = useCallback(() => {
setIsColumnMenuOpen((previousValue) => !previousValue);
}, []);
const handleAddViewField = useCallback(
(viewFieldDefinition: ViewFieldDefinition<ViewFieldMetadata>) => {
setIsColumnMenuOpen(false);
if (!objectName) return;
createViewFieldMutation({
variables: {
data: toViewFieldInput(objectName, {
...viewFieldDefinition,
columnOrder: viewFields.length + 1,
}),
},
refetchQueries: [getOperationName(GET_VIEW_FIELDS) ?? ''],
});
},
[createViewFieldMutation, objectName, viewFields.length],
);
return (
<thead>
<tr>
@ -150,15 +201,13 @@ export function EntityTableHeader() {
{viewFields.map((viewField) => (
<StyledColumnHeaderCell
key={viewField.columnOrder.toString()}
key={viewField.id}
isResizing={resizedFieldId === viewField.id}
style={{
width: Math.max(
columnWidths[viewField.id] +
(resizedFieldId === viewField.id ? offset : 0),
COLUMN_MIN_WIDTH,
),
}}
columnWidth={Math.max(
columnWidths[viewField.id] +
(resizedFieldId === viewField.id ? offset : 0),
COLUMN_MIN_WIDTH,
)}
>
<ColumnHead
viewName={viewField.columnLabel}
@ -173,7 +222,24 @@ export function EntityTableHeader() {
/>
</StyledColumnHeaderCell>
))}
<th></th>
<th>
{addableViewFieldDefinitions.length > 0 && (
<StyledAddIconButtonWrapper>
<StyledAddIconButton
size="large"
icon={<IconPlus size={theme.icon.size.md} />}
onClick={toggleColumnMenu}
/>
{isColumnMenuOpen && (
<StyledEntityTableColumnMenu
onAddViewField={handleAddViewField}
onClickOutside={toggleColumnMenu}
viewFieldDefinitions={addableViewFieldDefinitions}
/>
)}
</StyledAddIconButtonWrapper>
)}
</th>
</tr>
</thead>
);

View File

@ -13,7 +13,7 @@ const StyledRow = styled.tr<{ selected: boolean }>`
`;
export function EntityTableRow({ rowId }: { rowId: string }) {
const viewFields = useRecoilValue(viewFieldsState);
const { viewFields } = useRecoilValue(viewFieldsState);
return (
<StyledRow data-testid={`row-id-${rowId}`} selected={false}>

View File

@ -10,7 +10,7 @@ import {
import { entityTableDimensionsState } from '../states/entityTableDimensionsState';
import { viewFieldsState } from '../states/viewFieldsState';
import {
import type {
ViewFieldDefinition,
ViewFieldMetadata,
ViewFieldTextMetadata,
@ -22,6 +22,17 @@ const DEFAULT_VIEW_FIELD_METADATA: ViewFieldTextMetadata = {
fieldName: '',
};
export const toViewFieldInput = (
objectName: 'company' | 'person',
viewFieldDefinition: ViewFieldDefinition<ViewFieldMetadata>,
) => ({
fieldName: viewFieldDefinition.columnLabel,
index: viewFieldDefinition.columnOrder,
isVisible: true,
objectName,
sizeInPx: viewFieldDefinition.columnSize,
});
export const useLoadView = ({
objectName,
viewFieldDefinitions,
@ -32,7 +43,7 @@ export const useLoadView = ({
const setEntityTableDimensions = useSetRecoilState(
entityTableDimensionsState,
);
const setViewFields = useSetRecoilState(viewFieldsState);
const setViewFieldsState = useSetRecoilState(viewFieldsState);
const [createViewFieldsMutation] = useCreateViewFieldsMutation();
@ -43,36 +54,33 @@ export const useLoadView = ({
},
onCompleted: (data) => {
if (data.viewFields.length) {
setViewFields(
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,
}),
),
);
const viewFields = 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,
}));
setViewFieldsState({ objectName, viewFields });
setEntityTableDimensions((prevState) => ({
...prevState,
numberOfColumns: data.viewFields.length,
}));
return;
}
// Populate if empty
createViewFieldsMutation({
variables: {
data: viewFieldDefinitions.map((viewFieldDefinition) => ({
fieldName: viewFieldDefinition.columnLabel,
index: viewFieldDefinition.columnOrder,
isVisible: true,
objectName,
sizeInPx: viewFieldDefinition.columnSize,
})),
data: viewFieldDefinitions.map((viewFieldDefinition) =>
toViewFieldInput(objectName, viewFieldDefinition),
),
},
refetchQueries: [getOperationName(GET_VIEW_FIELDS) ?? ''],
});

View File

@ -1,8 +1,49 @@
import { atom } from 'recoil';
import { atom, selector } from 'recoil';
import { ViewFieldDefinition, ViewFieldMetadata } from '../types/ViewField';
import { companyViewFields } from '@/companies/constants/companyViewFields';
import { peopleViewFields } from '@/people/constants/peopleViewFields';
export const viewFieldsState = atom<ViewFieldDefinition<ViewFieldMetadata>[]>({
import type {
ViewFieldDefinition,
ViewFieldMetadata,
} from '../types/ViewField';
export const viewFieldsState = atom<{
objectName: 'company' | 'person' | '';
viewFields: ViewFieldDefinition<ViewFieldMetadata>[];
}>({
key: 'viewFieldsState',
default: [],
default: { objectName: '', viewFields: [] },
});
export const columnWidthByViewFieldIdState = selector({
key: 'columnWidthByViewFieldIdState',
get: ({ get }) =>
get(viewFieldsState).viewFields.reduce<Record<string, number>>(
(result, viewField) => ({
...result,
[viewField.id]: viewField.columnSize,
}),
{},
),
});
export const addableViewFieldDefinitionsState = selector({
key: 'addableViewFieldDefinitionsState',
get: ({ get }) => {
const { objectName, viewFields } = get(viewFieldsState);
if (!objectName) return [];
const existingColumnLabels = viewFields.map(
(viewField) => viewField.columnLabel,
);
const viewFieldDefinitions =
objectName === 'company' ? companyViewFields : peopleViewFields;
return viewFieldDefinitions.filter(
(viewFieldDefinition) =>
!existingColumnLabels.includes(viewFieldDefinition.columnLabel),
);
},
});

View File

@ -1,5 +1,17 @@
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) {

View File

@ -25,12 +25,33 @@ import {
import { UserAbility } from 'src/decorators/user-ability.decorator';
import { AbilityGuard } from 'src/guards/ability.guard';
import { JwtAuthGuard } from 'src/guards/jwt.auth.guard';
import { CreateOneViewFieldArgs } from 'src/core/@generated/view-field/create-one-view-field.args';
@UseGuards(JwtAuthGuard)
@Resolver(() => ViewField)
export class ViewFieldResolver {
constructor(private readonly viewFieldService: ViewFieldService) {}
@Mutation(() => ViewField, {
nullable: false,
})
@UseGuards(AbilityGuard)
@CheckAbilities(CreateViewFieldAbilityHandler)
async createOneViewField(
@Args() args: CreateOneViewFieldArgs,
@AuthWorkspace() workspace: Workspace,
@PrismaSelector({ modelName: 'ViewField' })
prismaSelect: PrismaSelect<'ViewField'>,
): Promise<Partial<ViewField>> {
return this.viewFieldService.create({
data: {
...args.data,
workspace: { connect: { id: workspace.id } },
},
select: prismaSelect.value,
});
}
@Mutation(() => AffectedRows)
@UseGuards(AbilityGuard)
@CheckAbilities(CreateViewFieldAbilityHandler)