Add "show company / people" view and "Notes" concept (#528)

* Begin adding show view and refactoring threads to become notes

* Progress on design

* Progress redesign timeline

* Dropdown button, design improvement

* Open comment thread edit mode in drawer

* Autosave local storage and commentThreadcount

* Improve display and fix missing key issue

* Remove some hardcoded CSS properties

* Create button

* Split company show into ui/business + fix eslint

* Fix font weight

* Begin auto-save on edit mode

* Save server-side query result to Apollo cache

* Fix save behavior

* Refetch timeline after creating note

* Rename createCommentThreadWithComment

* Improve styling

* Revert "Improve styling"

This reverts commit 9fbbf2db00.

* Improve CSS styling

* Bring back border radius inadvertently removed

* padding adjustment

* Improve blocknote design

* Improve edit mode display

* Remove Comments.tsx

* Remove irrelevant comment stories

* Removed un-necessary panel component

* stop using fragment, move trash icon

* Add a basic story for CompanyShow

* Add a basic People show view

* Fix storybook tests

* Add very basic Person story

* Refactor PR1

* Refactor part 2

* Refactor part 3

* Refactor part 4

* Fix tests

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Félix Malfait 2023-07-10 07:25:34 +02:00 committed by GitHub
parent ca180acf9f
commit 94a913a41f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
191 changed files with 5390 additions and 721 deletions

View File

@ -31,6 +31,7 @@
"!javascript",
"!json",
"!typescript",
"!typescriptreact",
"md",
"mdx"
],

View File

@ -1,12 +1,24 @@
const path = require("path");
module.exports = {
devServer: {
client: {
overlay: {
runtimeErrors: (error) => {
if (error.message === "ResizeObserver loop limit exceeded") {
return false;
}
return true;
},
},
}
},
webpack: {
alias: {
'~': path.resolve(__dirname, 'src'),
'@': path.resolve(__dirname, 'src/modules'),
'@testing': path.resolve(__dirname, 'src/testing'),
}
},
},
jest: {
configure: {

View File

@ -5,6 +5,8 @@
"dependencies": {
"@apollo/client": "^3.7.5",
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
"@blocknote/core": "^0.8.2",
"@blocknote/react": "^0.8.2",
"@emotion/react": "^11.10.6",
"@emotion/styled": "^11.10.5",
"@floating-ui/react": "^0.24.3",

View File

@ -17,6 +17,8 @@ import { People } from '~/pages/people/People';
import { SettingsProfile } from '~/pages/settings/SettingsProfile';
import { SettingsWorkspaceMembers } from '~/pages/settings/SettingsWorkspaceMembers';
import { CompanyShow } from './pages/companies/CompanyShow';
import { PersonShow } from './pages/people/PersonShow';
import { AppInternalHooks } from './AppInternalHooks';
/**
@ -65,7 +67,13 @@ export function App() {
<Routes>
<Route path="" element={<Navigate to="/people" replace />} />
<Route path="people" element={<People />} />
<Route path="person/:personId" element={<PersonShow />} />
<Route path="companies" element={<Companies />} />
<Route
path="companies/:companyId"
element={<CompanyShow />}
/>
<Route path="opportunities" element={<Opportunities />} />
<Route
path="settings/*"

View File

@ -206,24 +206,47 @@ export type CommentScalarWhereInput = {
export type CommentThread = {
__typename?: 'CommentThread';
author: User;
authorId: Scalars['String'];
body?: Maybe<Scalars['String']>;
commentThreadTargets?: Maybe<Array<CommentThreadTarget>>;
comments?: Maybe<Array<Comment>>;
createdAt: Scalars['DateTime'];
id: Scalars['ID'];
title?: Maybe<Scalars['String']>;
updatedAt: Scalars['DateTime'];
};
export type CommentThreadCreateInput = {
author: UserCreateNestedOneWithoutCommentThreadInput;
body?: InputMaybe<Scalars['String']>;
commentThreadTargets?: InputMaybe<CommentThreadTargetCreateNestedManyWithoutCommentThreadInput>;
comments?: InputMaybe<CommentCreateNestedManyWithoutCommentThreadInput>;
createdAt?: InputMaybe<Scalars['DateTime']>;
id?: InputMaybe<Scalars['String']>;
title?: InputMaybe<Scalars['String']>;
updatedAt?: InputMaybe<Scalars['DateTime']>;
};
export type CommentThreadCreateManyWorkspaceInput = {
export type CommentThreadCreateManyAuthorInput = {
body?: InputMaybe<Scalars['String']>;
createdAt?: InputMaybe<Scalars['DateTime']>;
id?: InputMaybe<Scalars['String']>;
title?: InputMaybe<Scalars['String']>;
updatedAt?: InputMaybe<Scalars['DateTime']>;
};
export type CommentThreadCreateManyAuthorInputEnvelope = {
data: Array<CommentThreadCreateManyAuthorInput>;
skipDuplicates?: InputMaybe<Scalars['Boolean']>;
};
export type CommentThreadCreateManyWorkspaceInput = {
authorId: Scalars['String'];
body?: InputMaybe<Scalars['String']>;
createdAt?: InputMaybe<Scalars['DateTime']>;
id?: InputMaybe<Scalars['String']>;
title?: InputMaybe<Scalars['String']>;
updatedAt?: InputMaybe<Scalars['DateTime']>;
};
@ -232,10 +255,22 @@ export type CommentThreadCreateManyWorkspaceInputEnvelope = {
skipDuplicates?: InputMaybe<Scalars['Boolean']>;
};
export type CommentThreadCreateNestedManyWithoutAuthorInput = {
connect?: InputMaybe<Array<CommentThreadWhereUniqueInput>>;
connectOrCreate?: InputMaybe<Array<CommentThreadCreateOrConnectWithoutAuthorInput>>;
create?: InputMaybe<Array<CommentThreadCreateWithoutAuthorInput>>;
createMany?: InputMaybe<CommentThreadCreateManyAuthorInputEnvelope>;
};
export type CommentThreadCreateNestedOneWithoutCommentsInput = {
connect?: InputMaybe<CommentThreadWhereUniqueInput>;
};
export type CommentThreadCreateOrConnectWithoutAuthorInput = {
create: CommentThreadCreateWithoutAuthorInput;
where: CommentThreadWhereUniqueInput;
};
export type CommentThreadCreateOrConnectWithoutCommentsInput = {
create: CommentThreadCreateWithoutCommentsInput;
where: CommentThreadWhereUniqueInput;
@ -246,26 +281,56 @@ export type CommentThreadCreateOrConnectWithoutWorkspaceInput = {
where: CommentThreadWhereUniqueInput;
};
export type CommentThreadCreateWithoutCommentsInput = {
commentThreadTargets?: InputMaybe<CommentThreadTargetCreateNestedManyWithoutCommentThreadInput>;
createdAt?: InputMaybe<Scalars['DateTime']>;
id?: InputMaybe<Scalars['String']>;
updatedAt?: InputMaybe<Scalars['DateTime']>;
};
export type CommentThreadCreateWithoutWorkspaceInput = {
export type CommentThreadCreateWithoutAuthorInput = {
body?: InputMaybe<Scalars['String']>;
commentThreadTargets?: InputMaybe<CommentThreadTargetCreateNestedManyWithoutCommentThreadInput>;
comments?: InputMaybe<CommentCreateNestedManyWithoutCommentThreadInput>;
createdAt?: InputMaybe<Scalars['DateTime']>;
id?: InputMaybe<Scalars['String']>;
title?: InputMaybe<Scalars['String']>;
updatedAt?: InputMaybe<Scalars['DateTime']>;
};
export type CommentThreadCreateWithoutCommentsInput = {
author: UserCreateNestedOneWithoutCommentThreadInput;
body?: InputMaybe<Scalars['String']>;
commentThreadTargets?: InputMaybe<CommentThreadTargetCreateNestedManyWithoutCommentThreadInput>;
createdAt?: InputMaybe<Scalars['DateTime']>;
id?: InputMaybe<Scalars['String']>;
title?: InputMaybe<Scalars['String']>;
updatedAt?: InputMaybe<Scalars['DateTime']>;
};
export type CommentThreadCreateWithoutWorkspaceInput = {
author: UserCreateNestedOneWithoutCommentThreadInput;
body?: InputMaybe<Scalars['String']>;
commentThreadTargets?: InputMaybe<CommentThreadTargetCreateNestedManyWithoutCommentThreadInput>;
comments?: InputMaybe<CommentCreateNestedManyWithoutCommentThreadInput>;
createdAt?: InputMaybe<Scalars['DateTime']>;
id?: InputMaybe<Scalars['String']>;
title?: InputMaybe<Scalars['String']>;
updatedAt?: InputMaybe<Scalars['DateTime']>;
};
export type CommentThreadListRelationFilter = {
every?: InputMaybe<CommentThreadWhereInput>;
none?: InputMaybe<CommentThreadWhereInput>;
some?: InputMaybe<CommentThreadWhereInput>;
};
export type CommentThreadOrderByRelationAggregateInput = {
_count?: InputMaybe<SortOrder>;
};
export type CommentThreadOrderByWithRelationInput = {
author?: InputMaybe<UserOrderByWithRelationInput>;
authorId?: InputMaybe<SortOrder>;
body?: InputMaybe<SortOrder>;
commentThreadTargets?: InputMaybe<CommentThreadTargetOrderByRelationAggregateInput>;
comments?: InputMaybe<CommentOrderByRelationAggregateInput>;
createdAt?: InputMaybe<SortOrder>;
id?: InputMaybe<SortOrder>;
title?: InputMaybe<SortOrder>;
updatedAt?: InputMaybe<SortOrder>;
};
@ -275,9 +340,12 @@ export type CommentThreadRelationFilter = {
};
export enum CommentThreadScalarFieldEnum {
AuthorId = 'authorId',
Body = 'body',
CreatedAt = 'createdAt',
DeletedAt = 'deletedAt',
Id = 'id',
Title = 'title',
UpdatedAt = 'updatedAt',
WorkspaceId = 'workspaceId'
}
@ -286,8 +354,11 @@ export type CommentThreadScalarWhereInput = {
AND?: InputMaybe<Array<CommentThreadScalarWhereInput>>;
NOT?: InputMaybe<Array<CommentThreadScalarWhereInput>>;
OR?: InputMaybe<Array<CommentThreadScalarWhereInput>>;
authorId?: InputMaybe<StringFilter>;
body?: InputMaybe<StringNullableFilter>;
createdAt?: InputMaybe<DateTimeFilter>;
id?: InputMaybe<StringFilter>;
title?: InputMaybe<StringNullableFilter>;
updatedAt?: InputMaybe<DateTimeFilter>;
};
@ -418,24 +489,48 @@ export type CommentThreadTargetWhereUniqueInput = {
};
export type CommentThreadUpdateInput = {
author?: InputMaybe<UserUpdateOneRequiredWithoutCommentThreadNestedInput>;
body?: InputMaybe<NullableStringFieldUpdateOperationsInput>;
commentThreadTargets?: InputMaybe<CommentThreadTargetUpdateManyWithoutCommentThreadNestedInput>;
comments?: InputMaybe<CommentUpdateManyWithoutCommentThreadNestedInput>;
createdAt?: InputMaybe<DateTimeFieldUpdateOperationsInput>;
id?: InputMaybe<StringFieldUpdateOperationsInput>;
title?: InputMaybe<NullableStringFieldUpdateOperationsInput>;
updatedAt?: InputMaybe<DateTimeFieldUpdateOperationsInput>;
};
export type CommentThreadUpdateManyMutationInput = {
body?: InputMaybe<NullableStringFieldUpdateOperationsInput>;
createdAt?: InputMaybe<DateTimeFieldUpdateOperationsInput>;
id?: InputMaybe<StringFieldUpdateOperationsInput>;
title?: InputMaybe<NullableStringFieldUpdateOperationsInput>;
updatedAt?: InputMaybe<DateTimeFieldUpdateOperationsInput>;
};
export type CommentThreadUpdateManyWithWhereWithoutAuthorInput = {
data: CommentThreadUpdateManyMutationInput;
where: CommentThreadScalarWhereInput;
};
export type CommentThreadUpdateManyWithWhereWithoutWorkspaceInput = {
data: CommentThreadUpdateManyMutationInput;
where: CommentThreadScalarWhereInput;
};
export type CommentThreadUpdateManyWithoutAuthorNestedInput = {
connect?: InputMaybe<Array<CommentThreadWhereUniqueInput>>;
connectOrCreate?: InputMaybe<Array<CommentThreadCreateOrConnectWithoutAuthorInput>>;
create?: InputMaybe<Array<CommentThreadCreateWithoutAuthorInput>>;
createMany?: InputMaybe<CommentThreadCreateManyAuthorInputEnvelope>;
delete?: InputMaybe<Array<CommentThreadWhereUniqueInput>>;
deleteMany?: InputMaybe<Array<CommentThreadScalarWhereInput>>;
disconnect?: InputMaybe<Array<CommentThreadWhereUniqueInput>>;
set?: InputMaybe<Array<CommentThreadWhereUniqueInput>>;
update?: InputMaybe<Array<CommentThreadUpdateWithWhereUniqueWithoutAuthorInput>>;
updateMany?: InputMaybe<Array<CommentThreadUpdateManyWithWhereWithoutAuthorInput>>;
upsert?: InputMaybe<Array<CommentThreadUpsertWithWhereUniqueWithoutAuthorInput>>;
};
export type CommentThreadUpdateManyWithoutWorkspaceNestedInput = {
connect?: InputMaybe<Array<CommentThreadWhereUniqueInput>>;
connectOrCreate?: InputMaybe<Array<CommentThreadCreateOrConnectWithoutWorkspaceInput>>;
@ -458,26 +553,53 @@ export type CommentThreadUpdateOneRequiredWithoutCommentsNestedInput = {
upsert?: InputMaybe<CommentThreadUpsertWithoutCommentsInput>;
};
export type CommentThreadUpdateWithWhereUniqueWithoutAuthorInput = {
data: CommentThreadUpdateWithoutAuthorInput;
where: CommentThreadWhereUniqueInput;
};
export type CommentThreadUpdateWithWhereUniqueWithoutWorkspaceInput = {
data: CommentThreadUpdateWithoutWorkspaceInput;
where: CommentThreadWhereUniqueInput;
};
export type CommentThreadUpdateWithoutCommentsInput = {
commentThreadTargets?: InputMaybe<CommentThreadTargetUpdateManyWithoutCommentThreadNestedInput>;
createdAt?: InputMaybe<DateTimeFieldUpdateOperationsInput>;
id?: InputMaybe<StringFieldUpdateOperationsInput>;
updatedAt?: InputMaybe<DateTimeFieldUpdateOperationsInput>;
};
export type CommentThreadUpdateWithoutWorkspaceInput = {
export type CommentThreadUpdateWithoutAuthorInput = {
body?: InputMaybe<NullableStringFieldUpdateOperationsInput>;
commentThreadTargets?: InputMaybe<CommentThreadTargetUpdateManyWithoutCommentThreadNestedInput>;
comments?: InputMaybe<CommentUpdateManyWithoutCommentThreadNestedInput>;
createdAt?: InputMaybe<DateTimeFieldUpdateOperationsInput>;
id?: InputMaybe<StringFieldUpdateOperationsInput>;
title?: InputMaybe<NullableStringFieldUpdateOperationsInput>;
updatedAt?: InputMaybe<DateTimeFieldUpdateOperationsInput>;
};
export type CommentThreadUpdateWithoutCommentsInput = {
author?: InputMaybe<UserUpdateOneRequiredWithoutCommentThreadNestedInput>;
body?: InputMaybe<NullableStringFieldUpdateOperationsInput>;
commentThreadTargets?: InputMaybe<CommentThreadTargetUpdateManyWithoutCommentThreadNestedInput>;
createdAt?: InputMaybe<DateTimeFieldUpdateOperationsInput>;
id?: InputMaybe<StringFieldUpdateOperationsInput>;
title?: InputMaybe<NullableStringFieldUpdateOperationsInput>;
updatedAt?: InputMaybe<DateTimeFieldUpdateOperationsInput>;
};
export type CommentThreadUpdateWithoutWorkspaceInput = {
author?: InputMaybe<UserUpdateOneRequiredWithoutCommentThreadNestedInput>;
body?: InputMaybe<NullableStringFieldUpdateOperationsInput>;
commentThreadTargets?: InputMaybe<CommentThreadTargetUpdateManyWithoutCommentThreadNestedInput>;
comments?: InputMaybe<CommentUpdateManyWithoutCommentThreadNestedInput>;
createdAt?: InputMaybe<DateTimeFieldUpdateOperationsInput>;
id?: InputMaybe<StringFieldUpdateOperationsInput>;
title?: InputMaybe<NullableStringFieldUpdateOperationsInput>;
updatedAt?: InputMaybe<DateTimeFieldUpdateOperationsInput>;
};
export type CommentThreadUpsertWithWhereUniqueWithoutAuthorInput = {
create: CommentThreadCreateWithoutAuthorInput;
update: CommentThreadUpdateWithoutAuthorInput;
where: CommentThreadWhereUniqueInput;
};
export type CommentThreadUpsertWithWhereUniqueWithoutWorkspaceInput = {
create: CommentThreadCreateWithoutWorkspaceInput;
update: CommentThreadUpdateWithoutWorkspaceInput;
@ -493,10 +615,14 @@ export type CommentThreadWhereInput = {
AND?: InputMaybe<Array<CommentThreadWhereInput>>;
NOT?: InputMaybe<Array<CommentThreadWhereInput>>;
OR?: InputMaybe<Array<CommentThreadWhereInput>>;
author?: InputMaybe<UserRelationFilter>;
authorId?: InputMaybe<StringFilter>;
body?: InputMaybe<StringNullableFilter>;
commentThreadTargets?: InputMaybe<CommentThreadTargetListRelationFilter>;
comments?: InputMaybe<CommentListRelationFilter>;
createdAt?: InputMaybe<DateTimeFilter>;
id?: InputMaybe<StringFilter>;
title?: InputMaybe<StringNullableFilter>;
updatedAt?: InputMaybe<DateTimeFilter>;
};
@ -651,7 +777,7 @@ export enum CommentableType {
export type Company = {
__typename?: 'Company';
_commentCount: Scalars['Int'];
_commentThreadCount: Scalars['Int'];
accountOwner?: Maybe<User>;
accountOwnerId?: Maybe<Scalars['String']>;
address: Scalars['String'];
@ -1270,13 +1396,14 @@ export type NullableStringFieldUpdateOperationsInput = {
export type Person = {
__typename?: 'Person';
_commentCount: Scalars['Int'];
_commentThreadCount: Scalars['Int'];
city: Scalars['String'];
commentThreads: Array<CommentThread>;
comments: Array<Comment>;
company?: Maybe<Company>;
companyId?: Maybe<Scalars['String']>;
createdAt: Scalars['DateTime'];
displayName: Scalars['String'];
email: Scalars['String'];
firstName: Scalars['String'];
id: Scalars['ID'];
@ -2303,6 +2430,8 @@ export type Query = {
findManyPipelineProgress: Array<PipelineProgress>;
findManyPipelineStage: Array<PipelineStage>;
findManyUser: Array<User>;
findUniqueCompany: Company;
findUniquePerson: Person;
};
@ -2380,6 +2509,16 @@ export type QueryFindManyUserArgs = {
where?: InputMaybe<UserWhereInput>;
};
export type QueryFindUniqueCompanyArgs = {
id: Scalars['String'];
};
export type QueryFindUniquePersonArgs = {
id: Scalars['String'];
};
export enum QueryMode {
Default = 'default',
Insensitive = 'insensitive'
@ -2432,6 +2571,7 @@ export type Telemetry = {
export type User = {
__typename?: 'User';
CommentThread?: Maybe<Array<CommentThread>>;
avatarUrl?: Maybe<Scalars['String']>;
comments?: Maybe<Array<Comment>>;
companies?: Maybe<Array<Company>>;
@ -2451,6 +2591,12 @@ export type User = {
workspaceMember?: Maybe<WorkspaceMember>;
};
export type UserCreateNestedOneWithoutCommentThreadInput = {
connect?: InputMaybe<UserWhereUniqueInput>;
connectOrCreate?: InputMaybe<UserCreateOrConnectWithoutCommentThreadInput>;
create?: InputMaybe<UserCreateWithoutCommentThreadInput>;
};
export type UserCreateNestedOneWithoutCommentsInput = {
connect?: InputMaybe<UserWhereUniqueInput>;
};
@ -2465,6 +2611,11 @@ export type UserCreateNestedOneWithoutWorkspaceMemberInput = {
create?: InputMaybe<UserCreateWithoutWorkspaceMemberInput>;
};
export type UserCreateOrConnectWithoutCommentThreadInput = {
create: UserCreateWithoutCommentThreadInput;
where: UserWhereUniqueInput;
};
export type UserCreateOrConnectWithoutCommentsInput = {
create: UserCreateWithoutCommentsInput;
where: UserWhereUniqueInput;
@ -2475,7 +2626,26 @@ export type UserCreateOrConnectWithoutWorkspaceMemberInput = {
where: UserWhereUniqueInput;
};
export type UserCreateWithoutCommentThreadInput = {
avatarUrl?: InputMaybe<Scalars['String']>;
comments?: InputMaybe<CommentCreateNestedManyWithoutAuthorInput>;
companies?: InputMaybe<CompanyCreateNestedManyWithoutAccountOwnerInput>;
createdAt?: InputMaybe<Scalars['DateTime']>;
disabled?: InputMaybe<Scalars['Boolean']>;
email: Scalars['String'];
emailVerified?: InputMaybe<Scalars['Boolean']>;
firstName?: InputMaybe<Scalars['String']>;
id?: InputMaybe<Scalars['String']>;
lastName?: InputMaybe<Scalars['String']>;
lastSeen?: InputMaybe<Scalars['DateTime']>;
locale: Scalars['String'];
metadata?: InputMaybe<Scalars['JSON']>;
phoneNumber?: InputMaybe<Scalars['String']>;
updatedAt?: InputMaybe<Scalars['DateTime']>;
};
export type UserCreateWithoutCommentsInput = {
CommentThread?: InputMaybe<CommentThreadCreateNestedManyWithoutAuthorInput>;
avatarUrl?: InputMaybe<Scalars['String']>;
companies?: InputMaybe<CompanyCreateNestedManyWithoutAccountOwnerInput>;
createdAt?: InputMaybe<Scalars['DateTime']>;
@ -2493,6 +2663,7 @@ export type UserCreateWithoutCommentsInput = {
};
export type UserCreateWithoutWorkspaceMemberInput = {
CommentThread?: InputMaybe<CommentThreadCreateNestedManyWithoutAuthorInput>;
avatarUrl?: InputMaybe<Scalars['String']>;
comments?: InputMaybe<CommentCreateNestedManyWithoutAuthorInput>;
companies?: InputMaybe<CompanyCreateNestedManyWithoutAccountOwnerInput>;
@ -2516,6 +2687,7 @@ export type UserExists = {
};
export type UserOrderByWithRelationInput = {
CommentThread?: InputMaybe<CommentThreadOrderByRelationAggregateInput>;
avatarUrl?: InputMaybe<SortOrder>;
comments?: InputMaybe<CommentOrderByRelationAggregateInput>;
companies?: InputMaybe<CompanyOrderByRelationAggregateInput>;
@ -2557,6 +2729,7 @@ export enum UserScalarFieldEnum {
}
export type UserUpdateInput = {
CommentThread?: InputMaybe<CommentThreadUpdateManyWithoutAuthorNestedInput>;
avatarUrl?: InputMaybe<NullableStringFieldUpdateOperationsInput>;
comments?: InputMaybe<CommentUpdateManyWithoutAuthorNestedInput>;
companies?: InputMaybe<CompanyUpdateManyWithoutAccountOwnerNestedInput>;
@ -2574,6 +2747,14 @@ export type UserUpdateInput = {
updatedAt?: InputMaybe<DateTimeFieldUpdateOperationsInput>;
};
export type UserUpdateOneRequiredWithoutCommentThreadNestedInput = {
connect?: InputMaybe<UserWhereUniqueInput>;
connectOrCreate?: InputMaybe<UserCreateOrConnectWithoutCommentThreadInput>;
create?: InputMaybe<UserCreateWithoutCommentThreadInput>;
update?: InputMaybe<UserUpdateWithoutCommentThreadInput>;
upsert?: InputMaybe<UserUpsertWithoutCommentThreadInput>;
};
export type UserUpdateOneRequiredWithoutCommentsNestedInput = {
connect?: InputMaybe<UserWhereUniqueInput>;
connectOrCreate?: InputMaybe<UserCreateOrConnectWithoutCommentsInput>;
@ -2594,24 +2775,7 @@ export type UserUpdateOneWithoutCompaniesNestedInput = {
connect?: InputMaybe<UserWhereUniqueInput>;
};
export type UserUpdateWithoutCommentsInput = {
avatarUrl?: InputMaybe<NullableStringFieldUpdateOperationsInput>;
companies?: InputMaybe<CompanyUpdateManyWithoutAccountOwnerNestedInput>;
createdAt?: InputMaybe<DateTimeFieldUpdateOperationsInput>;
disabled?: InputMaybe<BoolFieldUpdateOperationsInput>;
email?: InputMaybe<StringFieldUpdateOperationsInput>;
emailVerified?: InputMaybe<BoolFieldUpdateOperationsInput>;
firstName?: InputMaybe<NullableStringFieldUpdateOperationsInput>;
id?: InputMaybe<StringFieldUpdateOperationsInput>;
lastName?: InputMaybe<NullableStringFieldUpdateOperationsInput>;
lastSeen?: InputMaybe<NullableDateTimeFieldUpdateOperationsInput>;
locale?: InputMaybe<StringFieldUpdateOperationsInput>;
metadata?: InputMaybe<Scalars['JSON']>;
phoneNumber?: InputMaybe<NullableStringFieldUpdateOperationsInput>;
updatedAt?: InputMaybe<DateTimeFieldUpdateOperationsInput>;
};
export type UserUpdateWithoutWorkspaceMemberInput = {
export type UserUpdateWithoutCommentThreadInput = {
avatarUrl?: InputMaybe<NullableStringFieldUpdateOperationsInput>;
comments?: InputMaybe<CommentUpdateManyWithoutAuthorNestedInput>;
companies?: InputMaybe<CompanyUpdateManyWithoutAccountOwnerNestedInput>;
@ -2629,6 +2793,48 @@ export type UserUpdateWithoutWorkspaceMemberInput = {
updatedAt?: InputMaybe<DateTimeFieldUpdateOperationsInput>;
};
export type UserUpdateWithoutCommentsInput = {
CommentThread?: InputMaybe<CommentThreadUpdateManyWithoutAuthorNestedInput>;
avatarUrl?: InputMaybe<NullableStringFieldUpdateOperationsInput>;
companies?: InputMaybe<CompanyUpdateManyWithoutAccountOwnerNestedInput>;
createdAt?: InputMaybe<DateTimeFieldUpdateOperationsInput>;
disabled?: InputMaybe<BoolFieldUpdateOperationsInput>;
email?: InputMaybe<StringFieldUpdateOperationsInput>;
emailVerified?: InputMaybe<BoolFieldUpdateOperationsInput>;
firstName?: InputMaybe<NullableStringFieldUpdateOperationsInput>;
id?: InputMaybe<StringFieldUpdateOperationsInput>;
lastName?: InputMaybe<NullableStringFieldUpdateOperationsInput>;
lastSeen?: InputMaybe<NullableDateTimeFieldUpdateOperationsInput>;
locale?: InputMaybe<StringFieldUpdateOperationsInput>;
metadata?: InputMaybe<Scalars['JSON']>;
phoneNumber?: InputMaybe<NullableStringFieldUpdateOperationsInput>;
updatedAt?: InputMaybe<DateTimeFieldUpdateOperationsInput>;
};
export type UserUpdateWithoutWorkspaceMemberInput = {
CommentThread?: InputMaybe<CommentThreadUpdateManyWithoutAuthorNestedInput>;
avatarUrl?: InputMaybe<NullableStringFieldUpdateOperationsInput>;
comments?: InputMaybe<CommentUpdateManyWithoutAuthorNestedInput>;
companies?: InputMaybe<CompanyUpdateManyWithoutAccountOwnerNestedInput>;
createdAt?: InputMaybe<DateTimeFieldUpdateOperationsInput>;
disabled?: InputMaybe<BoolFieldUpdateOperationsInput>;
email?: InputMaybe<StringFieldUpdateOperationsInput>;
emailVerified?: InputMaybe<BoolFieldUpdateOperationsInput>;
firstName?: InputMaybe<NullableStringFieldUpdateOperationsInput>;
id?: InputMaybe<StringFieldUpdateOperationsInput>;
lastName?: InputMaybe<NullableStringFieldUpdateOperationsInput>;
lastSeen?: InputMaybe<NullableDateTimeFieldUpdateOperationsInput>;
locale?: InputMaybe<StringFieldUpdateOperationsInput>;
metadata?: InputMaybe<Scalars['JSON']>;
phoneNumber?: InputMaybe<NullableStringFieldUpdateOperationsInput>;
updatedAt?: InputMaybe<DateTimeFieldUpdateOperationsInput>;
};
export type UserUpsertWithoutCommentThreadInput = {
create: UserCreateWithoutCommentThreadInput;
update: UserUpdateWithoutCommentThreadInput;
};
export type UserUpsertWithoutCommentsInput = {
create: UserCreateWithoutCommentsInput;
update: UserUpdateWithoutCommentsInput;
@ -2641,6 +2847,7 @@ export type UserUpsertWithoutWorkspaceMemberInput = {
export type UserWhereInput = {
AND?: InputMaybe<Array<UserWhereInput>>;
CommentThread?: InputMaybe<CommentThreadListRelationFilter>;
NOT?: InputMaybe<Array<UserWhereInput>>;
OR?: InputMaybe<Array<UserWhereInput>>;
avatarUrl?: InputMaybe<StringNullableFilter>;
@ -2851,17 +3058,17 @@ export type CreateCommentMutationVariables = Exact<{
export type CreateCommentMutation = { __typename?: 'Mutation', createOneComment: { __typename?: 'Comment', id: string, createdAt: string, body: string, commentThreadId: string, author: { __typename?: 'User', id: string, displayName: string, firstName?: string | null, lastName?: string | null, avatarUrl?: string | null } } };
export type CreateCommentThreadWithCommentMutationVariables = Exact<{
export type CreateCommentThreadMutationVariables = Exact<{
commentThreadId: Scalars['String'];
commentText: Scalars['String'];
body?: InputMaybe<Scalars['String']>;
title?: InputMaybe<Scalars['String']>;
authorId: Scalars['String'];
createdAt: Scalars['DateTime'];
commentId: Scalars['String'];
commentThreadTargetArray: Array<CommentThreadTargetCreateManyCommentThreadInput> | CommentThreadTargetCreateManyCommentThreadInput;
}>;
export type CreateCommentThreadWithCommentMutation = { __typename?: 'Mutation', createOneCommentThread: { __typename?: 'CommentThread', id: string, createdAt: string, updatedAt: string, commentThreadTargets?: Array<{ __typename?: 'CommentThreadTarget', id: string, createdAt: string, updatedAt: string, commentThreadId: string, commentableType: CommentableType, commentableId: string }> | null, comments?: Array<{ __typename?: 'Comment', id: string, createdAt: string, updatedAt: string, body: string, author: { __typename?: 'User', id: string } }> | null } };
export type CreateCommentThreadMutation = { __typename?: 'Mutation', createOneCommentThread: { __typename?: 'CommentThread', id: string, createdAt: string, updatedAt: string, authorId: string, commentThreadTargets?: Array<{ __typename?: 'CommentThreadTarget', id: string, createdAt: string, updatedAt: string, commentThreadId: string, commentableType: CommentableType, commentableId: string }> | null, comments?: Array<{ __typename?: 'Comment', id: string, createdAt: string, updatedAt: string, body: string, author: { __typename?: 'User', id: string } }> | null } };
export type GetCommentThreadsByTargetsQueryVariables = Exact<{
commentThreadTargetIds: Array<Scalars['String']> | Scalars['String'];
@ -2869,14 +3076,14 @@ export type GetCommentThreadsByTargetsQueryVariables = Exact<{
}>;
export type GetCommentThreadsByTargetsQuery = { __typename?: 'Query', findManyCommentThreads: Array<{ __typename?: 'CommentThread', id: string, comments?: Array<{ __typename?: 'Comment', id: string, body: string, createdAt: string, updatedAt: string, author: { __typename?: 'User', id: string, displayName: string, firstName?: string | null, lastName?: string | null, avatarUrl?: string | null } }> | null, commentThreadTargets?: Array<{ __typename?: 'CommentThreadTarget', id: string, commentableId: string, commentableType: CommentableType }> | null }> };
export type GetCommentThreadsByTargetsQuery = { __typename?: 'Query', findManyCommentThreads: Array<{ __typename?: 'CommentThread', id: string, createdAt: string, title?: string | null, body?: string | null, author: { __typename?: 'User', id: string, firstName?: string | null, lastName?: string | null }, comments?: Array<{ __typename?: 'Comment', id: string, body: string, createdAt: string, updatedAt: string, author: { __typename?: 'User', id: string, displayName: string, firstName?: string | null, lastName?: string | null, avatarUrl?: string | null } }> | null, commentThreadTargets?: Array<{ __typename?: 'CommentThreadTarget', id: string, commentableId: string, commentableType: CommentableType }> | null }> };
export type GetCommentThreadQueryVariables = Exact<{
commentThreadId: Scalars['String'];
}>;
export type GetCommentThreadQuery = { __typename?: 'Query', findManyCommentThreads: Array<{ __typename?: 'CommentThread', id: string, comments?: Array<{ __typename?: 'Comment', id: string, body: string, createdAt: string, updatedAt: string, author: { __typename?: 'User', id: string, displayName: string, firstName?: string | null, lastName?: string | null, avatarUrl?: string | null } }> | null, commentThreadTargets?: Array<{ __typename?: 'CommentThreadTarget', commentableId: string, commentableType: CommentableType }> | null }> };
export type GetCommentThreadQuery = { __typename?: 'Query', findManyCommentThreads: Array<{ __typename?: 'CommentThread', id: string, createdAt: string, body?: string | null, title?: string | null, author: { __typename?: 'User', id: string, firstName?: string | null, lastName?: string | null }, comments?: Array<{ __typename?: 'Comment', id: string, body: string, createdAt: string, updatedAt: string, author: { __typename?: 'User', id: string, displayName: string, firstName?: string | null, lastName?: string | null, avatarUrl?: string | null } }> | null, commentThreadTargets?: Array<{ __typename?: 'CommentThreadTarget', id: string, commentableId: string, commentableType: CommentableType }> | null }> };
export type AddCommentThreadTargetOnCommentThreadMutationVariables = Exact<{
commentThreadId: Scalars['String'];
@ -2904,13 +3111,36 @@ export type DeleteCommentThreadMutationVariables = Exact<{
export type DeleteCommentThreadMutation = { __typename?: 'Mutation', deleteManyCommentThreads: { __typename?: 'AffectedRows', count: number } };
export type UpdateCommentThreadTitleMutationVariables = Exact<{
commentThreadId: Scalars['String'];
commentThreadTitle?: InputMaybe<Scalars['String']>;
}>;
export type UpdateCommentThreadTitleMutation = { __typename?: 'Mutation', updateOneCommentThread: { __typename?: 'CommentThread', id: string, title?: string | null } };
export type UpdateCommentThreadBodyMutationVariables = Exact<{
commentThreadId: Scalars['String'];
commentThreadBody?: InputMaybe<Scalars['String']>;
}>;
export type UpdateCommentThreadBodyMutation = { __typename?: 'Mutation', updateOneCommentThread: { __typename?: 'CommentThread', id: string, body?: string | null } };
export type GetCompaniesQueryVariables = Exact<{
orderBy?: InputMaybe<Array<CompanyOrderByWithRelationInput> | CompanyOrderByWithRelationInput>;
where?: InputMaybe<CompanyWhereInput>;
}>;
export type GetCompaniesQuery = { __typename?: 'Query', companies: Array<{ __typename?: 'Company', id: string, domainName: string, name: string, createdAt: string, address: string, employees?: number | null, _commentCount: number, accountOwner?: { __typename?: 'User', id: string, email: string, displayName: string, firstName?: string | null, lastName?: string | null } | null }> };
export type GetCompaniesQuery = { __typename?: 'Query', companies: Array<{ __typename?: 'Company', id: string, domainName: string, name: string, createdAt: string, address: string, employees?: number | null, _commentThreadCount: number, accountOwner?: { __typename?: 'User', id: string, email: string, displayName: string, firstName?: string | null, lastName?: string | null } | null }> };
export type GetCompanyQueryVariables = Exact<{
id: Scalars['String'];
}>;
export type GetCompanyQuery = { __typename?: 'Query', findUniqueCompany: { __typename?: 'Company', id: string, domainName: string, name: string, createdAt: string, address: string, employees?: number | null, _commentThreadCount: number, accountOwner?: { __typename?: 'User', id: string, email: string, displayName: string } | null } };
export type UpdateCompanyMutationVariables = Exact<{
id?: InputMaybe<Scalars['String']>;
@ -2951,7 +3181,14 @@ export type GetPeopleQueryVariables = Exact<{
}>;
export type GetPeopleQuery = { __typename?: 'Query', people: Array<{ __typename?: 'Person', id: string, phone: string, email: string, city: string, firstName: string, lastName: string, createdAt: string, _commentCount: number, company?: { __typename?: 'Company', id: string, name: string, domainName: string } | null }> };
export type GetPeopleQuery = { __typename?: 'Query', people: Array<{ __typename?: 'Person', id: string, phone: string, email: string, city: string, firstName: string, lastName: string, createdAt: string, _commentThreadCount: number, company?: { __typename?: 'Company', id: string, name: string, domainName: string } | null }> };
export type GetPersonQueryVariables = Exact<{
id: Scalars['String'];
}>;
export type GetPersonQuery = { __typename?: 'Query', findUniquePerson: { __typename?: 'Person', id: string, firstName: string, lastName: string, displayName: string, createdAt: string } };
export type UpdatePeopleMutationVariables = Exact<{
id?: InputMaybe<Scalars['String']>;
@ -3416,14 +3653,15 @@ export function useCreateCommentMutation(baseOptions?: Apollo.MutationHookOption
export type CreateCommentMutationHookResult = ReturnType<typeof useCreateCommentMutation>;
export type CreateCommentMutationResult = Apollo.MutationResult<CreateCommentMutation>;
export type CreateCommentMutationOptions = Apollo.BaseMutationOptions<CreateCommentMutation, CreateCommentMutationVariables>;
export const CreateCommentThreadWithCommentDocument = gql`
mutation CreateCommentThreadWithComment($commentThreadId: String!, $commentText: String!, $authorId: String!, $createdAt: DateTime!, $commentId: String!, $commentThreadTargetArray: [CommentThreadTargetCreateManyCommentThreadInput!]!) {
export const CreateCommentThreadDocument = gql`
mutation CreateCommentThread($commentThreadId: String!, $body: String, $title: String, $authorId: String!, $createdAt: DateTime!, $commentThreadTargetArray: [CommentThreadTargetCreateManyCommentThreadInput!]!) {
createOneCommentThread(
data: {id: $commentThreadId, createdAt: $createdAt, updatedAt: $createdAt, comments: {createMany: {data: {authorId: $authorId, id: $commentId, createdAt: $createdAt, body: $commentText}}}, commentThreadTargets: {createMany: {data: $commentThreadTargetArray, skipDuplicates: true}}}
data: {id: $commentThreadId, createdAt: $createdAt, updatedAt: $createdAt, author: {connect: {id: $authorId}}, body: $body, title: $title, commentThreadTargets: {createMany: {data: $commentThreadTargetArray, skipDuplicates: true}}}
) {
id
createdAt
updatedAt
authorId
commentThreadTargets {
id
createdAt
@ -3444,37 +3682,37 @@ export const CreateCommentThreadWithCommentDocument = gql`
}
}
`;
export type CreateCommentThreadWithCommentMutationFn = Apollo.MutationFunction<CreateCommentThreadWithCommentMutation, CreateCommentThreadWithCommentMutationVariables>;
export type CreateCommentThreadMutationFn = Apollo.MutationFunction<CreateCommentThreadMutation, CreateCommentThreadMutationVariables>;
/**
* __useCreateCommentThreadWithCommentMutation__
* __useCreateCommentThreadMutation__
*
* To run a mutation, you first call `useCreateCommentThreadWithCommentMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useCreateCommentThreadWithCommentMutation` returns a tuple that includes:
* To run a mutation, you first call `useCreateCommentThreadMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useCreateCommentThreadMutation` 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 [createCommentThreadWithCommentMutation, { data, loading, error }] = useCreateCommentThreadWithCommentMutation({
* const [createCommentThreadMutation, { data, loading, error }] = useCreateCommentThreadMutation({
* variables: {
* commentThreadId: // value for 'commentThreadId'
* commentText: // value for 'commentText'
* body: // value for 'body'
* title: // value for 'title'
* authorId: // value for 'authorId'
* createdAt: // value for 'createdAt'
* commentId: // value for 'commentId'
* commentThreadTargetArray: // value for 'commentThreadTargetArray'
* },
* });
*/
export function useCreateCommentThreadWithCommentMutation(baseOptions?: Apollo.MutationHookOptions<CreateCommentThreadWithCommentMutation, CreateCommentThreadWithCommentMutationVariables>) {
export function useCreateCommentThreadMutation(baseOptions?: Apollo.MutationHookOptions<CreateCommentThreadMutation, CreateCommentThreadMutationVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useMutation<CreateCommentThreadWithCommentMutation, CreateCommentThreadWithCommentMutationVariables>(CreateCommentThreadWithCommentDocument, options);
return Apollo.useMutation<CreateCommentThreadMutation, CreateCommentThreadMutationVariables>(CreateCommentThreadDocument, options);
}
export type CreateCommentThreadWithCommentMutationHookResult = ReturnType<typeof useCreateCommentThreadWithCommentMutation>;
export type CreateCommentThreadWithCommentMutationResult = Apollo.MutationResult<CreateCommentThreadWithCommentMutation>;
export type CreateCommentThreadWithCommentMutationOptions = Apollo.BaseMutationOptions<CreateCommentThreadWithCommentMutation, CreateCommentThreadWithCommentMutationVariables>;
export type CreateCommentThreadMutationHookResult = ReturnType<typeof useCreateCommentThreadMutation>;
export type CreateCommentThreadMutationResult = Apollo.MutationResult<CreateCommentThreadMutation>;
export type CreateCommentThreadMutationOptions = Apollo.BaseMutationOptions<CreateCommentThreadMutation, CreateCommentThreadMutationVariables>;
export const GetCommentThreadsByTargetsDocument = gql`
query GetCommentThreadsByTargets($commentThreadTargetIds: [String!]!, $orderBy: [CommentThreadOrderByWithRelationInput!]) {
findManyCommentThreads(
@ -3482,6 +3720,14 @@ export const GetCommentThreadsByTargetsDocument = gql`
where: {commentThreadTargets: {some: {commentableId: {in: $commentThreadTargetIds}}}}
) {
id
createdAt
title
body
author {
id
firstName
lastName
}
comments {
id
body
@ -3536,6 +3782,14 @@ export const GetCommentThreadDocument = gql`
query GetCommentThread($commentThreadId: String!) {
findManyCommentThreads(where: {id: {equals: $commentThreadId}}) {
id
createdAt
body
title
author {
id
firstName
lastName
}
comments {
id
body
@ -3550,6 +3804,7 @@ export const GetCommentThreadDocument = gql`
}
}
commentThreadTargets {
id
commentableId
commentableType
}
@ -3712,6 +3967,82 @@ export function useDeleteCommentThreadMutation(baseOptions?: Apollo.MutationHook
export type DeleteCommentThreadMutationHookResult = ReturnType<typeof useDeleteCommentThreadMutation>;
export type DeleteCommentThreadMutationResult = Apollo.MutationResult<DeleteCommentThreadMutation>;
export type DeleteCommentThreadMutationOptions = Apollo.BaseMutationOptions<DeleteCommentThreadMutation, DeleteCommentThreadMutationVariables>;
export const UpdateCommentThreadTitleDocument = gql`
mutation UpdateCommentThreadTitle($commentThreadId: String!, $commentThreadTitle: String) {
updateOneCommentThread(
where: {id: $commentThreadId}
data: {title: {set: $commentThreadTitle}}
) {
id
title
}
}
`;
export type UpdateCommentThreadTitleMutationFn = Apollo.MutationFunction<UpdateCommentThreadTitleMutation, UpdateCommentThreadTitleMutationVariables>;
/**
* __useUpdateCommentThreadTitleMutation__
*
* To run a mutation, you first call `useUpdateCommentThreadTitleMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useUpdateCommentThreadTitleMutation` 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 [updateCommentThreadTitleMutation, { data, loading, error }] = useUpdateCommentThreadTitleMutation({
* variables: {
* commentThreadId: // value for 'commentThreadId'
* commentThreadTitle: // value for 'commentThreadTitle'
* },
* });
*/
export function useUpdateCommentThreadTitleMutation(baseOptions?: Apollo.MutationHookOptions<UpdateCommentThreadTitleMutation, UpdateCommentThreadTitleMutationVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useMutation<UpdateCommentThreadTitleMutation, UpdateCommentThreadTitleMutationVariables>(UpdateCommentThreadTitleDocument, options);
}
export type UpdateCommentThreadTitleMutationHookResult = ReturnType<typeof useUpdateCommentThreadTitleMutation>;
export type UpdateCommentThreadTitleMutationResult = Apollo.MutationResult<UpdateCommentThreadTitleMutation>;
export type UpdateCommentThreadTitleMutationOptions = Apollo.BaseMutationOptions<UpdateCommentThreadTitleMutation, UpdateCommentThreadTitleMutationVariables>;
export const UpdateCommentThreadBodyDocument = gql`
mutation UpdateCommentThreadBody($commentThreadId: String!, $commentThreadBody: String) {
updateOneCommentThread(
where: {id: $commentThreadId}
data: {body: {set: $commentThreadBody}}
) {
id
body
}
}
`;
export type UpdateCommentThreadBodyMutationFn = Apollo.MutationFunction<UpdateCommentThreadBodyMutation, UpdateCommentThreadBodyMutationVariables>;
/**
* __useUpdateCommentThreadBodyMutation__
*
* To run a mutation, you first call `useUpdateCommentThreadBodyMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useUpdateCommentThreadBodyMutation` 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 [updateCommentThreadBodyMutation, { data, loading, error }] = useUpdateCommentThreadBodyMutation({
* variables: {
* commentThreadId: // value for 'commentThreadId'
* commentThreadBody: // value for 'commentThreadBody'
* },
* });
*/
export function useUpdateCommentThreadBodyMutation(baseOptions?: Apollo.MutationHookOptions<UpdateCommentThreadBodyMutation, UpdateCommentThreadBodyMutationVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useMutation<UpdateCommentThreadBodyMutation, UpdateCommentThreadBodyMutationVariables>(UpdateCommentThreadBodyDocument, options);
}
export type UpdateCommentThreadBodyMutationHookResult = ReturnType<typeof useUpdateCommentThreadBodyMutation>;
export type UpdateCommentThreadBodyMutationResult = Apollo.MutationResult<UpdateCommentThreadBodyMutation>;
export type UpdateCommentThreadBodyMutationOptions = Apollo.BaseMutationOptions<UpdateCommentThreadBodyMutation, UpdateCommentThreadBodyMutationVariables>;
export const GetCompaniesDocument = gql`
query GetCompanies($orderBy: [CompanyOrderByWithRelationInput!], $where: CompanyWhereInput) {
companies: findManyCompany(orderBy: $orderBy, where: $where) {
@ -3721,7 +4052,7 @@ export const GetCompaniesDocument = gql`
createdAt
address
employees
_commentCount
_commentThreadCount
accountOwner {
id
email
@ -3761,6 +4092,52 @@ export function useGetCompaniesLazyQuery(baseOptions?: Apollo.LazyQueryHookOptio
export type GetCompaniesQueryHookResult = ReturnType<typeof useGetCompaniesQuery>;
export type GetCompaniesLazyQueryHookResult = ReturnType<typeof useGetCompaniesLazyQuery>;
export type GetCompaniesQueryResult = Apollo.QueryResult<GetCompaniesQuery, GetCompaniesQueryVariables>;
export const GetCompanyDocument = gql`
query GetCompany($id: String!) {
findUniqueCompany(id: $id) {
id
domainName
name
createdAt
address
employees
_commentThreadCount
accountOwner {
id
email
displayName
}
}
}
`;
/**
* __useGetCompanyQuery__
*
* To run a query within a React component, call `useGetCompanyQuery` and pass it any options that fit your needs.
* When your component renders, `useGetCompanyQuery` 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 } = useGetCompanyQuery({
* variables: {
* id: // value for 'id'
* },
* });
*/
export function useGetCompanyQuery(baseOptions: Apollo.QueryHookOptions<GetCompanyQuery, GetCompanyQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<GetCompanyQuery, GetCompanyQueryVariables>(GetCompanyDocument, options);
}
export function useGetCompanyLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<GetCompanyQuery, GetCompanyQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<GetCompanyQuery, GetCompanyQueryVariables>(GetCompanyDocument, options);
}
export type GetCompanyQueryHookResult = ReturnType<typeof useGetCompanyQuery>;
export type GetCompanyLazyQueryHookResult = ReturnType<typeof useGetCompanyLazyQuery>;
export type GetCompanyQueryResult = Apollo.QueryResult<GetCompanyQuery, GetCompanyQueryVariables>;
export const UpdateCompanyDocument = gql`
mutation UpdateCompany($id: String, $name: String, $domainName: String, $accountOwnerId: String, $createdAt: DateTime, $address: String, $employees: Int) {
updateOneCompany(
@ -3903,7 +4280,7 @@ export const GetPeopleDocument = gql`
firstName
lastName
createdAt
_commentCount
_commentThreadCount
company {
id
name
@ -3942,6 +4319,45 @@ export function useGetPeopleLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<
export type GetPeopleQueryHookResult = ReturnType<typeof useGetPeopleQuery>;
export type GetPeopleLazyQueryHookResult = ReturnType<typeof useGetPeopleLazyQuery>;
export type GetPeopleQueryResult = Apollo.QueryResult<GetPeopleQuery, GetPeopleQueryVariables>;
export const GetPersonDocument = gql`
query GetPerson($id: String!) {
findUniquePerson(id: $id) {
id
firstName
lastName
displayName
createdAt
}
}
`;
/**
* __useGetPersonQuery__
*
* To run a query within a React component, call `useGetPersonQuery` and pass it any options that fit your needs.
* When your component renders, `useGetPersonQuery` 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 } = useGetPersonQuery({
* variables: {
* id: // value for 'id'
* },
* });
*/
export function useGetPersonQuery(baseOptions: Apollo.QueryHookOptions<GetPersonQuery, GetPersonQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<GetPersonQuery, GetPersonQueryVariables>(GetPersonDocument, options);
}
export function useGetPersonLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<GetPersonQuery, GetPersonQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<GetPersonQuery, GetPersonQueryVariables>(GetPersonDocument, options);
}
export type GetPersonQueryHookResult = ReturnType<typeof useGetPersonQuery>;
export type GetPersonLazyQueryHookResult = ReturnType<typeof useGetPersonLazyQuery>;
export type GetPersonQueryResult = Apollo.QueryResult<GetPersonQuery, GetPersonQueryVariables>;
export const UpdatePeopleDocument = gql`
mutation UpdatePeople($id: String, $firstName: String, $lastName: String, $phone: String, $city: String, $companyId: String, $email: String, $createdAt: DateTime) {
updateOnePerson(

View File

@ -7,7 +7,7 @@ const StyledContainer = styled.div`
align-items: center;
color: ${({ theme }) => theme.font.color.tertiary};
display: flex;
font-size: ${({ theme }) => theme.font.size.sm}px;
font-size: ${({ theme }) => theme.font.size.sm};
text-align: center;
`;

View File

@ -15,6 +15,7 @@ export const StyledDialog = styled(Command.Dialog)`
top: 50%;
transform: translate(-50%, -50%);
width: 100%;
z-index: 1000;
`;
export const StyledInput = styled(Command.Input)`

View File

@ -1,108 +0,0 @@
import { getOperationName } from '@apollo/client/utilities';
import styled from '@emotion/styled';
import { useRecoilValue } from 'recoil';
import { v4 } from 'uuid';
import { currentUserState } from '@/auth/states/currentUserState';
import { CommentThreadForDrawer } from '@/comments/types/CommentThreadForDrawer';
import { GET_COMPANIES } from '@/companies/services';
import { GET_PEOPLE } from '@/people/services';
import { AutosizeTextInput } from '@/ui/components/inputs/AutosizeTextInput';
import { logError } from '@/utils/logs/logError';
import { isDefined } from '@/utils/type-guards/isDefined';
import { isNonEmptyString } from '@/utils/type-guards/isNonEmptyString';
import { useCreateCommentMutation } from '~/generated/graphql';
import { GET_COMMENT_THREADS_BY_TARGETS } from '../services';
import { CommentThreadActionBar } from './CommentThreadActionBar';
import { CommentThreadItem } from './CommentThreadItem';
import { CommentThreadRelationPicker } from './CommentThreadRelationPicker';
type OwnProps = {
commentThread: CommentThreadForDrawer;
};
const StyledContainer = styled.div`
align-items: flex-start;
display: flex;
flex-direction: column;
gap: ${({ theme }) => theme.spacing(4)};
justify-content: flex-start;
padding: ${({ theme }) => theme.spacing(2)};
`;
const StyledThreadItemListContainer = styled.div`
align-items: flex-start;
display: flex;
flex-direction: column;
gap: ${({ theme }) => theme.spacing(4)};
justify-content: flex-start;
width: 100%;
`;
export function CommentThread({ commentThread }: OwnProps) {
const [createCommentMutation] = useCreateCommentMutation();
const currentUser = useRecoilValue(currentUserState);
function handleSendComment(commentText: string) {
if (!isNonEmptyString(commentText)) {
return;
}
if (!isDefined(currentUser)) {
logError(
'In handleSendComment, currentUser is not defined, this should not happen.',
);
return;
}
createCommentMutation({
variables: {
commentId: v4(),
authorId: currentUser.id,
commentThreadId: commentThread.id,
commentText,
createdAt: new Date().toISOString(),
},
refetchQueries: [
getOperationName(GET_COMPANIES) ?? '',
getOperationName(GET_PEOPLE) ?? '',
getOperationName(GET_COMMENT_THREADS_BY_TARGETS) ?? '',
],
onError: (error) => {
logError(
`In handleSendComment, createCommentMutation onError, error: ${error}`,
);
},
});
}
return (
<StyledContainer>
<StyledThreadItemListContainer>
{commentThread.comments?.map((comment, index) => (
<CommentThreadItem
key={comment.id}
comment={comment}
actionBar={
index === 0 ? (
<CommentThreadActionBar commentThreadId={commentThread.id} />
) : (
<></>
)
}
/>
))}
</StyledThreadItemListContainer>
<CommentThreadRelationPicker commentThread={commentThread} />
<AutosizeTextInput onValidate={handleSendComment} />
</StyledContainer>
);
}

View File

@ -1,148 +0,0 @@
import { getOperationName } from '@apollo/client/utilities';
import styled from '@emotion/styled';
import { useRecoilState, useRecoilValue } from 'recoil';
import { v4 } from 'uuid';
import { currentUserState } from '@/auth/states/currentUserState';
import { commentableEntityArrayState } from '@/comments/states/commentableEntityArrayState';
import { createdCommentThreadIdState } from '@/comments/states/createdCommentThreadIdState';
import { GET_COMPANIES } from '@/companies/services';
import { GET_PEOPLE } from '@/people/services';
import { AutosizeTextInput } from '@/ui/components/inputs/AutosizeTextInput';
import { useOpenRightDrawer } from '@/ui/layout/right-drawer/hooks/useOpenRightDrawer';
import { logError } from '@/utils/logs/logError';
import { isDefined } from '@/utils/type-guards/isDefined';
import { isNonEmptyString } from '@/utils/type-guards/isNonEmptyString';
import {
useCreateCommentMutation,
useCreateCommentThreadWithCommentMutation,
useGetCommentThreadQuery,
} from '~/generated/graphql';
import { GET_COMMENT_THREAD } from '../services';
import { CommentThreadItem } from './CommentThreadItem';
const StyledContainer = styled.div`
align-items: flex-start;
display: flex;
flex-direction: column;
gap: ${({ theme }) => theme.spacing(4)};
justify-content: flex-start;
max-height: calc(100% - 16px);
padding: ${({ theme }) => theme.spacing(2)};
`;
const StyledThreadItemListContainer = styled.div`
align-items: flex-start;
display: flex;
flex-direction: column-reverse;
gap: ${({ theme }) => theme.spacing(4)};
justify-content: flex-start;
overflow: auto;
width: 100%;
`;
export function CommentThreadCreateMode() {
const [commentableEntityArray] = useRecoilState(commentableEntityArrayState);
const [createdCommmentThreadId, setCreatedCommentThreadId] = useRecoilState(
createdCommentThreadIdState,
);
const openRightDrawer = useOpenRightDrawer();
const [createCommentMutation] = useCreateCommentMutation();
const [createCommentThreadWithComment] =
useCreateCommentThreadWithCommentMutation();
const { data } = useGetCommentThreadQuery({
variables: {
commentThreadId: createdCommmentThreadId ?? '',
},
skip: !createdCommmentThreadId,
});
const comments = data?.findManyCommentThreads[0]?.comments;
const displayCommentList = (comments?.length ?? 0) > 0;
const currentUser = useRecoilValue(currentUserState);
function handleNewComment(commentText: string) {
if (!isNonEmptyString(commentText)) {
return;
}
if (!isDefined(currentUser)) {
logError(
'In handleCreateCommentThread, currentUser is not defined, this should not happen.',
);
return;
}
if (!createdCommmentThreadId) {
createCommentThreadWithComment({
variables: {
authorId: currentUser.id,
commentId: v4(),
commentText: commentText,
commentThreadId: v4(),
createdAt: new Date().toISOString(),
commentThreadTargetArray: commentableEntityArray.map(
(commentableEntity) => ({
commentableId: commentableEntity.id,
commentableType: commentableEntity.type,
id: v4(),
createdAt: new Date().toISOString(),
}),
),
},
refetchQueries: [
getOperationName(GET_COMPANIES) ?? '',
getOperationName(GET_PEOPLE) ?? '',
getOperationName(GET_COMMENT_THREAD) ?? '',
],
onCompleted(data) {
setCreatedCommentThreadId(data.createOneCommentThread.id);
openRightDrawer('comments');
},
});
} else {
createCommentMutation({
variables: {
commentId: v4(),
authorId: currentUser.id,
commentThreadId: createdCommmentThreadId,
commentText,
createdAt: new Date().toISOString(),
},
refetchQueries: [getOperationName(GET_COMMENT_THREAD) ?? ''],
onError: (error) => {
logError(
`In handleCreateCommentThread, createCommentMutation onError, error: ${error}`,
);
},
});
}
}
return (
<StyledContainer>
{displayCommentList && (
<StyledThreadItemListContainer>
{comments?.map((comment) => (
<CommentThreadItem key={comment.id} comment={comment} />
))}
</StyledThreadItemListContainer>
)}
<AutosizeTextInput minRows={5} onValidate={handleNewComment} />
</StyledContainer>
);
}

View File

@ -1,51 +0,0 @@
import { useRecoilState } from 'recoil';
import { CommentThreadForDrawer } from '@/comments/types/CommentThreadForDrawer';
import { useHotkeysScopeOnMountOnly } from '@/hotkeys/hooks/useHotkeysScopeOnMountOnly';
import { InternalHotkeysScope } from '@/hotkeys/types/internal/InternalHotkeysScope';
import { RightDrawerBody } from '@/ui/layout/right-drawer/components/RightDrawerBody';
import { RightDrawerPage } from '@/ui/layout/right-drawer/components/RightDrawerPage';
import { RightDrawerTopBar } from '@/ui/layout/right-drawer/components/RightDrawerTopBar';
import {
SortOrder,
useGetCommentThreadsByTargetsQuery,
} from '~/generated/graphql';
import { commentableEntityArrayState } from '../states/commentableEntityArrayState';
import { CommentThread } from './CommentThread';
export function RightDrawerComments() {
const [commentableEntityArray] = useRecoilState(commentableEntityArrayState);
useHotkeysScopeOnMountOnly({
scope: InternalHotkeysScope.RightDrawer,
customScopes: { goto: false, 'command-menu': true },
});
const { data: queryResult } = useGetCommentThreadsByTargetsQuery({
variables: {
commentThreadTargetIds: commentableEntityArray.map(
(commentableEntity) => commentableEntity.id,
),
orderBy: [
{
createdAt: SortOrder.Desc,
},
],
},
});
const commentThreads: CommentThreadForDrawer[] =
queryResult?.findManyCommentThreads ?? [];
return (
<RightDrawerPage>
<RightDrawerTopBar title="Comments" />
<RightDrawerBody>
{commentThreads.map((commentThread) => (
<CommentThread key={commentThread.id} commentThread={commentThread} />
))}
</RightDrawerBody>
</RightDrawerPage>
);
}

View File

@ -0,0 +1,60 @@
import { useMemo } from 'react';
import { getOperationName } from '@apollo/client/utilities';
import { BlockNoteEditor } from '@blocknote/core';
import { useBlockNote } from '@blocknote/react';
import styled from '@emotion/styled';
import { GET_COMMENT_THREADS_BY_TARGETS } from '@/comments/services';
import { BlockEditor } from '@/ui/components/editor/BlockEditor';
import { debounce } from '@/utils/debounce';
import {
CommentThread,
useUpdateCommentThreadBodyMutation,
} from '~/generated/graphql';
const BlockNoteStyledContainer = styled.div`
width: 100%;
`;
type OwnProps = {
commentThread: Pick<CommentThread, 'id' | 'body'>;
onChange?: (commentThreadBody: string) => void;
};
export function CommentThreadBodyEditor({ commentThread, onChange }: OwnProps) {
const [updateCommentThreadBodyMutation] =
useUpdateCommentThreadBodyMutation();
const debounceOnChange = useMemo(() => {
function onInternalChange(commentThreadBody: string) {
onChange?.(commentThreadBody);
updateCommentThreadBodyMutation({
variables: {
commentThreadId: commentThread.id,
commentThreadBody: commentThreadBody,
},
refetchQueries: [
getOperationName(GET_COMMENT_THREADS_BY_TARGETS) ?? '',
],
});
}
return debounce(onInternalChange, 200);
}, [commentThread, updateCommentThreadBodyMutation, onChange]);
const editor: BlockNoteEditor | null = useBlockNote({
initialContent: commentThread.body
? JSON.parse(commentThread.body)
: undefined,
editorDOMAttributes: { class: 'editor-edit-mode' },
onEditorContentChange: (editor) => {
debounceOnChange(JSON.stringify(editor.topLevelBlocks) ?? '');
},
});
return (
<BlockNoteStyledContainer>
<BlockEditor editor={editor} />
</BlockNoteStyledContainer>
);
}

View File

@ -0,0 +1,84 @@
import { getOperationName } from '@apollo/client/utilities';
import styled from '@emotion/styled';
import { useRecoilValue } from 'recoil';
import { v4 } from 'uuid';
import { currentUserState } from '@/auth/states/currentUserState';
import { GET_COMMENT_THREADS_BY_TARGETS } from '@/comments/services';
import { CommentForDrawer } from '@/comments/types/CommentForDrawer';
import { AutosizeTextInput } from '@/ui/components/inputs/AutosizeTextInput';
import { isNonEmptyString } from '@/utils/type-guards/isNonEmptyString';
import { CommentThread, useCreateCommentMutation } from '~/generated/graphql';
import { CommentThreadItem } from '../comment/CommentThreadItem';
type OwnProps = {
commentThread: Pick<CommentThread, 'id'> & {
comments: Array<CommentForDrawer>;
};
};
const StyledThreadItemListContainer = styled.div`
align-items: flex-start;
border-top: 1px solid ${({ theme }) => theme.border.color.light};
box-sizing: border-box;
display: flex;
flex-direction: column;
gap: ${({ theme }) => theme.spacing(4)};
justify-content: flex-start;
padding: ${({ theme }) => theme.spacing(8)};
padding-left: ${({ theme }) => theme.spacing(12)};
width: 100%;
`;
const StyledCommentActionBar = styled.div`
border-top: 1px solid ${({ theme }) => theme.border.color.light};
display: flex;
padding: 16px 24px 16px 48px;
width: calc(${({ theme }) => theme.rightDrawerWidth} - 48px - 24px);
`;
export function CommentThreadComments({ commentThread }: OwnProps) {
const [createCommentMutation] = useCreateCommentMutation();
const currentUser = useRecoilValue(currentUserState);
if (!currentUser) {
return <></>;
}
function handleSendComment(commentText: string) {
if (!isNonEmptyString(commentText)) {
return;
}
createCommentMutation({
variables: {
commentId: v4(),
authorId: currentUser?.id ?? '',
commentThreadId: commentThread?.id ?? '',
commentText: commentText,
createdAt: new Date().toISOString(),
},
refetchQueries: [getOperationName(GET_COMMENT_THREADS_BY_TARGETS) ?? ''],
});
}
return (
<>
{commentThread?.comments.length > 0 && (
<StyledThreadItemListContainer>
{commentThread?.comments?.map((comment, index) => (
<CommentThreadItem key={comment.id} comment={comment} />
))}
</StyledThreadItemListContainer>
)}
<StyledCommentActionBar>
{currentUser && <AutosizeTextInput onValidate={handleSendComment} />}
</StyledCommentActionBar>
</>
);
}

View File

@ -1,5 +1,4 @@
import { useState } from 'react';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import {
autoUpdate,
@ -8,30 +7,33 @@ import {
size,
useFloating,
} from '@floating-ui/react';
import { IconArrowUpRight } from '@tabler/icons-react';
import { CommentThreadForDrawer } from '@/comments/types/CommentThreadForDrawer';
import { useHandleCheckableCommentThreadTargetChange } from '@/comments/hooks/useHandleCheckableCommentThreadTargetChange';
import { CommentableEntityForSelect } from '@/comments/types/CommentableEntityForSelect';
import CompanyChip from '@/companies/components/CompanyChip';
import { useScopedHotkeys } from '@/hotkeys/hooks/useScopedHotkeys';
import { InternalHotkeysScope } from '@/hotkeys/types/internal/InternalHotkeysScope';
import { PersonChip } from '@/people/components/PersonChip';
import { RecoilScope } from '@/recoil-scope/components/RecoilScope';
import { MultipleEntitySelect } from '@/relation-picker/components/MultipleEntitySelect';
import { useFilteredSearchEntityQuery } from '@/relation-picker/hooks/useFilteredSearchEntityQuery';
import { useListenClickOutsideArrayOfRef } from '@/ui/hooks/useListenClickOutsideArrayOfRef';
import { flatMapAndSortEntityForSelectArrayOfArrayByName } from '@/ui/utils/flatMapAndSortEntityForSelectArrayByName';
import { getLogoUrlFromDomainName } from '@/utils/utils';
import {
CommentableType,
CommentThread,
CommentThreadTarget,
useSearchCompanyQuery,
useSearchPeopleQuery,
} from '~/generated/graphql';
import { MultipleEntitySelect } from '../../relation-picker/components/MultipleEntitySelect';
import { useHandleCheckableCommentThreadTargetChange } from '../hooks/useHandleCheckableCommentThreadTargetChange';
import { CommentableEntityForSelect } from '../types/CommentableEntityForSelect';
type OwnProps = {
commentThread: CommentThreadForDrawer;
commentThread?: Pick<CommentThread, 'id'> & {
commentThreadTargets: Array<
Pick<CommentThreadTarget, 'id' | 'commentableId' | 'commentableType'>
>;
};
};
const StyledContainer = styled.div`
@ -44,25 +46,6 @@ const StyledContainer = styled.div`
width: 100%;
`;
const StyledLabelContainer = styled.div`
align-items: center;
display: flex;
flex-direction: row;
gap: ${({ theme }) => theme.spacing(2)};
padding-bottom: ${({ theme }) => theme.spacing(2)};
padding-top: ${({ theme }) => theme.spacing(2)};
`;
const StyledRelationLabel = styled.div`
color: ${({ theme }) => theme.font.color.secondary};
display: flex;
flex-direction: row;
user-select: none;
`;
const StyledRelationContainer = styled.div`
--horizontal-padding: ${({ theme }) => theme.spacing(1)};
--vertical-padding: ${({ theme }) => theme.spacing(1.5)};
@ -97,15 +80,12 @@ export function CommentThreadRelationPicker({ commentThread }: OwnProps) {
const [isMenuOpen, setIsMenuOpen] = useState(false);
const [searchFilter, setSearchFilter] = useState('');
const theme = useTheme();
const peopleIds =
commentThread.commentThreadTargets
commentThread?.commentThreadTargets
?.filter((relation) => relation.commentableType === 'Person')
.map((relation) => relation.commentableId) ?? [];
const companyIds =
commentThread.commentThreadTargets
commentThread?.commentThreadTargets
?.filter((relation) => relation.commentableType === 'Company')
.map((relation) => relation.commentableId) ?? [];
@ -203,10 +183,6 @@ export function CommentThreadRelationPicker({ commentThread }: OwnProps) {
return (
<StyledContainer>
<StyledLabelContainer>
<IconArrowUpRight size={16} color={theme.font.color.tertiary} />
<StyledRelationLabel>Relations</StyledRelationLabel>
</StyledLabelContainer>
<StyledRelationContainer
ref={refs.setReference}
onClick={handleRelationContainerClick}
@ -215,11 +191,12 @@ export function CommentThreadRelationPicker({ commentThread }: OwnProps) {
entity.entityType === CommentableType.Company ? (
<CompanyChip
key={entity.id}
id={entity.id}
name={entity.name}
picture={entity.avatarUrl}
/>
) : (
<PersonChip key={entity.id} name={entity.name} />
<PersonChip key={entity.id} name={entity.name} id={entity.id} />
),
)}
</StyledRelationContainer>

View File

@ -0,0 +1,18 @@
import {
DropdownButton,
DropdownOptionType,
} from '@/ui/components/buttons/DropdownButton';
import { IconNotes } from '@/ui/icons/index';
export function CommentThreadTypeDropdown() {
const options: DropdownOptionType[] = [
{ label: 'Notes', icon: <IconNotes /> },
// { label: 'Call', icon: <IconPhone /> },
];
const handleSelect = (selectedOption: DropdownOptionType) => {
// console.log(`You selected: ${selectedOption.label}`);
};
return <DropdownButton options={options} onSelection={handleSelect} />;
}

View File

@ -1,3 +1,4 @@
import { MemoryRouter } from 'react-router-dom';
import styled from '@emotion/styled';
import type { Meta, StoryObj } from '@storybook/react';
@ -24,8 +25,10 @@ type Story = StoryObj<typeof CommentThreadRelationPicker>;
export const Default: Story = {
render: getRenderWrapperForComponent(
<MemoryRouter>
<StyledContainer>
<CommentThreadRelationPicker commentThread={mockedCommentThreads[0]} />
</StyledContainer>,
</StyledContainer>
</MemoryRouter>,
),
};

View File

@ -22,7 +22,7 @@ const StyledCommentBody = styled.div`
color: ${({ theme }) => theme.font.color.secondary};
font-size: ${({ theme }) => theme.font.size.md};
line-height: ${({ theme }) => theme.text.lineHeight};
line-height: ${({ theme }) => theme.text.lineHeight.md};
overflow-wrap: anywhere;
padding-left: 24px;

View File

@ -6,8 +6,8 @@ import { CommentForDrawer } from '@/comments/types/CommentForDrawer';
import { mockedUsersData } from '~/testing/mock-data/users';
import { getRenderWrapperForComponent } from '~/testing/renderWrappers';
import { CommentThreadActionBar } from '../../right-drawer/CommentThreadActionBar';
import { CommentHeader } from '../CommentHeader';
import { CommentThreadActionBar } from '../CommentThreadActionBar';
const meta: Meta<typeof CommentHeader> = {
title: 'Modules/Comments/CommentHeader',

View File

@ -0,0 +1,164 @@
import React, { useMemo } from 'react';
import { getOperationName } from '@apollo/client/utilities';
import styled from '@emotion/styled';
import { GET_COMMENT_THREADS_BY_TARGETS } from '@/comments/services';
import { PropertyBox } from '@/ui/components/property-box/PropertyBox';
import { PropertyBoxItem } from '@/ui/components/property-box/PropertyBoxItem';
import { IconArrowUpRight } from '@/ui/icons/index';
import { debounce } from '@/utils/debounce';
import {
useGetCommentThreadQuery,
useUpdateCommentThreadTitleMutation,
} from '~/generated/graphql';
import { CommentThreadBodyEditor } from '../comment-thread/CommentThreadBodyEditor';
import { CommentThreadComments } from '../comment-thread/CommentThreadComments';
import { CommentThreadRelationPicker } from '../comment-thread/CommentThreadRelationPicker';
import { CommentThreadTypeDropdown } from '../comment-thread/CommentThreadTypeDropdown';
import { CommentThreadActionBar } from './CommentThreadActionBar';
import '@blocknote/core/style.css';
const StyledContainer = styled.div`
align-items: flex-start;
display: flex;
flex-direction: column;
gap: ${({ theme }) => theme.spacing(4)};
justify-content: flex-start;
`;
const StyledTopContainer = styled.div`
align-items: flex-start;
align-self: stretch;
background: ${({ theme }) => theme.background.secondary};
border-bottom: 1px solid ${({ theme }) => theme.border.color.medium};
display: flex;
flex-direction: column;
gap: 24px;
padding: 24px 24px 24px 48px;
`;
const StyledEditableTitleInput = styled.input`
background: transparent;
border: none;
color: ${({ theme }) => theme.font.color.primary};
display: flex;
flex: 1 0 0;
flex-direction: column;
font-family: Inter;
font-size: ${({ theme }) => theme.font.size.xl};
font-style: normal;
font-weight: ${({ theme }) => theme.font.weight.semiBold};
justify-content: center;
line-height: ${({ theme }) => theme.text.lineHeight.md};
outline: none;
width: calc(100% - ${({ theme }) => theme.spacing(2)});
:placeholder {
color: ${({ theme }) => theme.font.color.light};
}
`;
const StyledTopActionsContainer = styled.div`
align-items: center;
display: flex;
flex-direction: row;
justify-content: space-between;
width: 100%;
`;
type OwnProps = {
commentThreadId: string;
showComment?: boolean;
};
export function CommentThread({
commentThreadId,
showComment = true,
}: OwnProps) {
const { data } = useGetCommentThreadQuery({
variables: {
commentThreadId: commentThreadId ?? '',
},
skip: !commentThreadId,
});
const [updateCommentThreadTitleMutation] =
useUpdateCommentThreadTitleMutation();
const debounceUpdateTitle = useMemo(() => {
function updateTitle(title: string) {
updateCommentThreadTitleMutation({
variables: {
commentThreadId: commentThreadId,
commentThreadTitle: title ?? '',
},
refetchQueries: [
getOperationName(GET_COMMENT_THREADS_BY_TARGETS) ?? '',
],
});
}
return debounce(updateTitle, 200);
}, [commentThreadId, updateCommentThreadTitleMutation]);
function updateTitleFromBody(body: string) {
const title = JSON.parse(body)[0]?.content[0]?.text;
if (!commentThread?.title || commentThread?.title === '') {
debounceUpdateTitle(title);
}
}
const commentThread = data?.findManyCommentThreads[0];
if (!commentThread) {
return <></>;
}
return (
<StyledContainer>
<StyledTopContainer>
<StyledTopActionsContainer>
<CommentThreadTypeDropdown />
<CommentThreadActionBar commentThreadId={commentThread?.id ?? ''} />
</StyledTopActionsContainer>
<StyledEditableTitleInput
placeholder="Note title (optional)"
onChange={(event) => debounceUpdateTitle(event.target.value)}
value={commentThread?.title ?? ''}
/>
<PropertyBox>
<PropertyBoxItem
icon={<IconArrowUpRight />}
value={
<CommentThreadRelationPicker
commentThread={{
id: commentThread.id,
commentThreadTargets:
commentThread.commentThreadTargets ?? [],
}}
/>
}
label="Relations"
/>
</PropertyBox>
</StyledTopContainer>
<CommentThreadBodyEditor
commentThread={commentThread}
onChange={updateTitleFromBody}
/>
{showComment && (
<CommentThreadComments
commentThread={{
id: commentThread.id,
comments: commentThread.comments ?? [],
}}
/>
)}
</StyledContainer>
);
}

View File

@ -3,14 +3,13 @@ import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { useRecoilState } from 'recoil';
import { GET_COMMENT_THREADS_BY_TARGETS } from '@/comments/services';
import { GET_COMPANIES } from '@/companies/services';
import { GET_PEOPLE } from '@/people/services';
import { IconTrash } from '@/ui/icons';
import { isRightDrawerOpenState } from '@/ui/layout/right-drawer/states/isRightDrawerOpenState';
import { useDeleteCommentThreadMutation } from '~/generated/graphql';
import { GET_COMMENT_THREADS_BY_TARGETS } from '../services';
const StyledContainer = styled.div`
color: ${({ theme }) => theme.font.color.tertiary};
cursor: pointer;

View File

@ -0,0 +1,36 @@
import { useRecoilState } from 'recoil';
import { commentableEntityArrayState } from '@/comments/states/commentableEntityArrayState';
import { useHotkeysScopeOnMountOnly } from '@/hotkeys/hooks/useHotkeysScopeOnMountOnly';
import { InternalHotkeysScope } from '@/hotkeys/types/internal/InternalHotkeysScope';
import { RightDrawerBody } from '@/ui/layout/right-drawer/components/RightDrawerBody';
import { RightDrawerPage } from '@/ui/layout/right-drawer/components/RightDrawerPage';
import { RightDrawerTopBar } from '@/ui/layout/right-drawer/components/RightDrawerTopBar';
import { Timeline } from '../timeline/Timeline';
export function RightDrawerTimeline() {
const [commentableEntityArray] = useRecoilState(commentableEntityArrayState);
useHotkeysScopeOnMountOnly({
scope: InternalHotkeysScope.RightDrawer,
customScopes: { goto: false, 'command-menu': true },
});
return (
<RightDrawerPage>
<RightDrawerTopBar title="Timeline" />
<RightDrawerBody>
{commentableEntityArray.map((commentableEntity) => (
<Timeline
key={commentableEntity.id}
entity={{
id: commentableEntity?.id ?? '',
type: commentableEntity.type,
}}
/>
))}
</RightDrawerBody>
</RightDrawerPage>
);
}

View File

@ -0,0 +1,38 @@
import { useRecoilValue } from 'recoil';
import { viewableCommentThreadIdState } from '@/comments/states/viewableCommentThreadIdState';
import { useHotkeysScopeOnMountOnly } from '@/hotkeys/hooks/useHotkeysScopeOnMountOnly';
import { InternalHotkeysScope } from '@/hotkeys/types/internal/InternalHotkeysScope';
import { RightDrawerBody } from '@/ui/layout/right-drawer/components/RightDrawerBody';
import { RightDrawerPage } from '@/ui/layout/right-drawer/components/RightDrawerPage';
import { RightDrawerTopBar } from '@/ui/layout/right-drawer/components/RightDrawerTopBar';
import { CommentThread } from '../CommentThread';
export function RightDrawerCreateCommentThread() {
const commentThreadId = useRecoilValue(viewableCommentThreadIdState);
useHotkeysScopeOnMountOnly({
scope: InternalHotkeysScope.RightDrawer,
customScopes: { goto: false, 'command-menu': true },
});
return (
<RightDrawerPage>
<RightDrawerTopBar
title="New note"
onSave={() => {
return;
}}
/>
<RightDrawerBody>
{commentThreadId && (
<CommentThread
commentThreadId={commentThreadId}
showComment={false}
/>
)}
</RightDrawerBody>
</RightDrawerPage>
);
}

View File

@ -1,21 +1,25 @@
import { useRecoilValue } from 'recoil';
import { viewableCommentThreadIdState } from '@/comments/states/viewableCommentThreadIdState';
import { useHotkeysScopeOnMountOnly } from '@/hotkeys/hooks/useHotkeysScopeOnMountOnly';
import { InternalHotkeysScope } from '@/hotkeys/types/internal/InternalHotkeysScope';
import { RightDrawerBody } from '@/ui/layout/right-drawer/components/RightDrawerBody';
import { RightDrawerPage } from '@/ui/layout/right-drawer/components/RightDrawerPage';
import { RightDrawerTopBar } from '@/ui/layout/right-drawer/components/RightDrawerTopBar';
import { CommentThreadCreateMode } from './CommentThreadCreateMode';
import { CommentThread } from '../CommentThread';
export function RightDrawerCreateCommentThread() {
export function RightDrawerEditCommentThread() {
useHotkeysScopeOnMountOnly({
scope: InternalHotkeysScope.RightDrawer,
customScopes: { goto: false, 'command-menu': true },
});
const commentThreadId = useRecoilValue(viewableCommentThreadIdState);
return (
<RightDrawerPage>
<RightDrawerTopBar title="New comment" />
<RightDrawerTopBar title="" />
<RightDrawerBody>
<CommentThreadCreateMode />
{commentThreadId && <CommentThread commentThreadId={commentThreadId} />}
</RightDrawerBody>
</RightDrawerPage>
);

View File

@ -41,9 +41,8 @@ const StyledChip = styled.div`
const StyledCount = styled.div`
align-items: center;
display: flex;
font-size: 12px;
font-weight: 500;
font-size: ${({ theme }) => theme.font.size.sm};
font-weight: ${({ theme }) => theme.font.weight.medium};
justify-content: center;
`;

View File

@ -0,0 +1,288 @@
import React from 'react';
import { Tooltip } from 'react-tooltip';
import styled from '@emotion/styled';
import { useOpenCommentThreadRightDrawer } from '@/comments/hooks/useOpenCommentThreadRightDrawer';
import { useOpenCreateCommentThreadDrawer } from '@/comments/hooks/useOpenCreateCommentThreadDrawer';
import { CommentableEntity } from '@/comments/types/CommentableEntity';
import { CommentThreadForDrawer } from '@/comments/types/CommentThreadForDrawer';
import { TableActionBarButtonToggleComments } from '@/ui/components/table/action-bar/TableActionBarButtonOpenComments';
import { IconCirclePlus, IconNotes } from '@/ui/icons/index';
import {
beautifyExactDate,
beautifyPastDateRelativeToNow,
} from '@/utils/datetime/date-utils';
import {
SortOrder,
useGetCommentThreadsByTargetsQuery,
} from '~/generated/graphql';
const StyledMainContainer = styled.div`
align-items: flex-start;
align-self: stretch;
display: flex;
flex: 1 0 0;
flex-direction: column;
justify-content: center;
`;
const StyledTimelineContainer = styled.div`
align-items: center;
align-self: stretch;
display: flex;
flex: 1 0 0;
flex-direction: column;
gap: 4px;
justify-content: flex-start;
overflow-y: auto;
padding: 12px 16px 12px 16px;
`;
const StyledTimelineEmptyContainer = styled.div`
align-items: center;
align-self: stretch;
display: flex;
flex: 1 0 0;
flex-direction: column;
gap: 8px;
justify-content: center;
`;
const StyledEmptyTimelineTitle = styled.div`
color: ${({ theme }) => theme.font.color.secondary};
font-size: ${({ theme }) => theme.font.size.xxl};
font-weight: ${({ theme }) => theme.font.weight.semiBold};
line-height: ${({ theme }) => theme.text.lineHeight.md};
`;
const StyledEmptyTimelineSubTitle = styled.div`
color: ${({ theme }) => theme.font.color.extraLight};
font-size: ${({ theme }) => theme.font.size.xxl};
font-weight: ${({ theme }) => theme.font.weight.semiBold};
line-height: ${({ theme }) => theme.text.lineHeight.md};
`;
const StyledTimelineItemContainer = styled.div`
align-items: center;
align-self: stretch;
display: flex;
gap: 16px;
`;
const StyledIconContainer = styled.div`
align-items: center;
color: ${({ theme }) => theme.font.color.tertiary};
display: flex;
height: 20px;
justify-content: center;
width: 20px;
`;
const StyledItemTitleContainer = styled.div`
align-content: flex-start;
align-items: center;
color: ${({ theme }) => theme.font.color.tertiary};
display: flex;
flex: 1 0 0;
flex-wrap: wrap;
gap: 4px 8px;
height: 20px;
span {
color: ${({ theme }) => theme.font.color.secondary};
}
`;
const StyledItemTitleDate = styled.div`
align-items: center;
color: ${({ theme }) => theme.font.color.tertiary};
display: flex;
gap: 8px;
justify-content: flex-end;
`;
const StyledVerticalLineContainer = styled.div`
align-items: center;
align-self: stretch;
display: flex;
gap: 8px;
justify-content: center;
width: 20px;
`;
const StyledVerticalLine = styled.div`
align-self: stretch;
background: ${({ theme }) => theme.border.color.light};
flex-shrink: 0;
width: 2px;
`;
const StyledCardContainer = styled.div`
align-items: center;
cursor: pointer;
display: flex;
flex-direction: column;
gap: 8px;
padding: 4px 0px 20px 0px;
`;
const StyledCard = styled.div`
align-items: flex-start;
align-self: stretch;
background: ${({ theme }) => theme.background.secondary};
border: 1px solid ${({ theme }) => theme.border.color.medium};
border-radius: 4px;
display: flex;
flex-direction: column;
gap: 12px;
max-width: 400px;
padding: 12px;
`;
const StyledCardTitle = styled.div`
color: ${({ theme }) => theme.font.color.primary};
font-weight: ${({ theme }) => theme.font.weight.medium};
line-height: ${({ theme }) => theme.text.lineHeight.lg};
`;
const StyledCardContent = styled.div`
-webkit-box-orient: vertical;
-webkit-line-clamp: 3;
align-self: stretch;
color: ${({ theme }) => theme.font.color.secondary};
display: -webkit-box;
overflow: hidden;
text-overflow: ellipsis;
`;
const StyledTooltip = styled(Tooltip)`
background-color: ${({ theme }) => theme.background.primary};
box-shadow: 0px 2px 4px 3px
${({ theme }) => theme.background.transparent.light};
box-shadow: 2px 4px 16px 6px
${({ theme }) => theme.background.transparent.light};
color: ${({ theme }) => theme.font.color.primary};
opacity: 1;
padding: 8px;
`;
const StyledTopActionBar = styled.div`
align-items: flex-start;
align-self: stretch;
backdrop-filter: blur(5px);
border-bottom: 1px solid ${({ theme }) => theme.border.color.light};
border-top-right-radius: 8px;
display: flex;
flex-direction: column;
left: 0px;
padding: 12px 16px 12px 16px;
position: sticky;
top: 0px;
`;
export function Timeline({ entity }: { entity: CommentableEntity }) {
const { data: queryResult } = useGetCommentThreadsByTargetsQuery({
variables: {
commentThreadTargetIds: [entity.id],
orderBy: [
{
createdAt: SortOrder.Desc,
},
],
},
});
const openCommentThreadRightDrawer = useOpenCommentThreadRightDrawer();
const openCreateCommandThread = useOpenCreateCommentThreadDrawer();
const commentThreads: CommentThreadForDrawer[] =
queryResult?.findManyCommentThreads ?? [];
if (!commentThreads.length) {
return (
<StyledTimelineEmptyContainer>
<StyledEmptyTimelineTitle>No activity yet</StyledEmptyTimelineTitle>
<StyledEmptyTimelineSubTitle>Create one:</StyledEmptyTimelineSubTitle>
<TableActionBarButtonToggleComments
onClick={() => openCreateCommandThread(entity)}
/>
</StyledTimelineEmptyContainer>
);
}
return (
<StyledMainContainer>
<StyledTopActionBar>
<StyledTimelineItemContainer>
<StyledIconContainer>
<IconCirclePlus />
</StyledIconContainer>
<TableActionBarButtonToggleComments
onClick={() => openCreateCommandThread(entity)}
/>
</StyledTimelineItemContainer>
</StyledTopActionBar>
<StyledTimelineContainer>
{commentThreads.map((commentThread) => {
const beautifiedCreatedAt = beautifyPastDateRelativeToNow(
commentThread.createdAt,
);
const exactCreatedAt = beautifyExactDate(commentThread.createdAt);
const body = JSON.parse(commentThread.body ?? '{}')[0]?.content[0]
?.text;
return (
<React.Fragment key={commentThread.id}>
<StyledTimelineItemContainer>
<StyledIconContainer>
<IconNotes />
</StyledIconContainer>
<StyledItemTitleContainer>
<span>
{commentThread.author.firstName}{' '}
{commentThread.author.lastName}
</span>
created a note
</StyledItemTitleContainer>
<StyledItemTitleDate id={`id-${commentThread.id}`}>
{beautifiedCreatedAt} ago
</StyledItemTitleDate>
<StyledTooltip
anchorSelect={`#id-${commentThread.id}`}
content={exactCreatedAt}
clickable
noArrow
/>
</StyledTimelineItemContainer>
<StyledTimelineItemContainer>
<StyledVerticalLineContainer>
<StyledVerticalLine></StyledVerticalLine>
</StyledVerticalLineContainer>
<StyledCardContainer>
<StyledCard
onClick={() =>
openCommentThreadRightDrawer(commentThread.id)
}
>
<StyledCardTitle>
{commentThread.title ? commentThread.title : '(No title)'}
</StyledCardTitle>
<StyledCardContent>
{body ? body : '(No content)'}
</StyledCardContent>
</StyledCard>
</StyledCardContainer>
</StyledTimelineItemContainer>
</React.Fragment>
);
})}
</StyledTimelineContainer>
</StyledMainContainer>
);
}

View File

@ -4,18 +4,23 @@ import { v4 } from 'uuid';
import { GET_COMPANIES } from '@/companies/services';
import { GET_PEOPLE } from '@/people/services';
import {
CommentThread,
CommentThreadTarget,
useAddCommentThreadTargetOnCommentThreadMutation,
useRemoveCommentThreadTargetOnCommentThreadMutation,
} from '~/generated/graphql';
import { GET_COMMENT_THREADS_BY_TARGETS } from '../services';
import { CommentableEntityForSelect } from '../types/CommentableEntityForSelect';
import { CommentThreadForDrawer } from '../types/CommentThreadForDrawer';
export function useHandleCheckableCommentThreadTargetChange({
commentThread,
}: {
commentThread: CommentThreadForDrawer;
commentThread?: Pick<CommentThread, 'id'> & {
commentThreadTargets: Array<
Pick<CommentThreadTarget, 'id' | 'commentableId'>
>;
};
}) {
const [addCommentThreadTargetOnCommentThread] =
useAddCommentThreadTargetOnCommentThreadMutation({
@ -39,6 +44,9 @@ export function useHandleCheckableCommentThreadTargetChange({
newCheckedValue: boolean,
entity: CommentableEntityForSelect,
) {
if (!commentThread) {
return;
}
if (newCheckedValue) {
addCommentThreadTargetOnCommentThread({
variables: {

View File

@ -0,0 +1,18 @@
import { useRecoilState } from 'recoil';
import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPages';
import { useOpenRightDrawer } from '../../ui/layout/right-drawer/hooks/useOpenRightDrawer';
import { viewableCommentThreadIdState } from '../states/viewableCommentThreadIdState';
export function useOpenCommentThreadRightDrawer() {
const openRightDrawer = useOpenRightDrawer();
const [, setViewableCommentThreadId] = useRecoilState(
viewableCommentThreadIdState,
);
return function openCommentThreadRightDrawer(commentThreadId: string) {
setViewableCommentThreadId(commentThreadId);
openRightDrawer(RightDrawerPages.EditCommentThread);
};
}

View File

@ -1,38 +1,73 @@
import { getOperationName } from '@apollo/client/utilities/graphql/getFromAST';
import { useRecoilState, useRecoilValue } from 'recoil';
import { v4 } from 'uuid';
import { currentUserState } from '@/auth/states/currentUserState';
import { GET_COMPANIES } from '@/companies/services';
import { GET_PEOPLE } from '@/people/services';
import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPages';
import { selectedRowIdsState } from '@/ui/tables/states/selectedRowIdsState';
import { CommentableType } from '~/generated/graphql';
import {
CommentableType,
useCreateCommentThreadMutation,
} from '~/generated/graphql';
import { useOpenRightDrawer } from '../../ui/layout/right-drawer/hooks/useOpenRightDrawer';
import {
GET_COMMENT_THREAD,
GET_COMMENT_THREADS_BY_TARGETS,
} from '../services';
import { commentableEntityArrayState } from '../states/commentableEntityArrayState';
import { createdCommentThreadIdState } from '../states/createdCommentThreadIdState';
import { viewableCommentThreadIdState } from '../states/viewableCommentThreadIdState';
import { CommentableEntity } from '../types/CommentableEntity';
export function useOpenCreateCommentThreadDrawerForSelectedRowIds() {
const openRightDrawer = useOpenRightDrawer();
const [createCommentThreadMutation] = useCreateCommentThreadMutation();
const currentUser = useRecoilValue(currentUserState);
const [, setViewableCommentThreadId] = useRecoilState(
viewableCommentThreadIdState,
);
const [, setCommentableEntityArray] = useRecoilState(
commentableEntityArrayState,
);
const [, setCreatedCommentThreadId] = useRecoilState(
createdCommentThreadIdState,
);
const selectedPeopleIds = useRecoilValue(selectedRowIdsState);
const selectedEntityIds = useRecoilValue(selectedRowIdsState);
return function openCreateCommentDrawerForSelectedRowIds(
entityType: CommentableType,
) {
const commentableEntityArray: CommentableEntity[] = selectedPeopleIds.map(
const commentableEntityArray: CommentableEntity[] = selectedEntityIds.map(
(id) => ({
type: entityType,
id,
}),
);
setCreatedCommentThreadId(null);
createCommentThreadMutation({
variables: {
authorId: currentUser?.id ?? '',
commentThreadId: v4(),
createdAt: new Date().toISOString(),
commentThreadTargetArray: commentableEntityArray.map((entity) => ({
commentableId: entity.id,
commentableType: entity.type,
id: v4(),
createdAt: new Date().toISOString(),
})),
},
refetchQueries: [
getOperationName(GET_COMPANIES) ?? '',
getOperationName(GET_PEOPLE) ?? '',
getOperationName(GET_COMMENT_THREAD) ?? '',
getOperationName(GET_COMMENT_THREADS_BY_TARGETS) ?? '',
],
onCompleted(data) {
setViewableCommentThreadId(data.createOneCommentThread.id);
setCommentableEntityArray(commentableEntityArray);
openRightDrawer('create-comment-thread');
openRightDrawer(RightDrawerPages.CreateCommentThread);
},
});
};
}

View File

@ -0,0 +1,60 @@
import { getOperationName } from '@apollo/client/utilities';
import { useRecoilState, useRecoilValue } from 'recoil';
import { v4 } from 'uuid';
import { currentUserState } from '@/auth/states/currentUserState';
import { GET_COMPANIES } from '@/companies/services';
import { GET_PEOPLE } from '@/people/services';
import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPages';
import { useCreateCommentThreadMutation } from '~/generated/graphql';
import { useOpenRightDrawer } from '../../ui/layout/right-drawer/hooks/useOpenRightDrawer';
import {
GET_COMMENT_THREAD,
GET_COMMENT_THREADS_BY_TARGETS,
} from '../services';
import { commentableEntityArrayState } from '../states/commentableEntityArrayState';
import { viewableCommentThreadIdState } from '../states/viewableCommentThreadIdState';
import { CommentableEntity } from '../types/CommentableEntity';
export function useOpenCreateCommentThreadDrawer() {
const openRightDrawer = useOpenRightDrawer();
const [createCommentThreadMutation] = useCreateCommentThreadMutation();
const currentUser = useRecoilValue(currentUserState);
const [, setCommentableEntityArray] = useRecoilState(
commentableEntityArrayState,
);
const [, setViewableCommentThreadId] = useRecoilState(
viewableCommentThreadIdState,
);
return function openCreateCommentThreadDrawer(entity: CommentableEntity) {
createCommentThreadMutation({
variables: {
authorId: currentUser?.id ?? '',
commentThreadId: v4(),
createdAt: new Date().toISOString(),
commentThreadTargetArray: [
{
commentableId: entity.id,
commentableType: entity.type,
id: v4(),
createdAt: new Date().toISOString(),
},
],
},
refetchQueries: [
getOperationName(GET_COMPANIES) ?? '',
getOperationName(GET_PEOPLE) ?? '',
getOperationName(GET_COMMENT_THREAD) ?? '',
getOperationName(GET_COMMENT_THREADS_BY_TARGETS) ?? '',
],
onCompleted(data) {
setViewableCommentThreadId(data.createOneCommentThread.id);
setCommentableEntityArray([entity]);
openRightDrawer(RightDrawerPages.CreateCommentThread);
},
});
};
}

View File

@ -1,19 +1,21 @@
import { useRecoilState } from 'recoil';
import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPages';
import { useOpenRightDrawer } from '../../ui/layout/right-drawer/hooks/useOpenRightDrawer';
import { commentableEntityArrayState } from '../states/commentableEntityArrayState';
import { CommentableEntity } from '../types/CommentableEntity';
export function useOpenCommentRightDrawer() {
export function useOpenTimelineRightDrawer() {
const openRightDrawer = useOpenRightDrawer();
const [, setCommentableEntityArray] = useRecoilState(
commentableEntityArrayState,
);
return function openCommentRightDrawer(
return function openTimelineRightDrawer(
commentableEntityArray: CommentableEntity[],
) {
setCommentableEntityArray(commentableEntityArray);
openRightDrawer('comments');
openRightDrawer(RightDrawerPages.Timeline);
};
}

View File

@ -33,12 +33,12 @@ export const CREATE_COMMENT = gql`
`;
export const CREATE_COMMENT_THREAD_WITH_COMMENT = gql`
mutation CreateCommentThreadWithComment(
mutation CreateCommentThread(
$commentThreadId: String!
$commentText: String!
$body: String
$title: String
$authorId: String!
$createdAt: DateTime!
$commentId: String!
$commentThreadTargetArray: [CommentThreadTargetCreateManyCommentThreadInput!]!
) {
createOneCommentThread(
@ -46,16 +46,9 @@ export const CREATE_COMMENT_THREAD_WITH_COMMENT = gql`
id: $commentThreadId
createdAt: $createdAt
updatedAt: $createdAt
comments: {
createMany: {
data: {
authorId: $authorId
id: $commentId
createdAt: $createdAt
body: $commentText
}
}
}
author: { connect: { id: $authorId } }
body: $body
title: $title
commentThreadTargets: {
createMany: { data: $commentThreadTargetArray, skipDuplicates: true }
}
@ -64,6 +57,7 @@ export const CREATE_COMMENT_THREAD_WITH_COMMENT = gql`
id
createdAt
updatedAt
authorId
commentThreadTargets {
id
createdAt

View File

@ -14,6 +14,14 @@ export const GET_COMMENT_THREADS_BY_TARGETS = gql`
}
) {
id
createdAt
title
body
author {
id
firstName
lastName
}
comments {
id
body
@ -40,6 +48,14 @@ export const GET_COMMENT_THREAD = gql`
query GetCommentThread($commentThreadId: String!) {
findManyCommentThreads(where: { id: { equals: $commentThreadId } }) {
id
createdAt
body
title
author {
id
firstName
lastName
}
comments {
id
body
@ -54,6 +70,7 @@ export const GET_COMMENT_THREAD = gql`
}
}
commentThreadTargets {
id
commentableId
commentableType
}

View File

@ -68,3 +68,33 @@ export const DELETE_COMMENT_THREAD = gql`
}
}
`;
export const UPDATE_COMMENT_THREAD_TITLE = gql`
mutation UpdateCommentThreadTitle(
$commentThreadId: String!
$commentThreadTitle: String
) {
updateOneCommentThread(
where: { id: $commentThreadId }
data: { title: { set: $commentThreadTitle } }
) {
id
title
}
}
`;
export const UPDATE_COMMENT_THREAD_BODY = gql`
mutation UpdateCommentThreadBody(
$commentThreadId: String!
$commentThreadBody: String
) {
updateOneCommentThread(
where: { id: $commentThreadId }
data: { body: { set: $commentThreadBody } }
) {
id
body
}
}
`;

View File

@ -1,6 +0,0 @@
import { atom } from 'recoil';
export const createdCommentThreadIdState = atom<string | null>({
key: 'comments/created-comment-thread-id',
default: null,
});

View File

@ -0,0 +1,6 @@
import { atom } from 'recoil';
export const viewableCommentThreadIdState = atom<string | null>({
key: 'comments/viewable-comment-thread-id',
default: null,
});

View File

@ -1,23 +1,27 @@
import { Link } from 'react-router-dom';
import { Theme } from '@emotion/react';
import styled from '@emotion/styled';
import { Avatar } from '@/users/components/Avatar';
export type CompanyChipPropsType = {
id?: string;
name: string;
picture?: string;
};
const StyledContainer = styled.span`
const baseStyle = ({ theme }: { theme: Theme }) => `
align-items: center;
background-color: ${({ theme }) => theme.background.tertiary};
border-radius: ${({ theme }) => theme.spacing(1)};
color: ${({ theme }) => theme.font.color.primary};
background-color: ${theme.background.tertiary};
border-radius: ${theme.spacing(1)};
color: ${theme.font.color.primary};
display: inline-flex;
gap: ${({ theme }) => theme.spacing(1)};
height: calc(20px - 2 * ${({ theme }) => theme.spacing(1)});
gap: ${theme.spacing(1)};
height: calc(20px - 2 * ${theme.spacing(1)});
overflow: hidden;
padding: ${theme.spacing(1)};
padding: ${({ theme }) => theme.spacing(1)};
text-decoration: none;
user-select: none;
@ -38,9 +42,19 @@ const StyledName = styled.span`
white-space: nowrap;
`;
function CompanyChip({ name, picture }: CompanyChipPropsType) {
const StyledContainerLink = styled(Link)`
${baseStyle}
`;
const StyledContainerNoLink = styled.div`
${baseStyle}
`;
function CompanyChip({ id, name, picture }: CompanyChipPropsType) {
const ContainerComponent = id ? StyledContainerLink : StyledContainerNoLink;
return (
<StyledContainer data-testid="company-chip">
<ContainerComponent data-testid="company-chip" to={`/companies/${id}`}>
{picture && (
<Avatar
avatarUrl={picture?.toString()}
@ -50,7 +64,7 @@ function CompanyChip({ name, picture }: CompanyChipPropsType) {
/>
)}
<StyledName>{name}</StyledName>
</StyledContainer>
</ContainerComponent>
);
}

View File

@ -1,5 +1,5 @@
import { CellCommentChip } from '@/comments/components/CellCommentChip';
import { useOpenCommentRightDrawer } from '@/comments/hooks/useOpenCommentRightDrawer';
import { CellCommentChip } from '@/comments/components/table/CellCommentChip';
import { useOpenTimelineRightDrawer } from '@/comments/hooks/useOpenTimelineRightDrawer';
import { EditableCellChip } from '@/ui/components/editable-cell/types/EditableChip';
import { getLogoUrlFromDomainName } from '@/utils/utils';
import {
@ -13,12 +13,12 @@ import CompanyChip from './CompanyChip';
type OwnProps = {
company: Pick<
GetCompaniesQuery['companies'][0],
'id' | 'name' | 'domainName' | '_commentCount' | 'accountOwner'
'id' | 'name' | 'domainName' | '_commentThreadCount' | 'accountOwner'
>;
};
export function CompanyEditableNameChipCell({ company }: OwnProps) {
const openCommentRightDrawer = useOpenCommentRightDrawer();
const openCommentRightDrawer = useOpenTimelineRightDrawer();
const [updateCompany] = useUpdateCompanyMutation();
function handleCommentClick(event: React.MouseEvent<HTMLDivElement>) {
@ -38,6 +38,7 @@ export function CompanyEditableNameChipCell({ company }: OwnProps) {
value={company.name || ''}
placeholder="Name"
picture={getLogoUrlFromDomainName(company.domainName)}
id={company.id}
changeHandler={(value: string) => {
updateCompany({
variables: {
@ -50,7 +51,7 @@ export function CompanyEditableNameChipCell({ company }: OwnProps) {
ChipComponent={CompanyChip}
rightEndContents={[
<CellCommentChip
count={company._commentCount ?? 0}
count={company._commentThreadCount ?? 0}
onClick={handleCommentClick}
/>,
]}

View File

@ -33,8 +33,8 @@ export const SmallName: Story = {
render: getRenderWrapperForComponent(
<TestCellContainer>
<CompanyChip
name="Instragram"
picture="https://api.faviconkit.com/instagram.com/144"
name="Airbnb"
picture="https://api.faviconkit.com/airbnb.com/144"
/>
</TestCellContainer>,
),

View File

@ -1,2 +1,3 @@
export * from './select';
export * from './show';
export * from './update';

View File

@ -22,7 +22,7 @@ export const GET_COMPANIES = gql`
createdAt
address
employees
_commentCount
_commentThreadCount
accountOwner {
id
email

View File

@ -0,0 +1,26 @@
import { gql } from '@apollo/client';
import { useGetCompanyQuery } from '~/generated/graphql';
export const GET_COMPANY = gql`
query GetCompany($id: String!) {
findUniqueCompany(id: $id) {
id
domainName
name
createdAt
address
employees
_commentThreadCount
accountOwner {
id
email
displayName
}
}
}
`;
export function useCompanyQuery(id: string) {
return useGetCompanyQuery({ variables: { id } });
}

View File

@ -16,4 +16,5 @@ export enum InternalHotkeysScope {
PasswordLogin = 'password-login',
AuthIndex = 'auth-index',
CreateProfile = 'create-profile',
ShowPage = 'show-page',
}

View File

@ -1,15 +1,15 @@
import { useState } from 'react';
import styled from '@emotion/styled';
import { CellCommentChip } from '@/comments/components/CellCommentChip';
import { useOpenCommentRightDrawer } from '@/comments/hooks/useOpenCommentRightDrawer';
import { CellCommentChip } from '@/comments/components/table/CellCommentChip';
import { useOpenTimelineRightDrawer } from '@/comments/hooks/useOpenTimelineRightDrawer';
import { EditableCellDoubleText } from '@/ui/components/editable-cell/types/EditableCellDoubleText';
import { CommentableType, Person } from '~/generated/graphql';
import { PersonChip } from './PersonChip';
type OwnProps = {
person: Pick<Person, 'id' | 'firstName' | 'lastName' | '_commentCount'>;
person: Pick<Person, 'id' | 'firstName' | 'lastName' | '_commentThreadCount'>;
onChange: (firstName: string, lastName: string) => void;
};
@ -27,7 +27,7 @@ const RightContainer = styled.div`
export function EditablePeopleFullName({ person, onChange }: OwnProps) {
const [firstNameValue, setFirstNameValue] = useState(person.firstName ?? '');
const [lastNameValue, setLastNameValue] = useState(person.lastName ?? '');
const openCommentRightDrawer = useOpenCommentRightDrawer();
const openCommentRightDrawer = useOpenTimelineRightDrawer();
function handleDoubleTextChange(
firstValue: string,
@ -60,10 +60,13 @@ export function EditablePeopleFullName({ person, onChange }: OwnProps) {
onChange={handleDoubleTextChange}
nonEditModeContent={
<NoEditModeContainer>
<PersonChip name={person.firstName + ' ' + person.lastName} />
<PersonChip
name={person.firstName + ' ' + person.lastName}
id={person.id}
/>
<RightContainer>
<CellCommentChip
count={person._commentCount ?? 0}
count={person._commentThreadCount ?? 0}
onClick={handleCommentClick}
/>
</RightContainer>

View File

@ -30,6 +30,7 @@ export function PeopleCompanyCell({ people }: OwnProps) {
}
nonEditModeContent={
<CompanyChip
id={people.company?.id ?? ''}
name={people.company?.name ?? ''}
picture={getLogoUrlFromDomainName(people.company?.domainName)}
/>

View File

@ -1,54 +1,63 @@
import * as React from 'react';
import { Link } from 'react-router-dom';
import { Theme } from '@emotion/react';
import styled from '@emotion/styled';
import PersonPlaceholder from './person-placeholder.png';
export type PersonChipPropsType = {
id?: string;
name: string;
picture?: string;
};
const StyledContainer = styled.span`
const baseStyle = ({ theme }: { theme: Theme }) => `
align-items: center;
background-color: ${({ theme }) => theme.background.tertiary};
border-radius: ${({ theme }) => theme.spacing(1)};
color: ${({ theme }) => theme.font.color.primary};
background-color: ${theme.background.tertiary};
border-radius: ${theme.spacing(1)};
color: ${theme.font.color.primary};
display: inline-flex;
gap: ${({ theme }) => theme.spacing(1)};
gap: ${theme.spacing(1)};
height: 12px;
overflow: hidden;
padding: ${({ theme }) => theme.spacing(1)};
padding: ${theme.spacing(1)};
text-decoration: none;
:hover {
filter: brightness(95%);
}
img {
border-radius: 100%;
height: 14px;
object-fit: cover;
width: 14px;
}
white-space: nowrap;
`;
const StyledContainerLink = styled(Link)`
${baseStyle}
`;
const StyledContainerNoLink = styled.div`
${baseStyle}
`;
const StyledName = styled.span`
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
`;
export function PersonChip({ name, picture }: PersonChipPropsType) {
export function PersonChip({ id, name, picture }: PersonChipPropsType) {
const ContainerComponent = id ? StyledContainerLink : StyledContainerNoLink;
return (
<StyledContainer data-testid="person-chip">
<ContainerComponent data-testid="person-chip" to={`/person/${id}`}>
<img
data-testid="person-chip-image"
src={picture ? picture.toString() : PersonPlaceholder.toString()}
alt="person"
/>
<StyledName>{name}</StyledName>
</StyledContainer>
</ContainerComponent>
);
}

View File

@ -1,2 +1,3 @@
export * from './select';
export * from './show';
export * from './update';

View File

@ -24,7 +24,7 @@ export const GET_PEOPLE = gql`
firstName
lastName
createdAt
_commentCount
_commentThreadCount
company {
id
name

View File

@ -0,0 +1,19 @@
import { gql } from '@apollo/client';
import { useGetPersonQuery } from '~/generated/graphql';
export const GET_PERSON = gql`
query GetPerson($id: String!) {
findUniquePerson(id: $id) {
id
firstName
lastName
displayName
createdAt
}
}
`;
export function usePersonQuery(id: string) {
return useGetPersonQuery({ variables: { id } });
}

View File

@ -0,0 +1,94 @@
import React, { useState } from 'react';
import styled from '@emotion/styled';
import { IconChevronDown } from '@/ui/icons/index';
type ButtonProps = React.ComponentProps<'button'>;
export type DropdownOptionType = {
label: string;
icon: React.ReactNode;
};
type Props = {
options: DropdownOptionType[];
onSelection: (value: DropdownOptionType) => void;
} & ButtonProps;
const StyledButton = styled.button<ButtonProps>`
align-items: center;
background: ${({ theme }) => theme.background.tertiary};
border: 1px solid ${({ theme }) => theme.border.color.medium};
border-radius: 4px;
color: ${({ theme }) => theme.font.color.secondary};
display: flex;
font-size: ${({ theme }) => theme.font.size.md};
font-weight: ${({ theme }) => theme.font.weight.medium};
gap: 8px;
height: 24px;
line-height: ${({ theme }) => theme.text.lineHeight.lg};
padding: 3px 8px;
svg {
align-items: center;
display: flex;
height: 14px;
justify-content: center;
width: 14px;
}
`;
const DropdownContainer = styled.div`
position: relative;
`;
const DropdownMenu = styled.div`
display: flex;
flex-direction: column;
margin-top: -2px;
position: absolute;
`;
export function DropdownButton({
options,
onSelection,
...buttonProps
}: Props) {
const [isOpen, setIsOpen] = useState(false);
const [selectedOption, setSelectedOption] = useState(options[0]);
if (!options.length) {
throw new Error('You must provide at least one option.');
}
const handleSelect =
(option: DropdownOptionType) =>
(event: React.MouseEvent<HTMLButtonElement>) => {
event.preventDefault();
onSelection(option);
setSelectedOption(option);
setIsOpen(false);
};
return (
<DropdownContainer>
<StyledButton onClick={() => setIsOpen(!isOpen)} {...buttonProps}>
{selectedOption.icon}
{selectedOption.label}
{options.length > 1 && <IconChevronDown />}
</StyledButton>
{isOpen && (
<DropdownMenu>
{options
.filter((option) => option.label !== selectedOption.label)
.map((option, index) => (
<StyledButton key={index} onClick={handleSelect(option)}>
{option.icon}
{option.label}
</StyledButton>
))}
</DropdownMenu>
)}
</DropdownContainer>
);
}

View File

@ -6,17 +6,19 @@ import { textInputStyle } from '@/ui/themes/effects';
import { EditableCell } from '../EditableCell';
export type EditableChipProps = {
id: string;
placeholder?: string;
value: string;
picture: string;
changeHandler: (updated: string) => void;
editModeHorizontalAlign?: 'left' | 'right';
ChipComponent: ComponentType<{
id: string;
name: string;
picture: string;
isOverlapped?: boolean;
}>;
commentCount?: number;
commentThreadCount?: number;
onCommentClick?: (event: React.MouseEvent<HTMLDivElement>) => void;
rightEndContents?: ReactNode[];
};
@ -41,6 +43,7 @@ const RightContainer = styled.div`
// TODO: move right end content in EditableCell
export function EditableCellChip({
id,
value,
placeholder,
changeHandler,
@ -75,7 +78,7 @@ export function EditableCellChip({
}
nonEditModeContent={
<NoEditModeContainer>
<ChipComponent name={inputValue} picture={picture} />
<ChipComponent id={id} name={inputValue} picture={picture} />
<RightContainer>
{rightEndContents &&
rightEndContents.length > 0 &&

View File

@ -0,0 +1,28 @@
import { BlockNoteEditor } from '@blocknote/core';
import { BlockNoteView } from '@blocknote/react';
import styled from '@emotion/styled';
interface BlockEditorProps {
editor: BlockNoteEditor | null;
}
const StyledEditor = styled.div`
min-height: 200px;
width: 100%;
& .editor-create-mode,
.editor-edit-mode {
background: ${({ theme }) => theme.background.primary};
}
& .editor-create-mode [class^='_inlineContent']:before {
color: ${({ theme }) => theme.font.color.tertiary};
font-style: normal !important;
}
`;
export function BlockEditor({ editor }: BlockEditorProps) {
return (
<StyledEditor>
<BlockNoteView editor={editor} />
</StyledEditor>
);
}

View File

@ -22,7 +22,7 @@ const StyledContainer = styled.div`
font-size: ${({ theme }) => theme.font.size.md};
border: none;
display: block;
font-weight: 400;
font-weight: ${({ theme }) => theme.font.weight.regular};
}
& .react-datepicker-popper {

View File

@ -27,8 +27,8 @@ const StyledTextArea = styled(TextareaAutosize)`
border-radius: 5px;
color: ${({ theme }) => theme.font.color.primary};
font-family: inherit;
font-size: 13px;
font-weight: 400;
font-size: ${({ theme }) => theme.font.size.md};
font-weight: ${({ theme }) => theme.font.weight.regular};
line-height: 16px;
overflow: auto;
padding: 8px;
@ -42,7 +42,7 @@ const StyledTextArea = styled(TextareaAutosize)`
&::placeholder {
color: ${({ theme }) => theme.font.color.light};
font-weight: 400;
font-weight: ${({ theme }) => theme.font.weight.regular};
}
`;
@ -121,7 +121,7 @@ export function AutosizeTextInput({
<>
<StyledContainer>
<StyledTextArea
placeholder={placeholder || 'Write something...'}
placeholder={placeholder || 'Write a comment'}
maxRows={MAX_ROWS}
minRows={computedMinRows}
onChange={handleInputChange}

View File

@ -0,0 +1,21 @@
import styled from '@emotion/styled';
const StyledPropertyBoxContainer = styled.div`
align-self: stretch;
background: ${({ theme }) => theme.background.secondary};
border: 1px solid ${({ theme }) => theme.border.color.medium};
border-radius: 4px;
display: flex;
flex-direction: column;
gap: ${({ theme }) => theme.spacing(2)};
padding: ${({ theme }) => theme.spacing(2)} ${({ theme }) => theme.spacing(3)};
`;
interface PropertyBoxProps {
children: JSX.Element;
extraPadding?: boolean;
}
export function PropertyBox({ children }: PropertyBoxProps) {
return <StyledPropertyBoxContainer>{children}</StyledPropertyBoxContainer>;
}

View File

@ -0,0 +1,56 @@
import { ReactNode } from 'react';
import styled from '@emotion/styled';
const StyledPropertyBoxItem = styled.div`
display: flex;
gap: ${({ theme }) => theme.spacing(2)};
`;
const StyledIconContainer = styled.div`
align-items: center;
display: flex;
svg {
align-items: center;
display: flex;
height: 16px;
justify-content: center;
width: 16px;
}
`;
const StyledValueContainer = styled.div`
align-content: flex-start;
align-items: center;
color: ${({ theme }) => theme.font.color.primary};
display: flex;
flex: 1 0 0;
flex-wrap: wrap;
`;
const StyledLabelAndIconContainer = styled.div`
align-items: center;
color: ${({ theme }) => theme.font.color.tertiary};
display: flex;
gap: ${({ theme }) => theme.spacing(1)};
`;
export function PropertyBoxItem({
icon,
label,
value,
}: {
icon: ReactNode;
label?: string;
value: ReactNode;
}) {
return (
<StyledPropertyBoxItem>
<StyledLabelAndIconContainer>
<StyledIconContainer>{icon}</StyledIconContainer>
{label}
</StyledLabelAndIconContainer>
<StyledValueContainer>{value}</StyledValueContainer>
</StyledPropertyBoxItem>
);
}

View File

@ -9,7 +9,7 @@ const StyledMainSectionTitle = styled.h2`
color: ${({ theme }) => theme.font.color.primary};
font-size: ${({ theme }) => theme.font.size.xl};
font-weight: ${({ theme }) => theme.font.weight.semiBold};
line-height: 1.5;
line-height: ${({ theme }) => theme.text.lineHeight.lg};
margin: 0;
`;

View File

@ -10,7 +10,7 @@ const StyledTitle = styled.div`
align-items: center;
display: flex;
flex-direction: row;
font-weight: 500;
font-weight: ${({ theme }) => theme.font.weight.medium};
height: ${({ theme }) => theme.spacing(8)};
padding-left: ${({ theme }) => theme.spacing(2)};
`;

View File

@ -32,7 +32,7 @@ const StyledButton = styled.div<StyledButtonProps>`
`;
const StyledButtonLabel = styled.div`
font-weight: 500;
font-weight: ${({ theme }) => theme.font.weight.medium};
margin-left: ${({ theme }) => theme.spacing(2)};
`;

View File

@ -1,4 +1,4 @@
import { IconComment } from '@/ui/icons/index';
import { IconNotes } from '@/ui/icons/index';
import { EntityTableActionBarButton } from './EntityTableActionBarButton';
@ -9,8 +9,8 @@ type OwnProps = {
export function TableActionBarButtonToggleComments({ onClick }: OwnProps) {
return (
<EntityTableActionBarButton
label="Comment"
icon={<IconComment size={16} />}
label="Notes"
icon={<IconNotes size={16} />}
onClick={onClick}
/>
);

View File

@ -179,7 +179,7 @@ function DropdownButton({
};
const dropdownRef = useRef(null);
useOutsideAlerter(dropdownRef, onOutsideClick);
useOutsideAlerter({ ref: dropdownRef, callback: onOutsideClick });
return (
<StyledDropdownButtonContainer>

View File

@ -43,7 +43,7 @@ const StyledCancelButton = styled.button`
border: none;
color: ${({ theme }) => theme.font.color.secondary};
cursor: pointer;
font-weight: 500;
font-weight: ${({ theme }) => theme.font.weight.medium};
margin-left: auto;
margin-right: ${({ theme }) => theme.spacing(2)};
padding: ${(props) => {

View File

@ -45,7 +45,7 @@ const StyledDelete = styled.div`
`;
const StyledLabelKey = styled.div`
font-weight: 500;
font-weight: ${({ theme }) => theme.font.weight.medium};
`;
function SortOrFilterChip({

View File

@ -27,7 +27,7 @@ const StyledTableHeader = styled.div`
color: ${({ theme }) => theme.font.color.secondary};
display: flex;
flex-direction: row;
font-weight: 500;
font-weight: ${({ theme }) => theme.font.weight.medium};
height: 40px;
justify-content: space-between;
padding-left: ${({ theme }) => theme.spacing(3)};
@ -49,7 +49,7 @@ const StyledViewSection = styled.div`
const StyledFilters = styled.div`
display: flex;
font-weight: 400;
font-weight: ${({ theme }) => theme.font.weight.regular};
gap: 2px;
`;

View File

@ -7,7 +7,7 @@ const onOutsideClick = jest.fn();
function TestComponent() {
const buttonRef = useRef(null);
useOutsideAlerter(buttonRef, onOutsideClick);
useOutsideAlerter({ ref: buttonRef, callback: onOutsideClick });
return (
<div>

View File

@ -1,21 +1,52 @@
import { useEffect } from 'react';
declare type CallbackType = () => void;
export enum OutsideClickAlerterMode {
absolute = 'absolute',
dom = 'dom',
}
export function useOutsideAlerter(
ref: React.RefObject<HTMLInputElement>,
callback: CallbackType,
) {
type OwnProps = {
ref: React.RefObject<HTMLInputElement>;
callback: () => void;
mode?: OutsideClickAlerterMode;
};
export function useOutsideAlerter({
ref,
mode = OutsideClickAlerterMode.dom,
callback,
}: OwnProps) {
useEffect(() => {
function handleClickOutside(event: Event) {
function handleClickOutside(event: MouseEvent) {
const target = event.target as HTMLButtonElement;
if (ref.current && !ref.current.contains(target)) {
if (!ref.current) {
return;
}
if (
mode === OutsideClickAlerterMode.dom &&
!ref.current.contains(target)
) {
callback();
}
if (mode === OutsideClickAlerterMode.absolute) {
const { x, y, width, height } = ref.current.getBoundingClientRect();
const { clientX, clientY } = event;
if (
clientX < x ||
clientX > x + width ||
clientY < y ||
clientY > y + height
) {
callback();
}
}
}
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, [ref, callback]);
}, [ref, callback, mode]);
}

View File

@ -30,3 +30,6 @@ export { IconArrowUpRight } from '@tabler/icons-react';
export { IconBrandGoogle } from '@tabler/icons-react';
export { IconUpload } from '@tabler/icons-react';
export { IconFileUpload } from '@tabler/icons-react';
export { IconChevronsRight } from '@tabler/icons-react';
export { IconNotes } from '@tabler/icons-react';
export { IconCirclePlus } from '@tabler/icons-react';

View File

@ -10,7 +10,7 @@ type OwnProps = {
topMargin?: number;
};
const MainContainer = styled.div<{ topMargin: number }>`
const StyledMainContainer = styled.div<{ topMargin: number }>`
background: ${({ theme }) => theme.background.noisy};
display: flex;
@ -27,27 +27,21 @@ type LeftContainerProps = {
isRightDrawerOpen?: boolean;
};
const LeftContainer = styled.div<LeftContainerProps>`
const StyledLeftContainer = styled.div<LeftContainerProps>`
display: flex;
position: relative;
width: calc(
100% -
${(props) =>
props.isRightDrawerOpen
? `${props.theme.rightDrawerWidth} - ${props.theme.spacing(2)}`
: '0px'}
);
width: 100%;
`;
export function ContentContainer({ children, topMargin }: OwnProps) {
const [isRightDrawerOpen] = useRecoilState(isRightDrawerOpenState);
return (
<MainContainer topMargin={topMargin ?? 0}>
<LeftContainer isRightDrawerOpen={isRightDrawerOpen}>
<StyledMainContainer topMargin={topMargin ?? 0}>
<StyledLeftContainer isRightDrawerOpen={isRightDrawerOpen}>
<Panel>{children}</Panel>
</LeftContainer>
</StyledLeftContainer>
<RightDrawer />
</MainContainer>
</StyledMainContainer>
);
}

View File

@ -8,7 +8,7 @@ const StyledTitle = styled.div`
color: ${({ theme }) => theme.font.color.light};
display: flex;
font-size: ${({ theme }) => theme.font.size.xs};
font-weight: 600;
font-weight: ${({ theme }) => theme.font.weight.semiBold};
padding-bottom: ${({ theme }) => theme.spacing(2)};
padding-left: ${({ theme }) => theme.spacing(1)};
padding-top: ${({ theme }) => theme.spacing(8)};

View File

@ -42,7 +42,7 @@ const StyledName = styled.div`
color: ${({ theme }) => theme.font.color.primary};
font-family: 'Inter';
font-size: ${({ theme }) => theme.font.size.md};
font-weight: 500;
font-weight: ${({ theme }) => theme.font.weight.medium};
margin-left: ${({ theme }) => theme.spacing(2)};
`;

View File

@ -1,33 +1,61 @@
import { useRef } from 'react';
import styled from '@emotion/styled';
import { useRecoilState } from 'recoil';
import {
OutsideClickAlerterMode,
useOutsideAlerter,
} from '@/ui/hooks/useOutsideAlerter';
import { isDefined } from '@/utils/type-guards/isDefined';
import { Panel } from '../../Panel';
import { isRightDrawerOpenState } from '../states/isRightDrawerOpenState';
import { rightDrawerPageState } from '../states/rightDrawerPageState';
import { RightDrawerRouter } from './RightDrawerRouter';
const StyledContainer = styled.div`
background: ${({ theme }) => theme.background.primary};
box-shadow: ${({ theme }) => theme.boxShadow.strong};
height: 100%;
overflow-x: hidden;
position: fixed;
right: 0;
top: 0;
transition: width 0.5s;
width: ${({ theme }) => theme.rightDrawerWidth};
z-index: 2;
`;
const StyledRightDrawer = styled.div`
display: flex;
flex-direction: row;
width: ${({ theme }) => theme.rightDrawerWidth};
width: 100%;
`;
export function RightDrawer() {
const [isRightDrawerOpen] = useRecoilState(isRightDrawerOpenState);
const [isRightDrawerOpen, setIsRightDrawerOpen] = useRecoilState(
isRightDrawerOpenState,
);
const [rightDrawerPage] = useRecoilState(rightDrawerPageState);
const rightDrawerRef = useRef(null);
useOutsideAlerter({
ref: rightDrawerRef,
callback: () => setIsRightDrawerOpen(false),
mode: OutsideClickAlerterMode.absolute,
});
if (!isRightDrawerOpen || !isDefined(rightDrawerPage)) {
return <></>;
}
return (
<StyledRightDrawer>
<Panel>
<>
<StyledContainer>
<StyledRightDrawer ref={rightDrawerRef}>
<RightDrawerRouter />
</Panel>
</StyledRightDrawer>
</StyledContainer>
</>
);
}

View File

@ -3,5 +3,7 @@ import styled from '@emotion/styled';
export const RightDrawerBody = styled.div`
display: flex;
flex-direction: column;
height: calc(100vh - ${({ theme }) => theme.spacing(10)});
overflow: auto;
position: relative;
`;

View File

@ -1,10 +1,12 @@
import { useRecoilState } from 'recoil';
import { RightDrawerComments } from '@/comments/components/RightDrawerComments';
import { RightDrawerCreateCommentThread } from '@/comments/components/RightDrawerCreateCommentThread';
import { RightDrawerCreateCommentThread } from '@/comments/components/right-drawer/create/RightDrawerCreateCommentThread';
import { RightDrawerEditCommentThread } from '@/comments/components/right-drawer/edit/RightDrawerEditCommentThread';
import { RightDrawerTimeline } from '@/comments/components/right-drawer/RightDrawerTimeline';
import { isDefined } from '@/utils/type-guards/isDefined';
import { rightDrawerPageState } from '../states/rightDrawerPageState';
import { RightDrawerPages } from '../types/RightDrawerPages';
export function RightDrawerRouter() {
const [rightDrawerPage] = useRecoilState(rightDrawerPageState);
@ -13,11 +15,14 @@ export function RightDrawerRouter() {
return <></>;
}
return rightDrawerPage === 'comments' ? (
<RightDrawerComments />
) : rightDrawerPage === 'create-comment-thread' ? (
<RightDrawerCreateCommentThread />
) : (
<></>
);
switch (rightDrawerPage) {
case RightDrawerPages.Timeline:
return <RightDrawerTimeline />;
case RightDrawerPages.CreateCommentThread:
return <RightDrawerCreateCommentThread />;
case RightDrawerPages.EditCommentThread:
return <RightDrawerEditCommentThread />;
default:
return <></>;
}
}

View File

@ -1,5 +1,7 @@
import styled from '@emotion/styled';
import { Button } from '@/ui/components/buttons/Button';
import { RightDrawerTopBarCloseButton } from './RightDrawerTopBarCloseButton';
const StyledRightDrawerTopBar = styled.div`
@ -8,7 +10,7 @@ const StyledRightDrawerTopBar = styled.div`
color: ${({ theme }) => theme.font.color.secondary};
display: flex;
flex-direction: row;
font-size: 13px;
font-size: ${({ theme }) => theme.font.size.md};
justify-content: space-between;
min-height: 40px;
padding-left: 8px;
@ -17,19 +19,24 @@ const StyledRightDrawerTopBar = styled.div`
const StyledTopBarTitle = styled.div`
align-items: center;
font-weight: 500;
font-weight: ${({ theme }) => theme.font.weight.medium};
margin-right: ${({ theme }) => theme.spacing(1)};
`;
export function RightDrawerTopBar({
title,
}: {
type OwnProps = {
title: string | null | undefined;
}) {
onSave?: () => void;
};
export function RightDrawerTopBar({ title, onSave }: OwnProps) {
function handleOnClick() {
onSave?.();
}
return (
<StyledRightDrawerTopBar>
<StyledTopBarTitle>{title}</StyledTopBarTitle>
<RightDrawerTopBarCloseButton />
<StyledTopBarTitle>{title}</StyledTopBarTitle>
{onSave && <Button title="Save" onClick={handleOnClick} />}
</StyledRightDrawerTopBar>
);
}

View File

@ -1,7 +1,7 @@
import styled from '@emotion/styled';
import { useRecoilState } from 'recoil';
import { IconPlus } from '@/ui/icons/index';
import { IconChevronsRight } from '@/ui/icons/index';
import { isRightDrawerOpenState } from '../states/isRightDrawerOpenState';
@ -24,7 +24,6 @@ const StyledButton = styled.button`
}
svg {
color: ${({ theme }) => theme.font.color.tertiary};
transform: rotate(45deg);
}
`;
@ -37,7 +36,7 @@ export function RightDrawerTopBarCloseButton() {
return (
<StyledButton onClick={handleButtonClick}>
<IconPlus size={16} />
<IconChevronsRight size={16} />
</StyledButton>
);
}

View File

@ -2,13 +2,13 @@ import { useRecoilState } from 'recoil';
import { isRightDrawerOpenState } from '../states/isRightDrawerOpenState';
import { rightDrawerPageState } from '../states/rightDrawerPageState';
import { RightDrawerPage } from '../types/RightDrawerPage';
import { RightDrawerPages } from '../types/RightDrawerPages';
export function useOpenRightDrawer() {
const [, setIsRightDrawerOpen] = useRecoilState(isRightDrawerOpenState);
const [, setRightDrawerPage] = useRecoilState(rightDrawerPageState);
return function openRightDrawer(rightDrawerPage: RightDrawerPage) {
return function openRightDrawer(rightDrawerPage: RightDrawerPages) {
setRightDrawerPage(rightDrawerPage);
setIsRightDrawerOpen(true);
};

View File

@ -1,8 +1,8 @@
import { atom } from 'recoil';
import { RightDrawerPage } from '../types/RightDrawerPage';
import { RightDrawerPages } from '../types/RightDrawerPages';
export const rightDrawerPageState = atom<RightDrawerPage | null>({
export const rightDrawerPageState = atom<RightDrawerPages | null>({
key: 'ui/layout/right-drawer-page',
default: null,
});

View File

@ -1 +0,0 @@
export type RightDrawerPage = 'comments' | 'create-comment-thread';

View File

@ -0,0 +1,5 @@
export enum RightDrawerPages {
Timeline = 'timeline',
CreateCommentThread = 'create-comment-thread',
EditCommentThread = 'edit-comment-thread',
}

View File

@ -0,0 +1,85 @@
import { Tooltip } from 'react-tooltip';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { v4 as uuidV4 } from 'uuid';
import { Avatar } from '@/users/components/Avatar';
import {
beautifyExactDate,
beautifyPastDateRelativeToNow,
} from '@/utils/datetime/date-utils';
const StyledShowPageSummaryCard = styled.div`
align-items: center;
display: flex;
flex-direction: column;
gap: ${({ theme }) => theme.spacing(3)};
justify-content: center;
padding: ${({ theme }) => theme.spacing(6)} ${({ theme }) => theme.spacing(3)}
${({ theme }) => theme.spacing(3)} ${({ theme }) => theme.spacing(3)};
`;
const StyledInfoContainer = styled.div`
align-items: center;
display: flex;
flex-direction: column;
`;
const StyledDate = styled.div`
color: ${({ theme }) => theme.font.color.tertiary};
cursor: pointer;
`;
const StyledTitle = styled.div`
color: ${({ theme }) => theme.font.color.primary};
font-size: ${({ theme }) => theme.font.size.xl};
font-weight: ${({ theme }) => theme.font.weight.semiBold};
`;
const StyledTooltip = styled(Tooltip)`
background-color: ${({ theme }) => theme.background.primary};
box-shadow: ${({ theme }) => theme.boxShadow.light};
color: ${({ theme }) => theme.font.color.primary};
padding: ${({ theme }) => theme.spacing(2)};
`;
export function ShowPageSummaryCard({
logoOrAvatar,
title,
date,
}: {
logoOrAvatar?: string;
title: string;
date: string;
}) {
const beautifiedCreatedAt =
date !== '' ? beautifyPastDateRelativeToNow(date) : '';
const exactCreatedAt = date !== '' ? beautifyExactDate(date) : '';
const theme = useTheme();
const dateElementId = `date-id-${uuidV4()}`;
return (
<StyledShowPageSummaryCard>
<Avatar
avatarUrl={logoOrAvatar}
size={theme.icon.size.xl}
placeholder={title}
/>
<StyledInfoContainer>
<StyledTitle>{title}</StyledTitle>
<StyledDate id={dateElementId}>
Added {beautifiedCreatedAt} ago
</StyledDate>
<StyledTooltip
anchorSelect={`#${dateElementId}`}
content={exactCreatedAt}
clickable
noArrow
place="right"
/>
</StyledInfoContainer>
</StyledShowPageSummaryCard>
);
}

View File

@ -0,0 +1,9 @@
import styled from '@emotion/styled';
export const ShowPageLeftContainer = styled.div`
background: ${({ theme }) => theme.background.secondary};
border-radius: 8px;
border-right: 1px solid ${({ theme }) => theme.border.color.light};
padding: 0px ${({ theme }) => theme.spacing(3)};
width: 320px;
`;

View File

@ -0,0 +1,9 @@
import styled from '@emotion/styled';
export const ShowPageRightContainer = styled.div`
display: flex;
flex: 1 0 0;
flex-direction: column;
justify-content: center;
overflow: hidden;
`;

View File

@ -3,6 +3,7 @@ export const icon = {
sm: 14,
md: 16,
lg: 20,
xl: 40,
},
stroke: {
sm: 1.6,

View File

@ -18,7 +18,7 @@ const common = {
horizontalCellMargin: '8px',
checkboxColumnWidth: '32px',
},
rightDrawerWidth: '300px',
rightDrawerWidth: '500px',
clickableElementBackgroundTransition: 'background 0.1s ease',
lastLayerZIndex: 2147483647,
};
@ -32,6 +32,7 @@ export const lightTheme = {
selectedCardHover: color.blue20,
selectedCard: color.blue10,
font: fontLight,
name: 'light',
},
};
export type ThemeType = typeof lightTheme;
@ -45,6 +46,7 @@ export const darkTheme: ThemeType = {
selectedCardHover: color.blue70,
selectedCard: color.blue80,
font: fontDark,
name: 'dark',
},
};

View File

@ -17,8 +17,10 @@ export const StyledAvatar = styled.div<Omit<OwnProps, 'placeholder'>>`
!isNonEmptyString(props.avatarUrl)
? props.theme.background.tertiary
: 'none'};
background-image: url(${(props) =>
isNonEmptyString(props.avatarUrl) ? props.avatarUrl : 'none'});
${(props) =>
isNonEmptyString(props.avatarUrl)
? `background-image: url(${props.avatarUrl});`
: ''}
background-size: cover;
border-radius: ${(props) => (props.type === 'rounded' ? '50%' : '2px')};
color: ${({ theme }) => theme.font.color.primary};

View File

@ -1,10 +1,8 @@
import { getOperationName } from '@apollo/client/utilities';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { useRecoilState } from 'recoil';
import { v4 as uuidv4 } from 'uuid';
import { isMockModeState } from '@/auth/states/isMockModeState';
import { GET_COMPANIES } from '@/companies/services';
import { useHotkeysScopeOnMountOnly } from '@/hotkeys/hooks/useHotkeysScopeOnMountOnly';
import { InternalHotkeysScope } from '@/hotkeys/types/internal/InternalHotkeysScope';
@ -28,17 +26,10 @@ const StyledTableContainer = styled.div`
`;
export function Companies() {
const [isMockMode] = useRecoilState(isMockModeState);
const hotkeysEnabled = !isMockMode;
useHotkeysScopeOnMountOnly(
{
useHotkeysScopeOnMountOnly({
scope: InternalHotkeysScope.Table,
customScopes: { 'command-menu': true, goto: true },
},
hotkeysEnabled,
);
});
const [insertCompany] = useInsertCompanyMutation();

View File

@ -0,0 +1,75 @@
import { useParams } from 'react-router-dom';
import { useTheme } from '@emotion/react';
import { Timeline } from '@/comments/components/timeline/Timeline';
import { useCompanyQuery } from '@/companies/services';
import { useHotkeysScopeOnMountOnly } from '@/hotkeys/hooks/useHotkeysScopeOnMountOnly';
import { InternalHotkeysScope } from '@/hotkeys/types/internal/InternalHotkeysScope';
import { RawLink } from '@/ui/components/links/RawLink';
import { PropertyBox } from '@/ui/components/property-box/PropertyBox';
import { PropertyBoxItem } from '@/ui/components/property-box/PropertyBoxItem';
import { IconBuildingSkyscraper, IconLink, IconMap } from '@/ui/icons/index';
import { WithTopBarContainer } from '@/ui/layout/containers/WithTopBarContainer';
import { ShowPageLeftContainer } from '@/ui/layout/show-page/containers/ShowPageLeftContainer';
import { ShowPageRightContainer } from '@/ui/layout/show-page/containers/ShowPageRightContainer';
import { ShowPageSummaryCard } from '@/ui/layout/show-page/ShowPageSummaryCard';
import { getLogoUrlFromDomainName } from '@/utils/utils';
import { CommentableType } from '~/generated/graphql';
export function CompanyShow() {
const companyId = useParams().companyId ?? '';
useHotkeysScopeOnMountOnly({
scope: InternalHotkeysScope.ShowPage,
customScopes: { 'command-menu': true, goto: true },
});
const { data } = useCompanyQuery(companyId);
const company = data?.findUniqueCompany;
const theme = useTheme();
return (
<WithTopBarContainer
title={company?.name ?? ''}
icon={<IconBuildingSkyscraper size={theme.icon.size.md} />}
>
<>
<ShowPageLeftContainer>
<ShowPageSummaryCard
logoOrAvatar={getLogoUrlFromDomainName(company?.domainName ?? '')}
title={company?.name ?? 'No name'}
date={company?.createdAt ?? ''}
/>
<PropertyBox extraPadding={true}>
<>
<PropertyBoxItem
icon={<IconLink />}
value={
<RawLink
href={
company?.domainName
? 'https://' + company?.domainName
: ''
}
>
{company?.domainName}
</RawLink>
}
/>
<PropertyBoxItem
icon={<IconMap />}
value={company?.address ? company?.address : 'No address'}
/>
</>
</PropertyBox>
</ShowPageLeftContainer>
<ShowPageRightContainer>
<Timeline
entity={{ id: company?.id ?? '', type: CommentableType.Company }}
/>
</ShowPageRightContainer>
</>
</WithTopBarContainer>
);
}

View File

@ -1,38 +0,0 @@
import { expect } from '@storybook/jest';
import type { Meta } from '@storybook/react';
import { userEvent, within } from '@storybook/testing-library';
import { graphqlMocks } from '~/testing/graphqlMocks';
import { getRenderWrapperForPage } from '~/testing/renderWrappers';
import { Companies } from '../Companies';
import { Story } from './Companies.stories';
const meta: Meta<typeof Companies> = {
title: 'Pages/Companies/Comments',
component: Companies,
};
export default meta;
export const OpenCommentsSection: Story = {
render: getRenderWrapperForPage(<Companies />, '/companies'),
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const firstRow = await canvas.findByTestId('row-id-1');
expect(firstRow).toBeDefined();
const commentsChip = await within(firstRow).findByTestId('comment-chip');
expect(commentsChip).toBeDefined();
userEvent.click(commentsChip);
const commentSection = await canvas.findByText('Comments');
expect(commentSection).toBeDefined();
},
parameters: {
msw: graphqlMocks,
},
};

View File

@ -0,0 +1,80 @@
import { getOperationName } from '@apollo/client/utilities';
import type { Meta, StoryObj } from '@storybook/react';
import { within } from '@storybook/testing-library';
import { graphql } from 'msw';
import {
GET_COMMENT_THREAD,
GET_COMMENT_THREADS_BY_TARGETS,
} from '@/comments/services';
import { CREATE_COMMENT_THREAD_WITH_COMMENT } from '@/comments/services/create';
import { GET_COMPANY } from '@/companies/services';
import { graphqlMocks } from '~/testing/graphqlMocks';
import { mockedCommentThreads } from '~/testing/mock-data/comment-threads';
import { mockedCompaniesData } from '~/testing/mock-data/companies';
import { getRenderWrapperForPage } from '~/testing/renderWrappers';
import { CompanyShow } from '../CompanyShow';
const meta: Meta<typeof CompanyShow> = {
title: 'Pages/Companies/Company',
component: CompanyShow,
};
export default meta;
export type Story = StoryObj<typeof CompanyShow>;
export const Default: Story = {
render: getRenderWrapperForPage(
<CompanyShow />,
'/companies/89bb825c-171e-4bcc-9cf7-43448d6fb278',
),
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const notesButton = await canvas.findByText('Notes');
await notesButton.click();
},
parameters: {
msw: [
...graphqlMocks,
graphql.mutation(
getOperationName(CREATE_COMMENT_THREAD_WITH_COMMENT) ?? '',
(req, res, ctx) => {
return res(
ctx.data({
createOneCommentThread: mockedCommentThreads[0],
}),
);
},
),
graphql.query(
getOperationName(GET_COMMENT_THREADS_BY_TARGETS) ?? '',
(req, res, ctx) => {
return res(
ctx.data({
findManyCommentThreads: mockedCommentThreads,
}),
);
},
),
graphql.query(
getOperationName(GET_COMMENT_THREAD) ?? '',
(req, res, ctx) => {
return res(
ctx.data({
findManyCommentThreads: mockedCommentThreads[0],
}),
);
},
),
graphql.query(getOperationName(GET_COMPANY) ?? '', (req, res, ctx) => {
return res(
ctx.data({
findUniqueCompany: mockedCompaniesData[0],
}),
);
}),
],
},
};

View File

@ -0,0 +1,51 @@
import { useParams } from 'react-router-dom';
import { useTheme } from '@emotion/react';
import { Timeline } from '@/comments/components/timeline/Timeline';
import { usePersonQuery } from '@/people/services';
import { PropertyBox } from '@/ui/components/property-box/PropertyBox';
import { PropertyBoxItem } from '@/ui/components/property-box/PropertyBoxItem';
import { IconLink, IconUser } from '@/ui/icons/index';
import { WithTopBarContainer } from '@/ui/layout/containers/WithTopBarContainer';
import { ShowPageLeftContainer } from '@/ui/layout/show-page/containers/ShowPageLeftContainer';
import { ShowPageRightContainer } from '@/ui/layout/show-page/containers/ShowPageRightContainer';
import { ShowPageSummaryCard } from '@/ui/layout/show-page/ShowPageSummaryCard';
import { CommentableType } from '~/generated/graphql';
export function PersonShow() {
const personId = useParams().personId ?? '';
const { data } = usePersonQuery(personId);
const person = data?.findUniquePerson;
const theme = useTheme();
return (
<WithTopBarContainer
title={person?.firstName ?? ''}
icon={<IconUser size={theme.icon.size.md} />}
>
<>
<ShowPageLeftContainer>
<ShowPageSummaryCard
title={person?.displayName ?? 'No name'}
date={person?.createdAt ?? ''}
/>
<PropertyBox extraPadding={true}>
<>
<PropertyBoxItem
icon={<IconLink />}
value={person?.firstName ?? 'No First name'}
/>
</>
</PropertyBox>
</ShowPageLeftContainer>
<ShowPageRightContainer>
<Timeline
entity={{ id: person?.id ?? '', type: CommentableType.Person }}
/>
</ShowPageRightContainer>
</>
</WithTopBarContainer>
);
}

View File

@ -1,38 +0,0 @@
import { expect } from '@storybook/jest';
import type { Meta } from '@storybook/react';
import { userEvent, within } from '@storybook/testing-library';
import { graphqlMocks } from '~/testing/graphqlMocks';
import { getRenderWrapperForPage } from '~/testing/renderWrappers';
import { People } from '../People';
import { Story } from './People.stories';
const meta: Meta<typeof People> = {
title: 'Pages/People/Comments',
component: People,
};
export default meta;
export const OpenCommentsSection: Story = {
render: getRenderWrapperForPage(<People />, '/people'),
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const firstRow = await canvas.findByTestId('row-id-1');
expect(firstRow).toBeDefined();
const commentsChip = await within(firstRow).findByTestId('comment-chip');
expect(commentsChip).toBeDefined();
userEvent.click(commentsChip);
const commentSection = await canvas.findByText('Comments');
expect(commentSection).toBeDefined();
},
parameters: {
msw: graphqlMocks,
},
};

View File

@ -0,0 +1,25 @@
import type { Meta, StoryObj } from '@storybook/react';
import { graphqlMocks } from '~/testing/graphqlMocks';
import { getRenderWrapperForPage } from '~/testing/renderWrappers';
import { PersonShow } from '../PersonShow';
const meta: Meta<typeof PersonShow> = {
title: 'Pages/People/Person',
component: PersonShow,
};
export default meta;
export type Story = StoryObj<typeof PersonShow>;
export const Default: Story = {
render: getRenderWrapperForPage(
<PersonShow />,
'/companies/89bb825c-171e-4bcc-9cf7-43448d6fb278',
),
parameters: {
msw: graphqlMocks,
},
};

View File

@ -1,4 +1,5 @@
import {
Comment,
CommentableType,
CommentThread,
CommentThreadTarget,
@ -6,8 +7,21 @@ import {
type MockedCommentThread = Pick<
CommentThread,
'id' | 'createdAt' | 'updatedAt' | '__typename'
| 'id'
| 'createdAt'
| 'updatedAt'
| '__typename'
| 'body'
| 'title'
| 'authorId'
> & {
author: {
__typename?: 'User' | undefined;
id: string;
firstName: string;
lastName: string;
};
comments: Array<Pick<Comment, 'body'>>;
commentThreadTargets: Array<
Pick<
CommentThreadTarget,
@ -27,6 +41,15 @@ export const mockedCommentThreads: Array<MockedCommentThread> = [
id: '89bb825c-171e-4bcc-9cf7-43448d6fb230',
createdAt: '2023-04-26T10:12:42.33625+00:00',
updatedAt: '2023-04-26T10:23:42.33625+00:00',
title: 'My very first note',
body: null,
author: {
id: '374fe3a5-df1e-4119-afe0-2a62a2ba481e',
firstName: 'Charles',
lastName: 'Test',
},
authorId: '374fe3a5-df1e-4119-afe0-2a62a2ba481e',
comments: [],
commentThreadTargets: [
{
id: '89bb825c-171e-4bcc-9cf7-43448d6fb300',
@ -63,6 +86,15 @@ export const mockedCommentThreads: Array<MockedCommentThread> = [
id: '89bb825c-171e-4bcc-9cf7-43448d6fb278',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
title: 'Another note',
body: null,
author: {
id: '374fe3a5-df1e-4119-afe0-2a62a2ba481e',
firstName: 'Charles',
lastName: 'Test',
},
authorId: '374fe3a5-df1e-4119-afe0-2a62a2ba481e',
comments: [],
commentThreadTargets: [
{
id: '89bb825c-171e-4bcc-9cf7-43448d6fb278',

View File

@ -9,7 +9,7 @@ type MockedCompany = Pick<
| 'createdAt'
| 'address'
| 'employees'
| '_commentCount'
| '_commentThreadCount'
> & {
accountOwner: Pick<
User,
@ -25,7 +25,7 @@ export const mockedCompaniesData: Array<MockedCompany> = [
createdAt: '2023-04-26T10:08:54.724515+00:00',
address: '17 rue de clignancourt',
employees: 12,
_commentCount: 1,
_commentThreadCount: 1,
accountOwner: {
email: 'charles@test.com',
displayName: 'Charles Test',
@ -43,7 +43,7 @@ export const mockedCompaniesData: Array<MockedCompany> = [
createdAt: '2023-04-26T10:12:42.33625+00:00',
address: '',
employees: 1,
_commentCount: 1,
_commentThreadCount: 1,
accountOwner: null,
__typename: 'Company',
},
@ -54,7 +54,7 @@ export const mockedCompaniesData: Array<MockedCompany> = [
createdAt: '2023-04-26T10:10:32.530184+00:00',
address: '',
employees: 1,
_commentCount: 1,
_commentThreadCount: 1,
accountOwner: null,
__typename: 'Company',
},
@ -65,7 +65,7 @@ export const mockedCompaniesData: Array<MockedCompany> = [
createdAt: '2023-03-21T06:30:25.39474+00:00',
address: '',
employees: 10,
_commentCount: 0,
_commentThreadCount: 0,
accountOwner: null,
__typename: 'Company',
},
@ -76,7 +76,7 @@ export const mockedCompaniesData: Array<MockedCompany> = [
createdAt: '2023-04-26T10:13:29.712485+00:00',
address: '10 rue de la Paix',
employees: 1,
_commentCount: 2,
_commentThreadCount: 2,
accountOwner: null,
__typename: 'Company',
},
@ -87,7 +87,7 @@ export const mockedCompaniesData: Array<MockedCompany> = [
createdAt: '2023-04-26T10:09:25.656555+00:00',
address: '',
employees: 1,
_commentCount: 13,
_commentThreadCount: 13,
accountOwner: null,
__typename: 'Company',
},
@ -98,7 +98,7 @@ export const mockedCompaniesData: Array<MockedCompany> = [
createdAt: '2023-04-26T10:09:25.656555+00:00',
address: '',
employees: 1,
_commentCount: 1,
_commentThreadCount: 1,
accountOwner: null,
__typename: 'Company',
},

View File

@ -9,7 +9,7 @@ type MockedPerson = Pick<
| '__typename'
| 'phone'
| 'city'
| '_commentCount'
| '_commentThreadCount'
| 'createdAt'
> & {
company: Pick<Company, 'id' | 'name' | 'domainName' | '__typename'>;
@ -29,7 +29,7 @@ export const mockedPeopleData: Array<MockedPerson> = [
__typename: 'Company',
},
phone: '06 12 34 56 78',
_commentCount: 1,
_commentThreadCount: 1,
createdAt: '2023-04-20T13:20:09.158312+00:00',
city: 'Paris',
@ -47,7 +47,7 @@ export const mockedPeopleData: Array<MockedPerson> = [
__typename: 'Company',
},
phone: '06 12 34 56 78',
_commentCount: 1,
_commentThreadCount: 1,
createdAt: '2023-04-20T13:20:09.158312+00:00',
city: 'Paris',
@ -65,7 +65,7 @@ export const mockedPeopleData: Array<MockedPerson> = [
__typename: 'Company',
},
phone: '06 12 34 56 78',
_commentCount: 1,
_commentThreadCount: 1,
createdAt: '2023-04-20T13:20:09.158312+00:00',
city: 'Paris',
@ -83,7 +83,7 @@ export const mockedPeopleData: Array<MockedPerson> = [
__typename: 'Company',
},
phone: '06 12 34 56 78',
_commentCount: 2,
_commentThreadCount: 2,
createdAt: '2023-04-20T13:20:09.158312+00:00',
city: 'Paris',

File diff suppressed because it is too large Load Diff

View File

@ -11,6 +11,15 @@ export class CommentThreadCountAggregateInput {
@HideField()
workspaceId?: true;
@Field(() => Boolean, {nullable:true})
authorId?: true;
@Field(() => Boolean, {nullable:true})
body?: true;
@Field(() => Boolean, {nullable:true})
title?: true;
@HideField()
deletedAt?: true;

View File

@ -12,6 +12,15 @@ export class CommentThreadCountAggregate {
@HideField()
workspaceId!: number;
@Field(() => Int, {nullable:false})
authorId!: number;
@Field(() => Int, {nullable:false})
body!: number;
@Field(() => Int, {nullable:false})
title!: number;
@HideField()
deletedAt!: number;

View File

@ -12,6 +12,15 @@ export class CommentThreadCountOrderByAggregateInput {
@HideField()
workspaceId?: keyof typeof SortOrder;
@Field(() => SortOrder, {nullable:true})
authorId?: keyof typeof SortOrder;
@Field(() => SortOrder, {nullable:true})
body?: keyof typeof SortOrder;
@Field(() => SortOrder, {nullable:true})
title?: keyof typeof SortOrder;
@HideField()
deletedAt?: keyof typeof SortOrder;

Some files were not shown because too many files have changed in this diff Show More