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. You will be prompted to create a username and password for your Ubuntu installation.
<div style={{textAlign: 'center'}}> <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> </div>
## Setup your developer environment ## Setup your developer environment

View File

@ -846,6 +846,20 @@ export type EnumPipelineProgressableTypeFilter = {
notIn?: InputMaybe<Array<PipelineProgressableType>>; 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 { export enum FileFolder {
Attachment = 'Attachment', Attachment = 'Attachment',
PersonPicture = 'PersonPicture', PersonPicture = 'PersonPicture',
@ -902,6 +916,7 @@ export type Mutation = {
challenge: LoginToken; challenge: LoginToken;
createEvent: Analytics; createEvent: Analytics;
createManyViewField: AffectedRows; createManyViewField: AffectedRows;
createManyViewSort: AffectedRows;
createOneActivity: Activity; createOneActivity: Activity;
createOneComment: Comment; createOneComment: Comment;
createOneCompany: Company; createOneCompany: Company;
@ -913,6 +928,7 @@ export type Mutation = {
deleteManyCompany: AffectedRows; deleteManyCompany: AffectedRows;
deleteManyPerson: AffectedRows; deleteManyPerson: AffectedRows;
deleteManyPipelineProgress: AffectedRows; deleteManyPipelineProgress: AffectedRows;
deleteManyViewSort: AffectedRows;
deleteUserAccount: User; deleteUserAccount: User;
deleteWorkspaceMember: WorkspaceMember; deleteWorkspaceMember: WorkspaceMember;
impersonate: Verify; impersonate: Verify;
@ -924,6 +940,7 @@ export type Mutation = {
updateOnePipelineProgress?: Maybe<PipelineProgress>; updateOnePipelineProgress?: Maybe<PipelineProgress>;
updateOnePipelineStage?: Maybe<PipelineStage>; updateOnePipelineStage?: Maybe<PipelineStage>;
updateOneViewField: ViewField; updateOneViewField: ViewField;
updateOneViewSort: ViewSort;
updateUser: User; updateUser: User;
updateWorkspace: Workspace; updateWorkspace: Workspace;
uploadAttachment: Scalars['String']; uploadAttachment: Scalars['String'];
@ -959,6 +976,12 @@ export type MutationCreateManyViewFieldArgs = {
}; };
export type MutationCreateManyViewSortArgs = {
data: Array<ViewSortCreateManyInput>;
skipDuplicates?: InputMaybe<Scalars['Boolean']>;
};
export type MutationCreateOneActivityArgs = { export type MutationCreateOneActivityArgs = {
data: ActivityCreateInput; data: ActivityCreateInput;
}; };
@ -1009,6 +1032,11 @@ export type MutationDeleteManyPipelineProgressArgs = {
}; };
export type MutationDeleteManyViewSortArgs = {
where?: InputMaybe<ViewSortWhereInput>;
};
export type MutationDeleteWorkspaceMemberArgs = { export type MutationDeleteWorkspaceMemberArgs = {
where: WorkspaceMemberWhereUniqueInput; where: WorkspaceMemberWhereUniqueInput;
}; };
@ -1067,6 +1095,12 @@ export type MutationUpdateOneViewFieldArgs = {
}; };
export type MutationUpdateOneViewSortArgs = {
data: ViewSortUpdateInput;
where: ViewSortWhereUniqueInput;
};
export type MutationUpdateUserArgs = { export type MutationUpdateUserArgs = {
data: UserUpdateInput; data: UserUpdateInput;
where: UserWhereUniqueInput; where: UserWhereUniqueInput;
@ -1178,6 +1212,20 @@ export type NestedEnumPipelineProgressableTypeFilter = {
notIn?: InputMaybe<Array<PipelineProgressableType>>; 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 = { export type NestedIntFilter = {
equals?: InputMaybe<Scalars['Int']>; equals?: InputMaybe<Scalars['Int']>;
gt?: InputMaybe<Scalars['Int']>; gt?: InputMaybe<Scalars['Int']>;
@ -1766,6 +1814,7 @@ export type Query = {
findManyPipelineStage: Array<PipelineStage>; findManyPipelineStage: Array<PipelineStage>;
findManyUser: Array<User>; findManyUser: Array<User>;
findManyViewField: Array<ViewField>; findManyViewField: Array<ViewField>;
findManyViewSort: Array<ViewSort>;
findManyWorkspaceMember: Array<WorkspaceMember>; findManyWorkspaceMember: Array<WorkspaceMember>;
findUniqueCompany: Company; findUniqueCompany: Company;
findUniquePerson: Person; 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 = { export type QueryFindManyWorkspaceMemberArgs = {
cursor?: InputMaybe<WorkspaceMemberWhereUniqueInput>; cursor?: InputMaybe<WorkspaceMemberWhereUniqueInput>;
distinct?: InputMaybe<Array<WorkspaceMemberScalarFieldEnum>>; distinct?: InputMaybe<Array<WorkspaceMemberScalarFieldEnum>>;
@ -2161,6 +2220,20 @@ export type Verify = {
user: User; 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 = { export type ViewField = {
__typename?: 'ViewField'; __typename?: 'ViewField';
fieldName: Scalars['String']; fieldName: Scalars['String'];
@ -2169,6 +2242,8 @@ export type ViewField = {
isVisible: Scalars['Boolean']; isVisible: Scalars['Boolean'];
objectName: Scalars['String']; objectName: Scalars['String'];
sizeInPx: Scalars['Int']; sizeInPx: Scalars['Int'];
view?: Maybe<View>;
viewId?: Maybe<Scalars['String']>;
}; };
export type ViewFieldCreateInput = { export type ViewFieldCreateInput = {
@ -2178,6 +2253,7 @@ export type ViewFieldCreateInput = {
isVisible: Scalars['Boolean']; isVisible: Scalars['Boolean'];
objectName: Scalars['String']; objectName: Scalars['String'];
sizeInPx: Scalars['Int']; sizeInPx: Scalars['Int'];
view?: InputMaybe<ViewCreateNestedOneWithoutFieldsInput>;
}; };
export type ViewFieldCreateManyInput = { export type ViewFieldCreateManyInput = {
@ -2187,6 +2263,17 @@ export type ViewFieldCreateManyInput = {
isVisible: Scalars['Boolean']; isVisible: Scalars['Boolean'];
objectName: Scalars['String']; objectName: Scalars['String'];
sizeInPx: Scalars['Int']; 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 = { export type ViewFieldOrderByWithRelationInput = {
@ -2196,6 +2283,8 @@ export type ViewFieldOrderByWithRelationInput = {
isVisible?: InputMaybe<SortOrder>; isVisible?: InputMaybe<SortOrder>;
objectName?: InputMaybe<SortOrder>; objectName?: InputMaybe<SortOrder>;
sizeInPx?: InputMaybe<SortOrder>; sizeInPx?: InputMaybe<SortOrder>;
view?: InputMaybe<ViewOrderByWithRelationInput>;
viewId?: InputMaybe<SortOrder>;
}; };
export enum ViewFieldScalarFieldEnum { export enum ViewFieldScalarFieldEnum {
@ -2205,6 +2294,7 @@ export enum ViewFieldScalarFieldEnum {
IsVisible = 'isVisible', IsVisible = 'isVisible',
ObjectName = 'objectName', ObjectName = 'objectName',
SizeInPx = 'sizeInPx', SizeInPx = 'sizeInPx',
ViewId = 'viewId',
WorkspaceId = 'workspaceId' WorkspaceId = 'workspaceId'
} }
@ -2215,6 +2305,7 @@ export type ViewFieldUpdateInput = {
isVisible?: InputMaybe<Scalars['Boolean']>; isVisible?: InputMaybe<Scalars['Boolean']>;
objectName?: InputMaybe<Scalars['String']>; objectName?: InputMaybe<Scalars['String']>;
sizeInPx?: InputMaybe<Scalars['Int']>; sizeInPx?: InputMaybe<Scalars['Int']>;
view?: InputMaybe<ViewUpdateOneWithoutFieldsNestedInput>;
}; };
export type ViewFieldUpdateManyWithoutWorkspaceNestedInput = { export type ViewFieldUpdateManyWithoutWorkspaceNestedInput = {
@ -2233,10 +2324,156 @@ export type ViewFieldWhereInput = {
isVisible?: InputMaybe<BoolFilter>; isVisible?: InputMaybe<BoolFilter>;
objectName?: InputMaybe<StringFilter>; objectName?: InputMaybe<StringFilter>;
sizeInPx?: InputMaybe<IntFilter>; sizeInPx?: InputMaybe<IntFilter>;
view?: InputMaybe<ViewRelationFilter>;
viewId?: InputMaybe<StringNullableFilter>;
}; };
export type ViewFieldWhereUniqueInput = { export type ViewFieldWhereUniqueInput = {
id?: InputMaybe<Scalars['String']>; 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 = { export type Workspace = {
@ -2258,6 +2495,8 @@ export type Workspace = {
pipelines?: Maybe<Array<Pipeline>>; pipelines?: Maybe<Array<Pipeline>>;
updatedAt: Scalars['DateTime']; updatedAt: Scalars['DateTime'];
viewFields?: Maybe<Array<ViewField>>; viewFields?: Maybe<Array<ViewField>>;
viewSorts?: Maybe<Array<ViewSort>>;
views?: Maybe<Array<View>>;
workspaceMember?: Maybe<Array<WorkspaceMember>>; workspaceMember?: Maybe<Array<WorkspaceMember>>;
}; };
@ -2337,6 +2576,8 @@ export type WorkspaceUpdateInput = {
pipelines?: InputMaybe<PipelineUpdateManyWithoutWorkspaceNestedInput>; pipelines?: InputMaybe<PipelineUpdateManyWithoutWorkspaceNestedInput>;
updatedAt?: InputMaybe<Scalars['DateTime']>; updatedAt?: InputMaybe<Scalars['DateTime']>;
viewFields?: InputMaybe<ViewFieldUpdateManyWithoutWorkspaceNestedInput>; viewFields?: InputMaybe<ViewFieldUpdateManyWithoutWorkspaceNestedInput>;
viewSorts?: InputMaybe<ViewSortUpdateManyWithoutWorkspaceNestedInput>;
views?: InputMaybe<ViewUpdateManyWithoutWorkspaceNestedInput>;
workspaceMember?: InputMaybe<WorkspaceMemberUpdateManyWithoutWorkspaceNestedInput>; 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 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<{ export type InsertOneCompanyMutationVariables = Exact<{
data: CompanyCreateInput; 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<{ export type DeleteManyCompaniesMutationVariables = Exact<{
ids?: InputMaybe<Array<Scalars['String']> | Scalars['String']>; 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 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<{ export type InsertOnePersonMutationVariables = Exact<{
data: PersonCreateInput; 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<{ export type DeleteManyPersonMutationVariables = Exact<{
ids?: InputMaybe<Array<Scalars['String']> | Scalars['String']>; 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 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<{ export type GetViewFieldsQueryVariables = Exact<{
where?: InputMaybe<ViewFieldWhereInput>; where?: InputMaybe<ViewFieldWhereInput>;
orderBy?: InputMaybe<Array<ViewFieldOrderByWithRelationInput> | ViewFieldOrderByWithRelationInput>; 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 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<{ export type UpdateViewFieldMutationVariables = Exact<{
data: ViewFieldUpdateInput; data: ViewFieldUpdateInput;
where: ViewFieldWhereUniqueInput; 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 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; }>; 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` export const CreateCommentDocument = gql`
mutation CreateComment($commentId: String!, $commentText: String!, $authorId: String!, $activityId: String!, $createdAt: DateTime!) { mutation CreateComment($commentId: String!, $commentText: String!, $authorId: String!, $activityId: String!, $createdAt: DateTime!) {
createOneComment( createOneComment(
@ -3886,16 +4178,10 @@ export type UpdateOneCompanyMutationOptions = Apollo.BaseMutationOptions<UpdateO
export const InsertOneCompanyDocument = gql` export const InsertOneCompanyDocument = gql`
mutation InsertOneCompany($data: CompanyCreateInput!) { mutation InsertOneCompany($data: CompanyCreateInput!) {
createOneCompany(data: $data) { createOneCompany(data: $data) {
address ...InsertCompanyFragment
createdAt
domainName
linkedinUrl
employees
id
name
} }
} }
`; ${InsertCompanyFragmentFragmentDoc}`;
export type InsertOneCompanyMutationFn = Apollo.MutationFunction<InsertOneCompanyMutation, InsertOneCompanyMutationVariables>; export type InsertOneCompanyMutationFn = Apollo.MutationFunction<InsertOneCompanyMutation, InsertOneCompanyMutationVariables>;
/** /**
@ -4372,25 +4658,10 @@ export type UpdateOnePersonMutationOptions = Apollo.BaseMutationOptions<UpdateOn
export const InsertOnePersonDocument = gql` export const InsertOnePersonDocument = gql`
mutation InsertOnePerson($data: PersonCreateInput!) { mutation InsertOnePerson($data: PersonCreateInput!) {
createOnePerson(data: $data) { createOnePerson(data: $data) {
id ...InsertPersonFragment
city
company {
domainName
name
id
}
email
firstName
lastName
jobTitle
linkedinUrl
xUrl
displayName
phone
createdAt
} }
} }
`; ${InsertPersonFragmentFragmentDoc}`;
export type InsertOnePersonMutationFn = Apollo.MutationFunction<InsertOnePersonMutation, InsertOnePersonMutationVariables>; 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 CreateViewFieldsMutationHookResult = ReturnType<typeof useCreateViewFieldsMutation>;
export type CreateViewFieldsMutationResult = Apollo.MutationResult<CreateViewFieldsMutation>; export type CreateViewFieldsMutationResult = Apollo.MutationResult<CreateViewFieldsMutation>;
export type CreateViewFieldsMutationOptions = Apollo.BaseMutationOptions<CreateViewFieldsMutation, CreateViewFieldsMutationVariables>; 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` export const GetViewFieldsDocument = gql`
query GetViewFields($where: ViewFieldWhereInput, $orderBy: [ViewFieldOrderByWithRelationInput!]) { query GetViewFields($where: ViewFieldWhereInput, $orderBy: [ViewFieldOrderByWithRelationInput!]) {
viewFields: findManyViewField(where: $where, orderBy: $orderBy) { viewFields: findManyViewField(where: $where, orderBy: $orderBy) {
@ -5386,6 +5723,43 @@ export function useGetViewFieldsLazyQuery(baseOptions?: Apollo.LazyQueryHookOpti
export type GetViewFieldsQueryHookResult = ReturnType<typeof useGetViewFieldsQuery>; export type GetViewFieldsQueryHookResult = ReturnType<typeof useGetViewFieldsQuery>;
export type GetViewFieldsLazyQueryHookResult = ReturnType<typeof useGetViewFieldsLazyQuery>; export type GetViewFieldsLazyQueryHookResult = ReturnType<typeof useGetViewFieldsLazyQuery>;
export type GetViewFieldsQueryResult = Apollo.QueryResult<GetViewFieldsQuery, GetViewFieldsQueryVariables>; 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` export const UpdateViewFieldDocument = gql`
mutation UpdateViewField($data: ViewFieldUpdateInput!, $where: ViewFieldWhereUniqueInput!) { mutation UpdateViewField($data: ViewFieldUpdateInput!, $where: ViewFieldWhereUniqueInput!) {
updateOneViewField(data: $data, where: $where) { updateOneViewField(data: $data, where: $where) {
@ -5424,6 +5798,42 @@ export function useUpdateViewFieldMutation(baseOptions?: Apollo.MutationHookOpti
export type UpdateViewFieldMutationHookResult = ReturnType<typeof useUpdateViewFieldMutation>; export type UpdateViewFieldMutationHookResult = ReturnType<typeof useUpdateViewFieldMutation>;
export type UpdateViewFieldMutationResult = Apollo.MutationResult<UpdateViewFieldMutation>; export type UpdateViewFieldMutationResult = Apollo.MutationResult<UpdateViewFieldMutation>;
export type UpdateViewFieldMutationOptions = Apollo.BaseMutationOptions<UpdateViewFieldMutation, UpdateViewFieldMutationVariables>; 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` export const GetWorkspaceMembersDocument = gql`
query GetWorkspaceMembers { query GetWorkspaceMembers {
workspaceMembers: findManyWorkspaceMember { workspaceMembers: findManyWorkspaceMember {

View File

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

View File

@ -1,41 +1,19 @@
import { getOperationName } from '@apollo/client/utilities/graphql/getFromAST'; import { useRecoilValue } from 'recoil';
import { useRecoilState, useRecoilValue } from 'recoil';
import { v4 } from 'uuid';
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 { selectedRowIdsSelector } from '@/ui/table/states/selectedRowIdsSelector';
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope'; import { ActivityType, CommentableType } from '~/generated/graphql';
import {
ActivityType,
CommentableType,
useCreateActivityMutation,
} 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 { CommentableEntity } from '../types/CommentableEntity';
import { useOpenCreateActivityDrawer } from './useOpenCreateActivityDrawer';
export function useOpenCreateActivityDrawerForSelectedRowIds() { 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 selectedEntityIds = useRecoilValue(selectedRowIdsSelector);
const openCreateActivityDrawer = useOpenCreateActivityDrawer();
return function openCreateCommentDrawerForSelectedRowIds( return function openCreateCommentDrawerForSelectedRowIds(
type: ActivityType,
entityType: CommentableType, entityType: CommentableType,
) { ) {
const commentableEntityArray: CommentableEntity[] = selectedEntityIds.map( const commentableEntityArray: CommentableEntity[] = selectedEntityIds.map(
@ -44,45 +22,6 @@ export function useOpenCreateActivityDrawerForSelectedRowIds() {
id, id,
}), }),
); );
const now = new Date().toISOString(); openCreateActivityDrawer(type, commentableEntityArray);
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);
},
});
}; };
} }

View File

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

View File

@ -5,12 +5,11 @@ import { CompaniesSelectedSortType } from '../select';
describe('reduceSortsToOrderBy', () => { describe('reduceSortsToOrderBy', () => {
it('should return an array of objects with the id as key and the order as value', () => { it('should return an array of objects with the id as key and the order as value', () => {
const sorts = [ const sorts = [
{ key: 'name', label: 'name', order: 'asc', _type: 'default_sort' }, { key: 'name', label: 'name', order: 'asc' },
{ {
key: 'domainName', key: 'domainName',
label: 'domainName', label: 'domainName',
order: 'desc', order: 'desc',
_type: 'default_sort',
}, },
] satisfies CompaniesSelectedSortType[]; ] satisfies CompaniesSelectedSortType[];
const result = reduceSortsToOrderBy(sorts); 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` export const INSERT_ONE_COMPANY = gql`
mutation InsertOneCompany($data: CompanyCreateInput!) { mutation InsertOneCompany($data: CompanyCreateInput!) {
createOneCompany(data: $data) { createOneCompany(data: $data) {
address ...InsertCompanyFragment
createdAt
domainName
linkedinUrl
employees
id
name
} }
} }
`; `;

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

View File

@ -1,14 +1,24 @@
import { useOpenCreateActivityDrawerForSelectedRowIds } from '@/activities/hooks/useOpenCreateActivityDrawerForSelectedRowIds'; import { useOpenCreateActivityDrawerForSelectedRowIds } from '@/activities/hooks/useOpenCreateActivityDrawerForSelectedRowIds';
import { TableActionBarButtonToggleComments } from '@/ui/table/action-bar/components/TableActionBarButtonOpenComments'; 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() { export function TableActionBarButtonCreateActivityCompany() {
const openCreateActivityRightDrawer = const openCreateActivityRightDrawer =
useOpenCreateActivityDrawerForSelectedRowIds(); useOpenCreateActivityDrawerForSelectedRowIds();
async function handleButtonClick() { async function handleButtonClick(type: ActivityType) {
openCreateActivityRightDrawer(CommentableType.Company); 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 { 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 { GET_PIPELINES } from '@/pipeline/queries';
import { IconTrash } from '@/ui/icon/index'; import { IconTrash } from '@/ui/icon/index';
import { EntityTableActionBarButton } from '@/ui/table/action-bar/components/EntityTableActionBarButton'; import { EntityTableActionBarButton } from '@/ui/table/action-bar/components/EntityTableActionBarButton';
import { useResetTableRowSelection } from '@/ui/table/hooks/useResetTableRowSelection'; import { useResetTableRowSelection } from '@/ui/table/hooks/useResetTableRowSelection';
import { selectedRowIdsSelector } from '@/ui/table/states/selectedRowIdsSelector'; import { selectedRowIdsSelector } from '@/ui/table/states/selectedRowIdsSelector';
import { tableRowIdsState } from '@/ui/table/states/tableRowIdsState';
import { useDeleteManyCompaniesMutation } from '~/generated/graphql'; import { useDeleteManyCompaniesMutation } from '~/generated/graphql';
export function TableActionBarButtonDeleteCompanies() { export function TableActionBarButtonDeleteCompanies() {
@ -15,12 +15,11 @@ export function TableActionBarButtonDeleteCompanies() {
const resetRowSelection = useResetTableRowSelection(); const resetRowSelection = useResetTableRowSelection();
const [deleteCompanies] = useDeleteManyCompaniesMutation({ const [deleteCompanies] = useDeleteManyCompaniesMutation({
refetchQueries: [ refetchQueries: [getOperationName(GET_PIPELINES) ?? ''],
getOperationName(GET_COMPANIES) ?? '',
getOperationName(GET_PIPELINES) ?? '',
],
}); });
const [tableRowIds, setTableRowIds] = useRecoilState(tableRowIdsState);
async function handleDeleteClick() { async function handleDeleteClick() {
const rowIdsToDelete = selectedRowIds; const rowIdsToDelete = selectedRowIds;
@ -30,6 +29,17 @@ export function TableActionBarButtonDeleteCompanies() {
variables: { variables: {
ids: rowIdsToDelete, 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` export const INSERT_ONE_PERSON = gql`
mutation InsertOnePerson($data: PersonCreateInput!) { mutation InsertOnePerson($data: PersonCreateInput!) {
createOnePerson(data: $data) { createOnePerson(data: $data) {
id ...InsertPersonFragment
city
company {
domainName
name
id
}
email
firstName
lastName
jobTitle
linkedinUrl
xUrl
displayName
phone
createdAt
} }
} }
`; `;

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

View File

@ -1,14 +1,24 @@
import { useOpenCreateActivityDrawerForSelectedRowIds } from '@/activities/hooks/useOpenCreateActivityDrawerForSelectedRowIds'; import { useOpenCreateActivityDrawerForSelectedRowIds } from '@/activities/hooks/useOpenCreateActivityDrawerForSelectedRowIds';
import { TableActionBarButtonToggleComments } from '@/ui/table/action-bar/components/TableActionBarButtonOpenComments'; 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() { export function TableActionBarButtonCreateActivityPeople() {
const openCreateActivityRightDrawer = const openCreateActivityRightDrawer =
useOpenCreateActivityDrawerForSelectedRowIds(); useOpenCreateActivityDrawerForSelectedRowIds();
async function handleButtonClick() { async function handleButtonClick(type: ActivityType) {
openCreateActivityRightDrawer(CommentableType.Person); 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 { getOperationName } from '@apollo/client/utilities';
import { useRecoilValue } from 'recoil'; import { useRecoilState, useRecoilValue } from 'recoil';
import { GET_PEOPLE } from '@/people/queries'; import { GET_PEOPLE } from '@/people/queries';
import { IconTrash } from '@/ui/icon/index'; import { IconTrash } from '@/ui/icon/index';
import { EntityTableActionBarButton } from '@/ui/table/action-bar/components/EntityTableActionBarButton'; import { EntityTableActionBarButton } from '@/ui/table/action-bar/components/EntityTableActionBarButton';
import { useResetTableRowSelection } from '@/ui/table/hooks/useResetTableRowSelection'; import { useResetTableRowSelection } from '@/ui/table/hooks/useResetTableRowSelection';
import { selectedRowIdsSelector } from '@/ui/table/states/selectedRowIdsSelector'; import { selectedRowIdsSelector } from '@/ui/table/states/selectedRowIdsSelector';
import { tableRowIdsState } from '@/ui/table/states/tableRowIdsState';
import { useDeleteManyPersonMutation } from '~/generated/graphql'; import { useDeleteManyPersonMutation } from '~/generated/graphql';
export function TableActionBarButtonDeletePeople() { export function TableActionBarButtonDeletePeople() {
const selectedRowIds = useRecoilValue(selectedRowIdsSelector); const selectedRowIds = useRecoilValue(selectedRowIdsSelector);
const [tableRowIds, setTableRowIds] = useRecoilState(tableRowIdsState);
const resetRowSelection = useResetTableRowSelection(); const resetRowSelection = useResetTableRowSelection();
@ -26,6 +28,17 @@ export function TableActionBarButtonDeletePeople() {
variables: { variables: {
ids: rowIdsToDelete, 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'; 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>( export const reduceSortsToOrderBy = <OrderByTemplate>(
sorts: Array<SelectedSortType<OrderByTemplate>>, sorts: SelectedSortType<OrderByTemplate>[],
): OrderByTemplate[] => { ): OrderByTemplate[] =>
const mappedSorts = sorts.map((sort) => { sorts
if (sort.orderByTemplates) { .map((sort) => {
return sort.orderByTemplates?.map((orderByTemplate) => const order = sort.order === 'asc' ? Order_By.Asc : Order_By.Desc;
orderByTemplate(mapOrderToOrder_By(sort.order)), return (
sort.orderByTemplate?.(order) || [
{ [sort.key]: order } as OrderByTemplate,
]
); );
} })
.flat();
return defaultOrderByTemplateFactory(sort.key as string)(sort.order);
});
return mappedSorts.flat() as OrderByTemplate[];
};

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', key: 'sortScopedState',
default: [], 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; label: string;
key: string; key: string;
icon?: ReactNode; icon?: ReactNode;
orderByTemplates?: Array<(order: Order_By) => OrderByTemplate>; orderByTemplate?: (order: Order_By) => OrderByTemplate[];
}; };
export type SelectedSortType<OrderByTemplate> = SortType<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 { useRecoilCallback, useRecoilState, useRecoilValue } from 'recoil';
import { IconButton } from '@/ui/button/components/IconButton'; import { IconButton } from '@/ui/button/components/IconButton';
import type {
ViewFieldDefinition,
ViewFieldMetadata,
} from '@/ui/editable-field/types/ViewField';
import { IconPlus } from '@/ui/icon'; import { IconPlus } from '@/ui/icon';
import { useTrackPointer } from '@/ui/utilities/pointer-event/hooks/useTrackPointer'; import { useTrackPointer } from '@/ui/utilities/pointer-event/hooks/useTrackPointer';
import { GET_VIEW_FIELDS } from '@/views/queries/select'; import { GET_VIEW_FIELDS } from '@/views/queries/select';
import { currentViewIdState } from '@/views/states/currentViewIdState';
import { import {
useCreateViewFieldMutation, useCreateViewFieldMutation,
useUpdateViewFieldMutation, useUpdateViewFieldMutation,
} from '~/generated/graphql'; } from '~/generated/graphql';
import type { import { toViewFieldInput } from '../hooks/useLoadViewFields';
ViewFieldDefinition,
ViewFieldMetadata,
} from '../../editable-field/types/ViewField';
import { toViewFieldInput } from '../hooks/useLoadView';
import { resizeFieldOffsetState } from '../states/resizeFieldOffsetState'; import { resizeFieldOffsetState } from '../states/resizeFieldOffsetState';
import { import {
addableViewFieldDefinitionsState, addableViewFieldDefinitionsState,
@ -89,6 +90,7 @@ export function EntityTableHeader() {
const theme = useTheme(); const theme = useTheme();
const [{ objectName }, setViewFieldsState] = useRecoilState(viewFieldsState); const [{ objectName }, setViewFieldsState] = useRecoilState(viewFieldsState);
const currentViewId = useRecoilValue(currentViewIdState);
const viewFields = useRecoilValue(visibleViewFieldsState); const viewFields = useRecoilValue(visibleViewFieldsState);
const columnWidths = useRecoilValue(columnWidthByViewFieldIdState); const columnWidths = useRecoilValue(columnWidthByViewFieldIdState);
const addableViewFieldDefinitions = useRecoilValue( const addableViewFieldDefinitions = useRecoilValue(
@ -176,15 +178,18 @@ export function EntityTableHeader() {
createViewFieldMutation({ createViewFieldMutation({
variables: { variables: {
data: toViewFieldInput(objectName, { data: {
...toViewFieldInput(objectName, {
...viewFieldDefinition, ...viewFieldDefinition,
columnOrder: viewFields.length + 1, columnOrder: viewFields.length + 1,
}), }),
view: { connect: { id: currentViewId } },
},
}, },
refetchQueries: [getOperationName(GET_VIEW_FIELDS) ?? ''], refetchQueries: [getOperationName(GET_VIEW_FIELDS) ?? ''],
}); });
}, },
[createViewFieldMutation, objectName, viewFields.length], [createViewFieldMutation, currentViewId, objectName, viewFields.length],
); );
return ( return (

View File

@ -6,7 +6,7 @@ import {
import { FilterDefinition } from '@/ui/filter-n-sort/types/FilterDefinition'; import { FilterDefinition } from '@/ui/filter-n-sort/types/FilterDefinition';
import { useSetEntityTableData } from '@/ui/table/hooks/useSetEntityTableData'; import { useSetEntityTableData } from '@/ui/table/hooks/useSetEntityTableData';
import { useLoadView } from '../hooks/useLoadView'; import { useLoadViewFields } from '../hooks/useLoadViewFields';
export function GenericEntityTableData({ export function GenericEntityTableData({
objectName, objectName,
@ -27,7 +27,7 @@ export function GenericEntityTableData({
}) { }) {
const setEntityTableData = useSetEntityTableData(); const setEntityTableData = useSetEntityTableData();
useLoadView({ objectName, viewFieldDefinitions }); useLoadViewFields({ objectName, viewFieldDefinitions });
useGetRequest({ useGetRequest({
variables: { orderBy, where: whereFilters }, 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) { switch (viewField.metadata.entityType) {
case Entity.Company: { case Entity.Company: {

View File

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

View File

@ -1,18 +1,19 @@
import { getOperationName } from '@apollo/client/utilities'; 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 { GET_VIEW_FIELDS } from '@/views/queries/select';
import { currentViewIdState } from '@/views/states/currentViewIdState';
import { import {
SortOrder, SortOrder,
useCreateViewFieldsMutation, useCreateViewFieldsMutation,
useGetViewFieldsQuery, useGetViewFieldsQuery,
} from '~/generated/graphql'; } from '~/generated/graphql';
import type {
ViewFieldDefinition,
ViewFieldMetadata,
ViewFieldTextMetadata,
} from '../../editable-field/types/ViewField';
import { entityTableDimensionsState } from '../states/entityTableDimensionsState'; import { entityTableDimensionsState } from '../states/entityTableDimensionsState';
import { viewFieldsState } from '../states/viewFieldsState'; import { viewFieldsState } from '../states/viewFieldsState';
@ -33,13 +34,14 @@ export const toViewFieldInput = (
sizeInPx: viewFieldDefinition.columnSize, sizeInPx: viewFieldDefinition.columnSize,
}); });
export const useLoadView = ({ export const useLoadViewFields = ({
objectName, objectName,
viewFieldDefinitions, viewFieldDefinitions,
}: { }: {
objectName: 'company' | 'person'; objectName: 'company' | 'person';
viewFieldDefinitions: ViewFieldDefinition<ViewFieldMetadata>[]; viewFieldDefinitions: ViewFieldDefinition<ViewFieldMetadata>[];
}) => { }) => {
const currentViewId = useRecoilValue(currentViewIdState);
const setEntityTableDimensions = useSetRecoilState( const setEntityTableDimensions = useSetRecoilState(
entityTableDimensionsState, entityTableDimensionsState,
); );
@ -50,7 +52,10 @@ export const useLoadView = ({
useGetViewFieldsQuery({ useGetViewFieldsQuery({
variables: { variables: {
orderBy: { index: SortOrder.Asc }, orderBy: { index: SortOrder.Asc },
where: { objectName: { equals: objectName } }, where: {
objectName: { equals: objectName },
viewId: { equals: currentViewId ?? null },
},
}, },
onCompleted: (data) => { onCompleted: (data) => {
if (data.viewFields.length) { if (data.viewFields.length) {
@ -79,9 +84,10 @@ export const useLoadView = ({
// Populate if empty // Populate if empty
createViewFieldsMutation({ createViewFieldsMutation({
variables: { variables: {
data: viewFieldDefinitions.map((viewFieldDefinition) => data: viewFieldDefinitions.map((viewFieldDefinition) => ({
toViewFieldInput(objectName, viewFieldDefinition), ...toViewFieldInput(objectName, viewFieldDefinition),
), viewId: currentViewId,
})),
}, },
refetchQueries: [getOperationName(GET_VIEW_FIELDS) ?? ''], 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 styled from '@emotion/styled';
import { FilterDropdownButton } from '@/ui/filter-n-sort/components/FilterDropdownButton'; import { FilterDropdownButton } from '@/ui/filter-n-sort/components/FilterDropdownButton';
import SortAndFilterBar from '@/ui/filter-n-sort/components/SortAndFilterBar'; import SortAndFilterBar from '@/ui/filter-n-sort/components/SortAndFilterBar';
import { SortDropdownButton } from '@/ui/filter-n-sort/components/SortDropdownButton'; 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 { FiltersHotkeyScope } from '@/ui/filter-n-sort/types/FiltersHotkeyScope';
import { SelectedSortType, SortType } from '@/ui/filter-n-sort/types/interface'; import { SelectedSortType, SortType } from '@/ui/filter-n-sort/types/interface';
import { TopBar } from '@/ui/top-bar/TopBar'; import { TopBar } from '@/ui/top-bar/TopBar';
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
import { OptionsDropdownButton } from '@/views/components/OptionsDropdownButton'; import { OptionsDropdownButton } from '@/views/components/OptionsDropdownButton';
import { TableContext } from '../../states/TableContext'; import { TableContext } from '../../states/TableContext';
@ -34,26 +36,26 @@ export function TableHeader<SortField>({
availableSorts, availableSorts,
onSortsUpdate, onSortsUpdate,
}: OwnProps<SortField>) { }: OwnProps<SortField>) {
const [sorts, innerSetSorts] = useState<Array<SelectedSortType<SortField>>>( const [sorts, setSorts] = useRecoilScopedState<SelectedSortType<SortField>[]>(
[], sortScopedState,
TableContext,
); );
const handleSortsUpdate = onSortsUpdate ?? setSorts;
const sortSelect = useCallback( const sortSelect = useCallback(
(newSort: SelectedSortType<SortField>) => { (newSort: SelectedSortType<SortField>) => {
const newSorts = updateSortOrFilterByKey(sorts, newSort); const newSorts = updateSortOrFilterByKey(sorts, newSort);
innerSetSorts(newSorts); handleSortsUpdate(newSorts);
onSortsUpdate && onSortsUpdate(newSorts);
}, },
[onSortsUpdate, sorts], [handleSortsUpdate, sorts],
); );
const sortUnselect = useCallback( const sortUnselect = useCallback(
(sortKey: string) => { (sortKey: string) => {
const newSorts = sorts.filter((sort) => sort.key !== sortKey); const newSorts = sorts.filter((sort) => sort.key !== sortKey);
innerSetSorts(newSorts); handleSortsUpdate(newSorts);
onSortsUpdate && onSortsUpdate(newSorts);
}, },
[onSortsUpdate, sorts], [handleSortsUpdate, sorts],
); );
return ( return (
@ -88,8 +90,7 @@ export function TableHeader<SortField>({
sorts={sorts} sorts={sorts}
onRemoveSort={sortUnselect} onRemoveSort={sortUnselect}
onCancelClick={() => { onCancelClick={() => {
innerSetSorts([]); handleSortsUpdate([]);
onSortsUpdate && onSortsUpdate([]);
}} }}
/> />
} }

View File

@ -1,10 +1,10 @@
import { Context, useContext } from 'react'; import { Context, useContext } from 'react';
import { RecoilState, useRecoilValue } from 'recoil'; import { RecoilState, RecoilValueReadOnly, useRecoilValue } from 'recoil';
import { RecoilScopeContext } from '../states/RecoilScopeContext'; import { RecoilScopeContext } from '../states/RecoilScopeContext';
export function useRecoilScopedValue<T>( export function useRecoilScopedValue<T>(
recoilState: (param: string) => RecoilState<T>, recoilState: (param: string) => RecoilState<T> | RecoilValueReadOnly<T>,
SpecificContext?: Context<string | null>, SpecificContext?: Context<string | null>,
) { ) {
const recoilScopeId = useContext(SpecificContext ?? RecoilScopeContext); 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 { getOperationName } from '@apollo/client/utilities';
import { useTheme } from '@emotion/react'; import { useTheme } from '@emotion/react';
import styled from '@emotion/styled'; 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 { CompanyTable } from '@/companies/table/components/CompanyTable';
import { TableActionBarButtonCreateActivityCompany } from '@/companies/table/components/TableActionBarButtonCreateActivityCompany'; import { TableActionBarButtonCreateActivityCompany } from '@/companies/table/components/TableActionBarButtonCreateActivityCompany';
import { TableActionBarButtonDeleteCompanies } from '@/companies/table/components/TableActionBarButtonDeleteCompanies'; import { TableActionBarButtonDeleteCompanies } from '@/companies/table/components/TableActionBarButtonDeleteCompanies';
import { SEARCH_COMPANY_QUERY } from '@/search/queries/search';
import { IconBuildingSkyscraper } from '@/ui/icon'; import { IconBuildingSkyscraper } from '@/ui/icon';
import { WithTopBarContainer } from '@/ui/layout/components/WithTopBarContainer'; import { WithTopBarContainer } from '@/ui/layout/components/WithTopBarContainer';
import { EntityTableActionBar } from '@/ui/table/action-bar/components/EntityTableActionBar'; import { EntityTableActionBar } from '@/ui/table/action-bar/components/EntityTableActionBar';
import { TableContext } from '@/ui/table/states/TableContext'; import { TableContext } from '@/ui/table/states/TableContext';
import { tableRowIdsState } from '@/ui/table/states/tableRowIdsState';
import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope'; import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope';
import { useInsertOneCompanyMutation } from '~/generated/graphql'; import { useInsertOneCompanyMutation } from '~/generated/graphql';
import { SEARCH_COMPANY_QUERY } from '../../modules/search/queries/search';
const StyledTableContainer = styled.div` const StyledTableContainer = styled.div`
display: flex; display: flex;
width: 100%; width: 100%;
@ -22,20 +23,35 @@ const StyledTableContainer = styled.div`
export function Companies() { export function Companies() {
const [insertCompany] = useInsertOneCompanyMutation(); const [insertCompany] = useInsertOneCompanyMutation();
const [tableRowIds, setTableRowIds] = useRecoilState(tableRowIdsState);
async function handleAddButtonClick() { async function handleAddButtonClick() {
const newCompanyId: string = v4();
await insertCompany({ await insertCompany({
variables: { variables: {
data: { data: {
id: newCompanyId,
name: '', name: '',
domainName: '', domainName: '',
address: '', address: '',
}, },
}, },
refetchQueries: [ optimisticResponse: {
getOperationName(GET_COMPANIES) ?? '', __typename: 'Mutation',
getOperationName(SEARCH_COMPANY_QUERY) ?? '', 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'; } from '@/ui/icon/index';
import { CompanyOrderByWithRelationInput as Companies_Order_By } from '~/generated/graphql'; import { CompanyOrderByWithRelationInput as Companies_Order_By } from '~/generated/graphql';
export const availableSorts = [ export const availableSorts: SortType<Companies_Order_By>[] = [
{ {
key: 'name', key: 'name',
label: 'Name', label: 'Name',
@ -34,4 +34,4 @@ export const availableSorts = [
label: 'Creation', label: 'Creation',
icon: <IconCalendarEvent size={16} />, 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 { useTheme } from '@emotion/react';
import styled from '@emotion/styled'; 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 { PeopleTable } from '@/people/table/components/PeopleTable';
import { TableActionBarButtonCreateActivityPeople } from '@/people/table/components/TableActionBarButtonCreateActivityPeople'; import { TableActionBarButtonCreateActivityPeople } from '@/people/table/components/TableActionBarButtonCreateActivityPeople';
import { TableActionBarButtonDeletePeople } from '@/people/table/components/TableActionBarButtonDeletePeople'; import { TableActionBarButtonDeletePeople } from '@/people/table/components/TableActionBarButtonDeletePeople';
@ -10,6 +10,7 @@ import { IconUser } from '@/ui/icon';
import { WithTopBarContainer } from '@/ui/layout/components/WithTopBarContainer'; import { WithTopBarContainer } from '@/ui/layout/components/WithTopBarContainer';
import { EntityTableActionBar } from '@/ui/table/action-bar/components/EntityTableActionBar'; import { EntityTableActionBar } from '@/ui/table/action-bar/components/EntityTableActionBar';
import { TableContext } from '@/ui/table/states/TableContext'; import { TableContext } from '@/ui/table/states/TableContext';
import { tableRowIdsState } from '@/ui/table/states/tableRowIdsState';
import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope'; import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope';
import { useInsertOnePersonMutation } from '~/generated/graphql'; import { useInsertOnePersonMutation } from '~/generated/graphql';
@ -20,16 +21,33 @@ const StyledTableContainer = styled.div`
export function People() { export function People() {
const [insertOnePerson] = useInsertOnePersonMutation(); const [insertOnePerson] = useInsertOnePersonMutation();
const [tableRowIds, setTableRowIds] = useRecoilState(tableRowIdsState);
async function handleAddButtonClick() { async function handleAddButtonClick() {
const newPersonId: string = v4();
await insertOnePerson({ await insertOnePerson({
variables: { variables: {
data: { data: {
id: newPersonId,
firstName: '', firstName: '',
lastName: '', 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, SortOrder as Order_By,
} from '~/generated/graphql'; } from '~/generated/graphql';
export const availableSorts = [ export const availableSorts: SortType<People_Order_By>[] = [
{ {
key: 'fullname', key: 'fullname',
label: 'People', label: 'People',
icon: <IconUser size={16} />, icon: <IconUser size={16} />,
orderByTemplates: [ orderByTemplate: (order: Order_By) => [
(order: Order_By) => ({ { firstName: order },
firstName: order, { lastName: order },
}),
(order: Order_By) => ({
lastName: order,
}),
], ],
}, },
{ {
@ -32,7 +28,7 @@ export const availableSorts = [
label: 'Company', label: 'Company',
icon: <IconBuildingSkyscraper size={16} />, icon: <IconBuildingSkyscraper size={16} />,
orderByTemplates: [(order: Order_By) => ({ company: { name: order } })], orderByTemplate: (order: Order_By) => [{ company: { name: order } }],
}, },
{ {
key: 'email', key: 'email',
@ -54,4 +50,4 @@ export const availableSorts = [
label: 'City', label: 'City',
icon: <IconMap size={16} />, icon: <IconMap size={16} />,
}, },
] satisfies Array<SortType<People_Order_By>>; ];

View File

@ -186,14 +186,22 @@ export function AuthAutoRouter() {
label: 'Create Task', label: 'Create Task',
type: CommandType.Create, type: CommandType.Create,
icon: <IconCheckbox />, icon: <IconCheckbox />,
onCommandClick: () => openCreateActivity(ActivityType.Task, entity), onCommandClick: () =>
openCreateActivity(
ActivityType.Task,
entity ? [entity] : undefined,
),
}, },
{ {
to: '', to: '',
label: 'Create Note', label: 'Create Note',
type: CommandType.Create, type: CommandType.Create,
icon: <IconNotes />, icon: <IconNotes />,
onCommandClick: () => openCreateActivity(ActivityType.Note, entity), onCommandClick: () =>
openCreateActivity(
ActivityType.Note,
entity ? [entity] : undefined,
),
}, },
]); ]);
break; break;
@ -212,14 +220,22 @@ export function AuthAutoRouter() {
label: 'Create Task', label: 'Create Task',
type: CommandType.Create, type: CommandType.Create,
icon: <IconCheckbox />, icon: <IconCheckbox />,
onCommandClick: () => openCreateActivity(ActivityType.Task, entity), onCommandClick: () =>
openCreateActivity(
ActivityType.Task,
entity ? [entity] : undefined,
),
}, },
{ {
to: '', to: '',
label: 'Create Note', label: 'Create Note',
type: CommandType.Create, type: CommandType.Create,
icon: <IconNotes />, icon: <IconNotes />,
onCommandClick: () => openCreateActivity(ActivityType.Note, entity), onCommandClick: () =>
openCreateActivity(
ActivityType.Note,
entity ? [entity] : undefined,
),
}, },
]); ]);
break; 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, PipelineStage,
PipelineProgress, PipelineProgress,
UserSettings, UserSettings,
View,
ViewField, ViewField,
ViewSort,
} from '@prisma/client'; } from '@prisma/client';
import { AbilityAction } from './ability.action'; import { AbilityAction } from './ability.action';
@ -37,7 +39,9 @@ type SubjectsAbility = Subjects<{
PipelineProgress: PipelineProgress; PipelineProgress: PipelineProgress;
Attachment: Attachment; Attachment: Attachment;
UserSettings: UserSettings; UserSettings: UserSettings;
View: View;
ViewField: ViewField; ViewField: ViewField;
ViewSort: ViewSort;
}>; }>;
export type AppAbility = PureAbility< export type AppAbility = PureAbility<
@ -130,11 +134,22 @@ export class AbilityFactory {
workspaceId: workspace.id, 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 // ViewField
can(AbilityAction.Read, 'ViewField', { workspaceId: workspace.id }); can(AbilityAction.Read, 'ViewField', { workspaceId: workspace.id });
can(AbilityAction.Create, 'ViewField', { workspaceId: workspace.id }); can(AbilityAction.Create, 'ViewField', { workspaceId: workspace.id });
can(AbilityAction.Update, '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(); return build();
} }
} }

View File

@ -99,6 +99,12 @@ import {
ReadViewFieldAbilityHandler, ReadViewFieldAbilityHandler,
UpdateViewFieldAbilityHandler, UpdateViewFieldAbilityHandler,
} from './handlers/view-field.ability-handler'; } from './handlers/view-field.ability-handler';
import {
CreateViewSortAbilityHandler,
ReadViewSortAbilityHandler,
UpdateViewSortAbilityHandler,
DeleteViewSortAbilityHandler,
} from './handlers/view-sort.ability-handler';
@Global() @Global()
@Module({ @Module({
@ -187,6 +193,11 @@ import {
ReadViewFieldAbilityHandler, ReadViewFieldAbilityHandler,
CreateViewFieldAbilityHandler, CreateViewFieldAbilityHandler,
UpdateViewFieldAbilityHandler, UpdateViewFieldAbilityHandler,
// ViewSort
ReadViewSortAbilityHandler,
CreateViewSortAbilityHandler,
UpdateViewSortAbilityHandler,
DeleteViewSortAbilityHandler,
], ],
exports: [ exports: [
AbilityFactory, AbilityFactory,
@ -272,6 +283,11 @@ import {
ReadViewFieldAbilityHandler, ReadViewFieldAbilityHandler,
CreateViewFieldAbilityHandler, CreateViewFieldAbilityHandler,
UpdateViewFieldAbilityHandler, UpdateViewFieldAbilityHandler,
// ViewSort
ReadViewSortAbilityHandler,
CreateViewSortAbilityHandler,
UpdateViewSortAbilityHandler,
DeleteViewSortAbilityHandler,
], ],
}) })
export class AbilityModule {} 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>> { ): Promise<Partial<ViewField>> {
return this.viewFieldService.create({ return this.viewFieldService.create({
data: { data: {
...args.data, ...(args.data as Prisma.ViewFieldCreateInput),
workspace: { connect: { id: workspace.id } }, workspace: { connect: { id: workspace.id } },
}, },
select: prismaSelect.value, 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 { ViewFieldService } from './services/view-field.service';
import { ViewFieldResolver } from './resolvers/view-field.resolver'; import { ViewFieldResolver } from './resolvers/view-field.resolver';
import { ViewSortService } from './services/view-sort.service';
import { ViewSortResolver } from './resolvers/view-sort.resolver';
@Module({ @Module({
providers: [ViewFieldService, ViewFieldResolver], providers: [
ViewFieldService,
ViewSortService,
ViewFieldResolver,
ViewSortResolver,
],
}) })
export class ViewModule {} export class ViewModule {}

View File

@ -111,7 +111,9 @@ export class WorkspaceService {
comment, comment,
activityTarget, activityTarget,
activity, activity,
view,
viewField, viewField,
viewSort,
} = this.prismaService.client; } = this.prismaService.client;
const activitys = await activity.findMany({ const activitys = await activity.findMany({
@ -151,9 +153,15 @@ export class WorkspaceService {
activity.deleteMany({ activity.deleteMany({
where, where,
}), }),
view.deleteMany({
where,
}),
viewField.deleteMany({ viewField.deleteMany({
where, where,
}), }),
viewSort.deleteMany({
where,
}),
refreshToken.deleteMany({ refreshToken.deleteMany({
where: { userId }, 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[] pipelineProgresses PipelineProgress[]
activityTargets ActivityTarget[] activityTargets ActivityTarget[]
viewFields ViewField[] viewFields ViewField[]
views View[]
viewSorts ViewSort[]
/// @TypeGraphQL.omit(input: true, output: true) /// @TypeGraphQL.omit(input: true, output: true)
deletedAt DateTime? deletedAt DateTime?
@ -557,6 +559,53 @@ model Attachment {
@@map("attachments") @@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 { model ViewField {
/// @Validator.IsString() /// @Validator.IsString()
/// @Validator.IsOptional() /// @Validator.IsOptional()
@ -568,10 +617,14 @@ model ViewField {
objectName String objectName String
sizeInPx Int sizeInPx Int
view View? @relation(fields: [viewId], references: [id])
viewId String?
/// @TypeGraphQL.omit(input: true, output: true) /// @TypeGraphQL.omit(input: true, output: true)
workspace Workspace @relation(fields: [workspaceId], references: [id]) workspace Workspace @relation(fields: [workspaceId], references: [id])
/// @TypeGraphQL.omit(input: true, output: true) /// @TypeGraphQL.omit(input: true, output: true)
workspaceId String workspaceId String
@@unique([workspaceId, viewId, objectName, fieldName])
@@map("viewFields") @@map("viewFields")
} }

View File

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