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 9fbbf2db006e529330edc64f3eb8ff9ecdde6bb0.

* 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
191 changed files with 5390 additions and 721 deletions

View File

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

View File

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

View File

@ -5,6 +5,8 @@
"dependencies": { "dependencies": {
"@apollo/client": "^3.7.5", "@apollo/client": "^3.7.5",
"@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@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/react": "^11.10.6",
"@emotion/styled": "^11.10.5", "@emotion/styled": "^11.10.5",
"@floating-ui/react": "^0.24.3", "@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 { SettingsProfile } from '~/pages/settings/SettingsProfile';
import { SettingsWorkspaceMembers } from '~/pages/settings/SettingsWorkspaceMembers'; import { SettingsWorkspaceMembers } from '~/pages/settings/SettingsWorkspaceMembers';
import { CompanyShow } from './pages/companies/CompanyShow';
import { PersonShow } from './pages/people/PersonShow';
import { AppInternalHooks } from './AppInternalHooks'; import { AppInternalHooks } from './AppInternalHooks';
/** /**
@ -65,7 +67,13 @@ export function App() {
<Routes> <Routes>
<Route path="" element={<Navigate to="/people" replace />} /> <Route path="" element={<Navigate to="/people" replace />} />
<Route path="people" element={<People />} /> <Route path="people" element={<People />} />
<Route path="person/:personId" element={<PersonShow />} />
<Route path="companies" element={<Companies />} /> <Route path="companies" element={<Companies />} />
<Route
path="companies/:companyId"
element={<CompanyShow />}
/>
<Route path="opportunities" element={<Opportunities />} /> <Route path="opportunities" element={<Opportunities />} />
<Route <Route
path="settings/*" path="settings/*"

View File

@ -206,24 +206,47 @@ export type CommentScalarWhereInput = {
export type CommentThread = { export type CommentThread = {
__typename?: 'CommentThread'; __typename?: 'CommentThread';
author: User;
authorId: Scalars['String'];
body?: Maybe<Scalars['String']>;
commentThreadTargets?: Maybe<Array<CommentThreadTarget>>; commentThreadTargets?: Maybe<Array<CommentThreadTarget>>;
comments?: Maybe<Array<Comment>>; comments?: Maybe<Array<Comment>>;
createdAt: Scalars['DateTime']; createdAt: Scalars['DateTime'];
id: Scalars['ID']; id: Scalars['ID'];
title?: Maybe<Scalars['String']>;
updatedAt: Scalars['DateTime']; updatedAt: Scalars['DateTime'];
}; };
export type CommentThreadCreateInput = { export type CommentThreadCreateInput = {
author: UserCreateNestedOneWithoutCommentThreadInput;
body?: InputMaybe<Scalars['String']>;
commentThreadTargets?: InputMaybe<CommentThreadTargetCreateNestedManyWithoutCommentThreadInput>; commentThreadTargets?: InputMaybe<CommentThreadTargetCreateNestedManyWithoutCommentThreadInput>;
comments?: InputMaybe<CommentCreateNestedManyWithoutCommentThreadInput>; comments?: InputMaybe<CommentCreateNestedManyWithoutCommentThreadInput>;
createdAt?: InputMaybe<Scalars['DateTime']>; createdAt?: InputMaybe<Scalars['DateTime']>;
id?: InputMaybe<Scalars['String']>; id?: InputMaybe<Scalars['String']>;
title?: InputMaybe<Scalars['String']>;
updatedAt?: InputMaybe<Scalars['DateTime']>; updatedAt?: InputMaybe<Scalars['DateTime']>;
}; };
export type CommentThreadCreateManyWorkspaceInput = { export type CommentThreadCreateManyAuthorInput = {
body?: InputMaybe<Scalars['String']>;
createdAt?: InputMaybe<Scalars['DateTime']>; createdAt?: InputMaybe<Scalars['DateTime']>;
id?: InputMaybe<Scalars['String']>; 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']>; updatedAt?: InputMaybe<Scalars['DateTime']>;
}; };
@ -232,10 +255,22 @@ export type CommentThreadCreateManyWorkspaceInputEnvelope = {
skipDuplicates?: InputMaybe<Scalars['Boolean']>; 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 = { export type CommentThreadCreateNestedOneWithoutCommentsInput = {
connect?: InputMaybe<CommentThreadWhereUniqueInput>; connect?: InputMaybe<CommentThreadWhereUniqueInput>;
}; };
export type CommentThreadCreateOrConnectWithoutAuthorInput = {
create: CommentThreadCreateWithoutAuthorInput;
where: CommentThreadWhereUniqueInput;
};
export type CommentThreadCreateOrConnectWithoutCommentsInput = { export type CommentThreadCreateOrConnectWithoutCommentsInput = {
create: CommentThreadCreateWithoutCommentsInput; create: CommentThreadCreateWithoutCommentsInput;
where: CommentThreadWhereUniqueInput; where: CommentThreadWhereUniqueInput;
@ -246,26 +281,56 @@ export type CommentThreadCreateOrConnectWithoutWorkspaceInput = {
where: CommentThreadWhereUniqueInput; where: CommentThreadWhereUniqueInput;
}; };
export type CommentThreadCreateWithoutCommentsInput = { export type CommentThreadCreateWithoutAuthorInput = {
commentThreadTargets?: InputMaybe<CommentThreadTargetCreateNestedManyWithoutCommentThreadInput>; body?: InputMaybe<Scalars['String']>;
createdAt?: InputMaybe<Scalars['DateTime']>;
id?: InputMaybe<Scalars['String']>;
updatedAt?: InputMaybe<Scalars['DateTime']>;
};
export type CommentThreadCreateWithoutWorkspaceInput = {
commentThreadTargets?: InputMaybe<CommentThreadTargetCreateNestedManyWithoutCommentThreadInput>; commentThreadTargets?: InputMaybe<CommentThreadTargetCreateNestedManyWithoutCommentThreadInput>;
comments?: InputMaybe<CommentCreateNestedManyWithoutCommentThreadInput>; comments?: InputMaybe<CommentCreateNestedManyWithoutCommentThreadInput>;
createdAt?: InputMaybe<Scalars['DateTime']>; createdAt?: InputMaybe<Scalars['DateTime']>;
id?: InputMaybe<Scalars['String']>; id?: InputMaybe<Scalars['String']>;
title?: InputMaybe<Scalars['String']>;
updatedAt?: InputMaybe<Scalars['DateTime']>; 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 = { export type CommentThreadOrderByWithRelationInput = {
author?: InputMaybe<UserOrderByWithRelationInput>;
authorId?: InputMaybe<SortOrder>;
body?: InputMaybe<SortOrder>;
commentThreadTargets?: InputMaybe<CommentThreadTargetOrderByRelationAggregateInput>; commentThreadTargets?: InputMaybe<CommentThreadTargetOrderByRelationAggregateInput>;
comments?: InputMaybe<CommentOrderByRelationAggregateInput>; comments?: InputMaybe<CommentOrderByRelationAggregateInput>;
createdAt?: InputMaybe<SortOrder>; createdAt?: InputMaybe<SortOrder>;
id?: InputMaybe<SortOrder>; id?: InputMaybe<SortOrder>;
title?: InputMaybe<SortOrder>;
updatedAt?: InputMaybe<SortOrder>; updatedAt?: InputMaybe<SortOrder>;
}; };
@ -275,9 +340,12 @@ export type CommentThreadRelationFilter = {
}; };
export enum CommentThreadScalarFieldEnum { export enum CommentThreadScalarFieldEnum {
AuthorId = 'authorId',
Body = 'body',
CreatedAt = 'createdAt', CreatedAt = 'createdAt',
DeletedAt = 'deletedAt', DeletedAt = 'deletedAt',
Id = 'id', Id = 'id',
Title = 'title',
UpdatedAt = 'updatedAt', UpdatedAt = 'updatedAt',
WorkspaceId = 'workspaceId' WorkspaceId = 'workspaceId'
} }
@ -286,8 +354,11 @@ export type CommentThreadScalarWhereInput = {
AND?: InputMaybe<Array<CommentThreadScalarWhereInput>>; AND?: InputMaybe<Array<CommentThreadScalarWhereInput>>;
NOT?: InputMaybe<Array<CommentThreadScalarWhereInput>>; NOT?: InputMaybe<Array<CommentThreadScalarWhereInput>>;
OR?: InputMaybe<Array<CommentThreadScalarWhereInput>>; OR?: InputMaybe<Array<CommentThreadScalarWhereInput>>;
authorId?: InputMaybe<StringFilter>;
body?: InputMaybe<StringNullableFilter>;
createdAt?: InputMaybe<DateTimeFilter>; createdAt?: InputMaybe<DateTimeFilter>;
id?: InputMaybe<StringFilter>; id?: InputMaybe<StringFilter>;
title?: InputMaybe<StringNullableFilter>;
updatedAt?: InputMaybe<DateTimeFilter>; updatedAt?: InputMaybe<DateTimeFilter>;
}; };
@ -418,24 +489,48 @@ export type CommentThreadTargetWhereUniqueInput = {
}; };
export type CommentThreadUpdateInput = { export type CommentThreadUpdateInput = {
author?: InputMaybe<UserUpdateOneRequiredWithoutCommentThreadNestedInput>;
body?: InputMaybe<NullableStringFieldUpdateOperationsInput>;
commentThreadTargets?: InputMaybe<CommentThreadTargetUpdateManyWithoutCommentThreadNestedInput>; commentThreadTargets?: InputMaybe<CommentThreadTargetUpdateManyWithoutCommentThreadNestedInput>;
comments?: InputMaybe<CommentUpdateManyWithoutCommentThreadNestedInput>; comments?: InputMaybe<CommentUpdateManyWithoutCommentThreadNestedInput>;
createdAt?: InputMaybe<DateTimeFieldUpdateOperationsInput>; createdAt?: InputMaybe<DateTimeFieldUpdateOperationsInput>;
id?: InputMaybe<StringFieldUpdateOperationsInput>; id?: InputMaybe<StringFieldUpdateOperationsInput>;
title?: InputMaybe<NullableStringFieldUpdateOperationsInput>;
updatedAt?: InputMaybe<DateTimeFieldUpdateOperationsInput>; updatedAt?: InputMaybe<DateTimeFieldUpdateOperationsInput>;
}; };
export type CommentThreadUpdateManyMutationInput = { export type CommentThreadUpdateManyMutationInput = {
body?: InputMaybe<NullableStringFieldUpdateOperationsInput>;
createdAt?: InputMaybe<DateTimeFieldUpdateOperationsInput>; createdAt?: InputMaybe<DateTimeFieldUpdateOperationsInput>;
id?: InputMaybe<StringFieldUpdateOperationsInput>; id?: InputMaybe<StringFieldUpdateOperationsInput>;
title?: InputMaybe<NullableStringFieldUpdateOperationsInput>;
updatedAt?: InputMaybe<DateTimeFieldUpdateOperationsInput>; updatedAt?: InputMaybe<DateTimeFieldUpdateOperationsInput>;
}; };
export type CommentThreadUpdateManyWithWhereWithoutAuthorInput = {
data: CommentThreadUpdateManyMutationInput;
where: CommentThreadScalarWhereInput;
};
export type CommentThreadUpdateManyWithWhereWithoutWorkspaceInput = { export type CommentThreadUpdateManyWithWhereWithoutWorkspaceInput = {
data: CommentThreadUpdateManyMutationInput; data: CommentThreadUpdateManyMutationInput;
where: CommentThreadScalarWhereInput; 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 = { export type CommentThreadUpdateManyWithoutWorkspaceNestedInput = {
connect?: InputMaybe<Array<CommentThreadWhereUniqueInput>>; connect?: InputMaybe<Array<CommentThreadWhereUniqueInput>>;
connectOrCreate?: InputMaybe<Array<CommentThreadCreateOrConnectWithoutWorkspaceInput>>; connectOrCreate?: InputMaybe<Array<CommentThreadCreateOrConnectWithoutWorkspaceInput>>;
@ -458,26 +553,53 @@ export type CommentThreadUpdateOneRequiredWithoutCommentsNestedInput = {
upsert?: InputMaybe<CommentThreadUpsertWithoutCommentsInput>; upsert?: InputMaybe<CommentThreadUpsertWithoutCommentsInput>;
}; };
export type CommentThreadUpdateWithWhereUniqueWithoutAuthorInput = {
data: CommentThreadUpdateWithoutAuthorInput;
where: CommentThreadWhereUniqueInput;
};
export type CommentThreadUpdateWithWhereUniqueWithoutWorkspaceInput = { export type CommentThreadUpdateWithWhereUniqueWithoutWorkspaceInput = {
data: CommentThreadUpdateWithoutWorkspaceInput; data: CommentThreadUpdateWithoutWorkspaceInput;
where: CommentThreadWhereUniqueInput; where: CommentThreadWhereUniqueInput;
}; };
export type CommentThreadUpdateWithoutCommentsInput = { export type CommentThreadUpdateWithoutAuthorInput = {
commentThreadTargets?: InputMaybe<CommentThreadTargetUpdateManyWithoutCommentThreadNestedInput>; body?: InputMaybe<NullableStringFieldUpdateOperationsInput>;
createdAt?: InputMaybe<DateTimeFieldUpdateOperationsInput>;
id?: InputMaybe<StringFieldUpdateOperationsInput>;
updatedAt?: InputMaybe<DateTimeFieldUpdateOperationsInput>;
};
export type CommentThreadUpdateWithoutWorkspaceInput = {
commentThreadTargets?: InputMaybe<CommentThreadTargetUpdateManyWithoutCommentThreadNestedInput>; commentThreadTargets?: InputMaybe<CommentThreadTargetUpdateManyWithoutCommentThreadNestedInput>;
comments?: InputMaybe<CommentUpdateManyWithoutCommentThreadNestedInput>; comments?: InputMaybe<CommentUpdateManyWithoutCommentThreadNestedInput>;
createdAt?: InputMaybe<DateTimeFieldUpdateOperationsInput>; createdAt?: InputMaybe<DateTimeFieldUpdateOperationsInput>;
id?: InputMaybe<StringFieldUpdateOperationsInput>; id?: InputMaybe<StringFieldUpdateOperationsInput>;
title?: InputMaybe<NullableStringFieldUpdateOperationsInput>;
updatedAt?: InputMaybe<DateTimeFieldUpdateOperationsInput>; 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 = { export type CommentThreadUpsertWithWhereUniqueWithoutWorkspaceInput = {
create: CommentThreadCreateWithoutWorkspaceInput; create: CommentThreadCreateWithoutWorkspaceInput;
update: CommentThreadUpdateWithoutWorkspaceInput; update: CommentThreadUpdateWithoutWorkspaceInput;
@ -493,10 +615,14 @@ export type CommentThreadWhereInput = {
AND?: InputMaybe<Array<CommentThreadWhereInput>>; AND?: InputMaybe<Array<CommentThreadWhereInput>>;
NOT?: InputMaybe<Array<CommentThreadWhereInput>>; NOT?: InputMaybe<Array<CommentThreadWhereInput>>;
OR?: InputMaybe<Array<CommentThreadWhereInput>>; OR?: InputMaybe<Array<CommentThreadWhereInput>>;
author?: InputMaybe<UserRelationFilter>;
authorId?: InputMaybe<StringFilter>;
body?: InputMaybe<StringNullableFilter>;
commentThreadTargets?: InputMaybe<CommentThreadTargetListRelationFilter>; commentThreadTargets?: InputMaybe<CommentThreadTargetListRelationFilter>;
comments?: InputMaybe<CommentListRelationFilter>; comments?: InputMaybe<CommentListRelationFilter>;
createdAt?: InputMaybe<DateTimeFilter>; createdAt?: InputMaybe<DateTimeFilter>;
id?: InputMaybe<StringFilter>; id?: InputMaybe<StringFilter>;
title?: InputMaybe<StringNullableFilter>;
updatedAt?: InputMaybe<DateTimeFilter>; updatedAt?: InputMaybe<DateTimeFilter>;
}; };
@ -651,7 +777,7 @@ export enum CommentableType {
export type Company = { export type Company = {
__typename?: 'Company'; __typename?: 'Company';
_commentCount: Scalars['Int']; _commentThreadCount: Scalars['Int'];
accountOwner?: Maybe<User>; accountOwner?: Maybe<User>;
accountOwnerId?: Maybe<Scalars['String']>; accountOwnerId?: Maybe<Scalars['String']>;
address: Scalars['String']; address: Scalars['String'];
@ -1270,13 +1396,14 @@ export type NullableStringFieldUpdateOperationsInput = {
export type Person = { export type Person = {
__typename?: 'Person'; __typename?: 'Person';
_commentCount: Scalars['Int']; _commentThreadCount: Scalars['Int'];
city: Scalars['String']; city: Scalars['String'];
commentThreads: Array<CommentThread>; commentThreads: Array<CommentThread>;
comments: Array<Comment>; comments: Array<Comment>;
company?: Maybe<Company>; company?: Maybe<Company>;
companyId?: Maybe<Scalars['String']>; companyId?: Maybe<Scalars['String']>;
createdAt: Scalars['DateTime']; createdAt: Scalars['DateTime'];
displayName: Scalars['String'];
email: Scalars['String']; email: Scalars['String'];
firstName: Scalars['String']; firstName: Scalars['String'];
id: Scalars['ID']; id: Scalars['ID'];
@ -2303,6 +2430,8 @@ export type Query = {
findManyPipelineProgress: Array<PipelineProgress>; findManyPipelineProgress: Array<PipelineProgress>;
findManyPipelineStage: Array<PipelineStage>; findManyPipelineStage: Array<PipelineStage>;
findManyUser: Array<User>; findManyUser: Array<User>;
findUniqueCompany: Company;
findUniquePerson: Person;
}; };
@ -2380,6 +2509,16 @@ export type QueryFindManyUserArgs = {
where?: InputMaybe<UserWhereInput>; where?: InputMaybe<UserWhereInput>;
}; };
export type QueryFindUniqueCompanyArgs = {
id: Scalars['String'];
};
export type QueryFindUniquePersonArgs = {
id: Scalars['String'];
};
export enum QueryMode { export enum QueryMode {
Default = 'default', Default = 'default',
Insensitive = 'insensitive' Insensitive = 'insensitive'
@ -2432,6 +2571,7 @@ export type Telemetry = {
export type User = { export type User = {
__typename?: 'User'; __typename?: 'User';
CommentThread?: Maybe<Array<CommentThread>>;
avatarUrl?: Maybe<Scalars['String']>; avatarUrl?: Maybe<Scalars['String']>;
comments?: Maybe<Array<Comment>>; comments?: Maybe<Array<Comment>>;
companies?: Maybe<Array<Company>>; companies?: Maybe<Array<Company>>;
@ -2451,6 +2591,12 @@ export type User = {
workspaceMember?: Maybe<WorkspaceMember>; workspaceMember?: Maybe<WorkspaceMember>;
}; };
export type UserCreateNestedOneWithoutCommentThreadInput = {
connect?: InputMaybe<UserWhereUniqueInput>;
connectOrCreate?: InputMaybe<UserCreateOrConnectWithoutCommentThreadInput>;
create?: InputMaybe<UserCreateWithoutCommentThreadInput>;
};
export type UserCreateNestedOneWithoutCommentsInput = { export type UserCreateNestedOneWithoutCommentsInput = {
connect?: InputMaybe<UserWhereUniqueInput>; connect?: InputMaybe<UserWhereUniqueInput>;
}; };
@ -2465,6 +2611,11 @@ export type UserCreateNestedOneWithoutWorkspaceMemberInput = {
create?: InputMaybe<UserCreateWithoutWorkspaceMemberInput>; create?: InputMaybe<UserCreateWithoutWorkspaceMemberInput>;
}; };
export type UserCreateOrConnectWithoutCommentThreadInput = {
create: UserCreateWithoutCommentThreadInput;
where: UserWhereUniqueInput;
};
export type UserCreateOrConnectWithoutCommentsInput = { export type UserCreateOrConnectWithoutCommentsInput = {
create: UserCreateWithoutCommentsInput; create: UserCreateWithoutCommentsInput;
where: UserWhereUniqueInput; where: UserWhereUniqueInput;
@ -2475,7 +2626,26 @@ export type UserCreateOrConnectWithoutWorkspaceMemberInput = {
where: UserWhereUniqueInput; 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 = { export type UserCreateWithoutCommentsInput = {
CommentThread?: InputMaybe<CommentThreadCreateNestedManyWithoutAuthorInput>;
avatarUrl?: InputMaybe<Scalars['String']>; avatarUrl?: InputMaybe<Scalars['String']>;
companies?: InputMaybe<CompanyCreateNestedManyWithoutAccountOwnerInput>; companies?: InputMaybe<CompanyCreateNestedManyWithoutAccountOwnerInput>;
createdAt?: InputMaybe<Scalars['DateTime']>; createdAt?: InputMaybe<Scalars['DateTime']>;
@ -2493,6 +2663,7 @@ export type UserCreateWithoutCommentsInput = {
}; };
export type UserCreateWithoutWorkspaceMemberInput = { export type UserCreateWithoutWorkspaceMemberInput = {
CommentThread?: InputMaybe<CommentThreadCreateNestedManyWithoutAuthorInput>;
avatarUrl?: InputMaybe<Scalars['String']>; avatarUrl?: InputMaybe<Scalars['String']>;
comments?: InputMaybe<CommentCreateNestedManyWithoutAuthorInput>; comments?: InputMaybe<CommentCreateNestedManyWithoutAuthorInput>;
companies?: InputMaybe<CompanyCreateNestedManyWithoutAccountOwnerInput>; companies?: InputMaybe<CompanyCreateNestedManyWithoutAccountOwnerInput>;
@ -2516,6 +2687,7 @@ export type UserExists = {
}; };
export type UserOrderByWithRelationInput = { export type UserOrderByWithRelationInput = {
CommentThread?: InputMaybe<CommentThreadOrderByRelationAggregateInput>;
avatarUrl?: InputMaybe<SortOrder>; avatarUrl?: InputMaybe<SortOrder>;
comments?: InputMaybe<CommentOrderByRelationAggregateInput>; comments?: InputMaybe<CommentOrderByRelationAggregateInput>;
companies?: InputMaybe<CompanyOrderByRelationAggregateInput>; companies?: InputMaybe<CompanyOrderByRelationAggregateInput>;
@ -2557,6 +2729,7 @@ export enum UserScalarFieldEnum {
} }
export type UserUpdateInput = { export type UserUpdateInput = {
CommentThread?: InputMaybe<CommentThreadUpdateManyWithoutAuthorNestedInput>;
avatarUrl?: InputMaybe<NullableStringFieldUpdateOperationsInput>; avatarUrl?: InputMaybe<NullableStringFieldUpdateOperationsInput>;
comments?: InputMaybe<CommentUpdateManyWithoutAuthorNestedInput>; comments?: InputMaybe<CommentUpdateManyWithoutAuthorNestedInput>;
companies?: InputMaybe<CompanyUpdateManyWithoutAccountOwnerNestedInput>; companies?: InputMaybe<CompanyUpdateManyWithoutAccountOwnerNestedInput>;
@ -2574,6 +2747,14 @@ export type UserUpdateInput = {
updatedAt?: InputMaybe<DateTimeFieldUpdateOperationsInput>; updatedAt?: InputMaybe<DateTimeFieldUpdateOperationsInput>;
}; };
export type UserUpdateOneRequiredWithoutCommentThreadNestedInput = {
connect?: InputMaybe<UserWhereUniqueInput>;
connectOrCreate?: InputMaybe<UserCreateOrConnectWithoutCommentThreadInput>;
create?: InputMaybe<UserCreateWithoutCommentThreadInput>;
update?: InputMaybe<UserUpdateWithoutCommentThreadInput>;
upsert?: InputMaybe<UserUpsertWithoutCommentThreadInput>;
};
export type UserUpdateOneRequiredWithoutCommentsNestedInput = { export type UserUpdateOneRequiredWithoutCommentsNestedInput = {
connect?: InputMaybe<UserWhereUniqueInput>; connect?: InputMaybe<UserWhereUniqueInput>;
connectOrCreate?: InputMaybe<UserCreateOrConnectWithoutCommentsInput>; connectOrCreate?: InputMaybe<UserCreateOrConnectWithoutCommentsInput>;
@ -2594,24 +2775,7 @@ export type UserUpdateOneWithoutCompaniesNestedInput = {
connect?: InputMaybe<UserWhereUniqueInput>; connect?: InputMaybe<UserWhereUniqueInput>;
}; };
export type UserUpdateWithoutCommentsInput = { export type UserUpdateWithoutCommentThreadInput = {
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 = {
avatarUrl?: InputMaybe<NullableStringFieldUpdateOperationsInput>; avatarUrl?: InputMaybe<NullableStringFieldUpdateOperationsInput>;
comments?: InputMaybe<CommentUpdateManyWithoutAuthorNestedInput>; comments?: InputMaybe<CommentUpdateManyWithoutAuthorNestedInput>;
companies?: InputMaybe<CompanyUpdateManyWithoutAccountOwnerNestedInput>; companies?: InputMaybe<CompanyUpdateManyWithoutAccountOwnerNestedInput>;
@ -2629,6 +2793,48 @@ export type UserUpdateWithoutWorkspaceMemberInput = {
updatedAt?: InputMaybe<DateTimeFieldUpdateOperationsInput>; 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 = { export type UserUpsertWithoutCommentsInput = {
create: UserCreateWithoutCommentsInput; create: UserCreateWithoutCommentsInput;
update: UserUpdateWithoutCommentsInput; update: UserUpdateWithoutCommentsInput;
@ -2641,6 +2847,7 @@ export type UserUpsertWithoutWorkspaceMemberInput = {
export type UserWhereInput = { export type UserWhereInput = {
AND?: InputMaybe<Array<UserWhereInput>>; AND?: InputMaybe<Array<UserWhereInput>>;
CommentThread?: InputMaybe<CommentThreadListRelationFilter>;
NOT?: InputMaybe<Array<UserWhereInput>>; NOT?: InputMaybe<Array<UserWhereInput>>;
OR?: InputMaybe<Array<UserWhereInput>>; OR?: InputMaybe<Array<UserWhereInput>>;
avatarUrl?: InputMaybe<StringNullableFilter>; 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 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']; commentThreadId: Scalars['String'];
commentText: Scalars['String']; body?: InputMaybe<Scalars['String']>;
title?: InputMaybe<Scalars['String']>;
authorId: Scalars['String']; authorId: Scalars['String'];
createdAt: Scalars['DateTime']; createdAt: Scalars['DateTime'];
commentId: Scalars['String'];
commentThreadTargetArray: Array<CommentThreadTargetCreateManyCommentThreadInput> | CommentThreadTargetCreateManyCommentThreadInput; 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<{ export type GetCommentThreadsByTargetsQueryVariables = Exact<{
commentThreadTargetIds: Array<Scalars['String']> | Scalars['String']; 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<{ export type GetCommentThreadQueryVariables = Exact<{
commentThreadId: Scalars['String']; 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<{ export type AddCommentThreadTargetOnCommentThreadMutationVariables = Exact<{
commentThreadId: Scalars['String']; commentThreadId: Scalars['String'];
@ -2904,13 +3111,36 @@ export type DeleteCommentThreadMutationVariables = Exact<{
export type DeleteCommentThreadMutation = { __typename?: 'Mutation', deleteManyCommentThreads: { __typename?: 'AffectedRows', count: number } }; 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<{ export type GetCompaniesQueryVariables = Exact<{
orderBy?: InputMaybe<Array<CompanyOrderByWithRelationInput> | CompanyOrderByWithRelationInput>; orderBy?: InputMaybe<Array<CompanyOrderByWithRelationInput> | CompanyOrderByWithRelationInput>;
where?: InputMaybe<CompanyWhereInput>; 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<{ export type UpdateCompanyMutationVariables = Exact<{
id?: InputMaybe<Scalars['String']>; 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<{ export type UpdatePeopleMutationVariables = Exact<{
id?: InputMaybe<Scalars['String']>; id?: InputMaybe<Scalars['String']>;
@ -3416,14 +3653,15 @@ export function useCreateCommentMutation(baseOptions?: Apollo.MutationHookOption
export type CreateCommentMutationHookResult = ReturnType<typeof useCreateCommentMutation>; export type CreateCommentMutationHookResult = ReturnType<typeof useCreateCommentMutation>;
export type CreateCommentMutationResult = Apollo.MutationResult<CreateCommentMutation>; export type CreateCommentMutationResult = Apollo.MutationResult<CreateCommentMutation>;
export type CreateCommentMutationOptions = Apollo.BaseMutationOptions<CreateCommentMutation, CreateCommentMutationVariables>; export type CreateCommentMutationOptions = Apollo.BaseMutationOptions<CreateCommentMutation, CreateCommentMutationVariables>;
export const CreateCommentThreadWithCommentDocument = gql` export const CreateCommentThreadDocument = gql`
mutation CreateCommentThreadWithComment($commentThreadId: String!, $commentText: String!, $authorId: String!, $createdAt: DateTime!, $commentId: String!, $commentThreadTargetArray: [CommentThreadTargetCreateManyCommentThreadInput!]!) { mutation CreateCommentThread($commentThreadId: String!, $body: String, $title: String, $authorId: String!, $createdAt: DateTime!, $commentThreadTargetArray: [CommentThreadTargetCreateManyCommentThreadInput!]!) {
createOneCommentThread( 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 id
createdAt createdAt
updatedAt updatedAt
authorId
commentThreadTargets { commentThreadTargets {
id id
createdAt 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. * 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, `useCreateCommentThreadWithCommentMutation` returns a tuple that includes: * When your component renders, `useCreateCommentThreadMutation` returns a tuple that includes:
* - A mutate function that you can call at any time to execute the mutation * - 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 * - 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; * @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 * @example
* const [createCommentThreadWithCommentMutation, { data, loading, error }] = useCreateCommentThreadWithCommentMutation({ * const [createCommentThreadMutation, { data, loading, error }] = useCreateCommentThreadMutation({
* variables: { * variables: {
* commentThreadId: // value for 'commentThreadId' * commentThreadId: // value for 'commentThreadId'
* commentText: // value for 'commentText' * body: // value for 'body'
* title: // value for 'title'
* authorId: // value for 'authorId' * authorId: // value for 'authorId'
* createdAt: // value for 'createdAt' * createdAt: // value for 'createdAt'
* commentId: // value for 'commentId'
* commentThreadTargetArray: // value for 'commentThreadTargetArray' * commentThreadTargetArray: // value for 'commentThreadTargetArray'
* }, * },
* }); * });
*/ */
export function useCreateCommentThreadWithCommentMutation(baseOptions?: Apollo.MutationHookOptions<CreateCommentThreadWithCommentMutation, CreateCommentThreadWithCommentMutationVariables>) { export function useCreateCommentThreadMutation(baseOptions?: Apollo.MutationHookOptions<CreateCommentThreadMutation, CreateCommentThreadMutationVariables>) {
const options = {...defaultOptions, ...baseOptions} 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 CreateCommentThreadMutationHookResult = ReturnType<typeof useCreateCommentThreadMutation>;
export type CreateCommentThreadWithCommentMutationResult = Apollo.MutationResult<CreateCommentThreadWithCommentMutation>; export type CreateCommentThreadMutationResult = Apollo.MutationResult<CreateCommentThreadMutation>;
export type CreateCommentThreadWithCommentMutationOptions = Apollo.BaseMutationOptions<CreateCommentThreadWithCommentMutation, CreateCommentThreadWithCommentMutationVariables>; export type CreateCommentThreadMutationOptions = Apollo.BaseMutationOptions<CreateCommentThreadMutation, CreateCommentThreadMutationVariables>;
export const GetCommentThreadsByTargetsDocument = gql` export const GetCommentThreadsByTargetsDocument = gql`
query GetCommentThreadsByTargets($commentThreadTargetIds: [String!]!, $orderBy: [CommentThreadOrderByWithRelationInput!]) { query GetCommentThreadsByTargets($commentThreadTargetIds: [String!]!, $orderBy: [CommentThreadOrderByWithRelationInput!]) {
findManyCommentThreads( findManyCommentThreads(
@ -3482,6 +3720,14 @@ export const GetCommentThreadsByTargetsDocument = gql`
where: {commentThreadTargets: {some: {commentableId: {in: $commentThreadTargetIds}}}} where: {commentThreadTargets: {some: {commentableId: {in: $commentThreadTargetIds}}}}
) { ) {
id id
createdAt
title
body
author {
id
firstName
lastName
}
comments { comments {
id id
body body
@ -3536,6 +3782,14 @@ export const GetCommentThreadDocument = gql`
query GetCommentThread($commentThreadId: String!) { query GetCommentThread($commentThreadId: String!) {
findManyCommentThreads(where: {id: {equals: $commentThreadId}}) { findManyCommentThreads(where: {id: {equals: $commentThreadId}}) {
id id
createdAt
body
title
author {
id
firstName
lastName
}
comments { comments {
id id
body body
@ -3550,6 +3804,7 @@ export const GetCommentThreadDocument = gql`
} }
} }
commentThreadTargets { commentThreadTargets {
id
commentableId commentableId
commentableType commentableType
} }
@ -3712,6 +3967,82 @@ export function useDeleteCommentThreadMutation(baseOptions?: Apollo.MutationHook
export type DeleteCommentThreadMutationHookResult = ReturnType<typeof useDeleteCommentThreadMutation>; export type DeleteCommentThreadMutationHookResult = ReturnType<typeof useDeleteCommentThreadMutation>;
export type DeleteCommentThreadMutationResult = Apollo.MutationResult<DeleteCommentThreadMutation>; export type DeleteCommentThreadMutationResult = Apollo.MutationResult<DeleteCommentThreadMutation>;
export type DeleteCommentThreadMutationOptions = Apollo.BaseMutationOptions<DeleteCommentThreadMutation, DeleteCommentThreadMutationVariables>; 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` export const GetCompaniesDocument = gql`
query GetCompanies($orderBy: [CompanyOrderByWithRelationInput!], $where: CompanyWhereInput) { query GetCompanies($orderBy: [CompanyOrderByWithRelationInput!], $where: CompanyWhereInput) {
companies: findManyCompany(orderBy: $orderBy, where: $where) { companies: findManyCompany(orderBy: $orderBy, where: $where) {
@ -3721,7 +4052,7 @@ export const GetCompaniesDocument = gql`
createdAt createdAt
address address
employees employees
_commentCount _commentThreadCount
accountOwner { accountOwner {
id id
email email
@ -3761,6 +4092,52 @@ export function useGetCompaniesLazyQuery(baseOptions?: Apollo.LazyQueryHookOptio
export type GetCompaniesQueryHookResult = ReturnType<typeof useGetCompaniesQuery>; export type GetCompaniesQueryHookResult = ReturnType<typeof useGetCompaniesQuery>;
export type GetCompaniesLazyQueryHookResult = ReturnType<typeof useGetCompaniesLazyQuery>; export type GetCompaniesLazyQueryHookResult = ReturnType<typeof useGetCompaniesLazyQuery>;
export type GetCompaniesQueryResult = Apollo.QueryResult<GetCompaniesQuery, GetCompaniesQueryVariables>; 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` export const UpdateCompanyDocument = gql`
mutation UpdateCompany($id: String, $name: String, $domainName: String, $accountOwnerId: String, $createdAt: DateTime, $address: String, $employees: Int) { mutation UpdateCompany($id: String, $name: String, $domainName: String, $accountOwnerId: String, $createdAt: DateTime, $address: String, $employees: Int) {
updateOneCompany( updateOneCompany(
@ -3903,7 +4280,7 @@ export const GetPeopleDocument = gql`
firstName firstName
lastName lastName
createdAt createdAt
_commentCount _commentThreadCount
company { company {
id id
name name
@ -3942,6 +4319,45 @@ export function useGetPeopleLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<
export type GetPeopleQueryHookResult = ReturnType<typeof useGetPeopleQuery>; export type GetPeopleQueryHookResult = ReturnType<typeof useGetPeopleQuery>;
export type GetPeopleLazyQueryHookResult = ReturnType<typeof useGetPeopleLazyQuery>; export type GetPeopleLazyQueryHookResult = ReturnType<typeof useGetPeopleLazyQuery>;
export type GetPeopleQueryResult = Apollo.QueryResult<GetPeopleQuery, GetPeopleQueryVariables>; 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` export const UpdatePeopleDocument = gql`
mutation UpdatePeople($id: String, $firstName: String, $lastName: String, $phone: String, $city: String, $companyId: String, $email: String, $createdAt: DateTime) { mutation UpdatePeople($id: String, $firstName: String, $lastName: String, $phone: String, $city: String, $companyId: String, $email: String, $createdAt: DateTime) {
updateOnePerson( updateOnePerson(

View File

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

View File

@ -15,6 +15,7 @@ export const StyledDialog = styled(Command.Dialog)`
top: 50%; top: 50%;
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
width: 100%; width: 100%;
z-index: 1000;
`; `;
export const StyledInput = styled(Command.Input)` 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 { useState } from 'react';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { import {
autoUpdate, autoUpdate,
@ -8,30 +7,33 @@ import {
size, size,
useFloating, useFloating,
} from '@floating-ui/react'; } 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 CompanyChip from '@/companies/components/CompanyChip';
import { useScopedHotkeys } from '@/hotkeys/hooks/useScopedHotkeys'; import { useScopedHotkeys } from '@/hotkeys/hooks/useScopedHotkeys';
import { InternalHotkeysScope } from '@/hotkeys/types/internal/InternalHotkeysScope'; import { InternalHotkeysScope } from '@/hotkeys/types/internal/InternalHotkeysScope';
import { PersonChip } from '@/people/components/PersonChip'; import { PersonChip } from '@/people/components/PersonChip';
import { RecoilScope } from '@/recoil-scope/components/RecoilScope'; import { RecoilScope } from '@/recoil-scope/components/RecoilScope';
import { MultipleEntitySelect } from '@/relation-picker/components/MultipleEntitySelect';
import { useFilteredSearchEntityQuery } from '@/relation-picker/hooks/useFilteredSearchEntityQuery'; import { useFilteredSearchEntityQuery } from '@/relation-picker/hooks/useFilteredSearchEntityQuery';
import { useListenClickOutsideArrayOfRef } from '@/ui/hooks/useListenClickOutsideArrayOfRef'; import { useListenClickOutsideArrayOfRef } from '@/ui/hooks/useListenClickOutsideArrayOfRef';
import { flatMapAndSortEntityForSelectArrayOfArrayByName } from '@/ui/utils/flatMapAndSortEntityForSelectArrayByName'; import { flatMapAndSortEntityForSelectArrayOfArrayByName } from '@/ui/utils/flatMapAndSortEntityForSelectArrayByName';
import { getLogoUrlFromDomainName } from '@/utils/utils'; import { getLogoUrlFromDomainName } from '@/utils/utils';
import { import {
CommentableType, CommentableType,
CommentThread,
CommentThreadTarget,
useSearchCompanyQuery, useSearchCompanyQuery,
useSearchPeopleQuery, useSearchPeopleQuery,
} from '~/generated/graphql'; } from '~/generated/graphql';
import { MultipleEntitySelect } from '../../relation-picker/components/MultipleEntitySelect';
import { useHandleCheckableCommentThreadTargetChange } from '../hooks/useHandleCheckableCommentThreadTargetChange';
import { CommentableEntityForSelect } from '../types/CommentableEntityForSelect';
type OwnProps = { type OwnProps = {
commentThread: CommentThreadForDrawer; commentThread?: Pick<CommentThread, 'id'> & {
commentThreadTargets: Array<
Pick<CommentThreadTarget, 'id' | 'commentableId' | 'commentableType'>
>;
};
}; };
const StyledContainer = styled.div` const StyledContainer = styled.div`
@ -44,25 +46,6 @@ const StyledContainer = styled.div`
width: 100%; 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` const StyledRelationContainer = styled.div`
--horizontal-padding: ${({ theme }) => theme.spacing(1)}; --horizontal-padding: ${({ theme }) => theme.spacing(1)};
--vertical-padding: ${({ theme }) => theme.spacing(1.5)}; --vertical-padding: ${({ theme }) => theme.spacing(1.5)};
@ -97,15 +80,12 @@ export function CommentThreadRelationPicker({ commentThread }: OwnProps) {
const [isMenuOpen, setIsMenuOpen] = useState(false); const [isMenuOpen, setIsMenuOpen] = useState(false);
const [searchFilter, setSearchFilter] = useState(''); const [searchFilter, setSearchFilter] = useState('');
const theme = useTheme();
const peopleIds = const peopleIds =
commentThread.commentThreadTargets commentThread?.commentThreadTargets
?.filter((relation) => relation.commentableType === 'Person') ?.filter((relation) => relation.commentableType === 'Person')
.map((relation) => relation.commentableId) ?? []; .map((relation) => relation.commentableId) ?? [];
const companyIds = const companyIds =
commentThread.commentThreadTargets commentThread?.commentThreadTargets
?.filter((relation) => relation.commentableType === 'Company') ?.filter((relation) => relation.commentableType === 'Company')
.map((relation) => relation.commentableId) ?? []; .map((relation) => relation.commentableId) ?? [];
@ -203,10 +183,6 @@ export function CommentThreadRelationPicker({ commentThread }: OwnProps) {
return ( return (
<StyledContainer> <StyledContainer>
<StyledLabelContainer>
<IconArrowUpRight size={16} color={theme.font.color.tertiary} />
<StyledRelationLabel>Relations</StyledRelationLabel>
</StyledLabelContainer>
<StyledRelationContainer <StyledRelationContainer
ref={refs.setReference} ref={refs.setReference}
onClick={handleRelationContainerClick} onClick={handleRelationContainerClick}
@ -215,11 +191,12 @@ export function CommentThreadRelationPicker({ commentThread }: OwnProps) {
entity.entityType === CommentableType.Company ? ( entity.entityType === CommentableType.Company ? (
<CompanyChip <CompanyChip
key={entity.id} key={entity.id}
id={entity.id}
name={entity.name} name={entity.name}
picture={entity.avatarUrl} picture={entity.avatarUrl}
/> />
) : ( ) : (
<PersonChip key={entity.id} name={entity.name} /> <PersonChip key={entity.id} name={entity.name} id={entity.id} />
), ),
)} )}
</StyledRelationContainer> </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 styled from '@emotion/styled';
import type { Meta, StoryObj } from '@storybook/react'; import type { Meta, StoryObj } from '@storybook/react';
@ -24,8 +25,10 @@ type Story = StoryObj<typeof CommentThreadRelationPicker>;
export const Default: Story = { export const Default: Story = {
render: getRenderWrapperForComponent( render: getRenderWrapperForComponent(
<StyledContainer> <MemoryRouter>
<CommentThreadRelationPicker commentThread={mockedCommentThreads[0]} /> <StyledContainer>
</StyledContainer>, <CommentThreadRelationPicker commentThread={mockedCommentThreads[0]} />
</StyledContainer>
</MemoryRouter>,
), ),
}; };

View File

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

View File

@ -6,8 +6,8 @@ import { CommentForDrawer } from '@/comments/types/CommentForDrawer';
import { mockedUsersData } from '~/testing/mock-data/users'; import { mockedUsersData } from '~/testing/mock-data/users';
import { getRenderWrapperForComponent } from '~/testing/renderWrappers'; import { getRenderWrapperForComponent } from '~/testing/renderWrappers';
import { CommentThreadActionBar } from '../../right-drawer/CommentThreadActionBar';
import { CommentHeader } from '../CommentHeader'; import { CommentHeader } from '../CommentHeader';
import { CommentThreadActionBar } from '../CommentThreadActionBar';
const meta: Meta<typeof CommentHeader> = { const meta: Meta<typeof CommentHeader> = {
title: 'Modules/Comments/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 styled from '@emotion/styled';
import { useRecoilState } from 'recoil'; import { useRecoilState } from 'recoil';
import { GET_COMMENT_THREADS_BY_TARGETS } from '@/comments/services';
import { GET_COMPANIES } from '@/companies/services'; import { GET_COMPANIES } from '@/companies/services';
import { GET_PEOPLE } from '@/people/services'; import { GET_PEOPLE } from '@/people/services';
import { IconTrash } from '@/ui/icons'; import { IconTrash } from '@/ui/icons';
import { isRightDrawerOpenState } from '@/ui/layout/right-drawer/states/isRightDrawerOpenState'; import { isRightDrawerOpenState } from '@/ui/layout/right-drawer/states/isRightDrawerOpenState';
import { useDeleteCommentThreadMutation } from '~/generated/graphql'; import { useDeleteCommentThreadMutation } from '~/generated/graphql';
import { GET_COMMENT_THREADS_BY_TARGETS } from '../services';
const StyledContainer = styled.div` const StyledContainer = styled.div`
color: ${({ theme }) => theme.font.color.tertiary}; color: ${({ theme }) => theme.font.color.tertiary};
cursor: pointer; 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 { useHotkeysScopeOnMountOnly } from '@/hotkeys/hooks/useHotkeysScopeOnMountOnly';
import { InternalHotkeysScope } from '@/hotkeys/types/internal/InternalHotkeysScope'; import { InternalHotkeysScope } from '@/hotkeys/types/internal/InternalHotkeysScope';
import { RightDrawerBody } from '@/ui/layout/right-drawer/components/RightDrawerBody'; import { RightDrawerBody } from '@/ui/layout/right-drawer/components/RightDrawerBody';
import { RightDrawerPage } from '@/ui/layout/right-drawer/components/RightDrawerPage'; import { RightDrawerPage } from '@/ui/layout/right-drawer/components/RightDrawerPage';
import { RightDrawerTopBar } from '@/ui/layout/right-drawer/components/RightDrawerTopBar'; import { RightDrawerTopBar } from '@/ui/layout/right-drawer/components/RightDrawerTopBar';
import { CommentThreadCreateMode } from './CommentThreadCreateMode'; import { CommentThread } from '../CommentThread';
export function RightDrawerCreateCommentThread() { export function RightDrawerEditCommentThread() {
useHotkeysScopeOnMountOnly({ useHotkeysScopeOnMountOnly({
scope: InternalHotkeysScope.RightDrawer, scope: InternalHotkeysScope.RightDrawer,
customScopes: { goto: false, 'command-menu': true }, customScopes: { goto: false, 'command-menu': true },
}); });
const commentThreadId = useRecoilValue(viewableCommentThreadIdState);
return ( return (
<RightDrawerPage> <RightDrawerPage>
<RightDrawerTopBar title="New comment" /> <RightDrawerTopBar title="" />
<RightDrawerBody> <RightDrawerBody>
<CommentThreadCreateMode /> {commentThreadId && <CommentThread commentThreadId={commentThreadId} />}
</RightDrawerBody> </RightDrawerBody>
</RightDrawerPage> </RightDrawerPage>
); );

View File

@ -41,9 +41,8 @@ const StyledChip = styled.div`
const StyledCount = styled.div` const StyledCount = styled.div`
align-items: center; align-items: center;
display: flex; display: flex;
font-size: 12px; font-size: ${({ theme }) => theme.font.size.sm};
font-weight: ${({ theme }) => theme.font.weight.medium};
font-weight: 500;
justify-content: center; 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_COMPANIES } from '@/companies/services';
import { GET_PEOPLE } from '@/people/services'; import { GET_PEOPLE } from '@/people/services';
import { import {
CommentThread,
CommentThreadTarget,
useAddCommentThreadTargetOnCommentThreadMutation, useAddCommentThreadTargetOnCommentThreadMutation,
useRemoveCommentThreadTargetOnCommentThreadMutation, useRemoveCommentThreadTargetOnCommentThreadMutation,
} from '~/generated/graphql'; } from '~/generated/graphql';
import { GET_COMMENT_THREADS_BY_TARGETS } from '../services'; import { GET_COMMENT_THREADS_BY_TARGETS } from '../services';
import { CommentableEntityForSelect } from '../types/CommentableEntityForSelect'; import { CommentableEntityForSelect } from '../types/CommentableEntityForSelect';
import { CommentThreadForDrawer } from '../types/CommentThreadForDrawer';
export function useHandleCheckableCommentThreadTargetChange({ export function useHandleCheckableCommentThreadTargetChange({
commentThread, commentThread,
}: { }: {
commentThread: CommentThreadForDrawer; commentThread?: Pick<CommentThread, 'id'> & {
commentThreadTargets: Array<
Pick<CommentThreadTarget, 'id' | 'commentableId'>
>;
};
}) { }) {
const [addCommentThreadTargetOnCommentThread] = const [addCommentThreadTargetOnCommentThread] =
useAddCommentThreadTargetOnCommentThreadMutation({ useAddCommentThreadTargetOnCommentThreadMutation({
@ -39,6 +44,9 @@ export function useHandleCheckableCommentThreadTargetChange({
newCheckedValue: boolean, newCheckedValue: boolean,
entity: CommentableEntityForSelect, entity: CommentableEntityForSelect,
) { ) {
if (!commentThread) {
return;
}
if (newCheckedValue) { if (newCheckedValue) {
addCommentThreadTargetOnCommentThread({ addCommentThreadTargetOnCommentThread({
variables: { 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 { 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 { 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 { 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 { commentableEntityArrayState } from '../states/commentableEntityArrayState';
import { createdCommentThreadIdState } from '../states/createdCommentThreadIdState'; import { viewableCommentThreadIdState } from '../states/viewableCommentThreadIdState';
import { CommentableEntity } from '../types/CommentableEntity'; import { CommentableEntity } from '../types/CommentableEntity';
export function useOpenCreateCommentThreadDrawerForSelectedRowIds() { export function useOpenCreateCommentThreadDrawerForSelectedRowIds() {
const openRightDrawer = useOpenRightDrawer(); const openRightDrawer = useOpenRightDrawer();
const [createCommentThreadMutation] = useCreateCommentThreadMutation();
const currentUser = useRecoilValue(currentUserState);
const [, setViewableCommentThreadId] = useRecoilState(
viewableCommentThreadIdState,
);
const [, setCommentableEntityArray] = useRecoilState( const [, setCommentableEntityArray] = useRecoilState(
commentableEntityArrayState, commentableEntityArrayState,
); );
const [, setCreatedCommentThreadId] = useRecoilState( const selectedEntityIds = useRecoilValue(selectedRowIdsState);
createdCommentThreadIdState,
);
const selectedPeopleIds = useRecoilValue(selectedRowIdsState);
return function openCreateCommentDrawerForSelectedRowIds( return function openCreateCommentDrawerForSelectedRowIds(
entityType: CommentableType, entityType: CommentableType,
) { ) {
const commentableEntityArray: CommentableEntity[] = selectedPeopleIds.map( const commentableEntityArray: CommentableEntity[] = selectedEntityIds.map(
(id) => ({ (id) => ({
type: entityType, type: entityType,
id, id,
}), }),
); );
setCreatedCommentThreadId(null); createCommentThreadMutation({
setCommentableEntityArray(commentableEntityArray); variables: {
openRightDrawer('create-comment-thread'); 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(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 { useRecoilState } from 'recoil';
import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPages';
import { useOpenRightDrawer } from '../../ui/layout/right-drawer/hooks/useOpenRightDrawer'; import { useOpenRightDrawer } from '../../ui/layout/right-drawer/hooks/useOpenRightDrawer';
import { commentableEntityArrayState } from '../states/commentableEntityArrayState'; import { commentableEntityArrayState } from '../states/commentableEntityArrayState';
import { CommentableEntity } from '../types/CommentableEntity'; import { CommentableEntity } from '../types/CommentableEntity';
export function useOpenCommentRightDrawer() { export function useOpenTimelineRightDrawer() {
const openRightDrawer = useOpenRightDrawer(); const openRightDrawer = useOpenRightDrawer();
const [, setCommentableEntityArray] = useRecoilState( const [, setCommentableEntityArray] = useRecoilState(
commentableEntityArrayState, commentableEntityArrayState,
); );
return function openCommentRightDrawer( return function openTimelineRightDrawer(
commentableEntityArray: CommentableEntity[], commentableEntityArray: CommentableEntity[],
) { ) {
setCommentableEntityArray(commentableEntityArray); 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` export const CREATE_COMMENT_THREAD_WITH_COMMENT = gql`
mutation CreateCommentThreadWithComment( mutation CreateCommentThread(
$commentThreadId: String! $commentThreadId: String!
$commentText: String! $body: String
$title: String
$authorId: String! $authorId: String!
$createdAt: DateTime! $createdAt: DateTime!
$commentId: String!
$commentThreadTargetArray: [CommentThreadTargetCreateManyCommentThreadInput!]! $commentThreadTargetArray: [CommentThreadTargetCreateManyCommentThreadInput!]!
) { ) {
createOneCommentThread( createOneCommentThread(
@ -46,16 +46,9 @@ export const CREATE_COMMENT_THREAD_WITH_COMMENT = gql`
id: $commentThreadId id: $commentThreadId
createdAt: $createdAt createdAt: $createdAt
updatedAt: $createdAt updatedAt: $createdAt
comments: { author: { connect: { id: $authorId } }
createMany: { body: $body
data: { title: $title
authorId: $authorId
id: $commentId
createdAt: $createdAt
body: $commentText
}
}
}
commentThreadTargets: { commentThreadTargets: {
createMany: { data: $commentThreadTargetArray, skipDuplicates: true } createMany: { data: $commentThreadTargetArray, skipDuplicates: true }
} }
@ -64,6 +57,7 @@ export const CREATE_COMMENT_THREAD_WITH_COMMENT = gql`
id id
createdAt createdAt
updatedAt updatedAt
authorId
commentThreadTargets { commentThreadTargets {
id id
createdAt createdAt

View File

@ -14,6 +14,14 @@ export const GET_COMMENT_THREADS_BY_TARGETS = gql`
} }
) { ) {
id id
createdAt
title
body
author {
id
firstName
lastName
}
comments { comments {
id id
body body
@ -40,6 +48,14 @@ export const GET_COMMENT_THREAD = gql`
query GetCommentThread($commentThreadId: String!) { query GetCommentThread($commentThreadId: String!) {
findManyCommentThreads(where: { id: { equals: $commentThreadId } }) { findManyCommentThreads(where: { id: { equals: $commentThreadId } }) {
id id
createdAt
body
title
author {
id
firstName
lastName
}
comments { comments {
id id
body body
@ -54,6 +70,7 @@ export const GET_COMMENT_THREAD = gql`
} }
} }
commentThreadTargets { commentThreadTargets {
id
commentableId commentableId
commentableType 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 styled from '@emotion/styled';
import { Avatar } from '@/users/components/Avatar'; import { Avatar } from '@/users/components/Avatar';
export type CompanyChipPropsType = { export type CompanyChipPropsType = {
id?: string;
name: string; name: string;
picture?: string; picture?: string;
}; };
const StyledContainer = styled.span` const baseStyle = ({ theme }: { theme: Theme }) => `
align-items: center; align-items: center;
background-color: ${({ theme }) => theme.background.tertiary}; background-color: ${theme.background.tertiary};
border-radius: ${({ theme }) => theme.spacing(1)}; border-radius: ${theme.spacing(1)};
color: ${({ theme }) => theme.font.color.primary}; color: ${theme.font.color.primary};
display: inline-flex; display: inline-flex;
gap: ${({ theme }) => theme.spacing(1)}; gap: ${theme.spacing(1)};
height: calc(20px - 2 * ${({ theme }) => theme.spacing(1)}); height: calc(20px - 2 * ${theme.spacing(1)});
overflow: hidden; overflow: hidden;
padding: ${theme.spacing(1)};
padding: ${({ theme }) => theme.spacing(1)}; text-decoration: none;
user-select: none; user-select: none;
@ -38,9 +42,19 @@ const StyledName = styled.span`
white-space: nowrap; 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 ( return (
<StyledContainer data-testid="company-chip"> <ContainerComponent data-testid="company-chip" to={`/companies/${id}`}>
{picture && ( {picture && (
<Avatar <Avatar
avatarUrl={picture?.toString()} avatarUrl={picture?.toString()}
@ -50,7 +64,7 @@ function CompanyChip({ name, picture }: CompanyChipPropsType) {
/> />
)} )}
<StyledName>{name}</StyledName> <StyledName>{name}</StyledName>
</StyledContainer> </ContainerComponent>
); );
} }

View File

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

View File

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

View File

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

View File

@ -22,7 +22,7 @@ export const GET_COMPANIES = gql`
createdAt createdAt
address address
employees employees
_commentCount _commentThreadCount
accountOwner { accountOwner {
id id
email 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', PasswordLogin = 'password-login',
AuthIndex = 'auth-index', AuthIndex = 'auth-index',
CreateProfile = 'create-profile', CreateProfile = 'create-profile',
ShowPage = 'show-page',
} }

View File

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

View File

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

View File

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

View File

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

View File

@ -24,7 +24,7 @@ export const GET_PEOPLE = gql`
firstName firstName
lastName lastName
createdAt createdAt
_commentCount _commentThreadCount
company { company {
id id
name 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'; import { EditableCell } from '../EditableCell';
export type EditableChipProps = { export type EditableChipProps = {
id: string;
placeholder?: string; placeholder?: string;
value: string; value: string;
picture: string; picture: string;
changeHandler: (updated: string) => void; changeHandler: (updated: string) => void;
editModeHorizontalAlign?: 'left' | 'right'; editModeHorizontalAlign?: 'left' | 'right';
ChipComponent: ComponentType<{ ChipComponent: ComponentType<{
id: string;
name: string; name: string;
picture: string; picture: string;
isOverlapped?: boolean; isOverlapped?: boolean;
}>; }>;
commentCount?: number; commentThreadCount?: number;
onCommentClick?: (event: React.MouseEvent<HTMLDivElement>) => void; onCommentClick?: (event: React.MouseEvent<HTMLDivElement>) => void;
rightEndContents?: ReactNode[]; rightEndContents?: ReactNode[];
}; };
@ -41,6 +43,7 @@ const RightContainer = styled.div`
// TODO: move right end content in EditableCell // TODO: move right end content in EditableCell
export function EditableCellChip({ export function EditableCellChip({
id,
value, value,
placeholder, placeholder,
changeHandler, changeHandler,
@ -75,7 +78,7 @@ export function EditableCellChip({
} }
nonEditModeContent={ nonEditModeContent={
<NoEditModeContainer> <NoEditModeContainer>
<ChipComponent name={inputValue} picture={picture} /> <ChipComponent id={id} name={inputValue} picture={picture} />
<RightContainer> <RightContainer>
{rightEndContents && {rightEndContents &&
rightEndContents.length > 0 && 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}; font-size: ${({ theme }) => theme.font.size.md};
border: none; border: none;
display: block; display: block;
font-weight: 400; font-weight: ${({ theme }) => theme.font.weight.regular};
} }
& .react-datepicker-popper { & .react-datepicker-popper {

View File

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

View File

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

View File

@ -32,7 +32,7 @@ const StyledButton = styled.div<StyledButtonProps>`
`; `;
const StyledButtonLabel = styled.div` const StyledButtonLabel = styled.div`
font-weight: 500; font-weight: ${({ theme }) => theme.font.weight.medium};
margin-left: ${({ theme }) => theme.spacing(2)}; 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'; import { EntityTableActionBarButton } from './EntityTableActionBarButton';
@ -9,8 +9,8 @@ type OwnProps = {
export function TableActionBarButtonToggleComments({ onClick }: OwnProps) { export function TableActionBarButtonToggleComments({ onClick }: OwnProps) {
return ( return (
<EntityTableActionBarButton <EntityTableActionBarButton
label="Comment" label="Notes"
icon={<IconComment size={16} />} icon={<IconNotes size={16} />}
onClick={onClick} onClick={onClick}
/> />
); );

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,21 +1,52 @@
import { useEffect } from 'react'; import { useEffect } from 'react';
declare type CallbackType = () => void; export enum OutsideClickAlerterMode {
absolute = 'absolute',
dom = 'dom',
}
export function useOutsideAlerter( type OwnProps = {
ref: React.RefObject<HTMLInputElement>, ref: React.RefObject<HTMLInputElement>;
callback: CallbackType, callback: () => void;
) { mode?: OutsideClickAlerterMode;
};
export function useOutsideAlerter({
ref,
mode = OutsideClickAlerterMode.dom,
callback,
}: OwnProps) {
useEffect(() => { useEffect(() => {
function handleClickOutside(event: Event) { function handleClickOutside(event: MouseEvent) {
const target = event.target as HTMLButtonElement; 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(); 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); document.addEventListener('mousedown', handleClickOutside);
return () => { return () => {
document.removeEventListener('mousedown', handleClickOutside); 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 { IconBrandGoogle } from '@tabler/icons-react';
export { IconUpload } from '@tabler/icons-react'; export { IconUpload } from '@tabler/icons-react';
export { IconFileUpload } 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; topMargin?: number;
}; };
const MainContainer = styled.div<{ topMargin: number }>` const StyledMainContainer = styled.div<{ topMargin: number }>`
background: ${({ theme }) => theme.background.noisy}; background: ${({ theme }) => theme.background.noisy};
display: flex; display: flex;
@ -27,27 +27,21 @@ type LeftContainerProps = {
isRightDrawerOpen?: boolean; isRightDrawerOpen?: boolean;
}; };
const LeftContainer = styled.div<LeftContainerProps>` const StyledLeftContainer = styled.div<LeftContainerProps>`
display: flex; display: flex;
position: relative; position: relative;
width: calc( width: 100%;
100% -
${(props) =>
props.isRightDrawerOpen
? `${props.theme.rightDrawerWidth} - ${props.theme.spacing(2)}`
: '0px'}
);
`; `;
export function ContentContainer({ children, topMargin }: OwnProps) { export function ContentContainer({ children, topMargin }: OwnProps) {
const [isRightDrawerOpen] = useRecoilState(isRightDrawerOpenState); const [isRightDrawerOpen] = useRecoilState(isRightDrawerOpenState);
return ( return (
<MainContainer topMargin={topMargin ?? 0}> <StyledMainContainer topMargin={topMargin ?? 0}>
<LeftContainer isRightDrawerOpen={isRightDrawerOpen}> <StyledLeftContainer isRightDrawerOpen={isRightDrawerOpen}>
<Panel>{children}</Panel> <Panel>{children}</Panel>
</LeftContainer> </StyledLeftContainer>
<RightDrawer /> <RightDrawer />
</MainContainer> </StyledMainContainer>
); );
} }

View File

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

View File

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

View File

@ -1,33 +1,61 @@
import { useRef } from 'react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { useRecoilState } from 'recoil'; import { useRecoilState } from 'recoil';
import {
OutsideClickAlerterMode,
useOutsideAlerter,
} from '@/ui/hooks/useOutsideAlerter';
import { isDefined } from '@/utils/type-guards/isDefined'; import { isDefined } from '@/utils/type-guards/isDefined';
import { Panel } from '../../Panel';
import { isRightDrawerOpenState } from '../states/isRightDrawerOpenState'; import { isRightDrawerOpenState } from '../states/isRightDrawerOpenState';
import { rightDrawerPageState } from '../states/rightDrawerPageState'; import { rightDrawerPageState } from '../states/rightDrawerPageState';
import { RightDrawerRouter } from './RightDrawerRouter'; 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` const StyledRightDrawer = styled.div`
display: flex; display: flex;
flex-direction: row; flex-direction: row;
width: ${({ theme }) => theme.rightDrawerWidth}; width: 100%;
`; `;
export function RightDrawer() { export function RightDrawer() {
const [isRightDrawerOpen] = useRecoilState(isRightDrawerOpenState); const [isRightDrawerOpen, setIsRightDrawerOpen] = useRecoilState(
isRightDrawerOpenState,
);
const [rightDrawerPage] = useRecoilState(rightDrawerPageState); const [rightDrawerPage] = useRecoilState(rightDrawerPageState);
const rightDrawerRef = useRef(null);
useOutsideAlerter({
ref: rightDrawerRef,
callback: () => setIsRightDrawerOpen(false),
mode: OutsideClickAlerterMode.absolute,
});
if (!isRightDrawerOpen || !isDefined(rightDrawerPage)) { if (!isRightDrawerOpen || !isDefined(rightDrawerPage)) {
return <></>; return <></>;
} }
return ( return (
<StyledRightDrawer> <>
<Panel> <StyledContainer>
<RightDrawerRouter /> <StyledRightDrawer ref={rightDrawerRef}>
</Panel> <RightDrawerRouter />
</StyledRightDrawer> </StyledRightDrawer>
</StyledContainer>
</>
); );
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,8 +1,8 @@
import { atom } from 'recoil'; 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', key: 'ui/layout/right-drawer-page',
default: null, 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, sm: 14,
md: 16, md: 16,
lg: 20, lg: 20,
xl: 40,
}, },
stroke: { stroke: {
sm: 1.6, sm: 1.6,

View File

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

View File

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

View File

@ -1,10 +1,8 @@
import { getOperationName } from '@apollo/client/utilities'; import { getOperationName } from '@apollo/client/utilities';
import { useTheme } from '@emotion/react'; import { useTheme } from '@emotion/react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { useRecoilState } from 'recoil';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import { isMockModeState } from '@/auth/states/isMockModeState';
import { GET_COMPANIES } from '@/companies/services'; import { GET_COMPANIES } from '@/companies/services';
import { useHotkeysScopeOnMountOnly } from '@/hotkeys/hooks/useHotkeysScopeOnMountOnly'; import { useHotkeysScopeOnMountOnly } from '@/hotkeys/hooks/useHotkeysScopeOnMountOnly';
import { InternalHotkeysScope } from '@/hotkeys/types/internal/InternalHotkeysScope'; import { InternalHotkeysScope } from '@/hotkeys/types/internal/InternalHotkeysScope';
@ -28,17 +26,10 @@ const StyledTableContainer = styled.div`
`; `;
export function Companies() { export function Companies() {
const [isMockMode] = useRecoilState(isMockModeState); useHotkeysScopeOnMountOnly({
scope: InternalHotkeysScope.Table,
const hotkeysEnabled = !isMockMode; customScopes: { 'command-menu': true, goto: true },
});
useHotkeysScopeOnMountOnly(
{
scope: InternalHotkeysScope.Table,
customScopes: { 'command-menu': true, goto: true },
},
hotkeysEnabled,
);
const [insertCompany] = useInsertCompanyMutation(); 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 { import {
Comment,
CommentableType, CommentableType,
CommentThread, CommentThread,
CommentThreadTarget, CommentThreadTarget,
@ -6,8 +7,21 @@ import {
type MockedCommentThread = Pick< type MockedCommentThread = Pick<
CommentThread, 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< commentThreadTargets: Array<
Pick< Pick<
CommentThreadTarget, CommentThreadTarget,
@ -27,6 +41,15 @@ export const mockedCommentThreads: Array<MockedCommentThread> = [
id: '89bb825c-171e-4bcc-9cf7-43448d6fb230', id: '89bb825c-171e-4bcc-9cf7-43448d6fb230',
createdAt: '2023-04-26T10:12:42.33625+00:00', createdAt: '2023-04-26T10:12:42.33625+00:00',
updatedAt: '2023-04-26T10:23: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: [ commentThreadTargets: [
{ {
id: '89bb825c-171e-4bcc-9cf7-43448d6fb300', id: '89bb825c-171e-4bcc-9cf7-43448d6fb300',
@ -63,6 +86,15 @@ export const mockedCommentThreads: Array<MockedCommentThread> = [
id: '89bb825c-171e-4bcc-9cf7-43448d6fb278', id: '89bb825c-171e-4bcc-9cf7-43448d6fb278',
createdAt: new Date().toISOString(), createdAt: new Date().toISOString(),
updatedAt: 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: [ commentThreadTargets: [
{ {
id: '89bb825c-171e-4bcc-9cf7-43448d6fb278', id: '89bb825c-171e-4bcc-9cf7-43448d6fb278',

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@ -12,6 +12,15 @@ export class CommentThreadCountOrderByAggregateInput {
@HideField() @HideField()
workspaceId?: keyof typeof SortOrder; 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() @HideField()
deletedAt?: keyof typeof SortOrder; deletedAt?: keyof typeof SortOrder;

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