Merge commit '80a562d90d1d354c580351a2c94d32aa024b139e' into context-menu-vertical

This commit is contained in:
brendanlaschke 2023-08-10 20:27:05 +02:00
commit 807506549a
53 changed files with 1561 additions and 277 deletions

View File

@ -22,7 +22,7 @@ Upon restart, a powershell window will open and install Ubuntu. This may take a
You will be prompted to create a username and password for your Ubuntu installation.
<div style={{textAlign: 'center'}}>
<img src="/img/developer/ide-start-dev-container.png" alt="Visual Studio Code: Open in container" width="90%" />
<img src="/img/developer/wsl-complete.png" alt="Visual Studio Code: Open in container" width="90%" />
</div>
## Setup your developer environment

View File

@ -846,6 +846,20 @@ export type EnumPipelineProgressableTypeFilter = {
notIn?: InputMaybe<Array<PipelineProgressableType>>;
};
export type EnumViewSortDirectionFilter = {
equals?: InputMaybe<ViewSortDirection>;
in?: InputMaybe<Array<ViewSortDirection>>;
not?: InputMaybe<NestedEnumViewSortDirectionFilter>;
notIn?: InputMaybe<Array<ViewSortDirection>>;
};
export type EnumViewTypeFilter = {
equals?: InputMaybe<ViewType>;
in?: InputMaybe<Array<ViewType>>;
not?: InputMaybe<NestedEnumViewTypeFilter>;
notIn?: InputMaybe<Array<ViewType>>;
};
export enum FileFolder {
Attachment = 'Attachment',
PersonPicture = 'PersonPicture',
@ -902,6 +916,7 @@ export type Mutation = {
challenge: LoginToken;
createEvent: Analytics;
createManyViewField: AffectedRows;
createManyViewSort: AffectedRows;
createOneActivity: Activity;
createOneComment: Comment;
createOneCompany: Company;
@ -913,6 +928,7 @@ export type Mutation = {
deleteManyCompany: AffectedRows;
deleteManyPerson: AffectedRows;
deleteManyPipelineProgress: AffectedRows;
deleteManyViewSort: AffectedRows;
deleteUserAccount: User;
deleteWorkspaceMember: WorkspaceMember;
impersonate: Verify;
@ -924,6 +940,7 @@ export type Mutation = {
updateOnePipelineProgress?: Maybe<PipelineProgress>;
updateOnePipelineStage?: Maybe<PipelineStage>;
updateOneViewField: ViewField;
updateOneViewSort: ViewSort;
updateUser: User;
updateWorkspace: Workspace;
uploadAttachment: Scalars['String'];
@ -959,6 +976,12 @@ export type MutationCreateManyViewFieldArgs = {
};
export type MutationCreateManyViewSortArgs = {
data: Array<ViewSortCreateManyInput>;
skipDuplicates?: InputMaybe<Scalars['Boolean']>;
};
export type MutationCreateOneActivityArgs = {
data: ActivityCreateInput;
};
@ -1009,6 +1032,11 @@ export type MutationDeleteManyPipelineProgressArgs = {
};
export type MutationDeleteManyViewSortArgs = {
where?: InputMaybe<ViewSortWhereInput>;
};
export type MutationDeleteWorkspaceMemberArgs = {
where: WorkspaceMemberWhereUniqueInput;
};
@ -1067,6 +1095,12 @@ export type MutationUpdateOneViewFieldArgs = {
};
export type MutationUpdateOneViewSortArgs = {
data: ViewSortUpdateInput;
where: ViewSortWhereUniqueInput;
};
export type MutationUpdateUserArgs = {
data: UserUpdateInput;
where: UserWhereUniqueInput;
@ -1178,6 +1212,20 @@ export type NestedEnumPipelineProgressableTypeFilter = {
notIn?: InputMaybe<Array<PipelineProgressableType>>;
};
export type NestedEnumViewSortDirectionFilter = {
equals?: InputMaybe<ViewSortDirection>;
in?: InputMaybe<Array<ViewSortDirection>>;
not?: InputMaybe<NestedEnumViewSortDirectionFilter>;
notIn?: InputMaybe<Array<ViewSortDirection>>;
};
export type NestedEnumViewTypeFilter = {
equals?: InputMaybe<ViewType>;
in?: InputMaybe<Array<ViewType>>;
not?: InputMaybe<NestedEnumViewTypeFilter>;
notIn?: InputMaybe<Array<ViewType>>;
};
export type NestedIntFilter = {
equals?: InputMaybe<Scalars['Int']>;
gt?: InputMaybe<Scalars['Int']>;
@ -1766,6 +1814,7 @@ export type Query = {
findManyPipelineStage: Array<PipelineStage>;
findManyUser: Array<User>;
findManyViewField: Array<ViewField>;
findManyViewSort: Array<ViewSort>;
findManyWorkspaceMember: Array<WorkspaceMember>;
findUniqueCompany: Company;
findUniquePerson: Person;
@ -1863,6 +1912,16 @@ export type QueryFindManyViewFieldArgs = {
};
export type QueryFindManyViewSortArgs = {
cursor?: InputMaybe<ViewSortWhereUniqueInput>;
distinct?: InputMaybe<Array<ViewSortScalarFieldEnum>>;
orderBy?: InputMaybe<Array<ViewSortOrderByWithRelationInput>>;
skip?: InputMaybe<Scalars['Int']>;
take?: InputMaybe<Scalars['Int']>;
where?: InputMaybe<ViewSortWhereInput>;
};
export type QueryFindManyWorkspaceMemberArgs = {
cursor?: InputMaybe<WorkspaceMemberWhereUniqueInput>;
distinct?: InputMaybe<Array<WorkspaceMemberScalarFieldEnum>>;
@ -2161,6 +2220,20 @@ export type Verify = {
user: User;
};
export type View = {
__typename?: 'View';
fields?: Maybe<Array<ViewField>>;
id: Scalars['ID'];
name: Scalars['String'];
objectId: Scalars['String'];
sorts?: Maybe<Array<ViewSort>>;
type: ViewType;
};
export type ViewCreateNestedOneWithoutFieldsInput = {
connect?: InputMaybe<ViewWhereUniqueInput>;
};
export type ViewField = {
__typename?: 'ViewField';
fieldName: Scalars['String'];
@ -2169,6 +2242,8 @@ export type ViewField = {
isVisible: Scalars['Boolean'];
objectName: Scalars['String'];
sizeInPx: Scalars['Int'];
view?: Maybe<View>;
viewId?: Maybe<Scalars['String']>;
};
export type ViewFieldCreateInput = {
@ -2178,6 +2253,7 @@ export type ViewFieldCreateInput = {
isVisible: Scalars['Boolean'];
objectName: Scalars['String'];
sizeInPx: Scalars['Int'];
view?: InputMaybe<ViewCreateNestedOneWithoutFieldsInput>;
};
export type ViewFieldCreateManyInput = {
@ -2187,6 +2263,17 @@ export type ViewFieldCreateManyInput = {
isVisible: Scalars['Boolean'];
objectName: Scalars['String'];
sizeInPx: Scalars['Int'];
viewId?: InputMaybe<Scalars['String']>;
};
export type ViewFieldListRelationFilter = {
every?: InputMaybe<ViewFieldWhereInput>;
none?: InputMaybe<ViewFieldWhereInput>;
some?: InputMaybe<ViewFieldWhereInput>;
};
export type ViewFieldOrderByRelationAggregateInput = {
_count?: InputMaybe<SortOrder>;
};
export type ViewFieldOrderByWithRelationInput = {
@ -2196,6 +2283,8 @@ export type ViewFieldOrderByWithRelationInput = {
isVisible?: InputMaybe<SortOrder>;
objectName?: InputMaybe<SortOrder>;
sizeInPx?: InputMaybe<SortOrder>;
view?: InputMaybe<ViewOrderByWithRelationInput>;
viewId?: InputMaybe<SortOrder>;
};
export enum ViewFieldScalarFieldEnum {
@ -2205,6 +2294,7 @@ export enum ViewFieldScalarFieldEnum {
IsVisible = 'isVisible',
ObjectName = 'objectName',
SizeInPx = 'sizeInPx',
ViewId = 'viewId',
WorkspaceId = 'workspaceId'
}
@ -2215,6 +2305,7 @@ export type ViewFieldUpdateInput = {
isVisible?: InputMaybe<Scalars['Boolean']>;
objectName?: InputMaybe<Scalars['String']>;
sizeInPx?: InputMaybe<Scalars['Int']>;
view?: InputMaybe<ViewUpdateOneWithoutFieldsNestedInput>;
};
export type ViewFieldUpdateManyWithoutWorkspaceNestedInput = {
@ -2233,10 +2324,156 @@ export type ViewFieldWhereInput = {
isVisible?: InputMaybe<BoolFilter>;
objectName?: InputMaybe<StringFilter>;
sizeInPx?: InputMaybe<IntFilter>;
view?: InputMaybe<ViewRelationFilter>;
viewId?: InputMaybe<StringNullableFilter>;
};
export type ViewFieldWhereUniqueInput = {
id?: InputMaybe<Scalars['String']>;
workspaceId_viewId_objectName_fieldName?: InputMaybe<ViewFieldWorkspaceIdViewIdObjectNameFieldNameCompoundUniqueInput>;
};
export type ViewFieldWorkspaceIdViewIdObjectNameFieldNameCompoundUniqueInput = {
fieldName: Scalars['String'];
objectName: Scalars['String'];
viewId: Scalars['String'];
};
export type ViewOrderByWithRelationInput = {
fields?: InputMaybe<ViewFieldOrderByRelationAggregateInput>;
id?: InputMaybe<SortOrder>;
name?: InputMaybe<SortOrder>;
objectId?: InputMaybe<SortOrder>;
sorts?: InputMaybe<ViewSortOrderByRelationAggregateInput>;
type?: InputMaybe<SortOrder>;
};
export type ViewRelationFilter = {
is?: InputMaybe<ViewWhereInput>;
isNot?: InputMaybe<ViewWhereInput>;
};
export type ViewSort = {
__typename?: 'ViewSort';
direction: ViewSortDirection;
key: Scalars['String'];
name: Scalars['String'];
view: View;
viewId: Scalars['String'];
};
export type ViewSortCreateManyInput = {
direction: ViewSortDirection;
key: Scalars['String'];
name: Scalars['String'];
viewId: Scalars['String'];
};
export enum ViewSortDirection {
Asc = 'asc',
Desc = 'desc'
}
export type ViewSortListRelationFilter = {
every?: InputMaybe<ViewSortWhereInput>;
none?: InputMaybe<ViewSortWhereInput>;
some?: InputMaybe<ViewSortWhereInput>;
};
export type ViewSortOrderByRelationAggregateInput = {
_count?: InputMaybe<SortOrder>;
};
export type ViewSortOrderByWithRelationInput = {
direction?: InputMaybe<SortOrder>;
key?: InputMaybe<SortOrder>;
name?: InputMaybe<SortOrder>;
view?: InputMaybe<ViewOrderByWithRelationInput>;
viewId?: InputMaybe<SortOrder>;
};
export enum ViewSortScalarFieldEnum {
Direction = 'direction',
Key = 'key',
Name = 'name',
ViewId = 'viewId',
WorkspaceId = 'workspaceId'
}
export type ViewSortUpdateInput = {
direction?: InputMaybe<ViewSortDirection>;
key?: InputMaybe<Scalars['String']>;
name?: InputMaybe<Scalars['String']>;
view?: InputMaybe<ViewUpdateOneRequiredWithoutSortsNestedInput>;
};
export type ViewSortUpdateManyWithoutWorkspaceNestedInput = {
connect?: InputMaybe<Array<ViewSortWhereUniqueInput>>;
disconnect?: InputMaybe<Array<ViewSortWhereUniqueInput>>;
set?: InputMaybe<Array<ViewSortWhereUniqueInput>>;
};
export type ViewSortViewIdKeyCompoundUniqueInput = {
key: Scalars['String'];
viewId: Scalars['String'];
};
export type ViewSortWhereInput = {
AND?: InputMaybe<Array<ViewSortWhereInput>>;
NOT?: InputMaybe<Array<ViewSortWhereInput>>;
OR?: InputMaybe<Array<ViewSortWhereInput>>;
direction?: InputMaybe<EnumViewSortDirectionFilter>;
key?: InputMaybe<StringFilter>;
name?: InputMaybe<StringFilter>;
view?: InputMaybe<ViewRelationFilter>;
viewId?: InputMaybe<StringFilter>;
};
export type ViewSortWhereUniqueInput = {
viewId_key?: InputMaybe<ViewSortViewIdKeyCompoundUniqueInput>;
};
export enum ViewType {
Pipeline = 'Pipeline',
Table = 'Table'
}
export type ViewUpdateManyWithoutWorkspaceNestedInput = {
connect?: InputMaybe<Array<ViewWhereUniqueInput>>;
disconnect?: InputMaybe<Array<ViewWhereUniqueInput>>;
set?: InputMaybe<Array<ViewWhereUniqueInput>>;
};
export type ViewUpdateOneRequiredWithoutSortsNestedInput = {
connect?: InputMaybe<ViewWhereUniqueInput>;
};
export type ViewUpdateOneWithoutFieldsNestedInput = {
connect?: InputMaybe<ViewWhereUniqueInput>;
disconnect?: InputMaybe<Scalars['Boolean']>;
};
export type ViewWhereInput = {
AND?: InputMaybe<Array<ViewWhereInput>>;
NOT?: InputMaybe<Array<ViewWhereInput>>;
OR?: InputMaybe<Array<ViewWhereInput>>;
fields?: InputMaybe<ViewFieldListRelationFilter>;
id?: InputMaybe<StringFilter>;
name?: InputMaybe<StringFilter>;
objectId?: InputMaybe<StringFilter>;
sorts?: InputMaybe<ViewSortListRelationFilter>;
type?: InputMaybe<EnumViewTypeFilter>;
};
export type ViewWhereUniqueInput = {
id?: InputMaybe<Scalars['String']>;
workspaceId_type_objectId_name?: InputMaybe<ViewWorkspaceIdTypeObjectIdNameCompoundUniqueInput>;
};
export type ViewWorkspaceIdTypeObjectIdNameCompoundUniqueInput = {
name: Scalars['String'];
objectId: Scalars['String'];
type: ViewType;
};
export type Workspace = {
@ -2258,6 +2495,8 @@ export type Workspace = {
pipelines?: Maybe<Array<Pipeline>>;
updatedAt: Scalars['DateTime'];
viewFields?: Maybe<Array<ViewField>>;
viewSorts?: Maybe<Array<ViewSort>>;
views?: Maybe<Array<View>>;
workspaceMember?: Maybe<Array<WorkspaceMember>>;
};
@ -2337,6 +2576,8 @@ export type WorkspaceUpdateInput = {
pipelines?: InputMaybe<PipelineUpdateManyWithoutWorkspaceNestedInput>;
updatedAt?: InputMaybe<Scalars['DateTime']>;
viewFields?: InputMaybe<ViewFieldUpdateManyWithoutWorkspaceNestedInput>;
viewSorts?: InputMaybe<ViewSortUpdateManyWithoutWorkspaceNestedInput>;
views?: InputMaybe<ViewUpdateManyWithoutWorkspaceNestedInput>;
workspaceMember?: InputMaybe<WorkspaceMemberUpdateManyWithoutWorkspaceNestedInput>;
};
@ -2503,12 +2744,14 @@ export type UpdateOneCompanyMutationVariables = Exact<{
export type UpdateOneCompanyMutation = { __typename?: 'Mutation', updateOneCompany?: { __typename?: 'Company', address: string, createdAt: string, domainName: string, employees?: number | null, linkedinUrl?: string | null, id: string, name: string, accountOwner?: { __typename?: 'User', id: string, email: string, displayName: string, firstName?: string | null, lastName?: string | null } | null } | null };
export type InsertCompanyFragmentFragment = { __typename?: 'Company', domainName: string, address: string, id: string, name: string, createdAt: string };
export type InsertOneCompanyMutationVariables = Exact<{
data: CompanyCreateInput;
}>;
export type InsertOneCompanyMutation = { __typename?: 'Mutation', createOneCompany: { __typename?: 'Company', address: string, createdAt: string, domainName: string, linkedinUrl?: string | null, employees?: number | null, id: string, name: string } };
export type InsertOneCompanyMutation = { __typename?: 'Mutation', createOneCompany: { __typename?: 'Company', domainName: string, address: string, id: string, name: string, createdAt: string } };
export type DeleteManyCompaniesMutationVariables = Exact<{
ids?: InputMaybe<Array<Scalars['String']> | Scalars['String']>;
@ -2590,12 +2833,14 @@ export type UpdateOnePersonMutationVariables = Exact<{
export type UpdateOnePersonMutation = { __typename?: 'Mutation', updateOnePerson?: { __typename?: 'Person', id: string, city?: string | null, email?: string | null, jobTitle?: string | null, linkedinUrl?: string | null, xUrl?: string | null, firstName?: string | null, lastName?: string | null, displayName: string, phone?: string | null, createdAt: string, company?: { __typename?: 'Company', domainName: string, name: string, id: string } | null } | null };
export type InsertPersonFragmentFragment = { __typename?: 'Person', id: string, firstName?: string | null, lastName?: string | null, displayName: string, createdAt: string };
export type InsertOnePersonMutationVariables = Exact<{
data: PersonCreateInput;
}>;
export type InsertOnePersonMutation = { __typename?: 'Mutation', createOnePerson: { __typename?: 'Person', id: string, city?: string | null, email?: string | null, firstName?: string | null, lastName?: string | null, jobTitle?: string | null, linkedinUrl?: string | null, xUrl?: string | null, displayName: string, phone?: string | null, createdAt: string, company?: { __typename?: 'Company', domainName: string, name: string, id: string } | null } };
export type InsertOnePersonMutation = { __typename?: 'Mutation', createOnePerson: { __typename?: 'Person', id: string, firstName?: string | null, lastName?: string | null, displayName: string, createdAt: string } };
export type DeleteManyPersonMutationVariables = Exact<{
ids?: InputMaybe<Array<Scalars['String']> | Scalars['String']>;
@ -2774,6 +3019,20 @@ export type CreateViewFieldsMutationVariables = Exact<{
export type CreateViewFieldsMutation = { __typename?: 'Mutation', createManyViewField: { __typename?: 'AffectedRows', count: number } };
export type CreateViewSortsMutationVariables = Exact<{
data: Array<ViewSortCreateManyInput> | ViewSortCreateManyInput;
}>;
export type CreateViewSortsMutation = { __typename?: 'Mutation', createManyViewSort: { __typename?: 'AffectedRows', count: number } };
export type DeleteViewSortsMutationVariables = Exact<{
where: ViewSortWhereInput;
}>;
export type DeleteViewSortsMutation = { __typename?: 'Mutation', deleteManyViewSort: { __typename?: 'AffectedRows', count: number } };
export type GetViewFieldsQueryVariables = Exact<{
where?: InputMaybe<ViewFieldWhereInput>;
orderBy?: InputMaybe<Array<ViewFieldOrderByWithRelationInput> | ViewFieldOrderByWithRelationInput>;
@ -2782,6 +3041,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 GetViewSortsQueryVariables = Exact<{
where?: InputMaybe<ViewSortWhereInput>;
}>;
export type GetViewSortsQuery = { __typename?: 'Query', viewSorts: Array<{ __typename?: 'ViewSort', direction: ViewSortDirection, key: string, name: string }> };
export type UpdateViewFieldMutationVariables = Exact<{
data: ViewFieldUpdateInput;
where: ViewFieldWhereUniqueInput;
@ -2790,6 +3056,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 UpdateViewSortMutationVariables = Exact<{
data: ViewSortUpdateInput;
where: ViewSortWhereUniqueInput;
}>;
export type UpdateViewSortMutation = { __typename?: 'Mutation', viewSort: { __typename?: 'ViewSort', direction: ViewSortDirection, key: string, name: string } };
export type GetWorkspaceMembersQueryVariables = Exact<{ [key: string]: never; }>;
@ -2849,6 +3123,24 @@ export const ActivityUpdatePartsFragmentDoc = gql`
}
}
`;
export const InsertCompanyFragmentFragmentDoc = gql`
fragment InsertCompanyFragment on Company {
domainName
address
id
name
createdAt
}
`;
export const InsertPersonFragmentFragmentDoc = gql`
fragment InsertPersonFragment on Person {
id
firstName
lastName
displayName
createdAt
}
`;
export const CreateCommentDocument = gql`
mutation CreateComment($commentId: String!, $commentText: String!, $authorId: String!, $activityId: String!, $createdAt: DateTime!) {
createOneComment(
@ -3886,16 +4178,10 @@ export type UpdateOneCompanyMutationOptions = Apollo.BaseMutationOptions<UpdateO
export const InsertOneCompanyDocument = gql`
mutation InsertOneCompany($data: CompanyCreateInput!) {
createOneCompany(data: $data) {
address
createdAt
domainName
linkedinUrl
employees
id
name
...InsertCompanyFragment
}
}
`;
${InsertCompanyFragmentFragmentDoc}`;
export type InsertOneCompanyMutationFn = Apollo.MutationFunction<InsertOneCompanyMutation, InsertOneCompanyMutationVariables>;
/**
@ -4372,25 +4658,10 @@ export type UpdateOnePersonMutationOptions = Apollo.BaseMutationOptions<UpdateOn
export const InsertOnePersonDocument = gql`
mutation InsertOnePerson($data: PersonCreateInput!) {
createOnePerson(data: $data) {
id
city
company {
domainName
name
id
}
email
firstName
lastName
jobTitle
linkedinUrl
xUrl
displayName
phone
createdAt
...InsertPersonFragment
}
}
`;
${InsertPersonFragmentFragmentDoc}`;
export type InsertOnePersonMutationFn = Apollo.MutationFunction<InsertOnePersonMutation, InsertOnePersonMutationVariables>;
/**
@ -5346,6 +5617,72 @@ 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 CreateViewSortsDocument = gql`
mutation CreateViewSorts($data: [ViewSortCreateManyInput!]!) {
createManyViewSort(data: $data) {
count
}
}
`;
export type CreateViewSortsMutationFn = Apollo.MutationFunction<CreateViewSortsMutation, CreateViewSortsMutationVariables>;
/**
* __useCreateViewSortsMutation__
*
* To run a mutation, you first call `useCreateViewSortsMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useCreateViewSortsMutation` 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 [createViewSortsMutation, { data, loading, error }] = useCreateViewSortsMutation({
* variables: {
* data: // value for 'data'
* },
* });
*/
export function useCreateViewSortsMutation(baseOptions?: Apollo.MutationHookOptions<CreateViewSortsMutation, CreateViewSortsMutationVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useMutation<CreateViewSortsMutation, CreateViewSortsMutationVariables>(CreateViewSortsDocument, options);
}
export type CreateViewSortsMutationHookResult = ReturnType<typeof useCreateViewSortsMutation>;
export type CreateViewSortsMutationResult = Apollo.MutationResult<CreateViewSortsMutation>;
export type CreateViewSortsMutationOptions = Apollo.BaseMutationOptions<CreateViewSortsMutation, CreateViewSortsMutationVariables>;
export const DeleteViewSortsDocument = gql`
mutation DeleteViewSorts($where: ViewSortWhereInput!) {
deleteManyViewSort(where: $where) {
count
}
}
`;
export type DeleteViewSortsMutationFn = Apollo.MutationFunction<DeleteViewSortsMutation, DeleteViewSortsMutationVariables>;
/**
* __useDeleteViewSortsMutation__
*
* To run a mutation, you first call `useDeleteViewSortsMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useDeleteViewSortsMutation` 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 [deleteViewSortsMutation, { data, loading, error }] = useDeleteViewSortsMutation({
* variables: {
* where: // value for 'where'
* },
* });
*/
export function useDeleteViewSortsMutation(baseOptions?: Apollo.MutationHookOptions<DeleteViewSortsMutation, DeleteViewSortsMutationVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useMutation<DeleteViewSortsMutation, DeleteViewSortsMutationVariables>(DeleteViewSortsDocument, options);
}
export type DeleteViewSortsMutationHookResult = ReturnType<typeof useDeleteViewSortsMutation>;
export type DeleteViewSortsMutationResult = Apollo.MutationResult<DeleteViewSortsMutation>;
export type DeleteViewSortsMutationOptions = Apollo.BaseMutationOptions<DeleteViewSortsMutation, DeleteViewSortsMutationVariables>;
export const GetViewFieldsDocument = gql`
query GetViewFields($where: ViewFieldWhereInput, $orderBy: [ViewFieldOrderByWithRelationInput!]) {
viewFields: findManyViewField(where: $where, orderBy: $orderBy) {
@ -5386,6 +5723,43 @@ 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 GetViewSortsDocument = gql`
query GetViewSorts($where: ViewSortWhereInput) {
viewSorts: findManyViewSort(where: $where) {
direction
key
name
}
}
`;
/**
* __useGetViewSortsQuery__
*
* To run a query within a React component, call `useGetViewSortsQuery` and pass it any options that fit your needs.
* When your component renders, `useGetViewSortsQuery` 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 } = useGetViewSortsQuery({
* variables: {
* where: // value for 'where'
* },
* });
*/
export function useGetViewSortsQuery(baseOptions?: Apollo.QueryHookOptions<GetViewSortsQuery, GetViewSortsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<GetViewSortsQuery, GetViewSortsQueryVariables>(GetViewSortsDocument, options);
}
export function useGetViewSortsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<GetViewSortsQuery, GetViewSortsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<GetViewSortsQuery, GetViewSortsQueryVariables>(GetViewSortsDocument, options);
}
export type GetViewSortsQueryHookResult = ReturnType<typeof useGetViewSortsQuery>;
export type GetViewSortsLazyQueryHookResult = ReturnType<typeof useGetViewSortsLazyQuery>;
export type GetViewSortsQueryResult = Apollo.QueryResult<GetViewSortsQuery, GetViewSortsQueryVariables>;
export const UpdateViewFieldDocument = gql`
mutation UpdateViewField($data: ViewFieldUpdateInput!, $where: ViewFieldWhereUniqueInput!) {
updateOneViewField(data: $data, where: $where) {
@ -5424,6 +5798,42 @@ 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 UpdateViewSortDocument = gql`
mutation UpdateViewSort($data: ViewSortUpdateInput!, $where: ViewSortWhereUniqueInput!) {
viewSort: updateOneViewSort(data: $data, where: $where) {
direction
key
name
}
}
`;
export type UpdateViewSortMutationFn = Apollo.MutationFunction<UpdateViewSortMutation, UpdateViewSortMutationVariables>;
/**
* __useUpdateViewSortMutation__
*
* To run a mutation, you first call `useUpdateViewSortMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useUpdateViewSortMutation` 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 [updateViewSortMutation, { data, loading, error }] = useUpdateViewSortMutation({
* variables: {
* data: // value for 'data'
* where: // value for 'where'
* },
* });
*/
export function useUpdateViewSortMutation(baseOptions?: Apollo.MutationHookOptions<UpdateViewSortMutation, UpdateViewSortMutationVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useMutation<UpdateViewSortMutation, UpdateViewSortMutationVariables>(UpdateViewSortDocument, options);
}
export type UpdateViewSortMutationHookResult = ReturnType<typeof useUpdateViewSortMutation>;
export type UpdateViewSortMutationResult = Apollo.MutationResult<UpdateViewSortMutation>;
export type UpdateViewSortMutationOptions = Apollo.BaseMutationOptions<UpdateViewSortMutation, UpdateViewSortMutationVariables>;
export const GetWorkspaceMembersDocument = gql`
query GetWorkspaceMembers {
workspaceMembers: findManyWorkspaceMember {

View File

@ -37,7 +37,7 @@ export function useOpenCreateActivityDrawer() {
return function openCreateActivityDrawer(
type: ActivityType,
entity?: CommentableEntity,
entities?: CommentableEntity[],
) {
const now = new Date().toISOString();
@ -52,23 +52,19 @@ export function useOpenCreateActivityDrawer() {
type: type,
activityTargets: {
createMany: {
data: entity
? [
{
commentableId: entity.id,
commentableType: entity.type,
companyId:
entity.type === CommentableType.Company
? entity.id
: null,
personId:
entity.type === CommentableType.Person
? entity.id
: null,
id: v4(),
createdAt: now,
},
]
data: entities
? entities.map((entity) => ({
commentableId: entity.id,
commentableType: entity.type,
companyId:
entity.type === CommentableType.Company
? entity.id
: null,
personId:
entity.type === CommentableType.Person ? entity.id : null,
id: v4(),
createdAt: now,
}))
: [],
skipDuplicates: true,
},
@ -85,7 +81,7 @@ export function useOpenCreateActivityDrawer() {
onCompleted(data) {
setHotkeyScope(RightDrawerHotkeyScope.RightDrawer, { goto: false });
setViewableActivityId(data.createOneActivity.id);
setCommentableEntityArray(entity ? [entity] : []);
setCommentableEntityArray(entities ?? []);
openRightDrawer(RightDrawerPages.CreateActivity);
},
});

View File

@ -1,41 +1,19 @@
import { getOperationName } from '@apollo/client/utilities/graphql/getFromAST';
import { useRecoilState, useRecoilValue } from 'recoil';
import { v4 } from 'uuid';
import { useRecoilValue } from 'recoil';
import { currentUserState } from '@/auth/states/currentUserState';
import { GET_COMPANIES } from '@/companies/queries';
import { GET_PEOPLE } from '@/people/queries';
import { useRightDrawer } from '@/ui/right-drawer/hooks/useRightDrawer';
import { RightDrawerHotkeyScope } from '@/ui/right-drawer/types/RightDrawerHotkeyScope';
import { RightDrawerPages } from '@/ui/right-drawer/types/RightDrawerPages';
import { selectedRowIdsSelector } from '@/ui/table/states/selectedRowIdsSelector';
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
import {
ActivityType,
CommentableType,
useCreateActivityMutation,
} from '~/generated/graphql';
import { ActivityType, CommentableType } from '~/generated/graphql';
import { GET_ACTIVITIES_BY_TARGETS, GET_ACTIVITY } from '../queries';
import { commentableEntityArrayState } from '../states/commentableEntityArrayState';
import { viewableActivityIdState } from '../states/viewableActivityIdState';
import { CommentableEntity } from '../types/CommentableEntity';
import { useOpenCreateActivityDrawer } from './useOpenCreateActivityDrawer';
export function useOpenCreateActivityDrawerForSelectedRowIds() {
const { openRightDrawer } = useRightDrawer();
const [createActivityMutation] = useCreateActivityMutation();
const currentUser = useRecoilValue(currentUserState);
const [, setViewableActivityId] = useRecoilState(viewableActivityIdState);
const setHotkeyScope = useSetHotkeyScope();
const [, setCommentableEntityArray] = useRecoilState(
commentableEntityArrayState,
);
const selectedEntityIds = useRecoilValue(selectedRowIdsSelector);
const openCreateActivityDrawer = useOpenCreateActivityDrawer();
return function openCreateCommentDrawerForSelectedRowIds(
type: ActivityType,
entityType: CommentableType,
) {
const commentableEntityArray: CommentableEntity[] = selectedEntityIds.map(
@ -44,45 +22,6 @@ export function useOpenCreateActivityDrawerForSelectedRowIds() {
id,
}),
);
const now = new Date().toISOString();
createActivityMutation({
variables: {
data: {
id: v4(),
createdAt: now,
updatedAt: now,
author: { connect: { id: currentUser?.id ?? '' } },
type: ActivityType.Note,
activityTargets: {
createMany: {
data: commentableEntityArray.map((entity) => ({
commentableId: entity.id,
commentableType: entity.type,
id: v4(),
createdAt: new Date().toISOString(),
companyId:
entity.type === CommentableType.Company ? entity.id : null,
personId:
entity.type === CommentableType.Person ? entity.id : null,
})),
skipDuplicates: true,
},
},
},
},
refetchQueries: [
getOperationName(GET_COMPANIES) ?? '',
getOperationName(GET_PEOPLE) ?? '',
getOperationName(GET_ACTIVITY) ?? '',
getOperationName(GET_ACTIVITIES_BY_TARGETS) ?? '',
],
onCompleted(data) {
setHotkeyScope(RightDrawerHotkeyScope.RightDrawer, { goto: false });
setViewableActivityId(data.createOneActivity.id);
setCommentableEntityArray(commentableEntityArray);
openRightDrawer(RightDrawerPages.CreateActivity);
},
});
openCreateActivityDrawer(type, commentableEntityArray);
};
}

View File

@ -121,8 +121,8 @@ export function Timeline({ entity }: { entity: CommentableEntity }) {
<StyledEmptyTimelineTitle>No activity yet</StyledEmptyTimelineTitle>
<StyledEmptyTimelineSubTitle>Create one:</StyledEmptyTimelineSubTitle>
<ActivityCreateButton
onNoteClick={() => openCreateActivity(ActivityType.Note, entity)}
onTaskClick={() => openCreateActivity(ActivityType.Task, entity)}
onNoteClick={() => openCreateActivity(ActivityType.Note, [entity])}
onTaskClick={() => openCreateActivity(ActivityType.Task, [entity])}
/>
</StyledTimelineEmptyContainer>
);
@ -132,8 +132,8 @@ export function Timeline({ entity }: { entity: CommentableEntity }) {
<StyledMainContainer>
<StyledTopActionBar>
<ActivityCreateButton
onNoteClick={() => openCreateActivity(ActivityType.Note, entity)}
onTaskClick={() => openCreateActivity(ActivityType.Task, entity)}
onNoteClick={() => openCreateActivity(ActivityType.Note, [entity])}
onTaskClick={() => openCreateActivity(ActivityType.Task, [entity])}
/>
</StyledTopActionBar>
<StyledTimelineContainer>

View File

@ -5,12 +5,11 @@ import { CompaniesSelectedSortType } from '../select';
describe('reduceSortsToOrderBy', () => {
it('should return an array of objects with the id as key and the order as value', () => {
const sorts = [
{ key: 'name', label: 'name', order: 'asc', _type: 'default_sort' },
{ key: 'name', label: 'name', order: 'asc' },
{
key: 'domainName',
label: 'domainName',
order: 'desc',
_type: 'default_sort',
},
] satisfies CompaniesSelectedSortType[];
const result = reduceSortsToOrderBy(sorts);

View File

@ -24,16 +24,20 @@ export const UPDATE_ONE_COMPANY = gql`
}
`;
export const INSERT_COMPANY_FRAGMENT = gql`
fragment InsertCompanyFragment on Company {
domainName
address
id
name
createdAt
}
`;
export const INSERT_ONE_COMPANY = gql`
mutation InsertOneCompany($data: CompanyCreateInput!) {
createOneCompany(data: $data) {
address
createdAt
domainName
linkedinUrl
employees
id
name
...InsertCompanyFragment
}
}
`;

View File

@ -1,30 +1,33 @@
import { useCallback, useMemo, useState } from 'react';
import { useMemo } from 'react';
import { useRecoilValue } from 'recoil';
import { companyViewFields } from '@/companies/constants/companyViewFields';
import { CompaniesSelectedSortType, defaultOrderBy } from '@/companies/queries';
import { reduceSortsToOrderBy } from '@/ui/filter-n-sort/helpers';
import { filtersScopedState } from '@/ui/filter-n-sort/states/filtersScopedState';
import { sortsOrderByScopedState } from '@/ui/filter-n-sort/states/sortScopedState';
import { turnFilterIntoWhereClause } from '@/ui/filter-n-sort/utils/turnFilterIntoWhereClause';
import { IconList } from '@/ui/icon';
import { EntityTable } from '@/ui/table/components/EntityTable';
import { GenericEntityTableData } from '@/ui/table/components/GenericEntityTableData';
import { TableContext } from '@/ui/table/states/TableContext';
import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue';
import { useViewSorts } from '@/views/hooks/useViewSorts';
import { currentViewIdState } from '@/views/states/currentViewIdState';
import {
CompanyOrderByWithRelationInput,
useGetCompaniesQuery,
useUpdateOneCompanyMutation,
} from '~/generated/graphql';
import { companiesFilters } from '~/pages/companies/companies-filters';
import { availableSorts } from '~/pages/companies/companies-sorts';
export function CompanyTable() {
const [orderBy, setOrderBy] =
useState<CompanyOrderByWithRelationInput[]>(defaultOrderBy);
import { defaultOrderBy } from '../../queries';
const updateSorts = useCallback((sorts: Array<CompaniesSelectedSortType>) => {
setOrderBy(sorts.length ? reduceSortsToOrderBy(sorts) : defaultOrderBy);
}, []);
export function CompanyTable() {
const currentViewId = useRecoilValue(currentViewIdState);
const orderBy = useRecoilScopedValue(sortsOrderByScopedState, TableContext);
const { updateSorts } = useViewSorts({
availableSorts,
Context: TableContext,
});
const filters = useRecoilScopedValue(filtersScopedState, TableContext);
@ -38,7 +41,7 @@ export function CompanyTable() {
objectName="company"
getRequestResultKey="companies"
useGetRequest={useGetCompaniesQuery}
orderBy={orderBy}
orderBy={orderBy.length ? orderBy : defaultOrderBy}
whereFilters={whereFilters}
viewFieldDefinitions={companyViewFields}
filterDefinitionArray={companiesFilters}
@ -47,7 +50,7 @@ export function CompanyTable() {
viewName="All Companies"
viewIcon={<IconList size={16} />}
availableSorts={availableSorts}
onSortsUpdate={updateSorts}
onSortsUpdate={currentViewId ? updateSorts : undefined}
useUpdateEntityMutation={useUpdateOneCompanyMutation}
/>
</>

View File

@ -1,14 +1,24 @@
import { useOpenCreateActivityDrawerForSelectedRowIds } from '@/activities/hooks/useOpenCreateActivityDrawerForSelectedRowIds';
import { TableActionBarButtonToggleComments } from '@/ui/table/action-bar/components/TableActionBarButtonOpenComments';
import { CommentableType } from '~/generated/graphql';
import { TableActionBarButtonToggleTasks } from '@/ui/table/action-bar/components/TableActionBarButtonOpenTasks';
import { ActivityType, CommentableType } from '~/generated/graphql';
export function TableActionBarButtonCreateActivityCompany() {
const openCreateActivityRightDrawer =
useOpenCreateActivityDrawerForSelectedRowIds();
async function handleButtonClick() {
openCreateActivityRightDrawer(CommentableType.Company);
async function handleButtonClick(type: ActivityType) {
openCreateActivityRightDrawer(type, CommentableType.Company);
}
return <TableActionBarButtonToggleComments onClick={handleButtonClick} />;
return (
<>
<TableActionBarButtonToggleComments
onClick={() => handleButtonClick(ActivityType.Note)}
/>
<TableActionBarButtonToggleTasks
onClick={() => handleButtonClick(ActivityType.Task)}
/>
</>
);
}

View File

@ -1,12 +1,12 @@
import { getOperationName } from '@apollo/client/utilities';
import { useRecoilValue } from 'recoil';
import { useRecoilState, useRecoilValue } from 'recoil';
import { GET_COMPANIES } from '@/companies/queries';
import { GET_PIPELINES } from '@/pipeline/queries';
import { IconTrash } from '@/ui/icon/index';
import { EntityTableActionBarButton } from '@/ui/table/action-bar/components/EntityTableActionBarButton';
import { useResetTableRowSelection } from '@/ui/table/hooks/useResetTableRowSelection';
import { selectedRowIdsSelector } from '@/ui/table/states/selectedRowIdsSelector';
import { tableRowIdsState } from '@/ui/table/states/tableRowIdsState';
import { useDeleteManyCompaniesMutation } from '~/generated/graphql';
export function TableActionBarButtonDeleteCompanies() {
@ -15,12 +15,11 @@ export function TableActionBarButtonDeleteCompanies() {
const resetRowSelection = useResetTableRowSelection();
const [deleteCompanies] = useDeleteManyCompaniesMutation({
refetchQueries: [
getOperationName(GET_COMPANIES) ?? '',
getOperationName(GET_PIPELINES) ?? '',
],
refetchQueries: [getOperationName(GET_PIPELINES) ?? ''],
});
const [tableRowIds, setTableRowIds] = useRecoilState(tableRowIdsState);
async function handleDeleteClick() {
const rowIdsToDelete = selectedRowIds;
@ -30,6 +29,17 @@ export function TableActionBarButtonDeleteCompanies() {
variables: {
ids: rowIdsToDelete,
},
optimisticResponse: {
__typename: 'Mutation',
deleteManyCompany: {
count: rowIdsToDelete.length,
},
},
update: () => {
setTableRowIds(
tableRowIds.filter((id) => !rowIdsToDelete.includes(id)),
);
},
});
}

View File

@ -26,25 +26,20 @@ export const UPDATE_ONE_PERSON = gql`
}
`;
export const INSERT_PERSON_FRAGMENT = gql`
fragment InsertPersonFragment on Person {
id
firstName
lastName
displayName
createdAt
}
`;
export const INSERT_ONE_PERSON = gql`
mutation InsertOnePerson($data: PersonCreateInput!) {
createOnePerson(data: $data) {
id
city
company {
domainName
name
id
}
email
firstName
lastName
jobTitle
linkedinUrl
xUrl
displayName
phone
createdAt
...InsertPersonFragment
}
}
`;

View File

@ -1,31 +1,33 @@
import { useCallback, useMemo, useState } from 'react';
import { useMemo } from 'react';
import { useRecoilValue } from 'recoil';
import { defaultOrderBy } from '@/companies/queries';
import { peopleViewFields } from '@/people/constants/peopleViewFields';
import { PeopleSelectedSortType } from '@/people/queries';
import { reduceSortsToOrderBy } from '@/ui/filter-n-sort/helpers';
import { filtersScopedState } from '@/ui/filter-n-sort/states/filtersScopedState';
import { sortsOrderByScopedState } from '@/ui/filter-n-sort/states/sortScopedState';
import { turnFilterIntoWhereClause } from '@/ui/filter-n-sort/utils/turnFilterIntoWhereClause';
import { IconList } from '@/ui/icon';
import { EntityTable } from '@/ui/table/components/EntityTable';
import { GenericEntityTableData } from '@/ui/table/components/GenericEntityTableData';
import { TableContext } from '@/ui/table/states/TableContext';
import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue';
import { useViewSorts } from '@/views/hooks/useViewSorts';
import { currentViewIdState } from '@/views/states/currentViewIdState';
import {
PersonOrderByWithRelationInput,
useGetPeopleQuery,
useUpdateOnePersonMutation,
} from '~/generated/graphql';
import { peopleFilters } from '~/pages/people/people-filters';
import { availableSorts } from '~/pages/people/people-sorts';
export function PeopleTable() {
const [orderBy, setOrderBy] =
useState<PersonOrderByWithRelationInput[]>(defaultOrderBy);
import { defaultOrderBy } from '../../queries';
const updateSorts = useCallback((sorts: Array<PeopleSelectedSortType>) => {
setOrderBy(sorts.length ? reduceSortsToOrderBy(sorts) : defaultOrderBy);
}, []);
export function PeopleTable() {
const currentViewId = useRecoilValue(currentViewIdState);
const orderBy = useRecoilScopedValue(sortsOrderByScopedState, TableContext);
const { updateSorts } = useViewSorts({
availableSorts,
Context: TableContext,
});
const filters = useRecoilScopedValue(filtersScopedState, TableContext);
@ -39,7 +41,7 @@ export function PeopleTable() {
objectName="person"
getRequestResultKey="people"
useGetRequest={useGetPeopleQuery}
orderBy={orderBy}
orderBy={orderBy.length ? orderBy : defaultOrderBy}
whereFilters={whereFilters}
viewFieldDefinitions={peopleViewFields}
filterDefinitionArray={peopleFilters}
@ -48,7 +50,7 @@ export function PeopleTable() {
viewName="All People"
viewIcon={<IconList size={16} />}
availableSorts={availableSorts}
onSortsUpdate={updateSorts}
onSortsUpdate={currentViewId ? updateSorts : undefined}
useUpdateEntityMutation={useUpdateOnePersonMutation}
/>
</>

View File

@ -1,14 +1,24 @@
import { useOpenCreateActivityDrawerForSelectedRowIds } from '@/activities/hooks/useOpenCreateActivityDrawerForSelectedRowIds';
import { TableActionBarButtonToggleComments } from '@/ui/table/action-bar/components/TableActionBarButtonOpenComments';
import { CommentableType } from '~/generated/graphql';
import { TableActionBarButtonToggleTasks } from '@/ui/table/action-bar/components/TableActionBarButtonOpenTasks';
import { ActivityType, CommentableType } from '~/generated/graphql';
export function TableActionBarButtonCreateActivityPeople() {
const openCreateActivityRightDrawer =
useOpenCreateActivityDrawerForSelectedRowIds();
async function handleButtonClick() {
openCreateActivityRightDrawer(CommentableType.Person);
async function handleButtonClick(type: ActivityType) {
openCreateActivityRightDrawer(type, CommentableType.Person);
}
return <TableActionBarButtonToggleComments onClick={handleButtonClick} />;
return (
<>
<TableActionBarButtonToggleComments
onClick={() => handleButtonClick(ActivityType.Note)}
/>
<TableActionBarButtonToggleTasks
onClick={() => handleButtonClick(ActivityType.Task)}
/>
</>
);
}

View File

@ -1,15 +1,17 @@
import { getOperationName } from '@apollo/client/utilities';
import { useRecoilValue } from 'recoil';
import { useRecoilState, useRecoilValue } from 'recoil';
import { GET_PEOPLE } from '@/people/queries';
import { IconTrash } from '@/ui/icon/index';
import { EntityTableActionBarButton } from '@/ui/table/action-bar/components/EntityTableActionBarButton';
import { useResetTableRowSelection } from '@/ui/table/hooks/useResetTableRowSelection';
import { selectedRowIdsSelector } from '@/ui/table/states/selectedRowIdsSelector';
import { tableRowIdsState } from '@/ui/table/states/tableRowIdsState';
import { useDeleteManyPersonMutation } from '~/generated/graphql';
export function TableActionBarButtonDeletePeople() {
const selectedRowIds = useRecoilValue(selectedRowIdsSelector);
const [tableRowIds, setTableRowIds] = useRecoilState(tableRowIdsState);
const resetRowSelection = useResetTableRowSelection();
@ -26,6 +28,17 @@ export function TableActionBarButtonDeletePeople() {
variables: {
ids: rowIdsToDelete,
},
optimisticResponse: {
__typename: 'Mutation',
deleteManyPerson: {
count: rowIdsToDelete.length,
},
},
update: () => {
setTableRowIds(
tableRowIds.filter((id) => !rowIdsToDelete.includes(id)),
);
},
});
}

View File

@ -2,27 +2,16 @@ import { SortOrder as Order_By } from '~/generated/graphql';
import { SelectedSortType } from './types/interface';
const mapOrderToOrder_By = (order: string) => {
if (order === 'asc') return Order_By.Asc;
return Order_By.Desc;
};
export const defaultOrderByTemplateFactory =
(key: string) => (order: string) => ({
[key]: order,
});
export const reduceSortsToOrderBy = <OrderByTemplate>(
sorts: Array<SelectedSortType<OrderByTemplate>>,
): OrderByTemplate[] => {
const mappedSorts = sorts.map((sort) => {
if (sort.orderByTemplates) {
return sort.orderByTemplates?.map((orderByTemplate) =>
orderByTemplate(mapOrderToOrder_By(sort.order)),
sorts: SelectedSortType<OrderByTemplate>[],
): OrderByTemplate[] =>
sorts
.map((sort) => {
const order = sort.order === 'asc' ? Order_By.Asc : Order_By.Desc;
return (
sort.orderByTemplate?.(order) || [
{ [sort.key]: order } as OrderByTemplate,
]
);
}
return defaultOrderByTemplateFactory(sort.key as string)(sort.order);
});
return mappedSorts.flat() as OrderByTemplate[];
};
})
.flat();

View File

@ -1,8 +1,28 @@
import { atomFamily } from 'recoil';
import { atomFamily, selectorFamily } from 'recoil';
import { Filter } from '../types/Filter';
import { reduceSortsToOrderBy } from '../helpers';
import { SelectedSortType } from '../types/interface';
export const sortScopedState = atomFamily<Filter[], string>({
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

@ -6,7 +6,7 @@ export type SortType<OrderByTemplate> = {
label: string;
key: string;
icon?: ReactNode;
orderByTemplates?: Array<(order: Order_By) => OrderByTemplate>;
orderByTemplate?: (order: Order_By) => OrderByTemplate[];
};
export type SelectedSortType<OrderByTemplate> = SortType<OrderByTemplate> & {

View File

@ -0,0 +1,17 @@
import { IconCheckbox } from '@/ui/icon/index';
import { EntityTableActionBarButton } from './EntityTableActionBarButton';
type OwnProps = {
onClick: () => void;
};
export function TableActionBarButtonToggleTasks({ onClick }: OwnProps) {
return (
<EntityTableActionBarButton
label="Task"
icon={<IconCheckbox size={16} />}
onClick={onClick}
/>
);
}

View File

@ -5,19 +5,20 @@ import styled from '@emotion/styled';
import { useRecoilCallback, useRecoilState, useRecoilValue } from 'recoil';
import { IconButton } from '@/ui/button/components/IconButton';
import type {
ViewFieldDefinition,
ViewFieldMetadata,
} from '@/ui/editable-field/types/ViewField';
import { IconPlus } from '@/ui/icon';
import { useTrackPointer } from '@/ui/utilities/pointer-event/hooks/useTrackPointer';
import { GET_VIEW_FIELDS } from '@/views/queries/select';
import { currentViewIdState } from '@/views/states/currentViewIdState';
import {
useCreateViewFieldMutation,
useUpdateViewFieldMutation,
} from '~/generated/graphql';
import type {
ViewFieldDefinition,
ViewFieldMetadata,
} from '../../editable-field/types/ViewField';
import { toViewFieldInput } from '../hooks/useLoadView';
import { toViewFieldInput } from '../hooks/useLoadViewFields';
import { resizeFieldOffsetState } from '../states/resizeFieldOffsetState';
import {
addableViewFieldDefinitionsState,
@ -89,6 +90,7 @@ export function EntityTableHeader() {
const theme = useTheme();
const [{ objectName }, setViewFieldsState] = useRecoilState(viewFieldsState);
const currentViewId = useRecoilValue(currentViewIdState);
const viewFields = useRecoilValue(visibleViewFieldsState);
const columnWidths = useRecoilValue(columnWidthByViewFieldIdState);
const addableViewFieldDefinitions = useRecoilValue(
@ -176,15 +178,18 @@ export function EntityTableHeader() {
createViewFieldMutation({
variables: {
data: toViewFieldInput(objectName, {
...viewFieldDefinition,
columnOrder: viewFields.length + 1,
}),
data: {
...toViewFieldInput(objectName, {
...viewFieldDefinition,
columnOrder: viewFields.length + 1,
}),
view: { connect: { id: currentViewId } },
},
},
refetchQueries: [getOperationName(GET_VIEW_FIELDS) ?? ''],
});
},
[createViewFieldMutation, objectName, viewFields.length],
[createViewFieldMutation, currentViewId, objectName, viewFields.length],
);
return (

View File

@ -6,7 +6,7 @@ import {
import { FilterDefinition } from '@/ui/filter-n-sort/types/FilterDefinition';
import { useSetEntityTableData } from '@/ui/table/hooks/useSetEntityTableData';
import { useLoadView } from '../hooks/useLoadView';
import { useLoadViewFields } from '../hooks/useLoadViewFields';
export function GenericEntityTableData({
objectName,
@ -27,7 +27,7 @@ export function GenericEntityTableData({
}) {
const setEntityTableData = useSetEntityTableData();
useLoadView({ objectName, viewFieldDefinitions });
useLoadViewFields({ objectName, viewFieldDefinitions });
useGetRequest({
variables: { orderBy, where: whereFilters },

View File

@ -40,7 +40,8 @@ export function GenericEditableDoubleTextChipCellDisplayMode({
}),
);
const displayName = `${firstValue} ${secondValue}`;
const displayName =
firstValue || secondValue ? `${firstValue} ${secondValue}` : ' ';
switch (viewField.metadata.entityType) {
case Entity.Company: {

View File

@ -7,6 +7,7 @@ import {
import { useCurrentRowEntityId } from '@/ui/table/hooks/useCurrentEntityId';
import { useUpdateEntityField } from '@/ui/table/hooks/useUpdateEntityField';
import { tableEntityFieldFamilySelector } from '@/ui/table/states/tableEntityFieldFamilySelector';
import { isURL } from '~/utils/is-url';
import { TextCellEdit } from './TextCellEdit';
@ -30,6 +31,8 @@ export function GenericEditableURLCellEditMode({ viewField }: OwnProps) {
function handleSubmit(newText: string) {
if (newText === fieldValue) return;
if (!isURL(newText)) return;
setFieldValue(newText);
if (currentRowEntityId && updateField) {

View File

@ -1,18 +1,19 @@
import { getOperationName } from '@apollo/client/utilities';
import { useSetRecoilState } from 'recoil';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import type {
ViewFieldDefinition,
ViewFieldMetadata,
ViewFieldTextMetadata,
} from '@/ui/editable-field/types/ViewField';
import { GET_VIEW_FIELDS } from '@/views/queries/select';
import { currentViewIdState } from '@/views/states/currentViewIdState';
import {
SortOrder,
useCreateViewFieldsMutation,
useGetViewFieldsQuery,
} from '~/generated/graphql';
import type {
ViewFieldDefinition,
ViewFieldMetadata,
ViewFieldTextMetadata,
} from '../../editable-field/types/ViewField';
import { entityTableDimensionsState } from '../states/entityTableDimensionsState';
import { viewFieldsState } from '../states/viewFieldsState';
@ -33,13 +34,14 @@ export const toViewFieldInput = (
sizeInPx: viewFieldDefinition.columnSize,
});
export const useLoadView = ({
export const useLoadViewFields = ({
objectName,
viewFieldDefinitions,
}: {
objectName: 'company' | 'person';
viewFieldDefinitions: ViewFieldDefinition<ViewFieldMetadata>[];
}) => {
const currentViewId = useRecoilValue(currentViewIdState);
const setEntityTableDimensions = useSetRecoilState(
entityTableDimensionsState,
);
@ -50,7 +52,10 @@ export const useLoadView = ({
useGetViewFieldsQuery({
variables: {
orderBy: { index: SortOrder.Asc },
where: { objectName: { equals: objectName } },
where: {
objectName: { equals: objectName },
viewId: { equals: currentViewId ?? null },
},
},
onCompleted: (data) => {
if (data.viewFields.length) {
@ -79,9 +84,10 @@ export const useLoadView = ({
// Populate if empty
createViewFieldsMutation({
variables: {
data: viewFieldDefinitions.map((viewFieldDefinition) =>
toViewFieldInput(objectName, viewFieldDefinition),
),
data: viewFieldDefinitions.map((viewFieldDefinition) => ({
...toViewFieldInput(objectName, viewFieldDefinition),
viewId: currentViewId,
})),
},
refetchQueries: [getOperationName(GET_VIEW_FIELDS) ?? ''],
});

View File

@ -1,12 +1,14 @@
import { ReactNode, useCallback, useState } from 'react';
import { ReactNode, useCallback } from 'react';
import styled from '@emotion/styled';
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 { FiltersHotkeyScope } from '@/ui/filter-n-sort/types/FiltersHotkeyScope';
import { SelectedSortType, SortType } from '@/ui/filter-n-sort/types/interface';
import { TopBar } from '@/ui/top-bar/TopBar';
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
import { OptionsDropdownButton } from '@/views/components/OptionsDropdownButton';
import { TableContext } from '../../states/TableContext';
@ -34,26 +36,26 @@ export function TableHeader<SortField>({
availableSorts,
onSortsUpdate,
}: OwnProps<SortField>) {
const [sorts, innerSetSorts] = useState<Array<SelectedSortType<SortField>>>(
[],
const [sorts, setSorts] = useRecoilScopedState<SelectedSortType<SortField>[]>(
sortScopedState,
TableContext,
);
const handleSortsUpdate = onSortsUpdate ?? setSorts;
const sortSelect = useCallback(
(newSort: SelectedSortType<SortField>) => {
const newSorts = updateSortOrFilterByKey(sorts, newSort);
innerSetSorts(newSorts);
onSortsUpdate && onSortsUpdate(newSorts);
handleSortsUpdate(newSorts);
},
[onSortsUpdate, sorts],
[handleSortsUpdate, sorts],
);
const sortUnselect = useCallback(
(sortKey: string) => {
const newSorts = sorts.filter((sort) => sort.key !== sortKey);
innerSetSorts(newSorts);
onSortsUpdate && onSortsUpdate(newSorts);
handleSortsUpdate(newSorts);
},
[onSortsUpdate, sorts],
[handleSortsUpdate, sorts],
);
return (
@ -88,8 +90,7 @@ export function TableHeader<SortField>({
sorts={sorts}
onRemoveSort={sortUnselect}
onCancelClick={() => {
innerSetSorts([]);
onSortsUpdate && onSortsUpdate([]);
handleSortsUpdate([]);
}}
/>
}

View File

@ -1,10 +1,10 @@
import { Context, useContext } from 'react';
import { RecoilState, useRecoilValue } from 'recoil';
import { RecoilState, RecoilValueReadOnly, useRecoilValue } from 'recoil';
import { RecoilScopeContext } from '../states/RecoilScopeContext';
export function useRecoilScopedValue<T>(
recoilState: (param: string) => RecoilState<T>,
recoilState: (param: string) => RecoilState<T> | RecoilValueReadOnly<T>,
SpecificContext?: Context<string | null>,
) {
const recoilScopeId = useContext(SpecificContext ?? RecoilScopeContext);

View File

@ -0,0 +1,152 @@
import { Context, useCallback } from 'react';
import { getOperationName } from '@apollo/client/utilities';
import { useRecoilValue } from 'recoil';
import {
sortsByKeyScopedState,
sortScopedState,
} from '@/ui/filter-n-sort/states/sortScopedState';
import type {
SelectedSortType,
SortType,
} from '@/ui/filter-n-sort/types/interface';
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue';
import { currentViewIdState } from '@/views/states/currentViewIdState';
import {
useCreateViewSortsMutation,
useDeleteViewSortsMutation,
useGetViewSortsQuery,
useUpdateViewSortMutation,
ViewSortDirection,
} from '~/generated/graphql';
import { GET_VIEW_SORTS } from '../queries/select';
export const useViewSorts = <SortField>({
availableSorts,
Context,
}: {
availableSorts: SortType<SortField>[];
Context?: Context<string | null>;
}) => {
const currentViewId = useRecoilValue(currentViewIdState);
const [, setSorts] = useRecoilScopedState(sortScopedState, Context);
const sortsByKey = useRecoilScopedValue(sortsByKeyScopedState, Context);
useGetViewSortsQuery({
skip: !currentViewId,
variables: {
where: {
viewId: { equals: currentViewId },
},
},
onCompleted: (data) => {
setSorts(
data.viewSorts
.map((viewSort) => ({
...availableSorts.find((sort) => sort.key === viewSort.key),
label: viewSort.name,
order: viewSort.direction.toLowerCase(),
}))
.filter((sort): sort is SelectedSortType<SortField> => !!sort),
);
},
});
const [createViewSortsMutation] = useCreateViewSortsMutation();
const [updateViewSortMutation] = useUpdateViewSortMutation();
const [deleteViewSortsMutation] = useDeleteViewSortsMutation();
const createViewSorts = useCallback(
(sorts: SelectedSortType<SortField>[]) => {
if (!currentViewId || !sorts.length) return;
return createViewSortsMutation({
variables: {
data: sorts.map((sort) => ({
key: sort.key,
direction: sort.order as ViewSortDirection,
name: sort.label,
viewId: currentViewId,
})),
},
refetchQueries: [getOperationName(GET_VIEW_SORTS) ?? ''],
});
},
[createViewSortsMutation, currentViewId],
);
const updateViewSorts = useCallback(
(sorts: SelectedSortType<SortField>[]) => {
if (!currentViewId || !sorts.length) return;
return Promise.all(
sorts.map((sort) =>
updateViewSortMutation({
variables: {
data: {
direction: sort.order as ViewSortDirection,
},
where: {
viewId_key: { key: sort.key, viewId: currentViewId },
},
},
refetchQueries: [getOperationName(GET_VIEW_SORTS) ?? ''],
}),
),
);
},
[currentViewId, updateViewSortMutation],
);
const deleteViewSorts = useCallback(
(sortKeys: string[]) => {
if (!currentViewId || !sortKeys.length) return;
return deleteViewSortsMutation({
variables: {
where: {
key: { in: sortKeys },
viewId: { equals: currentViewId },
},
},
refetchQueries: [getOperationName(GET_VIEW_SORTS) ?? ''],
});
},
[currentViewId, deleteViewSortsMutation],
);
const updateSorts = useCallback(
async (nextSorts: SelectedSortType<SortField>[]) => {
if (!currentViewId) return;
const sortsToCreate = nextSorts.filter(
(nextSort) => !sortsByKey[nextSort.key],
);
await createViewSorts(sortsToCreate);
const sortsToUpdate = nextSorts.filter(
(nextSort) =>
sortsByKey[nextSort.key] &&
sortsByKey[nextSort.key].order !== nextSort.order,
);
await updateViewSorts(sortsToUpdate);
const nextSortKeys = nextSorts.map((nextSort) => nextSort.key);
const sortKeysToDelete = Object.keys(sortsByKey).filter(
(previousSortKey) => !nextSortKeys.includes(previousSortKey),
);
return deleteViewSorts(sortKeysToDelete);
},
[
createViewSorts,
currentViewId,
deleteViewSorts,
sortsByKey,
updateViewSorts,
],
);
return { updateSorts };
};

View File

@ -19,3 +19,11 @@ export const CREATE_VIEW_FIELDS = gql`
}
}
`;
export const CREATE_VIEW_SORTS = gql`
mutation CreateViewSorts($data: [ViewSortCreateManyInput!]!) {
createManyViewSort(data: $data) {
count
}
}
`;

View File

@ -0,0 +1,9 @@
import { gql } from '@apollo/client';
export const DELETE_VIEW_SORTS = gql`
mutation DeleteViewSorts($where: ViewSortWhereInput!) {
deleteManyViewSort(where: $where) {
count
}
}
`;

View File

@ -14,3 +14,13 @@ export const GET_VIEW_FIELDS = gql`
}
}
`;
export const GET_VIEW_SORTS = gql`
query GetViewSorts($where: ViewSortWhereInput) {
viewSorts: findManyViewSort(where: $where) {
direction
key
name
}
}
`;

View File

@ -14,3 +14,16 @@ export const UPDATE_VIEW_FIELD = gql`
}
}
`;
export const UPDATE_VIEW_SORT = gql`
mutation UpdateViewSort(
$data: ViewSortUpdateInput!
$where: ViewSortWhereUniqueInput!
) {
viewSort: updateOneViewSort(data: $data, where: $where) {
direction
key
name
}
}
`;

View File

@ -0,0 +1,6 @@
import { atom } from 'recoil';
export const currentViewIdState = atom<string | undefined>({
key: 'currentViewIdState',
default: undefined,
});

View File

@ -1,20 +1,21 @@
import { getOperationName } from '@apollo/client/utilities';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { useRecoilState } from 'recoil';
import { v4 } from 'uuid';
import { GET_COMPANIES } from '@/companies/queries';
import { CompanyTable } from '@/companies/table/components/CompanyTable';
import { TableActionBarButtonCreateActivityCompany } from '@/companies/table/components/TableActionBarButtonCreateActivityCompany';
import { TableActionBarButtonDeleteCompanies } from '@/companies/table/components/TableActionBarButtonDeleteCompanies';
import { SEARCH_COMPANY_QUERY } from '@/search/queries/search';
import { IconBuildingSkyscraper } from '@/ui/icon';
import { WithTopBarContainer } from '@/ui/layout/components/WithTopBarContainer';
import { EntityTableActionBar } from '@/ui/table/action-bar/components/EntityTableActionBar';
import { TableContext } from '@/ui/table/states/TableContext';
import { tableRowIdsState } from '@/ui/table/states/tableRowIdsState';
import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope';
import { useInsertOneCompanyMutation } from '~/generated/graphql';
import { SEARCH_COMPANY_QUERY } from '../../modules/search/queries/search';
const StyledTableContainer = styled.div`
display: flex;
width: 100%;
@ -22,20 +23,35 @@ const StyledTableContainer = styled.div`
export function Companies() {
const [insertCompany] = useInsertOneCompanyMutation();
const [tableRowIds, setTableRowIds] = useRecoilState(tableRowIdsState);
async function handleAddButtonClick() {
const newCompanyId: string = v4();
await insertCompany({
variables: {
data: {
id: newCompanyId,
name: '',
domainName: '',
address: '',
},
},
refetchQueries: [
getOperationName(GET_COMPANIES) ?? '',
getOperationName(SEARCH_COMPANY_QUERY) ?? '',
],
optimisticResponse: {
__typename: 'Mutation',
createOneCompany: {
__typename: 'Company',
id: newCompanyId,
name: '',
domainName: '',
address: '',
createdAt: '',
},
},
update: (cache, { data }) => {
data?.createOneCompany.id &&
setTableRowIds([data?.createOneCompany.id, ...tableRowIds]);
},
refetchQueries: [getOperationName(SEARCH_COMPANY_QUERY) ?? ''],
});
}

View File

@ -8,7 +8,7 @@ import {
} from '@/ui/icon/index';
import { CompanyOrderByWithRelationInput as Companies_Order_By } from '~/generated/graphql';
export const availableSorts = [
export const availableSorts: SortType<Companies_Order_By>[] = [
{
key: 'name',
label: 'Name',
@ -34,4 +34,4 @@ export const availableSorts = [
label: 'Creation',
icon: <IconCalendarEvent size={16} />,
},
] satisfies Array<SortType<Companies_Order_By>>;
];

View File

@ -1,8 +1,8 @@
import { getOperationName } from '@apollo/client/utilities';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { useRecoilState } from 'recoil';
import { v4 } from 'uuid';
import { GET_PEOPLE } from '@/people/queries';
import { PeopleTable } from '@/people/table/components/PeopleTable';
import { TableActionBarButtonCreateActivityPeople } from '@/people/table/components/TableActionBarButtonCreateActivityPeople';
import { TableActionBarButtonDeletePeople } from '@/people/table/components/TableActionBarButtonDeletePeople';
@ -10,6 +10,7 @@ import { IconUser } from '@/ui/icon';
import { WithTopBarContainer } from '@/ui/layout/components/WithTopBarContainer';
import { EntityTableActionBar } from '@/ui/table/action-bar/components/EntityTableActionBar';
import { TableContext } from '@/ui/table/states/TableContext';
import { tableRowIdsState } from '@/ui/table/states/tableRowIdsState';
import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope';
import { useInsertOnePersonMutation } from '~/generated/graphql';
@ -20,16 +21,33 @@ const StyledTableContainer = styled.div`
export function People() {
const [insertOnePerson] = useInsertOnePersonMutation();
const [tableRowIds, setTableRowIds] = useRecoilState(tableRowIdsState);
async function handleAddButtonClick() {
const newPersonId: string = v4();
await insertOnePerson({
variables: {
data: {
id: newPersonId,
firstName: '',
lastName: '',
},
},
refetchQueries: [getOperationName(GET_PEOPLE) ?? ''],
optimisticResponse: {
__typename: 'Mutation',
createOnePerson: {
__typename: 'Person',
id: newPersonId,
firstName: '',
lastName: '',
displayName: '',
createdAt: '',
},
},
update: (cache, { data }) => {
data?.createOnePerson?.id &&
setTableRowIds([data?.createOnePerson.id, ...tableRowIds]);
},
});
}

View File

@ -12,19 +12,15 @@ import {
SortOrder as Order_By,
} from '~/generated/graphql';
export const availableSorts = [
export const availableSorts: SortType<People_Order_By>[] = [
{
key: 'fullname',
label: 'People',
icon: <IconUser size={16} />,
orderByTemplates: [
(order: Order_By) => ({
firstName: order,
}),
(order: Order_By) => ({
lastName: order,
}),
orderByTemplate: (order: Order_By) => [
{ firstName: order },
{ lastName: order },
],
},
{
@ -32,7 +28,7 @@ export const availableSorts = [
label: 'Company',
icon: <IconBuildingSkyscraper size={16} />,
orderByTemplates: [(order: Order_By) => ({ company: { name: order } })],
orderByTemplate: (order: Order_By) => [{ company: { name: order } }],
},
{
key: 'email',
@ -54,4 +50,4 @@ export const availableSorts = [
label: 'City',
icon: <IconMap size={16} />,
},
] satisfies Array<SortType<People_Order_By>>;
];

View File

@ -186,14 +186,22 @@ export function AuthAutoRouter() {
label: 'Create Task',
type: CommandType.Create,
icon: <IconCheckbox />,
onCommandClick: () => openCreateActivity(ActivityType.Task, entity),
onCommandClick: () =>
openCreateActivity(
ActivityType.Task,
entity ? [entity] : undefined,
),
},
{
to: '',
label: 'Create Note',
type: CommandType.Create,
icon: <IconNotes />,
onCommandClick: () => openCreateActivity(ActivityType.Note, entity),
onCommandClick: () =>
openCreateActivity(
ActivityType.Note,
entity ? [entity] : undefined,
),
},
]);
break;
@ -212,14 +220,22 @@ export function AuthAutoRouter() {
label: 'Create Task',
type: CommandType.Create,
icon: <IconCheckbox />,
onCommandClick: () => openCreateActivity(ActivityType.Task, entity),
onCommandClick: () =>
openCreateActivity(
ActivityType.Task,
entity ? [entity] : undefined,
),
},
{
to: '',
label: 'Create Note',
type: CommandType.Create,
icon: <IconNotes />,
onCommandClick: () => openCreateActivity(ActivityType.Note, entity),
onCommandClick: () =>
openCreateActivity(
ActivityType.Note,
entity ? [entity] : undefined,
),
},
]);
break;

View File

@ -0,0 +1,35 @@
import { isURL } from '~/utils/is-url';
describe('isURL', () => {
it(`should return false if null`, () => {
expect(isURL(null)).toBeFalsy();
});
it(`should return false if undefined`, () => {
expect(isURL(undefined)).toBeFalsy();
});
it(`should return true if string google`, () => {
expect(isURL('google')).toBeFalsy();
});
it(`should return true if string google.com`, () => {
expect(isURL('google.com')).toBeTruthy();
});
it(`should return true if string bbc.co.uk`, () => {
expect(isURL('bbc.co.uk')).toBeTruthy();
});
it(`should return true if string web.io`, () => {
expect(isURL('web.io')).toBeTruthy();
});
it(`should return true if string x.com`, () => {
expect(isURL('x.com')).toBeTruthy();
});
it(`should return true if string 2.com`, () => {
expect(isURL('2.com')).toBeTruthy();
});
});

10
front/src/utils/is-url.ts Normal file
View File

@ -0,0 +1,10 @@
import { isDefined } from './isDefined';
export function isURL(url: string | undefined | null) {
return (
isDefined(url) &&
/^((?!-))(xn--)?[a-z0-9][a-z0-9-_]{0,61}[a-z0-9]{0,1}\.(xn--)?([a-z0-9-]{1,61}|[a-z0-9-]{1,30}\.[a-z]{2,})$/.test(
url,
)
);
}

View File

@ -17,7 +17,9 @@ import {
PipelineStage,
PipelineProgress,
UserSettings,
View,
ViewField,
ViewSort,
} from '@prisma/client';
import { AbilityAction } from './ability.action';
@ -37,7 +39,9 @@ type SubjectsAbility = Subjects<{
PipelineProgress: PipelineProgress;
Attachment: Attachment;
UserSettings: UserSettings;
View: View;
ViewField: ViewField;
ViewSort: ViewSort;
}>;
export type AppAbility = PureAbility<
@ -130,11 +134,22 @@ export class AbilityFactory {
workspaceId: workspace.id,
});
// View
can(AbilityAction.Read, 'View', { workspaceId: workspace.id });
can(AbilityAction.Create, 'View', { workspaceId: workspace.id });
can(AbilityAction.Update, 'View', { workspaceId: workspace.id });
// ViewField
can(AbilityAction.Read, 'ViewField', { workspaceId: workspace.id });
can(AbilityAction.Create, 'ViewField', { workspaceId: workspace.id });
can(AbilityAction.Update, 'ViewField', { workspaceId: workspace.id });
// ViewSort
can(AbilityAction.Read, 'ViewSort', { workspaceId: workspace.id });
can(AbilityAction.Create, 'ViewSort', { workspaceId: workspace.id });
can(AbilityAction.Update, 'ViewSort', { workspaceId: workspace.id });
can(AbilityAction.Delete, 'ViewSort', { workspaceId: workspace.id });
return build();
}
}

View File

@ -99,6 +99,12 @@ import {
ReadViewFieldAbilityHandler,
UpdateViewFieldAbilityHandler,
} from './handlers/view-field.ability-handler';
import {
CreateViewSortAbilityHandler,
ReadViewSortAbilityHandler,
UpdateViewSortAbilityHandler,
DeleteViewSortAbilityHandler,
} from './handlers/view-sort.ability-handler';
@Global()
@Module({
@ -187,6 +193,11 @@ import {
ReadViewFieldAbilityHandler,
CreateViewFieldAbilityHandler,
UpdateViewFieldAbilityHandler,
// ViewSort
ReadViewSortAbilityHandler,
CreateViewSortAbilityHandler,
UpdateViewSortAbilityHandler,
DeleteViewSortAbilityHandler,
],
exports: [
AbilityFactory,
@ -272,6 +283,11 @@ import {
ReadViewFieldAbilityHandler,
CreateViewFieldAbilityHandler,
UpdateViewFieldAbilityHandler,
// ViewSort
ReadViewSortAbilityHandler,
CreateViewSortAbilityHandler,
UpdateViewSortAbilityHandler,
DeleteViewSortAbilityHandler,
],
})
export class AbilityModule {}

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 { ViewSortWhereUniqueInput } from 'src/core/@generated/view-sort/view-sort-where-unique.input';
import { ViewSortWhereInput } from 'src/core/@generated/view-sort/view-sort-where.input';
class ViewSortArgs {
where?: ViewSortWhereInput | ViewSortWhereUniqueInput;
[key: string]: any;
}
const isViewSortWhereUniqueInput = (
input: ViewSortWhereInput | ViewSortWhereUniqueInput,
): input is ViewSortWhereUniqueInput => 'viewId_key' in input;
@Injectable()
export class ReadViewSortAbilityHandler implements IAbilityHandler {
handle(ability: AppAbility) {
return ability.can(AbilityAction.Read, 'ViewSort');
}
}
@Injectable()
export class CreateViewSortAbilityHandler 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(
'ViewSort',
ability,
this.prismaService.client,
args,
);
if (!allowed) {
return false;
}
return ability.can(AbilityAction.Create, 'ViewSort');
}
}
@Injectable()
export class UpdateViewSortAbilityHandler implements IAbilityHandler {
constructor(private readonly prismaService: PrismaService) {}
async handle(ability: AppAbility, context: ExecutionContext) {
const gqlContext = GqlExecutionContext.create(context);
const args = gqlContext.getArgs<ViewSortArgs>();
const viewSort = await this.prismaService.client.viewSort.findFirst({
where:
args.where && isViewSortWhereUniqueInput(args.where)
? args.where.viewId_key
: args.where,
});
assert(viewSort, '', NotFoundException);
const allowed = await relationAbilityChecker(
'ViewSort',
ability,
this.prismaService.client,
args,
);
if (!allowed) {
return false;
}
return ability.can(AbilityAction.Update, subject('ViewSort', viewSort));
}
}
@Injectable()
export class DeleteViewSortAbilityHandler implements IAbilityHandler {
constructor(private readonly prismaService: PrismaService) {}
async handle(ability: AppAbility, context: ExecutionContext) {
const gqlContext = GqlExecutionContext.create(context);
const args = gqlContext.getArgs<ViewSortArgs>();
const where = convertToWhereInput(
args.where && isViewSortWhereUniqueInput(args.where)
? args.where.viewId_key
: args.where,
);
const viewSorts = await this.prismaService.client.viewSort.findMany({
where,
});
assert(viewSorts.length, '', NotFoundException);
for (const viewSort of viewSorts) {
const allowed = ability.can(
AbilityAction.Delete,
subject('ViewSort', viewSort),
);
if (!allowed) {
return false;
}
}
return true;
}
}

View File

@ -0,0 +1,79 @@
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 { relationAbilityChecker } from 'src/ability/ability.util';
import { ViewWhereInput } from 'src/core/@generated/view/view-where.input';
import { PrismaService } from 'src/database/prisma.service';
import { assert } from 'src/utils/assert';
class ViewArgs {
where?: ViewWhereInput;
[key: string]: any;
}
@Injectable()
export class ReadViewAbilityHandler implements IAbilityHandler {
handle(ability: AppAbility) {
return ability.can(AbilityAction.Read, 'View');
}
}
@Injectable()
export class CreateViewAbilityHandler 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(
'View',
ability,
this.prismaService.client,
args,
);
if (!allowed) {
return false;
}
return ability.can(AbilityAction.Create, 'View');
}
}
@Injectable()
export class UpdateViewAbilityHandler implements IAbilityHandler {
constructor(private readonly prismaService: PrismaService) {}
async handle(ability: AppAbility, context: ExecutionContext) {
const gqlContext = GqlExecutionContext.create(context);
const args = gqlContext.getArgs<ViewArgs>();
const view = await this.prismaService.client.view.findFirst({
where: args.where,
});
assert(view, '', NotFoundException);
const allowed = await relationAbilityChecker(
'View',
ability,
this.prismaService.client,
args,
);
if (!allowed) {
return false;
}
return ability.can(AbilityAction.Update, subject('View', view));
}
}

View File

@ -45,7 +45,7 @@ export class ViewFieldResolver {
): Promise<Partial<ViewField>> {
return this.viewFieldService.create({
data: {
...args.data,
...(args.data as Prisma.ViewFieldCreateInput),
workspace: { connect: { id: workspace.id } },
},
select: prismaSelect.value,

View File

@ -0,0 +1,32 @@
import { Test, TestingModule } from '@nestjs/testing';
import { ViewSortService } from 'src/core/view/services/view-sort.service';
import { AbilityFactory } from 'src/ability/ability.factory';
import { ViewSortResolver } from './view-sort.resolver';
describe('ViewSortResolver', () => {
let resolver: ViewSortResolver;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
ViewSortResolver,
{
provide: ViewSortService,
useValue: {},
},
{
provide: AbilityFactory,
useValue: {},
},
],
}).compile();
resolver = module.get<ViewSortResolver>(ViewSortResolver);
});
it('should be defined', () => {
expect(resolver).toBeDefined();
});
});

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 {
CreateViewSortAbilityHandler,
DeleteViewSortAbilityHandler,
ReadViewSortAbilityHandler,
UpdateViewSortAbilityHandler,
} from 'src/ability/handlers/view-sort.ability-handler';
import { FindManyViewSortArgs } from 'src/core/@generated/view-sort/find-many-view-sort.args';
import { ViewSort } from 'src/core/@generated/view-sort/view-sort.model';
import { ViewSortService } from 'src/core/view/services/view-sort.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 { UpdateOneViewSortArgs } from 'src/core/@generated/view-sort/update-one-view-sort.args';
import { AuthWorkspace } from 'src/decorators/auth-workspace.decorator';
import { AffectedRows } from 'src/core/@generated/prisma/affected-rows.output';
import { DeleteManyViewSortArgs } from 'src/core/@generated/view-sort/delete-many-view-sort.args';
import { CreateManyViewSortArgs } from 'src/core/@generated/view-sort/create-many-view-sort.args';
@UseGuards(JwtAuthGuard)
@Resolver(() => ViewSort)
export class ViewSortResolver {
constructor(private readonly viewSortService: ViewSortService) {}
@Mutation(() => AffectedRows)
@UseGuards(AbilityGuard)
@CheckAbilities(CreateViewSortAbilityHandler)
async createManyViewSort(
@Args() args: CreateManyViewSortArgs,
@AuthWorkspace() workspace: Workspace,
): Promise<AffectedRows> {
return this.viewSortService.createMany({
data: args.data.map((data) => ({
...data,
workspaceId: workspace.id,
})),
});
}
@Query(() => [ViewSort])
@UseGuards(AbilityGuard)
@CheckAbilities(ReadViewSortAbilityHandler)
async findManyViewSort(
@Args() args: FindManyViewSortArgs,
@UserAbility() ability: AppAbility,
@PrismaSelector({ modelName: 'ViewSort' })
prismaSelect: PrismaSelect<'ViewSort'>,
): Promise<Partial<ViewSort>[]> {
return this.viewSortService.findMany({
where: args.where
? {
AND: [args.where, accessibleBy(ability).ViewSort],
}
: accessibleBy(ability).ViewSort,
orderBy: args.orderBy,
cursor: args.cursor,
take: args.take,
skip: args.skip,
distinct: args.distinct,
select: prismaSelect.value,
});
}
@Mutation(() => ViewSort)
@UseGuards(AbilityGuard)
@CheckAbilities(UpdateViewSortAbilityHandler)
async updateOneViewSort(
@Args() args: UpdateOneViewSortArgs,
@PrismaSelector({ modelName: 'ViewSort' })
prismaSelect: PrismaSelect<'ViewSort'>,
): Promise<Partial<ViewSort>> {
return this.viewSortService.update({
data: args.data,
where: args.where,
select: prismaSelect.value,
} as Prisma.ViewSortUpdateArgs);
}
@Mutation(() => AffectedRows, {
nullable: false,
})
@UseGuards(AbilityGuard)
@CheckAbilities(DeleteViewSortAbilityHandler)
async deleteManyViewSort(
@Args() args: DeleteManyViewSortArgs,
): Promise<AffectedRows> {
return this.viewSortService.deleteMany({
where: args.where,
});
}
}

View File

@ -0,0 +1,28 @@
import { Test, TestingModule } from '@nestjs/testing';
import { PrismaService } from 'src/database/prisma.service';
import { prismaMock } from 'src/database/client-mock/jest-prisma-singleton';
import { ViewSortService } from './view-sort.service';
describe('ViewSortService', () => {
let service: ViewSortService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
ViewSortService,
{
provide: PrismaService,
useValue: prismaMock,
},
],
}).compile();
service = module.get<ViewSortService>(ViewSortService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

View File

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

View File

@ -2,8 +2,15 @@ import { Module } from '@nestjs/common';
import { ViewFieldService } from './services/view-field.service';
import { ViewFieldResolver } from './resolvers/view-field.resolver';
import { ViewSortService } from './services/view-sort.service';
import { ViewSortResolver } from './resolvers/view-sort.resolver';
@Module({
providers: [ViewFieldService, ViewFieldResolver],
providers: [
ViewFieldService,
ViewSortService,
ViewFieldResolver,
ViewSortResolver,
],
})
export class ViewModule {}

View File

@ -111,7 +111,9 @@ export class WorkspaceService {
comment,
activityTarget,
activity,
view,
viewField,
viewSort,
} = this.prismaService.client;
const activitys = await activity.findMany({
@ -151,9 +153,15 @@ export class WorkspaceService {
activity.deleteMany({
where,
}),
view.deleteMany({
where,
}),
viewField.deleteMany({
where,
}),
viewSort.deleteMany({
where,
}),
refreshToken.deleteMany({
where: { userId },
}),

View File

@ -0,0 +1,40 @@
/*
Warnings:
- A unique constraint covering the columns `[workspaceId,viewId,objectName,fieldName]` on the table `viewFields` will be added. If there are existing duplicate values, this will fail.
*/
-- CreateEnum
CREATE TYPE "ViewType" AS ENUM ('Table', 'Pipeline');
-- AlterTable
ALTER TABLE "viewFields" ADD COLUMN "viewId" TEXT;
-- CreateTable
CREATE TABLE "views" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"objectId" TEXT NOT NULL,
"type" "ViewType" NOT NULL,
"workspaceId" TEXT NOT NULL,
CONSTRAINT "views_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "views_workspaceId_type_objectId_name_key" ON "views"("workspaceId", "type", "objectId", "name");
-- CreateIndex
CREATE UNIQUE INDEX "viewFields_workspaceId_viewId_objectName_fieldName_key" ON "viewFields"("workspaceId", "viewId", "objectName", "fieldName");
-- AddForeignKey
ALTER TABLE "pipeline_progresses" ADD CONSTRAINT "pipeline_progresses_companyId_fkey" FOREIGN KEY ("companyId") REFERENCES "companies"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "pipeline_progresses" ADD CONSTRAINT "pipeline_progresses_personId_fkey" FOREIGN KEY ("personId") REFERENCES "people"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "views" ADD CONSTRAINT "views_workspaceId_fkey" FOREIGN KEY ("workspaceId") REFERENCES "workspaces"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "viewFields" ADD CONSTRAINT "viewFields_viewId_fkey" FOREIGN KEY ("viewId") REFERENCES "views"("id") ON DELETE SET NULL ON UPDATE CASCADE;

View File

@ -0,0 +1,19 @@
-- CreateEnum
CREATE TYPE "ViewSortDirection" AS ENUM ('asc', 'desc');
-- CreateTable
CREATE TABLE "viewSorts" (
"direction" "ViewSortDirection" NOT NULL,
"key" TEXT NOT NULL,
"name" TEXT NOT NULL,
"viewId" TEXT NOT NULL,
"workspaceId" TEXT NOT NULL,
CONSTRAINT "viewSorts_pkey" PRIMARY KEY ("viewId","key")
);
-- AddForeignKey
ALTER TABLE "viewSorts" ADD CONSTRAINT "viewSorts_viewId_fkey" FOREIGN KEY ("viewId") REFERENCES "views"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "viewSorts" ADD CONSTRAINT "viewSorts_workspaceId_fkey" FOREIGN KEY ("workspaceId") REFERENCES "workspaces"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@ -174,6 +174,8 @@ model Workspace {
pipelineProgresses PipelineProgress[]
activityTargets ActivityTarget[]
viewFields ViewField[]
views View[]
viewSorts ViewSort[]
/// @TypeGraphQL.omit(input: true, output: true)
deletedAt DateTime?
@ -266,7 +268,7 @@ model Person {
linkedinUrl String?
/// @Validator.IsString()
/// @Validator.IsOptional()
xUrl String?
xUrl String?
/// @Validator.IsString()
/// @Validator.IsOptional()
jobTitle String?
@ -557,6 +559,53 @@ model Attachment {
@@map("attachments")
}
enum ViewType {
Table
Pipeline
}
model View {
/// @Validator.IsString()
/// @Validator.IsOptional()
id String @id @default(uuid())
fields ViewField[]
name String
objectId String
sorts ViewSort[]
type ViewType
/// @TypeGraphQL.omit(input: true, output: true)
workspace Workspace @relation(fields: [workspaceId], references: [id])
/// @TypeGraphQL.omit(input: true, output: true)
workspaceId String
@@unique([workspaceId, type, objectId, name])
@@map("views")
}
enum ViewSortDirection {
asc
desc
}
model ViewSort {
direction ViewSortDirection
key String
name String
view View @relation(fields: [viewId], references: [id])
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("viewSorts")
}
model ViewField {
/// @Validator.IsString()
/// @Validator.IsOptional()
@ -568,10 +617,14 @@ model ViewField {
objectName String
sizeInPx Int
view View? @relation(fields: [viewId], references: [id])
viewId String?
/// @TypeGraphQL.omit(input: true, output: true)
workspace Workspace @relation(fields: [workspaceId], references: [id])
/// @TypeGraphQL.omit(input: true, output: true)
workspaceId String
@@unique([workspaceId, viewId, objectName, fieldName])
@@map("viewFields")
}

View File

@ -16,5 +16,7 @@ export type ModelSelectMap = {
PipelineStage: Prisma.PipelineStageSelect;
PipelineProgress: Prisma.PipelineProgressSelect;
Attachment: Prisma.AttachmentSelect;
View: Prisma.ViewSelect;
ViewSort: Prisma.ViewSortSelect;
ViewField: Prisma.ViewFieldSelect;
};