feat: persist view filters and sorts on Update View button click (#1290)

* feat: add viewFilters table

Closes #1121

* feat: add Update View button + Create View dropdown

Closes #1124, #1289

* feat: add View Filter resolvers

* feat: persist view filters and sorts on Update View button click

Closes #1123

* refactor: code review

- Rename recoil selectors
- Rename filters `field` property to `key`
This commit is contained in:
Thaïs
2023-08-23 18:20:43 +02:00
committed by GitHub
parent 76246ec880
commit 74ab0142c7
54 changed files with 1331 additions and 277 deletions

View File

@ -829,6 +829,13 @@ export type EnumPipelineProgressableTypeFilter = {
notIn?: InputMaybe<Array<PipelineProgressableType>>;
};
export type EnumViewFilterOperandFilter = {
equals?: InputMaybe<ViewFilterOperand>;
in?: InputMaybe<Array<ViewFilterOperand>>;
not?: InputMaybe<NestedEnumViewFilterOperandFilter>;
notIn?: InputMaybe<Array<ViewFilterOperand>>;
};
export type EnumViewSortDirectionFilter = {
equals?: InputMaybe<ViewSortDirection>;
in?: InputMaybe<Array<ViewSortDirection>>;
@ -969,6 +976,7 @@ export type Mutation = {
createManyPerson: AffectedRows;
createManyView: AffectedRows;
createManyViewField: AffectedRows;
createManyViewFilter: AffectedRows;
createManyViewSort: AffectedRows;
createOneActivity: Activity;
createOneComment: Comment;
@ -983,6 +991,7 @@ export type Mutation = {
deleteManyPerson: AffectedRows;
deleteManyPipelineProgress: AffectedRows;
deleteManyView: AffectedRows;
deleteManyViewFilter: AffectedRows;
deleteManyViewSort: AffectedRows;
deleteUserAccount: User;
deleteWorkspaceMember: WorkspaceMember;
@ -996,6 +1005,7 @@ export type Mutation = {
updateOnePipelineStage?: Maybe<PipelineStage>;
updateOneView: View;
updateOneViewField: ViewField;
updateOneViewFilter: ViewFilter;
updateOneViewSort: ViewSort;
updateUser: User;
updateWorkspace: Workspace;
@ -1060,6 +1070,12 @@ export type MutationCreateManyViewFieldArgs = {
};
export type MutationCreateManyViewFilterArgs = {
data: Array<ViewFilterCreateManyInput>;
skipDuplicates?: InputMaybe<Scalars['Boolean']>;
};
export type MutationCreateManyViewSortArgs = {
data: Array<ViewSortCreateManyInput>;
skipDuplicates?: InputMaybe<Scalars['Boolean']>;
@ -1126,6 +1142,11 @@ export type MutationDeleteManyViewArgs = {
};
export type MutationDeleteManyViewFilterArgs = {
where?: InputMaybe<ViewFilterWhereInput>;
};
export type MutationDeleteManyViewSortArgs = {
where?: InputMaybe<ViewSortWhereInput>;
};
@ -1195,6 +1216,12 @@ export type MutationUpdateOneViewFieldArgs = {
};
export type MutationUpdateOneViewFilterArgs = {
data: ViewFilterUpdateInput;
where: ViewFilterWhereUniqueInput;
};
export type MutationUpdateOneViewSortArgs = {
data: ViewSortUpdateInput;
where: ViewSortWhereUniqueInput;
@ -1305,6 +1332,13 @@ export type NestedEnumPipelineProgressableTypeFilter = {
notIn?: InputMaybe<Array<PipelineProgressableType>>;
};
export type NestedEnumViewFilterOperandFilter = {
equals?: InputMaybe<ViewFilterOperand>;
in?: InputMaybe<Array<ViewFilterOperand>>;
not?: InputMaybe<NestedEnumViewFilterOperandFilter>;
notIn?: InputMaybe<Array<ViewFilterOperand>>;
};
export type NestedEnumViewSortDirectionFilter = {
equals?: InputMaybe<ViewSortDirection>;
in?: InputMaybe<Array<ViewSortDirection>>;
@ -1930,6 +1964,7 @@ export type Query = {
findManyUser: Array<User>;
findManyView: Array<View>;
findManyViewField: Array<ViewField>;
findManyViewFilter: Array<ViewFilter>;
findManyViewSort: Array<ViewSort>;
findManyWorkspaceMember: Array<WorkspaceMember>;
findUniqueCompany: Company;
@ -2038,6 +2073,16 @@ export type QueryFindManyViewFieldArgs = {
};
export type QueryFindManyViewFilterArgs = {
cursor?: InputMaybe<ViewFilterWhereUniqueInput>;
distinct?: InputMaybe<Array<ViewFilterScalarFieldEnum>>;
orderBy?: InputMaybe<Array<ViewFilterOrderByWithRelationInput>>;
skip?: InputMaybe<Scalars['Int']>;
take?: InputMaybe<Scalars['Int']>;
where?: InputMaybe<ViewFilterWhereInput>;
};
export type QueryFindManyViewSortArgs = {
cursor?: InputMaybe<ViewSortWhereUniqueInput>;
distinct?: InputMaybe<Array<ViewSortScalarFieldEnum>>;
@ -2349,6 +2394,7 @@ export type Verify = {
export type View = {
__typename?: 'View';
fields?: Maybe<Array<ViewField>>;
filters?: Maybe<Array<ViewFilter>>;
id: Scalars['ID'];
name: Scalars['String'];
objectId: Scalars['String'];
@ -2478,8 +2524,111 @@ export type ViewFieldWorkspaceIdViewIdObjectNameFieldNameCompoundUniqueInput = {
viewId: Scalars['String'];
};
export type ViewFilter = {
__typename?: 'ViewFilter';
displayValue: Scalars['String'];
key: Scalars['String'];
name: Scalars['String'];
operand: ViewFilterOperand;
value: Scalars['String'];
view: View;
viewId: Scalars['String'];
};
export type ViewFilterCreateManyInput = {
displayValue: Scalars['String'];
key: Scalars['String'];
name: Scalars['String'];
operand: ViewFilterOperand;
value: Scalars['String'];
viewId: Scalars['String'];
};
export type ViewFilterListRelationFilter = {
every?: InputMaybe<ViewFilterWhereInput>;
none?: InputMaybe<ViewFilterWhereInput>;
some?: InputMaybe<ViewFilterWhereInput>;
};
export enum ViewFilterOperand {
Contains = 'Contains',
DoesNotContain = 'DoesNotContain',
GreaterThan = 'GreaterThan',
Is = 'Is',
IsNot = 'IsNot',
LessThan = 'LessThan'
}
export type ViewFilterOrderByRelationAggregateInput = {
_count?: InputMaybe<SortOrder>;
};
export type ViewFilterOrderByWithRelationInput = {
displayValue?: InputMaybe<SortOrder>;
key?: InputMaybe<SortOrder>;
name?: InputMaybe<SortOrder>;
operand?: InputMaybe<SortOrder>;
value?: InputMaybe<SortOrder>;
view?: InputMaybe<ViewOrderByWithRelationInput>;
viewId?: InputMaybe<SortOrder>;
};
export enum ViewFilterScalarFieldEnum {
DisplayValue = 'displayValue',
Key = 'key',
Name = 'name',
Operand = 'operand',
Value = 'value',
ViewId = 'viewId',
WorkspaceId = 'workspaceId'
}
export type ViewFilterUpdateInput = {
displayValue?: InputMaybe<Scalars['String']>;
key?: InputMaybe<Scalars['String']>;
name?: InputMaybe<Scalars['String']>;
operand?: InputMaybe<ViewFilterOperand>;
value?: InputMaybe<Scalars['String']>;
view?: InputMaybe<ViewUpdateOneRequiredWithoutFiltersNestedInput>;
};
export type ViewFilterUpdateManyWithoutViewNestedInput = {
connect?: InputMaybe<Array<ViewFilterWhereUniqueInput>>;
disconnect?: InputMaybe<Array<ViewFilterWhereUniqueInput>>;
set?: InputMaybe<Array<ViewFilterWhereUniqueInput>>;
};
export type ViewFilterUpdateManyWithoutWorkspaceNestedInput = {
connect?: InputMaybe<Array<ViewFilterWhereUniqueInput>>;
disconnect?: InputMaybe<Array<ViewFilterWhereUniqueInput>>;
set?: InputMaybe<Array<ViewFilterWhereUniqueInput>>;
};
export type ViewFilterViewIdKeyCompoundUniqueInput = {
key: Scalars['String'];
viewId: Scalars['String'];
};
export type ViewFilterWhereInput = {
AND?: InputMaybe<Array<ViewFilterWhereInput>>;
NOT?: InputMaybe<Array<ViewFilterWhereInput>>;
OR?: InputMaybe<Array<ViewFilterWhereInput>>;
displayValue?: InputMaybe<StringFilter>;
key?: InputMaybe<StringFilter>;
name?: InputMaybe<StringFilter>;
operand?: InputMaybe<EnumViewFilterOperandFilter>;
value?: InputMaybe<StringFilter>;
view?: InputMaybe<ViewRelationFilter>;
viewId?: InputMaybe<StringFilter>;
};
export type ViewFilterWhereUniqueInput = {
viewId_key?: InputMaybe<ViewFilterViewIdKeyCompoundUniqueInput>;
};
export type ViewOrderByWithRelationInput = {
fields?: InputMaybe<ViewFieldOrderByRelationAggregateInput>;
filters?: InputMaybe<ViewFilterOrderByRelationAggregateInput>;
id?: InputMaybe<SortOrder>;
name?: InputMaybe<SortOrder>;
objectId?: InputMaybe<SortOrder>;
@ -2593,6 +2742,7 @@ export enum ViewType {
export type ViewUpdateInput = {
fields?: InputMaybe<ViewFieldUpdateManyWithoutViewNestedInput>;
filters?: InputMaybe<ViewFilterUpdateManyWithoutViewNestedInput>;
id?: InputMaybe<Scalars['String']>;
name?: InputMaybe<Scalars['String']>;
objectId?: InputMaybe<Scalars['String']>;
@ -2606,6 +2756,10 @@ export type ViewUpdateManyWithoutWorkspaceNestedInput = {
set?: InputMaybe<Array<ViewWhereUniqueInput>>;
};
export type ViewUpdateOneRequiredWithoutFiltersNestedInput = {
connect?: InputMaybe<ViewWhereUniqueInput>;
};
export type ViewUpdateOneRequiredWithoutSortsNestedInput = {
connect?: InputMaybe<ViewWhereUniqueInput>;
};
@ -2620,6 +2774,7 @@ export type ViewWhereInput = {
NOT?: InputMaybe<Array<ViewWhereInput>>;
OR?: InputMaybe<Array<ViewWhereInput>>;
fields?: InputMaybe<ViewFieldListRelationFilter>;
filters?: InputMaybe<ViewFilterListRelationFilter>;
id?: InputMaybe<StringFilter>;
name?: InputMaybe<StringFilter>;
objectId?: InputMaybe<StringFilter>;
@ -2657,6 +2812,7 @@ export type Workspace = {
pipelines?: Maybe<Array<Pipeline>>;
updatedAt: Scalars['DateTime'];
viewFields?: Maybe<Array<ViewField>>;
viewFilters?: Maybe<Array<ViewFilter>>;
viewSorts?: Maybe<Array<ViewSort>>;
views?: Maybe<Array<View>>;
workspaceMember?: Maybe<Array<WorkspaceMember>>;
@ -2741,6 +2897,7 @@ export type WorkspaceUpdateInput = {
pipelines?: InputMaybe<PipelineUpdateManyWithoutWorkspaceNestedInput>;
updatedAt?: InputMaybe<Scalars['DateTime']>;
viewFields?: InputMaybe<ViewFieldUpdateManyWithoutWorkspaceNestedInput>;
viewFilters?: InputMaybe<ViewFilterUpdateManyWithoutWorkspaceNestedInput>;
viewSorts?: InputMaybe<ViewSortUpdateManyWithoutWorkspaceNestedInput>;
views?: InputMaybe<ViewUpdateManyWithoutWorkspaceNestedInput>;
workspaceMember?: InputMaybe<WorkspaceMemberUpdateManyWithoutWorkspaceNestedInput>;
@ -2838,7 +2995,7 @@ export type CreateEventMutationVariables = Exact<{
export type CreateEventMutation = { __typename?: 'Mutation', createEvent: { __typename?: 'Analytics', success: boolean } };
export type UserQueryFragmentFragment = { __typename?: 'User', id: string, email: string, displayName: string, firstName?: string | null, lastName?: string | null, canImpersonate: boolean, supportUserHash?: string | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: string, allowImpersonation: boolean, workspace: { __typename?: 'Workspace', id: string, domainName?: string | null, displayName?: string | null, logo?: string | null, inviteHash?: string | null } } | null, settings: { __typename?: 'UserSettings', id: string, colorScheme: ColorScheme, locale: string } };
export type UserQueryFragmentFragment = { __typename?: 'User', id: string, email: string, displayName: string, firstName?: string | null, lastName?: string | null, canImpersonate: boolean, supportUserHash?: string | null, avatarUrl?: string | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: string, allowImpersonation: boolean, workspace: { __typename?: 'Workspace', id: string, domainName?: string | null, displayName?: string | null, logo?: string | null, inviteHash?: string | null } } | null, settings: { __typename?: 'UserSettings', id: string, colorScheme: ColorScheme, locale: string } };
export type ChallengeMutationVariables = Exact<{
email: Scalars['String'];
@ -2853,7 +3010,7 @@ export type ImpersonateMutationVariables = Exact<{
}>;
export type ImpersonateMutation = { __typename?: 'Mutation', impersonate: { __typename?: 'Verify', user: { __typename?: 'User', id: string, email: string, displayName: string, firstName?: string | null, lastName?: string | null, canImpersonate: boolean, supportUserHash?: string | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: string, allowImpersonation: boolean, workspace: { __typename?: 'Workspace', id: string, domainName?: string | null, displayName?: string | null, logo?: string | null, inviteHash?: string | null } } | null, settings: { __typename?: 'UserSettings', id: string, colorScheme: ColorScheme, locale: string } }, tokens: { __typename?: 'AuthTokenPair', accessToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, refreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } } };
export type ImpersonateMutation = { __typename?: 'Mutation', impersonate: { __typename?: 'Verify', user: { __typename?: 'User', id: string, email: string, displayName: string, firstName?: string | null, lastName?: string | null, canImpersonate: boolean, supportUserHash?: string | null, avatarUrl?: string | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: string, allowImpersonation: boolean, workspace: { __typename?: 'Workspace', id: string, domainName?: string | null, displayName?: string | null, logo?: string | null, inviteHash?: string | null } } | null, settings: { __typename?: 'UserSettings', id: string, colorScheme: ColorScheme, locale: string } }, tokens: { __typename?: 'AuthTokenPair', accessToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, refreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } } };
export type RenewTokenMutationVariables = Exact<{
refreshToken: Scalars['String'];
@ -2876,7 +3033,7 @@ export type VerifyMutationVariables = Exact<{
}>;
export type VerifyMutation = { __typename?: 'Mutation', verify: { __typename?: 'Verify', user: { __typename?: 'User', id: string, email: string, displayName: string, firstName?: string | null, lastName?: string | null, canImpersonate: boolean, supportUserHash?: string | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: string, allowImpersonation: boolean, workspace: { __typename?: 'Workspace', id: string, domainName?: string | null, displayName?: string | null, logo?: string | null, inviteHash?: string | null } } | null, settings: { __typename?: 'UserSettings', id: string, colorScheme: ColorScheme, locale: string } }, tokens: { __typename?: 'AuthTokenPair', accessToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, refreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } } };
export type VerifyMutation = { __typename?: 'Mutation', verify: { __typename?: 'Verify', user: { __typename?: 'User', id: string, email: string, displayName: string, firstName?: string | null, lastName?: string | null, canImpersonate: boolean, supportUserHash?: string | null, avatarUrl?: string | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: string, allowImpersonation: boolean, workspace: { __typename?: 'Workspace', id: string, domainName?: string | null, displayName?: string | null, logo?: string | null, inviteHash?: string | null } } | null, settings: { __typename?: 'UserSettings', id: string, colorScheme: ColorScheme, locale: string } }, tokens: { __typename?: 'AuthTokenPair', accessToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, refreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } } };
export type CheckUserExistsQueryVariables = Exact<{
email: Scalars['String'];
@ -3216,6 +3373,13 @@ export type CreateViewFieldsMutationVariables = Exact<{
export type CreateViewFieldsMutation = { __typename?: 'Mutation', createManyViewField: { __typename?: 'AffectedRows', count: number } };
export type CreateViewFiltersMutationVariables = Exact<{
data: Array<ViewFilterCreateManyInput> | ViewFilterCreateManyInput;
}>;
export type CreateViewFiltersMutation = { __typename?: 'Mutation', createManyViewFilter: { __typename?: 'AffectedRows', count: number } };
export type CreateViewSortsMutationVariables = Exact<{
data: Array<ViewSortCreateManyInput> | ViewSortCreateManyInput;
}>;
@ -3230,7 +3394,6 @@ export type CreateViewsMutationVariables = Exact<{
export type CreateViewsMutation = { __typename?: 'Mutation', createManyView: { __typename?: 'AffectedRows', count: number } };
export type DeleteViewsMutationVariables = Exact<{
where: ViewWhereInput;
}>;
@ -3238,6 +3401,12 @@ export type DeleteViewsMutationVariables = Exact<{
export type DeleteViewsMutation = { __typename?: 'Mutation', deleteManyView: { __typename?: 'AffectedRows', count: number } };
export type DeleteViewFiltersMutationVariables = Exact<{
where: ViewFilterWhereInput;
}>;
export type DeleteViewFiltersMutation = { __typename?: 'Mutation', deleteManyViewFilter: { __typename?: 'AffectedRows', count: number } };
export type DeleteViewSortsMutationVariables = Exact<{
where: ViewSortWhereInput;
@ -3262,6 +3431,14 @@ export type UpdateViewFieldMutationVariables = Exact<{
export type UpdateViewFieldMutation = { __typename?: 'Mutation', updateOneViewField: { __typename?: 'ViewField', id: string, fieldName: string, isVisible: boolean, sizeInPx: number, index: number } };
export type UpdateViewFilterMutationVariables = Exact<{
data: ViewFilterUpdateInput;
where: ViewFilterWhereUniqueInput;
}>;
export type UpdateViewFilterMutation = { __typename?: 'Mutation', viewFilter: { __typename?: 'ViewFilter', displayValue: string, key: string, name: string, operand: ViewFilterOperand, value: string } };
export type UpdateViewSortMutationVariables = Exact<{
data: ViewSortUpdateInput;
where: ViewSortWhereUniqueInput;
@ -3278,6 +3455,13 @@ export type GetViewFieldsQueryVariables = Exact<{
export type GetViewFieldsQuery = { __typename?: 'Query', viewFields: Array<{ __typename?: 'ViewField', id: string, fieldName: string, isVisible: boolean, sizeInPx: number, index: number }> };
export type GetViewFiltersQueryVariables = Exact<{
where?: InputMaybe<ViewFilterWhereInput>;
}>;
export type GetViewFiltersQuery = { __typename?: 'Query', viewFilters: Array<{ __typename?: 'ViewFilter', displayValue: string, key: string, name: string, operand: ViewFilterOperand, value: string }> };
export type GetViewSortsQueryVariables = Exact<{
where?: InputMaybe<ViewSortWhereInput>;
}>;
@ -5934,6 +6118,39 @@ export function useCreateViewFieldsMutation(baseOptions?: Apollo.MutationHookOpt
export type CreateViewFieldsMutationHookResult = ReturnType<typeof useCreateViewFieldsMutation>;
export type CreateViewFieldsMutationResult = Apollo.MutationResult<CreateViewFieldsMutation>;
export type CreateViewFieldsMutationOptions = Apollo.BaseMutationOptions<CreateViewFieldsMutation, CreateViewFieldsMutationVariables>;
export const CreateViewFiltersDocument = gql`
mutation CreateViewFilters($data: [ViewFilterCreateManyInput!]!) {
createManyViewFilter(data: $data) {
count
}
}
`;
export type CreateViewFiltersMutationFn = Apollo.MutationFunction<CreateViewFiltersMutation, CreateViewFiltersMutationVariables>;
/**
* __useCreateViewFiltersMutation__
*
* To run a mutation, you first call `useCreateViewFiltersMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useCreateViewFiltersMutation` 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 [createViewFiltersMutation, { data, loading, error }] = useCreateViewFiltersMutation({
* variables: {
* data: // value for 'data'
* },
* });
*/
export function useCreateViewFiltersMutation(baseOptions?: Apollo.MutationHookOptions<CreateViewFiltersMutation, CreateViewFiltersMutationVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useMutation<CreateViewFiltersMutation, CreateViewFiltersMutationVariables>(CreateViewFiltersDocument, options);
}
export type CreateViewFiltersMutationHookResult = ReturnType<typeof useCreateViewFiltersMutation>;
export type CreateViewFiltersMutationResult = Apollo.MutationResult<CreateViewFiltersMutation>;
export type CreateViewFiltersMutationOptions = Apollo.BaseMutationOptions<CreateViewFiltersMutation, CreateViewFiltersMutationVariables>;
export const CreateViewSortsDocument = gql`
mutation CreateViewSorts($data: [ViewSortCreateManyInput!]!) {
createManyViewSort(data: $data) {
@ -6000,7 +6217,6 @@ export function useCreateViewsMutation(baseOptions?: Apollo.MutationHookOptions<
export type CreateViewsMutationHookResult = ReturnType<typeof useCreateViewsMutation>;
export type CreateViewsMutationResult = Apollo.MutationResult<CreateViewsMutation>;
export type CreateViewsMutationOptions = Apollo.BaseMutationOptions<CreateViewsMutation, CreateViewsMutationVariables>;
export const DeleteViewsDocument = gql`
mutation DeleteViews($where: ViewWhereInput!) {
deleteManyView(where: $where) {
@ -6034,7 +6250,39 @@ export function useDeleteViewsMutation(baseOptions?: Apollo.MutationHookOptions<
export type DeleteViewsMutationHookResult = ReturnType<typeof useDeleteViewsMutation>;
export type DeleteViewsMutationResult = Apollo.MutationResult<DeleteViewsMutation>;
export type DeleteViewsMutationOptions = Apollo.BaseMutationOptions<DeleteViewsMutation, DeleteViewsMutationVariables>;
export const DeleteViewFiltersDocument = gql`
mutation DeleteViewFilters($where: ViewFilterWhereInput!) {
deleteManyViewFilter(where: $where) {
count
}
}
`;
export type DeleteViewFiltersMutationFn = Apollo.MutationFunction<DeleteViewFiltersMutation, DeleteViewFiltersMutationVariables>;
/**
* __useDeleteViewFiltersMutation__
*
* To run a mutation, you first call `useDeleteViewFiltersMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useDeleteViewFiltersMutation` 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 [deleteViewFiltersMutation, { data, loading, error }] = useDeleteViewFiltersMutation({
* variables: {
* where: // value for 'where'
* },
* });
*/
export function useDeleteViewFiltersMutation(baseOptions?: Apollo.MutationHookOptions<DeleteViewFiltersMutation, DeleteViewFiltersMutationVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useMutation<DeleteViewFiltersMutation, DeleteViewFiltersMutationVariables>(DeleteViewFiltersDocument, options);
}
export type DeleteViewFiltersMutationHookResult = ReturnType<typeof useDeleteViewFiltersMutation>;
export type DeleteViewFiltersMutationResult = Apollo.MutationResult<DeleteViewFiltersMutation>;
export type DeleteViewFiltersMutationOptions = Apollo.BaseMutationOptions<DeleteViewFiltersMutation, DeleteViewFiltersMutationVariables>;
export const DeleteViewSortsDocument = gql`
mutation DeleteViewSorts($where: ViewSortWhereInput!) {
deleteManyViewSort(where: $where) {
@ -6141,6 +6389,44 @@ export function useUpdateViewFieldMutation(baseOptions?: Apollo.MutationHookOpti
export type UpdateViewFieldMutationHookResult = ReturnType<typeof useUpdateViewFieldMutation>;
export type UpdateViewFieldMutationResult = Apollo.MutationResult<UpdateViewFieldMutation>;
export type UpdateViewFieldMutationOptions = Apollo.BaseMutationOptions<UpdateViewFieldMutation, UpdateViewFieldMutationVariables>;
export const UpdateViewFilterDocument = gql`
mutation UpdateViewFilter($data: ViewFilterUpdateInput!, $where: ViewFilterWhereUniqueInput!) {
viewFilter: updateOneViewFilter(data: $data, where: $where) {
displayValue
key
name
operand
value
}
}
`;
export type UpdateViewFilterMutationFn = Apollo.MutationFunction<UpdateViewFilterMutation, UpdateViewFilterMutationVariables>;
/**
* __useUpdateViewFilterMutation__
*
* To run a mutation, you first call `useUpdateViewFilterMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useUpdateViewFilterMutation` 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 [updateViewFilterMutation, { data, loading, error }] = useUpdateViewFilterMutation({
* variables: {
* data: // value for 'data'
* where: // value for 'where'
* },
* });
*/
export function useUpdateViewFilterMutation(baseOptions?: Apollo.MutationHookOptions<UpdateViewFilterMutation, UpdateViewFilterMutationVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useMutation<UpdateViewFilterMutation, UpdateViewFilterMutationVariables>(UpdateViewFilterDocument, options);
}
export type UpdateViewFilterMutationHookResult = ReturnType<typeof useUpdateViewFilterMutation>;
export type UpdateViewFilterMutationResult = Apollo.MutationResult<UpdateViewFilterMutation>;
export type UpdateViewFilterMutationOptions = Apollo.BaseMutationOptions<UpdateViewFilterMutation, UpdateViewFilterMutationVariables>;
export const UpdateViewSortDocument = gql`
mutation UpdateViewSort($data: ViewSortUpdateInput!, $where: ViewSortWhereUniqueInput!) {
viewSort: updateOneViewSort(data: $data, where: $where) {
@ -6217,6 +6503,45 @@ export function useGetViewFieldsLazyQuery(baseOptions?: Apollo.LazyQueryHookOpti
export type GetViewFieldsQueryHookResult = ReturnType<typeof useGetViewFieldsQuery>;
export type GetViewFieldsLazyQueryHookResult = ReturnType<typeof useGetViewFieldsLazyQuery>;
export type GetViewFieldsQueryResult = Apollo.QueryResult<GetViewFieldsQuery, GetViewFieldsQueryVariables>;
export const GetViewFiltersDocument = gql`
query GetViewFilters($where: ViewFilterWhereInput) {
viewFilters: findManyViewFilter(where: $where) {
displayValue
key
name
operand
value
}
}
`;
/**
* __useGetViewFiltersQuery__
*
* To run a query within a React component, call `useGetViewFiltersQuery` and pass it any options that fit your needs.
* When your component renders, `useGetViewFiltersQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useGetViewFiltersQuery({
* variables: {
* where: // value for 'where'
* },
* });
*/
export function useGetViewFiltersQuery(baseOptions?: Apollo.QueryHookOptions<GetViewFiltersQuery, GetViewFiltersQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<GetViewFiltersQuery, GetViewFiltersQueryVariables>(GetViewFiltersDocument, options);
}
export function useGetViewFiltersLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<GetViewFiltersQuery, GetViewFiltersQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<GetViewFiltersQuery, GetViewFiltersQueryVariables>(GetViewFiltersDocument, options);
}
export type GetViewFiltersQueryHookResult = ReturnType<typeof useGetViewFiltersQuery>;
export type GetViewFiltersLazyQueryHookResult = ReturnType<typeof useGetViewFiltersLazyQuery>;
export type GetViewFiltersQueryResult = Apollo.QueryResult<GetViewFiltersQuery, GetViewFiltersQueryVariables>;
export const GetViewSortsDocument = gql`
query GetViewSorts($where: ViewSortWhereInput) {
viewSorts: findManyViewSort(where: $where) {

View File

@ -4,6 +4,7 @@ import { useRecoilState } from 'recoil';
import { currentUserState } from '@/auth/states/currentUserState';
import { filtersScopedState } from '@/ui/filter-n-sort/states/filtersScopedState';
import { FilterOperand } from '@/ui/filter-n-sort/types/FilterOperand';
import { turnFilterIntoWhereClause } from '@/ui/filter-n-sort/utils/turnFilterIntoWhereClause';
import { activeTabIdScopedState } from '@/ui/tab/states/activeTabIdScopedState';
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
@ -37,10 +38,10 @@ export function useTasks() {
if (currentUser && !filters.length) {
setFilters([
{
field: 'assigneeId',
key: 'assigneeId',
type: 'entity',
value: currentUser.id,
operand: 'is',
operand: FilterOperand.Is,
displayValue: currentUser.displayName,
displayAvatarUrl: currentUser.avatarUrl ?? undefined,
},

View File

@ -1,20 +1,20 @@
import { useMemo } from 'react';
import { useCallback, useMemo } from 'react';
import { companyViewFields } from '@/companies/constants/companyViewFields';
import { useCompanyTableActionBarEntries } from '@/companies/hooks/useCompanyTableActionBarEntries';
import { useCompanyTableContextMenuEntries } from '@/companies/hooks/useCompanyTableContextMenuEntries';
import { useSpreadsheetCompanyImport } from '@/companies/hooks/useSpreadsheetCompanyImport';
import { filtersScopedState } from '@/ui/filter-n-sort/states/filtersScopedState';
import { sortsOrderByScopedState } from '@/ui/filter-n-sort/states/sortScopedState';
import { sortsOrderByScopedSelector } from '@/ui/filter-n-sort/states/sortsOrderByScopedSelector';
import { turnFilterIntoWhereClause } from '@/ui/filter-n-sort/utils/turnFilterIntoWhereClause';
import { EntityTable } from '@/ui/table/components/EntityTable';
import { GenericEntityTableData } from '@/ui/table/components/GenericEntityTableData';
import { useUpsertEntityTableItem } from '@/ui/table/hooks/useUpsertEntityTableItem';
import { TableRecoilScopeContext } from '@/ui/table/states/recoil-scope-contexts/TableRecoilScopeContext';
import { currentTableViewIdState } from '@/ui/table/states/tableViewsState';
import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue';
import { useTableViewFields } from '@/views/hooks/useTableViewFields';
import { useTableViews } from '@/views/hooks/useTableViews';
import { useViewFilters } from '@/views/hooks/useViewFilters';
import { useViewSorts } from '@/views/hooks/useViewSorts';
import {
SortOrder,
@ -26,12 +26,8 @@ import { companiesFilters } from '~/pages/companies/companies-filters';
import { availableSorts } from '~/pages/companies/companies-sorts';
export function CompanyTable() {
const currentViewId = useRecoilScopedValue(
currentTableViewIdState,
TableRecoilScopeContext,
);
const orderBy = useRecoilScopedValue(
sortsOrderByScopedState,
sortsOrderByScopedSelector,
TableRecoilScopeContext,
);
const [updateEntityMutation] = useUpdateOneCompanyMutation();
@ -44,7 +40,10 @@ export function CompanyTable() {
viewFieldDefinitions: companyViewFields,
});
const { handleSortsChange } = useViewSorts({ availableSorts });
const { persistFilters } = useViewFilters({
availableFilters: companiesFilters,
});
const { persistSorts } = useViewSorts({ availableSorts });
const { openCompanySpreadsheetImport } = useSpreadsheetCompanyImport();
const filters = useRecoilScopedValue(
@ -59,6 +58,11 @@ export function CompanyTable() {
const { setContextMenuEntries } = useCompanyTableContextMenuEntries();
const { setActionBarEntries } = useCompanyTableActionBarEntries();
const handleViewSubmit = useCallback(async () => {
await persistFilters();
await persistSorts();
}, [persistFilters, persistSorts]);
function handleImport() {
openCompanySpreadsheetImport();
}
@ -68,15 +72,7 @@ export function CompanyTable() {
<GenericEntityTableData
getRequestResultKey="companies"
useGetRequest={useGetCompaniesQuery}
orderBy={
orderBy.length
? orderBy
: [
{
createdAt: SortOrder.Desc,
},
]
}
orderBy={orderBy.length ? orderBy : [{ createdAt: SortOrder.Desc }]}
whereFilters={whereFilters}
filterDefinitionArray={companiesFilters}
setContextMenuEntries={setContextMenuEntries}
@ -86,8 +82,8 @@ export function CompanyTable() {
viewName="All Companies"
availableSorts={availableSorts}
onColumnsChange={handleColumnsChange}
onSortsUpdate={currentViewId ? handleSortsChange : undefined}
onViewsChange={handleViewsChange}
onViewSubmit={handleViewSubmit}
onImport={handleImport}
updateEntityMutation={({
variables,

View File

@ -1,20 +1,20 @@
import { useMemo } from 'react';
import { useCallback, useMemo } from 'react';
import { peopleViewFields } from '@/people/constants/peopleViewFields';
import { usePersonTableContextMenuEntries } from '@/people/hooks/usePeopleTableContextMenuEntries';
import { usePersonTableActionBarEntries } from '@/people/hooks/usePersonTableActionBarEntries';
import { useSpreadsheetPersonImport } from '@/people/hooks/useSpreadsheetPersonImport';
import { filtersScopedState } from '@/ui/filter-n-sort/states/filtersScopedState';
import { sortsOrderByScopedState } from '@/ui/filter-n-sort/states/sortScopedState';
import { sortsOrderByScopedSelector } from '@/ui/filter-n-sort/states/sortsOrderByScopedSelector';
import { turnFilterIntoWhereClause } from '@/ui/filter-n-sort/utils/turnFilterIntoWhereClause';
import { EntityTable } from '@/ui/table/components/EntityTable';
import { GenericEntityTableData } from '@/ui/table/components/GenericEntityTableData';
import { useUpsertEntityTableItem } from '@/ui/table/hooks/useUpsertEntityTableItem';
import { TableRecoilScopeContext } from '@/ui/table/states/recoil-scope-contexts/TableRecoilScopeContext';
import { currentTableViewIdState } from '@/ui/table/states/tableViewsState';
import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue';
import { useTableViewFields } from '@/views/hooks/useTableViewFields';
import { useTableViews } from '@/views/hooks/useTableViews';
import { useViewFilters } from '@/views/hooks/useViewFilters';
import { useViewSorts } from '@/views/hooks/useViewSorts';
import {
SortOrder,
@ -26,12 +26,8 @@ import { peopleFilters } from '~/pages/people/people-filters';
import { availableSorts } from '~/pages/people/people-sorts';
export function PeopleTable() {
const currentViewId = useRecoilScopedValue(
currentTableViewIdState,
TableRecoilScopeContext,
);
const orderBy = useRecoilScopedValue(
sortsOrderByScopedState,
sortsOrderByScopedSelector,
TableRecoilScopeContext,
);
const [updateEntityMutation] = useUpdateOnePersonMutation();
@ -44,7 +40,10 @@ export function PeopleTable() {
objectName: objectId,
viewFieldDefinitions: peopleViewFields,
});
const { handleSortsChange } = useViewSorts({ availableSorts });
const { persistFilters } = useViewFilters({
availableFilters: peopleFilters,
});
const { persistSorts } = useViewSorts({ availableSorts });
const filters = useRecoilScopedValue(
filtersScopedState,
@ -58,6 +57,11 @@ export function PeopleTable() {
const { setContextMenuEntries } = usePersonTableContextMenuEntries();
const { setActionBarEntries } = usePersonTableActionBarEntries();
const handleViewSubmit = useCallback(async () => {
await persistFilters();
await persistSorts();
}, [persistFilters, persistSorts]);
function handleImport() {
openPersonSpreadsheetImport();
}
@ -67,15 +71,7 @@ export function PeopleTable() {
<GenericEntityTableData
getRequestResultKey="people"
useGetRequest={useGetPeopleQuery}
orderBy={
orderBy.length
? orderBy
: [
{
createdAt: SortOrder.Desc,
},
]
}
orderBy={orderBy.length ? orderBy : [{ createdAt: SortOrder.Desc }]}
whereFilters={whereFilters}
filterDefinitionArray={peopleFilters}
setContextMenuEntries={setContextMenuEntries}
@ -85,8 +81,8 @@ export function PeopleTable() {
viewName="All People"
availableSorts={availableSorts}
onColumnsChange={handleColumnsChange}
onSortsUpdate={currentViewId ? handleSortsChange : undefined}
onViewsChange={handleViewsChange}
onViewSubmit={handleViewSubmit}
onImport={handleImport}
updateEntityMutation={({
variables,

View File

@ -1,4 +1,4 @@
import React from 'react';
import React, { ReactNode } from 'react';
import styled from '@emotion/styled';
import { ButtonPosition, ButtonProps } from './Button';
@ -9,13 +9,15 @@ const StyledButtonGroupContainer = styled.div`
`;
type ButtonGroupProps = Pick<ButtonProps, 'variant' | 'size'> & {
children: React.ReactElement[];
children: ReactNode[];
};
export function ButtonGroup({ children, variant, size }: ButtonGroupProps) {
return (
<StyledButtonGroupContainer>
{React.Children.map(children, (child, index) => {
if (!React.isValidElement(child)) return null;
let position: ButtonPosition;
if (index === 0) {

View File

@ -28,7 +28,7 @@ export function FilterDropdownDateSearchInput({
if (!filterDefinitionUsedInDropdown || !selectedOperandInDropdown) return;
upsertFilter({
field: filterDefinitionUsedInDropdown.field,
key: filterDefinitionUsedInDropdown.key,
type: filterDefinitionUsedInDropdown.type,
value: date.toISOString(),
operand: selectedOperandInDropdown,

View File

@ -51,14 +51,14 @@ export function FilterDropdownEntitySearchSelect({
selectedEntity.id === filterDropdownSelectedEntityId;
if (clickedOnAlreadySelectedEntity) {
removeFilter(filterDefinitionUsedInDropdown.field);
removeFilter(filterDefinitionUsedInDropdown.key);
setFilterDropdownSelectedEntityId(null);
} else {
setFilterDropdownSelectedEntityId(selectedEntity.id);
upsertFilter({
displayValue: selectedEntity.name,
field: filterDefinitionUsedInDropdown.field,
key: filterDefinitionUsedInDropdown.key,
operand: selectedOperandInDropdown,
type: filterDefinitionUsedInDropdown.type,
value: selectedEntity.id,

View File

@ -34,10 +34,10 @@ export function FilterDropdownNumberSearchInput({
placeholder={filterDefinitionUsedInDropdown.label}
onChange={(event: ChangeEvent<HTMLInputElement>) => {
if (event.target.value === '') {
removeFilter(filterDefinitionUsedInDropdown.field);
removeFilter(filterDefinitionUsedInDropdown.key);
} else {
upsertFilter({
field: filterDefinitionUsedInDropdown.field,
key: filterDefinitionUsedInDropdown.key,
type: filterDefinitionUsedInDropdown.type,
value: event.target.value,
operand: selectedOperandInDropdown,

View File

@ -48,7 +48,7 @@ export function FilterDropdownOperandSelect({
if (filterDefinitionUsedInDropdown && filterCurrentlyEdited) {
upsertFilter({
field: filterCurrentlyEdited.field,
key: filterCurrentlyEdited.key,
displayValue: filterCurrentlyEdited.displayValue,
operand: newOperand,
type: filterCurrentlyEdited.type,

View File

@ -44,10 +44,10 @@ export function FilterDropdownTextSearchInput({
setFilterDropdownSearchInput(event.target.value);
if (event.target.value === '') {
removeFilter(filterDefinitionUsedInDropdown.field);
removeFilter(filterDefinitionUsedInDropdown.key);
} else {
upsertFilter({
field: filterDefinitionUsedInDropdown.field,
key: filterDefinitionUsedInDropdown.key,
type: filterDefinitionUsedInDropdown.type,
value: event.target.value,
operand: selectedOperandInDropdown,

View File

@ -1,4 +1,4 @@
import { Context } from 'react';
import type { Context, ReactNode } from 'react';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
@ -26,6 +26,7 @@ type OwnProps<SortField> = {
onRemoveSort: (sortId: SelectedSortType<SortField>['key']) => void;
onCancelClick: () => void;
hasFilterButton?: boolean;
rightComponent?: ReactNode;
};
const StyledBar = styled.div`
@ -97,6 +98,7 @@ function SortAndFilterBar<SortField>({
onRemoveSort,
onCancelClick,
hasFilterButton = false,
rightComponent,
}: OwnProps<SortField>) {
const theme = useTheme();
@ -117,7 +119,7 @@ function SortAndFilterBar<SortField>({
const filtersWithDefinition = filters.map((filter) => {
const filterDefinition = availableFilters.find((availableFilter) => {
return availableFilter.field === filter.field;
return availableFilter.key === filter.key;
});
return {
@ -170,15 +172,15 @@ function SortAndFilterBar<SortField>({
{filtersWithDefinition.map((filter) => {
return (
<SortOrFilterChip
key={filter.field}
key={filter.key}
labelKey={filter.label}
labelValue={`${getOperandLabelShort(filter.operand)} ${
filter.displayValue
}`}
id={filter.field}
id={filter.key}
icon={filter.icon}
onRemove={() => {
removeFilter(filter.field);
removeFilter(filter.key);
}}
/>
);
@ -190,18 +192,19 @@ function SortAndFilterBar<SortField>({
HotkeyScope={FiltersHotkeyScope.FilterDropdownButton}
color={theme.font.color.tertiary}
icon={<IconPlus size={theme.icon.size.md} />}
label={`Add filter`}
label="Add filter"
/>
)}
</StyledFilterContainer>
{filters.length + sorts.length > 0 && (
<StyledCancelButton
data-testid={'cancel-button'}
data-testid="cancel-button"
onClick={handleCancelClick}
>
Cancel
</StyledCancelButton>
)}
{rightComponent}
</StyledBar>
);
}

View File

@ -15,7 +15,7 @@ export function useFilterCurrentlyEdited(context: Context<string | null>) {
return useMemo(() => {
return filters.find(
(filter) => filter.field === filterDefinitionUsedInDropdown?.field,
(filter) => filter.key === filterDefinitionUsedInDropdown?.key,
);
}, [filterDefinitionUsedInDropdown, filters]);
}

View File

@ -7,10 +7,10 @@ import { filtersScopedState } from '../states/filtersScopedState';
export function useRemoveFilter(context: Context<string | null>) {
const [, setFilters] = useRecoilScopedState(filtersScopedState, context);
return function removeFilter(filterField: string) {
return function removeFilter(filterKey: string) {
setFilters((filters) => {
return filters.filter((filter) => {
return filter.field !== filterField;
return filter.key !== filterKey;
});
});
};

View File

@ -13,7 +13,7 @@ export function useUpsertFilter(context: Context<string | null>) {
setFilters((filters) => {
return produce(filters, (filtersDraft) => {
const index = filtersDraft.findIndex(
(filter) => filter.field === filterToUpsert.field,
(filter) => filter.key === filterToUpsert.key,
);
if (index === -1) {

View File

@ -1,6 +1,6 @@
import { atomFamily } from 'recoil';
import { Filter } from '../types/Filter';
import type { Filter } from '../types/Filter';
export const filtersScopedState = atomFamily<Filter[], string>({
key: 'filtersScopedState',

View File

@ -0,0 +1,16 @@
import { selectorFamily } from 'recoil';
import type { Filter } from '../types/Filter';
import { savedFiltersScopedState } from './savedFiltersScopedState';
export const savedFiltersByKeyScopedSelector = selectorFamily({
key: 'savedFiltersByKeyScopedSelector',
get:
(param: string | undefined) =>
({ get }) =>
get(savedFiltersScopedState(param)).reduce<Record<string, Filter>>(
(result, filter) => ({ ...result, [filter.key]: filter }),
{},
),
});

View File

@ -0,0 +1,10 @@
import { atomFamily } from 'recoil';
import type { Filter } from '../types/Filter';
export const savedFiltersScopedState = atomFamily<Filter[], string | undefined>(
{
key: 'savedFiltersScopedState',
default: [],
},
);

View File

@ -0,0 +1,15 @@
import { selectorFamily } from 'recoil';
import type { SelectedSortType } from '../types/interface';
import { savedSortsScopedState } from './savedSortsScopedState';
export const savedSortsByKeyScopedSelector = selectorFamily({
key: 'savedSortsByKeyScopedSelector',
get:
(viewId: string | undefined) =>
({ get }) =>
get(savedSortsScopedState(viewId)).reduce<
Record<string, SelectedSortType<any>>
>((result, sort) => ({ ...result, [sort.key]: sort }), {}),
});

View File

@ -0,0 +1,11 @@
import { atomFamily } from 'recoil';
import type { SelectedSortType } from '../types/interface';
export const savedSortsScopedState = atomFamily<
SelectedSortType<any>[],
string | undefined
>({
key: 'savedSortsScopedState',
default: [],
});

View File

@ -1,28 +0,0 @@
import { atomFamily, selectorFamily } from 'recoil';
import { reduceSortsToOrderBy } from '../helpers';
import { SelectedSortType } from '../types/interface';
export const sortScopedState = atomFamily<SelectedSortType<any>[], string>({
key: 'sortScopedState',
default: [],
});
export const sortsByKeyScopedState = selectorFamily({
key: 'sortsByKeyScopedState',
get:
(param: string) =>
({ get }) =>
get(sortScopedState(param)).reduce<Record<string, SelectedSortType<any>>>(
(result, sort) => ({ ...result, [sort.key]: sort }),
{},
),
});
export const sortsOrderByScopedState = selectorFamily({
key: 'sortsOrderByScopedState',
get:
(param: string) =>
({ get }) =>
reduceSortsToOrderBy(get(sortScopedState(param))),
});

View File

@ -0,0 +1,13 @@
import { selectorFamily } from 'recoil';
import { reduceSortsToOrderBy } from '../helpers';
import { sortsScopedState } from './sortsScopedState';
export const sortsOrderByScopedSelector = selectorFamily({
key: 'sortsOrderByScopedSelector',
get:
(param: string) =>
({ get }) =>
reduceSortsToOrderBy(get(sortsScopedState(param))),
});

View File

@ -0,0 +1,8 @@
import { atomFamily } from 'recoil';
import type { SelectedSortType } from '../types/interface';
export const sortsScopedState = atomFamily<SelectedSortType<any>[], string>({
key: 'sortsScopedState',
default: [],
});

View File

@ -2,7 +2,7 @@ import { FilterOperand } from './FilterOperand';
import { FilterType } from './FilterType';
export type Filter = {
field: string;
key: string;
type: FilterType;
value: string;
displayValue: string;

View File

@ -1,7 +1,7 @@
import { FilterType } from './FilterType';
export type FilterDefinition = {
field: string;
key: string;
label: string;
icon: JSX.Element;
type: FilterType;

View File

@ -1,5 +1,5 @@
import { FilterDefinition } from './FilterDefinition';
export type FilterDefinitionByEntity<T> = FilterDefinition & {
field: keyof T;
key: keyof T;
};

View File

@ -1,7 +1 @@
export type FilterOperand =
| 'contains'
| 'does-not-contain'
| 'greater-than'
| 'less-than'
| 'is'
| 'is-not';
export { ViewFilterOperand as FilterOperand } from '~/generated/graphql';

View File

@ -2,17 +2,17 @@ import { FilterOperand } from '../types/FilterOperand';
export function getOperandLabel(operand: FilterOperand | null | undefined) {
switch (operand) {
case 'contains':
case FilterOperand.Contains:
return 'Contains';
case 'does-not-contain':
case FilterOperand.DoesNotContain:
return "Doesn't contain";
case 'greater-than':
case FilterOperand.GreaterThan:
return 'Greater than';
case 'less-than':
case FilterOperand.LessThan:
return 'Less than';
case 'is':
case FilterOperand.Is:
return 'Is';
case 'is-not':
case FilterOperand.IsNot:
return 'Is not';
default:
return '';
@ -22,15 +22,15 @@ export function getOperandLabelShort(
operand: FilterOperand | null | undefined,
) {
switch (operand) {
case 'is':
case 'contains':
case FilterOperand.Is:
case FilterOperand.Contains:
return ': ';
case 'is-not':
case 'does-not-contain':
case FilterOperand.IsNot:
case FilterOperand.DoesNotContain:
return ': Not';
case 'greater-than':
case FilterOperand.GreaterThan:
return '\u00A0> ';
case 'less-than':
case FilterOperand.LessThan:
return '\u00A0< ';
default:
return ': ';

View File

@ -6,12 +6,12 @@ export function getOperandsForFilterType(
): FilterOperand[] {
switch (filterType) {
case 'text':
return ['contains', 'does-not-contain'];
return [FilterOperand.Contains, FilterOperand.DoesNotContain];
case 'number':
case 'date':
return ['greater-than', 'less-than'];
return [FilterOperand.GreaterThan, FilterOperand.LessThan];
case 'entity':
return ['is', 'is-not'];
return [FilterOperand.Is, FilterOperand.IsNot];
default:
return [];
}

View File

@ -1,21 +1,22 @@
import { QueryMode } from '~/generated/graphql';
import { Filter } from '../types/Filter';
import { FilterOperand } from '../types/FilterOperand';
export function turnFilterIntoWhereClause(filter: Filter) {
switch (filter.type) {
case 'text':
switch (filter.operand) {
case 'contains':
case FilterOperand.Contains:
return {
[filter.field]: {
[filter.key]: {
contains: filter.value,
mode: QueryMode.Insensitive,
},
};
case 'does-not-contain':
case FilterOperand.DoesNotContain:
return {
[filter.field]: {
[filter.key]: {
not: {
contains: filter.value,
mode: QueryMode.Insensitive,
@ -29,15 +30,15 @@ export function turnFilterIntoWhereClause(filter: Filter) {
}
case 'number':
switch (filter.operand) {
case 'greater-than':
case FilterOperand.GreaterThan:
return {
[filter.field]: {
[filter.key]: {
gte: parseFloat(filter.value),
},
};
case 'less-than':
case FilterOperand.LessThan:
return {
[filter.field]: {
[filter.key]: {
lte: parseFloat(filter.value),
},
};
@ -48,15 +49,15 @@ export function turnFilterIntoWhereClause(filter: Filter) {
}
case 'date':
switch (filter.operand) {
case 'greater-than':
case FilterOperand.GreaterThan:
return {
[filter.field]: {
[filter.key]: {
gte: filter.value,
},
};
case 'less-than':
case FilterOperand.LessThan:
return {
[filter.field]: {
[filter.key]: {
lte: filter.value,
},
};
@ -67,15 +68,15 @@ export function turnFilterIntoWhereClause(filter: Filter) {
}
case 'entity':
switch (filter.operand) {
case 'is':
case FilterOperand.Is:
return {
[filter.field]: {
[filter.key]: {
equals: filter.value,
},
};
case 'is-not':
case FilterOperand.IsNot:
return {
[filter.field]: {
[filter.key]: {
not: { equals: filter.value },
},
};

View File

@ -5,7 +5,7 @@ import type {
ViewFieldDefinition,
ViewFieldMetadata,
} from '@/ui/editable-field/types/ViewField';
import { SelectedSortType, SortType } from '@/ui/filter-n-sort/types/interface';
import { 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';
@ -97,8 +97,8 @@ type OwnProps<SortField> = {
viewIcon?: React.ReactNode;
availableSorts?: Array<SortType<SortField>>;
onColumnsChange?: (columns: ViewFieldDefinition<ViewFieldMetadata>[]) => void;
onSortsUpdate?: (sorts: Array<SelectedSortType<SortField>>) => void;
onViewsChange?: (views: TableView[]) => void;
onViewSubmit?: () => void;
onImport?: () => void;
updateEntityMutation: any;
};
@ -107,8 +107,8 @@ export function EntityTable<SortField>({
viewName,
availableSorts,
onColumnsChange,
onSortsUpdate,
onViewsChange,
onViewSubmit,
onImport,
updateEntityMutation,
}: OwnProps<SortField>) {
@ -136,8 +136,8 @@ export function EntityTable<SortField>({
viewName={viewName}
availableSorts={availableSorts}
onColumnsChange={onColumnsChange}
onSortsUpdate={onSortsUpdate}
onViewsChange={onViewsChange}
onViewSubmit={onViewSubmit}
onImport={onImport}
/>
<StyledTableWrapper>

View File

@ -0,0 +1,123 @@
import { useCallback, useState } from 'react';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { useRecoilCallback, useSetRecoilState } from 'recoil';
import { Key } from 'ts-key-enum';
import { Button, ButtonSize } from '@/ui/button/components/Button';
import { ButtonGroup } from '@/ui/button/components/ButtonGroup';
import { DropdownMenuItem } from '@/ui/dropdown/components/DropdownMenuItem';
import { DropdownMenuItemsContainer } from '@/ui/dropdown/components/DropdownMenuItemsContainer';
import { DropdownMenuContainer } from '@/ui/filter-n-sort/components/DropdownMenuContainer';
import { filtersScopedState } from '@/ui/filter-n-sort/states/filtersScopedState';
import { savedFiltersScopedState } from '@/ui/filter-n-sort/states/savedFiltersScopedState';
import { savedSortsScopedState } from '@/ui/filter-n-sort/states/savedSortsScopedState';
import { sortsScopedState } from '@/ui/filter-n-sort/states/sortsScopedState';
import { IconChevronDown, IconPlus } from '@/ui/icon';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { useContextScopeId } from '@/ui/utilities/recoil-scope/hooks/useContextScopeId';
import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue';
import { TableRecoilScopeContext } from '../../states/recoil-scope-contexts/TableRecoilScopeContext';
import {
currentTableViewIdState,
tableViewEditModeState,
} from '../../states/tableViewsState';
const StyledContainer = styled.div`
display: inline-flex;
margin-right: ${({ theme }) => theme.spacing(2)};
position: relative;
`;
const StyledDropdownMenuContainer = styled(DropdownMenuContainer)`
z-index: 1;
`;
type TableUpdateViewButtonGroupProps = {
onViewSubmit?: () => void;
HotkeyScope: string;
};
export const TableUpdateViewButtonGroup = ({
onViewSubmit,
HotkeyScope,
}: TableUpdateViewButtonGroupProps) => {
const theme = useTheme();
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
const tableScopeId = useContextScopeId(TableRecoilScopeContext);
const currentViewId = useRecoilScopedValue(
currentTableViewIdState,
TableRecoilScopeContext,
);
const setViewEditMode = useSetRecoilState(tableViewEditModeState);
const handleArrowDownButtonClick = useCallback(() => {
setIsDropdownOpen((previousIsOpen) => !previousIsOpen);
}, []);
const handleCreateViewButtonClick = useCallback(() => {
setViewEditMode({ mode: 'create', viewId: undefined });
setIsDropdownOpen(false);
}, [setViewEditMode]);
const handleDropdownClose = useCallback(() => {
setIsDropdownOpen(false);
}, []);
const handleViewSubmit = useRecoilCallback(
({ set, snapshot }) =>
async () => {
await Promise.resolve(onViewSubmit?.());
const selectedFilters = await snapshot.getPromise(
filtersScopedState(tableScopeId),
);
set(savedFiltersScopedState(currentViewId), selectedFilters);
const selectedSorts = await snapshot.getPromise(
sortsScopedState(tableScopeId),
);
set(savedSortsScopedState(currentViewId), selectedSorts);
},
[currentViewId, onViewSubmit],
);
useScopedHotkeys(
[Key.Enter, Key.Escape],
handleDropdownClose,
HotkeyScope,
[],
);
return (
<StyledContainer>
<ButtonGroup size={ButtonSize.Small}>
<Button
title="Update view"
disabled={!currentViewId}
onClick={handleViewSubmit}
/>
<Button
size={ButtonSize.Small}
icon={<IconChevronDown />}
onClick={handleArrowDownButtonClick}
/>
</ButtonGroup>
{isDropdownOpen && (
<StyledDropdownMenuContainer onClose={handleDropdownClose}>
<DropdownMenuItemsContainer>
<DropdownMenuItem onClick={handleCreateViewButtonClick}>
<IconPlus size={theme.icon.size.md} />
Create view
</DropdownMenuItem>
</DropdownMenuItemsContainer>
</StyledDropdownMenuContainer>
)}
</StyledContainer>
);
};

View File

@ -1,13 +1,17 @@
import { type MouseEvent, useCallback, useEffect, useState } from 'react';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { useSetRecoilState } from 'recoil';
import { useRecoilCallback, useSetRecoilState } from 'recoil';
import { IconButton } from '@/ui/button/components/IconButton';
import { DropdownMenuItem } from '@/ui/dropdown/components/DropdownMenuItem';
import { DropdownMenuItemsContainer } from '@/ui/dropdown/components/DropdownMenuItemsContainer';
import { DropdownMenuSeparator } from '@/ui/dropdown/components/DropdownMenuSeparator';
import DropdownButton from '@/ui/filter-n-sort/components/DropdownButton';
import { filtersScopedState } from '@/ui/filter-n-sort/states/filtersScopedState';
import { savedFiltersScopedState } from '@/ui/filter-n-sort/states/savedFiltersScopedState';
import { savedSortsScopedState } from '@/ui/filter-n-sort/states/savedSortsScopedState';
import { sortsScopedState } from '@/ui/filter-n-sort/states/sortsScopedState';
import {
IconChevronDown,
IconList,
@ -23,6 +27,7 @@ import {
tableViewsState,
} from '@/ui/table/states/tableViewsState';
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
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';
@ -59,6 +64,8 @@ export const TableViewsDropdownButton = ({
const theme = useTheme();
const [isUnfolded, setIsUnfolded] = useState(false);
const tableScopeId = useContextScopeId(TableRecoilScopeContext);
const currentView = useRecoilScopedValue(
currentTableViewState,
TableRecoilScopeContext,
@ -78,11 +85,21 @@ export const TableViewsDropdownButton = ({
setHotkeyScopeAndMemorizePreviousScope,
} = usePreviousHotkeyScope();
const handleViewSelect = useCallback(
(viewId?: string) => {
setCurrentViewId(viewId);
setIsUnfolded(false);
},
const handleViewSelect = useRecoilCallback(
({ set, snapshot }) =>
async (viewId?: string) => {
const savedFilters = await snapshot.getPromise(
savedFiltersScopedState(viewId),
);
const savedSorts = await snapshot.getPromise(
savedSortsScopedState(viewId),
);
set(filtersScopedState(tableScopeId), savedFilters);
set(sortsScopedState(tableScopeId), savedSorts);
setCurrentViewId(viewId);
setIsUnfolded(false);
},
[setCurrentViewId],
);

View File

@ -7,13 +7,14 @@ import type {
import { FilterDropdownButton } from '@/ui/filter-n-sort/components/FilterDropdownButton';
import SortAndFilterBar from '@/ui/filter-n-sort/components/SortAndFilterBar';
import { SortDropdownButton } from '@/ui/filter-n-sort/components/SortDropdownButton';
import { sortScopedState } from '@/ui/filter-n-sort/states/sortScopedState';
import { sortsScopedState } from '@/ui/filter-n-sort/states/sortsScopedState';
import { FiltersHotkeyScope } from '@/ui/filter-n-sort/types/FiltersHotkeyScope';
import { SelectedSortType, SortType } from '@/ui/filter-n-sort/types/interface';
import { TableOptionsDropdownButton } from '@/ui/table/options/components/TableOptionsDropdownButton';
import { TopBar } from '@/ui/top-bar/TopBar';
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
import { TableUpdateViewButtonGroup } from '../../options/components/TableUpdateViewButtonGroup';
import { TableViewsDropdownButton } from '../../options/components/TableViewsDropdownButton';
import { TableRecoilScopeContext } from '../../states/recoil-scope-contexts/TableRecoilScopeContext';
import type { TableView } from '../../states/tableViewsState';
@ -24,8 +25,8 @@ type OwnProps<SortField> = {
viewName: string;
availableSorts?: Array<SortType<SortField>>;
onColumnsChange?: (columns: ViewFieldDefinition<ViewFieldMetadata>[]) => void;
onSortsUpdate?: (sorts: Array<SelectedSortType<SortField>>) => void;
onViewsChange?: (views: TableView[]) => void;
onViewSubmit?: () => void;
onImport?: () => void;
};
@ -33,30 +34,29 @@ export function TableHeader<SortField>({
viewName,
availableSorts,
onColumnsChange,
onSortsUpdate,
onViewsChange,
onViewSubmit,
onImport,
}: OwnProps<SortField>) {
const [sorts, setSorts] = useRecoilScopedState<SelectedSortType<SortField>[]>(
sortScopedState,
sortsScopedState,
TableRecoilScopeContext,
);
const handleSortsUpdate = onSortsUpdate ?? setSorts;
const sortSelect = useCallback(
(newSort: SelectedSortType<SortField>) => {
const newSorts = updateSortOrFilterByKey(sorts, newSort);
handleSortsUpdate(newSorts);
setSorts(newSorts);
},
[handleSortsUpdate, sorts],
[setSorts, sorts],
);
const sortUnselect = useCallback(
(sortKey: string) => {
const newSorts = sorts.filter((sort) => sort.key !== sortKey);
handleSortsUpdate(newSorts);
setSorts(newSorts);
},
[handleSortsUpdate, sorts],
[setSorts, sorts],
);
return (
@ -65,7 +65,7 @@ export function TableHeader<SortField>({
<TableViewsDropdownButton
defaultViewName={viewName}
onViewsChange={onViewsChange}
HotkeyScope={TableViewsHotkeyScope.Dropdown}
HotkeyScope={TableViewsHotkeyScope.ListDropdown}
/>
}
displayBottomBorder={false}
@ -97,10 +97,14 @@ export function TableHeader<SortField>({
context={TableRecoilScopeContext}
sorts={sorts}
onRemoveSort={sortUnselect}
onCancelClick={() => {
handleSortsUpdate([]);
}}
onCancelClick={() => setSorts([])}
hasFilterButton
rightComponent={
<TableUpdateViewButtonGroup
onViewSubmit={onViewSubmit}
HotkeyScope={TableViewsHotkeyScope.CreateDropdown}
/>
}
/>
}
/>

View File

@ -1,3 +1,4 @@
export enum TableViewsHotkeyScope {
Dropdown = 'table-views-dropdown',
ListDropdown = 'table-views-list-dropdown',
CreateDropdown = 'table-views-create-dropdown',
}

View File

@ -0,0 +1,9 @@
import { gql } from '@apollo/client';
export const CREATE_VIEW_FILTERS = gql`
mutation CreateViewFilters($data: [ViewFilterCreateManyInput!]!) {
createManyViewFilter(data: $data) {
count
}
}
`;

View File

@ -0,0 +1,9 @@
import { gql } from '@apollo/client';
export const DELETE_VIEW_FILTERS = gql`
mutation DeleteViewFilters($where: ViewFilterWhereInput!) {
deleteManyViewFilter(where: $where) {
count
}
}
`;

View File

@ -0,0 +1,16 @@
import { gql } from '@apollo/client';
export const UPDATE_VIEW_FILTER = gql`
mutation UpdateViewFilter(
$data: ViewFilterUpdateInput!
$where: ViewFilterWhereUniqueInput!
) {
viewFilter: updateOneViewFilter(data: $data, where: $where) {
displayValue
key
name
operand
value
}
}
`;

View File

@ -0,0 +1,13 @@
import { gql } from '@apollo/client';
export const GET_VIEW_FILTERS = gql`
query GetViewFilters($where: ViewFilterWhereInput) {
viewFilters: findManyViewFilter(where: $where) {
displayValue
key
name
operand
value
}
}
`;

View File

@ -0,0 +1,172 @@
import { useCallback } from 'react';
import { useRecoilState, useRecoilValue } from 'recoil';
import { filtersScopedState } from '@/ui/filter-n-sort/states/filtersScopedState';
import { savedFiltersByKeyScopedSelector } from '@/ui/filter-n-sort/states/savedFiltersByKeyScopedSelector';
import { savedFiltersScopedState } from '@/ui/filter-n-sort/states/savedFiltersScopedState';
import type { Filter } from '@/ui/filter-n-sort/types/Filter';
import type { FilterDefinitionByEntity } from '@/ui/filter-n-sort/types/FilterDefinitionByEntity';
import { TableRecoilScopeContext } from '@/ui/table/states/recoil-scope-contexts/TableRecoilScopeContext';
import { currentTableViewIdState } from '@/ui/table/states/tableViewsState';
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue';
import {
useCreateViewFiltersMutation,
useDeleteViewFiltersMutation,
useGetViewFiltersQuery,
useUpdateViewFilterMutation,
} from '~/generated/graphql';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
export const useViewFilters = <Entity>({
availableFilters,
}: {
availableFilters: FilterDefinitionByEntity<Entity>[];
}) => {
const currentViewId = useRecoilScopedValue(
currentTableViewIdState,
TableRecoilScopeContext,
);
const [filters, setFilters] = useRecoilScopedState(
filtersScopedState,
TableRecoilScopeContext,
);
const [, setSavedFilters] = useRecoilState(
savedFiltersScopedState(currentViewId),
);
const savedFiltersByKey = useRecoilValue(
savedFiltersByKeyScopedSelector(currentViewId),
);
const { refetch } = useGetViewFiltersQuery({
skip: !currentViewId,
variables: {
where: {
viewId: { equals: currentViewId },
},
},
onCompleted: (data) => {
const nextFilters = data.viewFilters
.map(({ __typename, name: _name, ...viewFilter }) => {
const availableFilter = availableFilters.find(
(filter) => filter.key === viewFilter.key,
);
return availableFilter
? {
...viewFilter,
displayValue: viewFilter.displayValue ?? viewFilter.value,
type: availableFilter.type,
}
: undefined;
})
.filter((filter): filter is Filter => !!filter);
if (!isDeeplyEqual(filters, nextFilters)) {
setSavedFilters(nextFilters);
setFilters(nextFilters);
}
},
});
const [createViewFiltersMutation] = useCreateViewFiltersMutation();
const [updateViewFilterMutation] = useUpdateViewFilterMutation();
const [deleteViewFiltersMutation] = useDeleteViewFiltersMutation();
const createViewFilters = useCallback(
(filters: Filter[]) => {
if (!currentViewId || !filters.length) return;
return createViewFiltersMutation({
variables: {
data: filters.map((filter) => ({
displayValue: filter.displayValue ?? filter.value,
key: filter.key,
name:
availableFilters.find(({ key }) => key === filter.key)?.label ??
'',
operand: filter.operand,
value: filter.value,
viewId: currentViewId,
})),
},
});
},
[availableFilters, createViewFiltersMutation, currentViewId],
);
const updateViewFilters = useCallback(
(filters: Filter[]) => {
if (!currentViewId || !filters.length) return;
return Promise.all(
filters.map((filter) =>
updateViewFilterMutation({
variables: {
data: {
displayValue: filter.displayValue ?? filter.value,
operand: filter.operand,
value: filter.value,
},
where: {
viewId_key: { key: filter.key, viewId: currentViewId },
},
},
}),
),
);
},
[currentViewId, updateViewFilterMutation],
);
const deleteViewFilters = useCallback(
(filterKeys: string[]) => {
if (!currentViewId || !filterKeys.length) return;
return deleteViewFiltersMutation({
variables: {
where: {
key: { in: filterKeys },
viewId: { equals: currentViewId },
},
},
});
},
[currentViewId, deleteViewFiltersMutation],
);
const persistFilters = useCallback(async () => {
if (!currentViewId) return;
const filtersToCreate = filters.filter(
(filter) => !savedFiltersByKey[filter.key],
);
await createViewFilters(filtersToCreate);
const filtersToUpdate = filters.filter(
(filter) =>
savedFiltersByKey[filter.key] &&
(savedFiltersByKey[filter.key].operand !== filter.operand ||
savedFiltersByKey[filter.key].value !== filter.value),
);
await updateViewFilters(filtersToUpdate);
const filterKeys = filters.map((filter) => filter.key);
const filterKeysToDelete = Object.keys(savedFiltersByKey).filter(
(previousFilterKey) => !filterKeys.includes(previousFilterKey),
);
await deleteViewFilters(filterKeysToDelete);
return refetch();
}, [
currentViewId,
filters,
createViewFilters,
updateViewFilters,
savedFiltersByKey,
deleteViewFilters,
refetch,
]);
return { persistFilters };
};

View File

@ -1,10 +1,9 @@
import { useCallback, useEffect } from 'react';
import { getOperationName } from '@apollo/client/utilities';
import { useCallback } from 'react';
import { useRecoilState, useRecoilValue } from 'recoil';
import {
sortsByKeyScopedState,
sortScopedState,
} from '@/ui/filter-n-sort/states/sortScopedState';
import { savedSortsByKeyScopedSelector } from '@/ui/filter-n-sort/states/savedSortsByKeyScopedSelector';
import { savedSortsScopedState } from '@/ui/filter-n-sort/states/savedSortsScopedState';
import { sortsScopedState } from '@/ui/filter-n-sort/states/sortsScopedState';
import type {
SelectedSortType,
SortType,
@ -20,8 +19,7 @@ import {
useUpdateViewSortMutation,
ViewSortDirection,
} from '~/generated/graphql';
import { GET_VIEW_SORTS } from '../graphql/queries/getViewSorts';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
export const useViewSorts = <SortField>({
availableSorts,
@ -32,20 +30,18 @@ export const useViewSorts = <SortField>({
currentTableViewIdState,
TableRecoilScopeContext,
);
const [, setSorts] = useRecoilScopedState(
sortScopedState,
const [sorts, setSorts] = useRecoilScopedState(
sortsScopedState,
TableRecoilScopeContext,
);
const sortsByKey = useRecoilScopedValue(
sortsByKeyScopedState,
TableRecoilScopeContext,
const [, setSavedSorts] = useRecoilState(
savedSortsScopedState(currentViewId),
);
const savedSortsByKey = useRecoilValue(
savedSortsByKeyScopedSelector(currentViewId),
);
useEffect(() => {
if (!currentViewId) setSorts([]);
}, [currentViewId, setSorts]);
useGetViewSortsQuery({
const { refetch } = useGetViewSortsQuery({
skip: !currentViewId,
variables: {
where: {
@ -53,23 +49,26 @@ export const useViewSorts = <SortField>({
},
},
onCompleted: (data) => {
setSorts(
data.viewSorts
.map((viewSort) => {
const availableSort = availableSorts.find(
(sort) => sort.key === viewSort.key,
);
const nextSorts = data.viewSorts
.map((viewSort) => {
const availableSort = availableSorts.find(
(sort) => sort.key === viewSort.key,
);
return availableSort
? {
...availableSort,
label: viewSort.name,
order: viewSort.direction.toLowerCase(),
}
: undefined;
})
.filter((sort): sort is SelectedSortType<SortField> => !!sort),
);
return availableSort
? {
...availableSort,
label: viewSort.name,
order: viewSort.direction.toLowerCase(),
}
: undefined;
})
.filter((sort): sort is SelectedSortType<SortField> => !!sort);
if (!isDeeplyEqual(sorts, nextSorts)) {
setSavedSorts(nextSorts);
setSorts(nextSorts);
}
},
});
@ -90,7 +89,6 @@ export const useViewSorts = <SortField>({
viewId: currentViewId,
})),
},
refetchQueries: [getOperationName(GET_VIEW_SORTS) ?? ''],
});
},
[createViewSortsMutation, currentViewId],
@ -111,7 +109,6 @@ export const useViewSorts = <SortField>({
viewId_key: { key: sort.key, viewId: currentViewId },
},
},
refetchQueries: [getOperationName(GET_VIEW_SORTS) ?? ''],
}),
),
);
@ -130,45 +127,40 @@ export const useViewSorts = <SortField>({
viewId: { equals: currentViewId },
},
},
refetchQueries: [getOperationName(GET_VIEW_SORTS) ?? ''],
});
},
[currentViewId, deleteViewSortsMutation],
);
const handleSortsChange = useCallback(
async (nextSorts: SelectedSortType<SortField>[]) => {
if (!currentViewId) return;
const persistSorts = useCallback(async () => {
if (!currentViewId) return;
setSorts(nextSorts);
const sortsToCreate = sorts.filter((sort) => !savedSortsByKey[sort.key]);
await createViewSorts(sortsToCreate);
const sortsToCreate = nextSorts.filter(
(nextSort) => !sortsByKey[nextSort.key],
);
await createViewSorts(sortsToCreate);
const sortsToUpdate = sorts.filter(
(sort) =>
savedSortsByKey[sort.key] &&
savedSortsByKey[sort.key].order !== sort.order,
);
await updateViewSorts(sortsToUpdate);
const sortsToUpdate = nextSorts.filter(
(nextSort) =>
sortsByKey[nextSort.key] &&
sortsByKey[nextSort.key].order !== nextSort.order,
);
await updateViewSorts(sortsToUpdate);
const sortKeys = sorts.map((sort) => sort.key);
const sortKeysToDelete = Object.keys(savedSortsByKey).filter(
(previousSortKey) => !sortKeys.includes(previousSortKey),
);
await deleteViewSorts(sortKeysToDelete);
const nextSortKeys = nextSorts.map((nextSort) => nextSort.key);
const sortKeysToDelete = Object.keys(sortsByKey).filter(
(previousSortKey) => !nextSortKeys.includes(previousSortKey),
);
return deleteViewSorts(sortKeysToDelete);
},
[
createViewSorts,
currentViewId,
deleteViewSorts,
setSorts,
sortsByKey,
updateViewSorts,
],
);
return refetch();
}, [
currentViewId,
sorts,
createViewSorts,
updateViewSorts,
savedSortsByKey,
deleteViewSorts,
refetch,
]);
return { handleSortsChange };
return { persistSorts };
};

View File

@ -14,7 +14,7 @@ import { Company } from '~/generated/graphql';
export const companiesFilters: FilterDefinitionByEntity<Company>[] = [
{
field: 'name',
key: 'name',
label: 'Name',
icon: (
<IconBuildingSkyscraper size={icon.size.md} stroke={icon.stroke.sm} />
@ -22,31 +22,31 @@ export const companiesFilters: FilterDefinitionByEntity<Company>[] = [
type: 'text',
},
{
field: 'employees',
key: 'employees',
label: 'Employees',
icon: <IconUsers size={icon.size.md} stroke={icon.stroke.sm} />,
type: 'number',
},
{
field: 'domainName',
key: 'domainName',
label: 'URL',
icon: <IconLink size={icon.size.md} stroke={icon.stroke.sm} />,
type: 'text',
},
{
field: 'address',
key: 'address',
label: 'Address',
icon: <IconMap size={icon.size.md} stroke={icon.stroke.sm} />,
type: 'text',
},
{
field: 'createdAt',
key: 'createdAt',
label: 'Created at',
icon: <IconCalendarEvent size={icon.size.md} stroke={icon.stroke.sm} />,
type: 'date',
},
{
field: 'accountOwnerId',
key: 'accountOwnerId',
label: 'Account owner',
icon: <IconUser size={icon.size.md} stroke={icon.stroke.sm} />,
type: 'entity',

View File

@ -15,19 +15,19 @@ import { FilterDropdownPeopleSearchSelect } from '../../modules/people/component
export const opportunitiesFilters: FilterDefinitionByEntity<PipelineProgress>[] =
[
{
field: 'amount',
key: 'amount',
label: 'Amount',
icon: <IconCurrencyDollar size={icon.size.md} stroke={icon.stroke.sm} />,
type: 'number',
},
{
field: 'closeDate',
key: 'closeDate',
label: 'Close date',
icon: <IconCalendarEvent size={icon.size.md} stroke={icon.stroke.sm} />,
type: 'date',
},
{
field: 'companyId',
key: 'companyId',
label: 'Company',
icon: (
<IconBuildingSkyscraper size={icon.size.md} stroke={icon.stroke.sm} />
@ -40,7 +40,7 @@ export const opportunitiesFilters: FilterDefinitionByEntity<PipelineProgress>[]
),
},
{
field: 'pointOfContactId',
key: 'pointOfContactId',
label: 'Point of contact',
icon: <IconUser size={icon.size.md} stroke={icon.stroke.sm} />,
type: 'entity',

View File

@ -14,25 +14,25 @@ import { Person } from '~/generated/graphql';
export const peopleFilters: FilterDefinitionByEntity<Person>[] = [
{
field: 'firstName',
key: 'firstName',
label: 'First name',
icon: <IconUser size={icon.size.md} stroke={icon.stroke.sm} />,
type: 'text',
},
{
field: 'lastName',
key: 'lastName',
label: 'Last name',
icon: <IconUser size={icon.size.md} stroke={icon.stroke.sm} />,
type: 'text',
},
{
field: 'email',
key: 'email',
label: 'Email',
icon: <IconMail size={icon.size.md} stroke={icon.stroke.sm} />,
type: 'text',
},
{
field: 'companyId',
key: 'companyId',
label: 'Company',
icon: (
<IconBuildingSkyscraper size={icon.size.md} stroke={icon.stroke.sm} />
@ -43,19 +43,19 @@ export const peopleFilters: FilterDefinitionByEntity<Person>[] = [
),
},
{
field: 'phone',
key: 'phone',
label: 'Phone',
icon: <IconPhone size={icon.size.md} stroke={icon.stroke.sm} />,
type: 'text',
},
{
field: 'createdAt',
key: 'createdAt',
label: 'Created at',
icon: <IconCalendarEvent size={icon.size.md} stroke={icon.stroke.sm} />,
type: 'date',
},
{
field: 'city',
key: 'city',
label: 'City',
icon: <IconMap size={icon.size.md} stroke={icon.stroke.sm} />,
type: 'text',

View File

@ -7,7 +7,7 @@ import { Activity } from '~/generated/graphql';
export const tasksFilters: FilterDefinitionByEntity<Activity>[] = [
{
field: 'assigneeId',
key: 'assigneeId',
label: 'Assignee',
icon: <IconUser />,
type: 'entity',

View File

@ -3,47 +3,49 @@ import { Injectable } from '@nestjs/common';
import { PureAbility, AbilityBuilder } from '@casl/ability';
import { createPrismaAbility, PrismaQuery, Subjects } from '@casl/prisma';
import {
Attachment,
Activity,
Company,
ActivityTarget,
Attachment,
Comment,
Company,
Favorite,
Person,
Pipeline,
PipelineProgress,
PipelineStage,
RefreshToken,
User,
Workspace,
WorkspaceMember,
ActivityTarget,
Pipeline,
PipelineStage,
PipelineProgress,
UserSettings,
View,
ViewField,
Favorite,
ViewFilter,
ViewSort,
Workspace,
WorkspaceMember,
} from '@prisma/client';
import { AbilityAction } from './ability.action';
type SubjectsAbility = Subjects<{
User: User;
Workspace: Workspace;
WorkspaceMember: WorkspaceMember;
Company: Company;
Person: Person;
RefreshToken: RefreshToken;
Activity: Activity;
Comment: Comment;
ActivityTarget: ActivityTarget;
Pipeline: Pipeline;
PipelineStage: PipelineStage;
PipelineProgress: PipelineProgress;
Attachment: Attachment;
Comment: Comment;
Company: Company;
Favorite: Favorite;
Person: Person;
Pipeline: Pipeline;
PipelineProgress: PipelineProgress;
PipelineStage: PipelineStage;
RefreshToken: RefreshToken;
User: User;
UserSettings: UserSettings;
View: View;
ViewField: ViewField;
Favorite: Favorite;
ViewFilter: ViewFilter;
ViewSort: ViewSort;
Workspace: Workspace;
WorkspaceMember: WorkspaceMember;
}>;
export type AppAbility = PureAbility<
@ -147,10 +149,11 @@ export class AbilityFactory {
can(AbilityAction.Create, 'ViewField', { workspaceId: workspace.id });
can(AbilityAction.Update, 'ViewField', { workspaceId: workspace.id });
//Favorite
can(AbilityAction.Read, 'Favorite', { workspaceId: workspace.id });
can(AbilityAction.Create, 'Favorite');
can(AbilityAction.Delete, 'Favorite', { workspaceId: workspace.id });
// ViewFilter
can(AbilityAction.Read, 'ViewFilter', { workspaceId: workspace.id });
can(AbilityAction.Create, 'ViewFilter', { workspaceId: workspace.id });
can(AbilityAction.Update, 'ViewFilter', { workspaceId: workspace.id });
can(AbilityAction.Delete, 'ViewFilter', { workspaceId: workspace.id });
// ViewSort
can(AbilityAction.Read, 'ViewSort', { workspaceId: workspace.id });
@ -158,6 +161,11 @@ export class AbilityFactory {
can(AbilityAction.Update, 'ViewSort', { workspaceId: workspace.id });
can(AbilityAction.Delete, 'ViewSort', { workspaceId: workspace.id });
// Favorite
can(AbilityAction.Read, 'Favorite', { workspaceId: workspace.id });
can(AbilityAction.Create, 'Favorite');
can(AbilityAction.Delete, 'Favorite', { workspaceId: workspace.id });
return build();
}
}

View File

@ -116,6 +116,12 @@ import {
UpdateViewAbilityHandler,
DeleteViewAbilityHandler,
} from './handlers/view.ability-handler';
import {
CreateViewFilterAbilityHandler,
DeleteViewFilterAbilityHandler,
ReadViewFilterAbilityHandler,
UpdateViewFilterAbilityHandler,
} from './handlers/view-filter.ability-handler';
@Global()
@Module({
@ -213,6 +219,11 @@ import {
ReadViewFieldAbilityHandler,
CreateViewFieldAbilityHandler,
UpdateViewFieldAbilityHandler,
// ViewFilter
ReadViewFilterAbilityHandler,
CreateViewFilterAbilityHandler,
UpdateViewFilterAbilityHandler,
DeleteViewFilterAbilityHandler,
// ViewSort
ReadViewSortAbilityHandler,
CreateViewSortAbilityHandler,
@ -312,6 +323,11 @@ import {
ReadViewFieldAbilityHandler,
CreateViewFieldAbilityHandler,
UpdateViewFieldAbilityHandler,
// ViewFilter
ReadViewFilterAbilityHandler,
CreateViewFilterAbilityHandler,
UpdateViewFilterAbilityHandler,
DeleteViewFilterAbilityHandler,
// ViewSort
ReadViewSortAbilityHandler,
CreateViewSortAbilityHandler,

View File

@ -0,0 +1,122 @@
import {
ExecutionContext,
Injectable,
NotFoundException,
} from '@nestjs/common';
import { GqlExecutionContext } from '@nestjs/graphql';
import { subject } from '@casl/ability';
import { IAbilityHandler } from 'src/ability/interfaces/ability-handler.interface';
import { AbilityAction } from 'src/ability/ability.action';
import { AppAbility } from 'src/ability/ability.factory';
import {
convertToWhereInput,
relationAbilityChecker,
} from 'src/ability/ability.util';
import { PrismaService } from 'src/database/prisma.service';
import { assert } from 'src/utils/assert';
import { ViewFilterWhereUniqueInput } from 'src/core/@generated/view-filter/view-filter-where-unique.input';
import { ViewFilterWhereInput } from 'src/core/@generated/view-filter/view-filter-where.input';
class ViewFilterArgs {
where?: ViewFilterWhereInput | ViewFilterWhereUniqueInput;
[key: string]: any;
}
const isViewFilterWhereUniqueInput = (
input: ViewFilterWhereInput | ViewFilterWhereUniqueInput,
): input is ViewFilterWhereUniqueInput => 'viewId_key' in input;
@Injectable()
export class ReadViewFilterAbilityHandler implements IAbilityHandler {
handle(ability: AppAbility) {
return ability.can(AbilityAction.Read, 'ViewFilter');
}
}
@Injectable()
export class CreateViewFilterAbilityHandler implements IAbilityHandler {
constructor(private readonly prismaService: PrismaService) {}
async handle(ability: AppAbility, context: ExecutionContext) {
const gqlContext = GqlExecutionContext.create(context);
const args = gqlContext.getArgs();
const allowed = await relationAbilityChecker(
'ViewFilter',
ability,
this.prismaService.client,
args,
);
if (!allowed) {
return false;
}
return ability.can(AbilityAction.Create, 'ViewFilter');
}
}
@Injectable()
export class UpdateViewFilterAbilityHandler implements IAbilityHandler {
constructor(private readonly prismaService: PrismaService) {}
async handle(ability: AppAbility, context: ExecutionContext) {
const gqlContext = GqlExecutionContext.create(context);
const args = gqlContext.getArgs<ViewFilterArgs>();
const viewFilter = await this.prismaService.client.viewFilter.findFirst({
where:
args.where && isViewFilterWhereUniqueInput(args.where)
? args.where.viewId_key
: args.where,
});
assert(viewFilter, '', NotFoundException);
const allowed = await relationAbilityChecker(
'ViewFilter',
ability,
this.prismaService.client,
args,
);
if (!allowed) {
return false;
}
return ability.can(AbilityAction.Update, subject('ViewFilter', viewFilter));
}
}
@Injectable()
export class DeleteViewFilterAbilityHandler implements IAbilityHandler {
constructor(private readonly prismaService: PrismaService) {}
async handle(ability: AppAbility, context: ExecutionContext) {
const gqlContext = GqlExecutionContext.create(context);
const args = gqlContext.getArgs<ViewFilterArgs>();
const where = convertToWhereInput(
args.where && isViewFilterWhereUniqueInput(args.where)
? args.where.viewId_key
: args.where,
);
const viewFilters = await this.prismaService.client.viewFilter.findMany({
where,
});
assert(viewFilters.length, '', NotFoundException);
for (const viewFilter of viewFilters) {
const allowed = ability.can(
AbilityAction.Delete,
subject('ViewFilter', viewFilter),
);
if (!allowed) {
return false;
}
}
return true;
}
}

View File

@ -0,0 +1,102 @@
import { UseGuards } from '@nestjs/common';
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
import { accessibleBy } from '@casl/prisma';
import { Prisma, Workspace } from '@prisma/client';
import { AppAbility } from 'src/ability/ability.factory';
import {
CreateViewFilterAbilityHandler,
DeleteViewFilterAbilityHandler,
ReadViewFilterAbilityHandler,
UpdateViewFilterAbilityHandler,
} from 'src/ability/handlers/view-filter.ability-handler';
import { FindManyViewFilterArgs } from 'src/core/@generated/view-filter/find-many-view-filter.args';
import { ViewFilter } from 'src/core/@generated/view-filter/view-filter.model';
import { ViewFilterService } from 'src/core/view/services/view-filter.service';
import { CheckAbilities } from 'src/decorators/check-abilities.decorator';
import {
PrismaSelect,
PrismaSelector,
} from 'src/decorators/prisma-select.decorator';
import { UserAbility } from 'src/decorators/user-ability.decorator';
import { AbilityGuard } from 'src/guards/ability.guard';
import { JwtAuthGuard } from 'src/guards/jwt.auth.guard';
import { UpdateOneViewFilterArgs } from 'src/core/@generated/view-filter/update-one-view-filter.args';
import { AuthWorkspace } from 'src/decorators/auth-workspace.decorator';
import { AffectedRows } from 'src/core/@generated/prisma/affected-rows.output';
import { DeleteManyViewFilterArgs } from 'src/core/@generated/view-filter/delete-many-view-filter.args';
import { CreateManyViewFilterArgs } from 'src/core/@generated/view-filter/create-many-view-filter.args';
@UseGuards(JwtAuthGuard)
@Resolver(() => ViewFilter)
export class ViewFilterResolver {
constructor(private readonly viewFilterService: ViewFilterService) {}
@Mutation(() => AffectedRows)
@UseGuards(AbilityGuard)
@CheckAbilities(CreateViewFilterAbilityHandler)
async createManyViewFilter(
@Args() args: CreateManyViewFilterArgs,
@AuthWorkspace() workspace: Workspace,
): Promise<AffectedRows> {
return this.viewFilterService.createMany({
data: args.data.map((data) => ({
...data,
workspaceId: workspace.id,
})),
});
}
@Query(() => [ViewFilter])
@UseGuards(AbilityGuard)
@CheckAbilities(ReadViewFilterAbilityHandler)
async findManyViewFilter(
@Args() args: FindManyViewFilterArgs,
@UserAbility() ability: AppAbility,
@PrismaSelector({ modelName: 'ViewFilter' })
prismaSelect: PrismaSelect<'ViewFilter'>,
): Promise<Partial<ViewFilter>[]> {
return this.viewFilterService.findMany({
where: args.where
? {
AND: [args.where, accessibleBy(ability).ViewFilter],
}
: accessibleBy(ability).ViewFilter,
orderBy: args.orderBy,
cursor: args.cursor,
take: args.take,
skip: args.skip,
distinct: args.distinct,
select: prismaSelect.value,
});
}
@Mutation(() => ViewFilter)
@UseGuards(AbilityGuard)
@CheckAbilities(UpdateViewFilterAbilityHandler)
async updateOneViewFilter(
@Args() args: UpdateOneViewFilterArgs,
@PrismaSelector({ modelName: 'ViewFilter' })
prismaSelect: PrismaSelect<'ViewFilter'>,
): Promise<Partial<ViewFilter>> {
return this.viewFilterService.update({
data: args.data,
where: args.where,
select: prismaSelect.value,
} as Prisma.ViewFilterUpdateArgs);
}
@Mutation(() => AffectedRows, {
nullable: false,
})
@UseGuards(AbilityGuard)
@CheckAbilities(DeleteViewFilterAbilityHandler)
async deleteManyViewFilter(
@Args() args: DeleteManyViewFilterArgs,
): Promise<AffectedRows> {
return this.viewFilterService.deleteMany({
where: args.where,
});
}
}

View File

@ -0,0 +1,39 @@
import { Injectable } from '@nestjs/common';
import { PrismaService } from 'src/database/prisma.service';
@Injectable()
export class ViewFilterService {
constructor(private readonly prismaService: PrismaService) {}
// Find
findFirst = this.prismaService.client.viewFilter.findFirst;
findFirstOrThrow = this.prismaService.client.viewFilter.findFirstOrThrow;
findUnique = this.prismaService.client.viewFilter.findUnique;
findUniqueOrThrow = this.prismaService.client.viewFilter.findUniqueOrThrow;
findMany = this.prismaService.client.viewFilter.findMany;
// Create
create = this.prismaService.client.viewFilter.create;
createMany = this.prismaService.client.viewFilter.createMany;
// Update
update = this.prismaService.client.viewFilter.update;
upsert = this.prismaService.client.viewFilter.upsert;
updateMany = this.prismaService.client.viewFilter.updateMany;
// Delete
delete = this.prismaService.client.viewFilter.delete;
deleteMany = this.prismaService.client.viewFilter.deleteMany;
// Aggregate
aggregate = this.prismaService.client.viewFilter.aggregate;
// Count
count = this.prismaService.client.viewFilter.count;
// GroupBy
groupBy = this.prismaService.client.viewFilter.groupBy;
}

View File

@ -6,14 +6,18 @@ import { ViewSortService } from './services/view-sort.service';
import { ViewSortResolver } from './resolvers/view-sort.resolver';
import { ViewService } from './services/view.service';
import { ViewResolver } from './resolvers/view.resolver';
import { ViewFilterService } from './services/view-filter.service';
import { ViewFilterResolver } from './resolvers/view-filter.resolver';
@Module({
providers: [
ViewService,
ViewFieldService,
ViewFilterService,
ViewSortService,
ViewResolver,
ViewFieldResolver,
ViewFilterResolver,
ViewSortResolver,
],
})

View File

@ -112,8 +112,6 @@ export class WorkspaceService {
activityTarget,
activity,
view,
viewField,
viewSort,
} = this.prismaService.client;
const activitys = await activity.findMany({
@ -156,12 +154,6 @@ export class WorkspaceService {
view.deleteMany({
where,
}),
viewField.deleteMany({
where,
}),
viewSort.deleteMany({
where,
}),
refreshToken.deleteMany({
where: { userId },
}),

View File

@ -0,0 +1,21 @@
-- CreateEnum
CREATE TYPE "ViewFilterOperand" AS ENUM ('Contains', 'DoesNotContain', 'GreaterThan', 'LessThan', 'Is', 'IsNot');
-- CreateTable
CREATE TABLE "viewFilters" (
"displayValue" TEXT NOT NULL,
"key" TEXT NOT NULL,
"name" TEXT NOT NULL,
"operand" "ViewFilterOperand" NOT NULL,
"value" TEXT NOT NULL,
"viewId" TEXT NOT NULL,
"workspaceId" TEXT NOT NULL,
CONSTRAINT "viewFilters_pkey" PRIMARY KEY ("viewId","key")
);
-- AddForeignKey
ALTER TABLE "viewFilters" ADD CONSTRAINT "viewFilters_viewId_fkey" FOREIGN KEY ("viewId") REFERENCES "views"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "viewFilters" ADD CONSTRAINT "viewFilters_workspaceId_fkey" FOREIGN KEY ("workspaceId") REFERENCES "workspaces"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@ -174,6 +174,7 @@ model Workspace {
pipelineProgresses PipelineProgress[]
activityTargets ActivityTarget[]
viewFields ViewField[]
viewFilters ViewFilter[]
views View[]
viewSorts ViewSort[]
@ -582,6 +583,7 @@ model View {
id String @id @default(uuid())
fields ViewField[]
filters ViewFilter[]
name String
objectId String
sorts ViewSort[]
@ -596,6 +598,34 @@ model View {
@@map("views")
}
enum ViewFilterOperand {
Contains
DoesNotContain
GreaterThan
LessThan
Is
IsNot
}
model ViewFilter {
displayValue String
key String
name String
operand ViewFilterOperand
value String
view View @relation(fields: [viewId], references: [id], onDelete: Cascade)
viewId String
/// @TypeGraphQL.omit(input: true, output: true)
workspace Workspace @relation(fields: [workspaceId], references: [id])
/// @TypeGraphQL.omit(input: true, output: true)
workspaceId String
@@id([viewId, key])
@@map("viewFilters")
}
enum ViewSortDirection {
asc
desc

View File

@ -18,6 +18,7 @@ export type ModelSelectMap = {
Attachment: Prisma.AttachmentSelect;
Favorite: Prisma.FavoriteSelect;
View: Prisma.ViewSelect;
ViewFilter: Prisma.ViewFilterSelect;
ViewSort: Prisma.ViewSortSelect;
ViewField: Prisma.ViewFieldSelect;
};