Refactor Checkbox (#932)
* Refactor Checkbox * Complete note completion * Fixes * Fix login
This commit is contained in:
@ -5,7 +5,7 @@ module.exports = {
|
||||
tsconfigRootDir: __dirname,
|
||||
sourceType: 'module',
|
||||
},
|
||||
plugins: ['@typescript-eslint/eslint-plugin', 'simple-import-sort', 'twenty', 'unused-imports'],
|
||||
plugins: ['@typescript-eslint/eslint-plugin', 'unused-imports', 'simple-import-sort', 'twenty'],
|
||||
extends: [
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:prettier/recommended',
|
||||
@ -48,12 +48,12 @@ module.exports = {
|
||||
'twenty/sort-css-properties-alphabetically': 'error',
|
||||
'twenty/no-hardcoded-colors': 'error',
|
||||
'func-style':['error', 'declaration', { 'allowArrowFunctions': true }],
|
||||
"no-unused-vars": "off",
|
||||
"@typescript-eslint/no-unused-vars": "off",
|
||||
"unused-imports/no-unused-imports": "warn",
|
||||
"unused-imports/no-unused-vars": [
|
||||
"warn",
|
||||
{ "vars": "all", "varsIgnorePattern": "^_", "args": "after-used", "argsIgnorePattern": "^_" }
|
||||
]
|
||||
"no-unused-vars": "off",
|
||||
"unused-imports/no-unused-imports": "warn",
|
||||
"unused-imports/no-unused-vars": [
|
||||
"warn",
|
||||
{ "vars": "all", "varsIgnorePattern": "^_", "args": "after-used", "argsIgnorePattern": "^_" }
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
@ -1992,14 +1992,14 @@ export type GetCommentThreadsByTargetsQueryVariables = Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type GetCommentThreadsByTargetsQuery = { __typename?: 'Query', findManyCommentThreads: Array<{ __typename?: 'CommentThread', id: string, createdAt: string, title?: string | null, body?: string | null, type: ActivityType, author: { __typename?: 'User', id: string, firstName?: string | null, lastName?: string | null, displayName: 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, type: ActivityType, completedAt?: string | null, author: { __typename?: 'User', id: string, firstName?: string | null, lastName?: string | null, displayName: 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 GetCommentThreadQueryVariables = Exact<{
|
||||
commentThreadId: Scalars['String'];
|
||||
}>;
|
||||
|
||||
|
||||
export type GetCommentThreadQuery = { __typename?: 'Query', findManyCommentThreads: Array<{ __typename?: 'CommentThread', id: string, createdAt: string, body?: string | null, title?: string | null, type: ActivityType, author: { __typename?: 'User', id: string, firstName?: string | null, lastName?: string | null, displayName: 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 GetCommentThreadQuery = { __typename?: 'Query', findManyCommentThreads: Array<{ __typename?: 'CommentThread', id: string, createdAt: string, body?: string | null, title?: string | null, type: ActivityType, completedAt?: string | null, author: { __typename?: 'User', id: string, firstName?: string | null, lastName?: string | null, displayName: 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 AddCommentThreadTargetsOnCommentThreadMutationVariables = Exact<{
|
||||
commentThreadId: Scalars['String'];
|
||||
@ -2029,10 +2029,11 @@ export type UpdateCommentThreadMutationVariables = Exact<{
|
||||
body?: InputMaybe<Scalars['String']>;
|
||||
title?: InputMaybe<Scalars['String']>;
|
||||
type?: InputMaybe<ActivityType>;
|
||||
completedAt?: InputMaybe<Scalars['DateTime']>;
|
||||
}>;
|
||||
|
||||
|
||||
export type UpdateCommentThreadMutation = { __typename?: 'Mutation', updateOneCommentThread: { __typename?: 'CommentThread', id: string, body?: string | null, title?: string | null, type: ActivityType } };
|
||||
export type UpdateCommentThreadMutation = { __typename?: 'Mutation', updateOneCommentThread: { __typename?: 'CommentThread', id: string, body?: string | null, title?: string | null, type: ActivityType, completedAt?: string | null } };
|
||||
|
||||
export type UploadAttachmentMutationVariables = Exact<{
|
||||
file: Scalars['Upload'];
|
||||
@ -2495,6 +2496,7 @@ export const GetCommentThreadsByTargetsDocument = gql`
|
||||
title
|
||||
body
|
||||
type
|
||||
completedAt
|
||||
author {
|
||||
id
|
||||
firstName
|
||||
@ -2559,6 +2561,7 @@ export const GetCommentThreadDocument = gql`
|
||||
body
|
||||
title
|
||||
type
|
||||
completedAt
|
||||
author {
|
||||
id
|
||||
firstName
|
||||
@ -2740,15 +2743,16 @@ export type DeleteCommentThreadMutationHookResult = ReturnType<typeof useDeleteC
|
||||
export type DeleteCommentThreadMutationResult = Apollo.MutationResult<DeleteCommentThreadMutation>;
|
||||
export type DeleteCommentThreadMutationOptions = Apollo.BaseMutationOptions<DeleteCommentThreadMutation, DeleteCommentThreadMutationVariables>;
|
||||
export const UpdateCommentThreadDocument = gql`
|
||||
mutation UpdateCommentThread($id: String!, $body: String, $title: String, $type: ActivityType) {
|
||||
mutation UpdateCommentThread($id: String!, $body: String, $title: String, $type: ActivityType, $completedAt: DateTime) {
|
||||
updateOneCommentThread(
|
||||
where: {id: $id}
|
||||
data: {body: $body, title: $title, type: $type}
|
||||
data: {body: $body, title: $title, type: $type, completedAt: $completedAt}
|
||||
) {
|
||||
id
|
||||
body
|
||||
title
|
||||
type
|
||||
completedAt
|
||||
}
|
||||
}
|
||||
`;
|
||||
@ -2771,6 +2775,7 @@ export type UpdateCommentThreadMutationFn = Apollo.MutationFunction<UpdateCommen
|
||||
* body: // value for 'body'
|
||||
* title: // value for 'title'
|
||||
* type: // value for 'type'
|
||||
* completedAt: // value for 'completedAt'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
|
||||
@ -24,22 +24,22 @@ const root = ReactDOM.createRoot(
|
||||
|
||||
root.render(
|
||||
<RecoilRoot>
|
||||
<ApolloProvider>
|
||||
<UserProvider>
|
||||
<ClientConfigProvider>
|
||||
<AppThemeProvider>
|
||||
<SnackBarProvider>
|
||||
<BrowserRouter>
|
||||
<BrowserRouter>
|
||||
<ApolloProvider>
|
||||
<UserProvider>
|
||||
<ClientConfigProvider>
|
||||
<AppThemeProvider>
|
||||
<SnackBarProvider>
|
||||
<AuthAutoRouter />
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>
|
||||
</BrowserRouter>
|
||||
</SnackBarProvider>
|
||||
</AppThemeProvider>
|
||||
</ClientConfigProvider>
|
||||
</UserProvider>
|
||||
</ApolloProvider>
|
||||
</SnackBarProvider>
|
||||
</AppThemeProvider>
|
||||
</ClientConfigProvider>
|
||||
</UserProvider>
|
||||
</ApolloProvider>
|
||||
</BrowserRouter>
|
||||
</RecoilRoot>,
|
||||
);
|
||||
|
||||
|
||||
@ -21,6 +21,8 @@ import { debounce } from '~/utils/debounce';
|
||||
import { CommentThreadActionBar } from '../right-drawer/components/CommentThreadActionBar';
|
||||
import { CommentForDrawer } from '../types/CommentForDrawer';
|
||||
|
||||
import { CommentThreadTitle } from './CommentThreadTitle';
|
||||
|
||||
import '@blocknote/core/style.css';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
@ -54,29 +56,6 @@ const StyledTopContainer = styled.div`
|
||||
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;
|
||||
@ -86,7 +65,10 @@ const StyledTopActionsContainer = styled.div`
|
||||
`;
|
||||
|
||||
type OwnProps = {
|
||||
commentThread: Pick<CommentThread, 'id' | 'title' | 'body' | 'type'> & {
|
||||
commentThread: Pick<
|
||||
CommentThread,
|
||||
'id' | 'title' | 'body' | 'type' | 'completedAt'
|
||||
> & {
|
||||
comments?: Array<CommentForDrawer> | null;
|
||||
} & {
|
||||
commentThreadTargets?: Array<
|
||||
@ -106,6 +88,9 @@ export function CommentThreadEditor({
|
||||
useState<boolean>(false);
|
||||
|
||||
const [title, setTitle] = useState<string | null>(commentThread.title ?? '');
|
||||
const [completedAt, setCompletedAt] = useState<string | null>(
|
||||
commentThread.completedAt ?? '',
|
||||
);
|
||||
|
||||
const [updateCommentThreadMutation] = useUpdateCommentThreadMutation();
|
||||
|
||||
@ -123,6 +108,23 @@ export function CommentThreadEditor({
|
||||
},
|
||||
[commentThread, updateCommentThreadMutation],
|
||||
);
|
||||
|
||||
const handleActivityCompletionChange = useCallback(
|
||||
(value: boolean) => {
|
||||
updateCommentThreadMutation({
|
||||
variables: {
|
||||
id: commentThread.id,
|
||||
completedAt: value ? new Date().toISOString() : null,
|
||||
},
|
||||
refetchQueries: [
|
||||
getOperationName(GET_COMMENT_THREADS_BY_TARGETS) ?? '',
|
||||
],
|
||||
});
|
||||
setCompletedAt(value ? new Date().toISOString() : null);
|
||||
},
|
||||
[commentThread, updateCommentThreadMutation],
|
||||
);
|
||||
|
||||
const debouncedUpdateTitle = debounce(updateTitle, 200);
|
||||
|
||||
function updateTitleFromBody(body: string) {
|
||||
@ -145,16 +147,16 @@ export function CommentThreadEditor({
|
||||
<CommentThreadTypeDropdown commentThread={commentThread} />
|
||||
<CommentThreadActionBar commentThreadId={commentThread?.id ?? ''} />
|
||||
</StyledTopActionsContainer>
|
||||
<StyledEditableTitleInput
|
||||
autoComplete="off"
|
||||
autoFocus
|
||||
placeholder={`${commentThread.type} title (optional)`}
|
||||
onChange={(event) => {
|
||||
<CommentThreadTitle
|
||||
title={title ?? ''}
|
||||
completed={!!completedAt}
|
||||
type={commentThread.type}
|
||||
onTitleChange={(newTitle) => {
|
||||
setTitle(newTitle);
|
||||
setHasUserManuallySetTitle(true);
|
||||
setTitle(event.target.value);
|
||||
debouncedUpdateTitle(event.target.value);
|
||||
debouncedUpdateTitle(newTitle);
|
||||
}}
|
||||
value={title ?? ''}
|
||||
onCompletionChange={handleActivityCompletionChange}
|
||||
/>
|
||||
<PropertyBox>
|
||||
<PropertyBoxItem
|
||||
|
||||
@ -0,0 +1,82 @@
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import {
|
||||
Checkbox,
|
||||
CheckboxShape,
|
||||
CheckboxSize,
|
||||
} from '@/ui/input/components/Checkbox';
|
||||
import { ActivityType } from '~/generated/graphql';
|
||||
|
||||
const StyledEditableTitleInput = styled.input<{
|
||||
completed: boolean;
|
||||
value: string;
|
||||
}>`
|
||||
background: transparent;
|
||||
|
||||
border: none;
|
||||
color: ${({ theme, value }) =>
|
||||
value ? theme.font.color.primary : theme.font.color.light};
|
||||
display: flex;
|
||||
|
||||
flex-direction: column;
|
||||
font-size: ${({ theme }) => theme.font.size.xl};
|
||||
font-weight: ${({ theme }) => theme.font.weight.semiBold};
|
||||
|
||||
line-height: ${({ theme }) => theme.text.lineHeight.md};
|
||||
outline: none;
|
||||
text-decoration: ${({ completed }) => (completed ? 'line-through' : 'none')};
|
||||
&::placeholder {
|
||||
color: ${({ theme }) => theme.font.color.light};
|
||||
}
|
||||
width: calc(100% - ${({ theme }) => theme.spacing(2)});
|
||||
`;
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
const StyledCheckboxContainer = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
type OwnProps = {
|
||||
title: string;
|
||||
type: ActivityType;
|
||||
completed: boolean;
|
||||
onTitleChange: (title: string) => void;
|
||||
onCompletionChange: (value: boolean) => void;
|
||||
};
|
||||
|
||||
export function CommentThreadTitle({
|
||||
title,
|
||||
completed,
|
||||
type,
|
||||
onTitleChange,
|
||||
onCompletionChange,
|
||||
}: OwnProps) {
|
||||
return (
|
||||
<StyledContainer>
|
||||
{type === ActivityType.Task && (
|
||||
<StyledCheckboxContainer onClick={() => onCompletionChange(!completed)}>
|
||||
<Checkbox
|
||||
size={CheckboxSize.Large}
|
||||
shape={CheckboxShape.Rounded}
|
||||
checked={completed}
|
||||
/>
|
||||
</StyledCheckboxContainer>
|
||||
)}
|
||||
<StyledEditableTitleInput
|
||||
autoComplete="off"
|
||||
autoFocus
|
||||
placeholder={`${type} title (optional)`}
|
||||
onChange={(event) => onTitleChange(event.target.value)}
|
||||
value={title}
|
||||
completed={completed}
|
||||
/>
|
||||
</StyledContainer>
|
||||
);
|
||||
}
|
||||
@ -18,6 +18,7 @@ export const GET_COMMENT_THREADS_BY_TARGETS = gql`
|
||||
title
|
||||
body
|
||||
type
|
||||
completedAt
|
||||
author {
|
||||
id
|
||||
firstName
|
||||
@ -54,6 +55,7 @@ export const GET_COMMENT_THREAD = gql`
|
||||
body
|
||||
title
|
||||
type
|
||||
completedAt
|
||||
author {
|
||||
id
|
||||
firstName
|
||||
|
||||
@ -68,15 +68,22 @@ export const UPDATE_COMMENT_THREAD = gql`
|
||||
$body: String
|
||||
$title: String
|
||||
$type: ActivityType
|
||||
$completedAt: DateTime
|
||||
) {
|
||||
updateOneCommentThread(
|
||||
where: { id: $id }
|
||||
data: { body: $body, title: $title, type: $type }
|
||||
data: {
|
||||
body: $body
|
||||
title: $title
|
||||
type: $type
|
||||
completedAt: $completedAt
|
||||
}
|
||||
) {
|
||||
id
|
||||
body
|
||||
title
|
||||
type
|
||||
completedAt
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@ -1,25 +1,18 @@
|
||||
import React from 'react';
|
||||
import { Tooltip } from 'react-tooltip';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { CommentThreadCreateButton } from '@/activities/components/CommentThreadCreateButton';
|
||||
import { useOpenCommentThreadRightDrawer } from '@/activities/hooks/useOpenCommentThreadRightDrawer';
|
||||
import { useOpenCreateCommentThreadDrawer } from '@/activities/hooks/useOpenCreateCommentThreadDrawer';
|
||||
import { CommentableEntity } from '@/activities/types/CommentableEntity';
|
||||
import { CommentThreadForDrawer } from '@/activities/types/CommentThreadForDrawer';
|
||||
import { useIsMobile } from '@/ui/hooks/useIsMobile';
|
||||
import { IconNotes } from '@/ui/icon/index';
|
||||
import {
|
||||
ActivityType,
|
||||
SortOrder,
|
||||
useGetCommentThreadsByTargetsQuery,
|
||||
} from '~/generated/graphql';
|
||||
import {
|
||||
beautifyExactDate,
|
||||
beautifyPastDateRelativeToNow,
|
||||
} from '~/utils/date-utils';
|
||||
|
||||
import { OverflowingTextWithTooltip } from '../../../ui/tooltip/OverflowingTextWithTooltip';
|
||||
import { TimelineActivity } from './TimelineActivity';
|
||||
|
||||
const StyledMainContainer = styled.div`
|
||||
align-items: flex-start;
|
||||
@ -72,112 +65,6 @@ const StyledEmptyTimelineSubTitle = styled.div`
|
||||
margin-bottom: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
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: center;
|
||||
align-items: center;
|
||||
color: ${({ theme }) => theme.font.color.tertiary};
|
||||
display: flex;
|
||||
flex: 1 0 0;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
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: ${({ theme }) => theme.border.radius.sm};
|
||||
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};
|
||||
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledCardContent = styled.div`
|
||||
align-self: stretch;
|
||||
color: ${({ theme }) => theme.font.color.secondary};
|
||||
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
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;
|
||||
@ -208,8 +95,6 @@ export function Timeline({ entity }: { entity: CommentableEntity }) {
|
||||
},
|
||||
});
|
||||
|
||||
const openCommentThreadRightDrawer = useOpenCommentThreadRightDrawer();
|
||||
|
||||
const openCreateCommandThread = useOpenCreateCommentThreadDrawer();
|
||||
|
||||
const commentThreads: CommentThreadForDrawer[] =
|
||||
@ -235,76 +120,18 @@ export function Timeline({ entity }: { entity: CommentableEntity }) {
|
||||
return (
|
||||
<StyledMainContainer>
|
||||
<StyledTopActionBar>
|
||||
<StyledTimelineItemContainer>
|
||||
<CommentThreadCreateButton
|
||||
onNoteClick={() =>
|
||||
openCreateCommandThread(entity, ActivityType.Note)
|
||||
}
|
||||
onTaskClick={() =>
|
||||
openCreateCommandThread(entity, ActivityType.Task)
|
||||
}
|
||||
/>
|
||||
</StyledTimelineItemContainer>
|
||||
<CommentThreadCreateButton
|
||||
onNoteClick={() => openCreateCommandThread(entity, ActivityType.Note)}
|
||||
onTaskClick={() => openCreateCommandThread(entity, ActivityType.Task)}
|
||||
/>
|
||||
</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.displayName}</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>
|
||||
<OverflowingTextWithTooltip
|
||||
text={
|
||||
commentThread.title
|
||||
? commentThread.title
|
||||
: '(No title)'
|
||||
}
|
||||
/>
|
||||
</StyledCardTitle>
|
||||
<StyledCardContent>
|
||||
<OverflowingTextWithTooltip
|
||||
text={body ? body : '(No content)'}
|
||||
/>
|
||||
</StyledCardContent>
|
||||
</StyledCard>
|
||||
</StyledCardContainer>
|
||||
</StyledTimelineItemContainer>
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
{commentThreads.map((commentThread) => (
|
||||
<TimelineActivity
|
||||
key={commentThread.id}
|
||||
commentThread={commentThread}
|
||||
/>
|
||||
))}
|
||||
</StyledTimelineContainer>
|
||||
</StyledMainContainer>
|
||||
);
|
||||
|
||||
@ -0,0 +1,191 @@
|
||||
import { useCallback } from 'react';
|
||||
import { Tooltip } from 'react-tooltip';
|
||||
import { getOperationName } from '@apollo/client/utilities';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { useOpenCommentThreadRightDrawer } from '@/activities/hooks/useOpenCommentThreadRightDrawer';
|
||||
import { GET_COMMENT_THREADS_BY_TARGETS } from '@/activities/queries';
|
||||
import { IconNotes } from '@/ui/icon';
|
||||
import { OverflowingTextWithTooltip } from '@/ui/tooltip/OverflowingTextWithTooltip';
|
||||
import {
|
||||
CommentThread,
|
||||
useUpdateCommentThreadMutation,
|
||||
} from '~/generated/graphql';
|
||||
import {
|
||||
beautifyExactDate,
|
||||
beautifyPastDateRelativeToNow,
|
||||
} from '~/utils/date-utils';
|
||||
|
||||
import { TimelineActivityTitle } from './TimelineActivityTitle';
|
||||
|
||||
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: center;
|
||||
align-items: center;
|
||||
color: ${({ theme }) => theme.font.color.tertiary};
|
||||
display: flex;
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
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: ${({ theme }) => theme.spacing(2)};
|
||||
justify-content: flex-end;
|
||||
`;
|
||||
|
||||
const StyledVerticalLineContainer = styled.div`
|
||||
align-items: center;
|
||||
align-self: stretch;
|
||||
display: flex;
|
||||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
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: ${({ theme }) => theme.spacing(2)};
|
||||
padding: 4px 0px 20px 0px;
|
||||
`;
|
||||
|
||||
const StyledCard = styled.div`
|
||||
align-items: flex-start;
|
||||
background: ${({ theme }) => theme.background.secondary};
|
||||
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: ${({ theme }) => theme.spacing(3)};
|
||||
max-width: 400px;
|
||||
padding: ${({ theme }) => theme.spacing(3)};
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
const StyledCardContent = styled.div`
|
||||
align-self: stretch;
|
||||
color: ${({ theme }) => theme.font.color.secondary};
|
||||
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
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: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
const StyledTimelineItemContainer = styled.div`
|
||||
align-items: center;
|
||||
align-self: stretch;
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
`;
|
||||
|
||||
type OwnProps = {
|
||||
commentThread: Pick<
|
||||
CommentThread,
|
||||
'id' | 'title' | 'body' | 'createdAt' | 'completedAt' | 'type'
|
||||
> & { author: Pick<CommentThread['author'], 'displayName'> };
|
||||
};
|
||||
|
||||
export function TimelineActivity({ commentThread }: OwnProps) {
|
||||
const beautifiedCreatedAt = beautifyPastDateRelativeToNow(
|
||||
commentThread.createdAt,
|
||||
);
|
||||
const exactCreatedAt = beautifyExactDate(commentThread.createdAt);
|
||||
const body = JSON.parse(commentThread.body ?? '{}')[0]?.content[0]?.text;
|
||||
|
||||
const openCommentThreadRightDrawer = useOpenCommentThreadRightDrawer();
|
||||
const [updateCommentThreadMutation] = useUpdateCommentThreadMutation();
|
||||
|
||||
const handleActivityCompletionChange = useCallback(
|
||||
(value: boolean) => {
|
||||
updateCommentThreadMutation({
|
||||
variables: {
|
||||
id: commentThread.id,
|
||||
completedAt: value ? new Date().toISOString() : null,
|
||||
},
|
||||
refetchQueries: [
|
||||
getOperationName(GET_COMMENT_THREADS_BY_TARGETS) ?? '',
|
||||
],
|
||||
});
|
||||
},
|
||||
[commentThread, updateCommentThreadMutation],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<StyledTimelineItemContainer>
|
||||
<StyledIconContainer>
|
||||
<IconNotes />
|
||||
</StyledIconContainer>
|
||||
<StyledItemTitleContainer>
|
||||
<span>{commentThread.author.displayName}</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)}
|
||||
>
|
||||
<TimelineActivityTitle
|
||||
title={commentThread.title ?? ''}
|
||||
completed={!!commentThread.completedAt}
|
||||
type={commentThread.type}
|
||||
onCompletionChange={handleActivityCompletionChange}
|
||||
/>
|
||||
<StyledCardContent>
|
||||
<OverflowingTextWithTooltip text={body ? body : '(No content)'} />
|
||||
</StyledCardContent>
|
||||
</StyledCard>
|
||||
</StyledCardContainer>
|
||||
</StyledTimelineItemContainer>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,64 @@
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { Checkbox, CheckboxShape } from '@/ui/input/components/Checkbox';
|
||||
import { OverflowingTextWithTooltip } from '@/ui/tooltip/OverflowingTextWithTooltip';
|
||||
import { ActivityType } from '~/generated/graphql';
|
||||
|
||||
const StyledTitleContainer = styled.div`
|
||||
color: ${({ theme }) => theme.font.color.primary};
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
font-weight: ${({ theme }) => theme.font.weight.medium};
|
||||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
line-height: ${({ theme }) => theme.text.lineHeight.lg};
|
||||
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledTitleText = styled.div<{ completed?: boolean }>`
|
||||
text-decoration: ${({ completed }) => (completed ? 'line-through' : 'none')};
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledCheckboxContainer = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
type OwnProps = {
|
||||
title: string;
|
||||
completed?: boolean;
|
||||
type: ActivityType;
|
||||
onCompletionChange?: (value: boolean) => void;
|
||||
};
|
||||
|
||||
export function TimelineActivityTitle({
|
||||
title,
|
||||
completed,
|
||||
type,
|
||||
onCompletionChange,
|
||||
}: OwnProps) {
|
||||
return (
|
||||
<StyledTitleContainer>
|
||||
{type === ActivityType.Task && (
|
||||
<StyledCheckboxContainer
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
onCompletionChange?.(!completed);
|
||||
}}
|
||||
>
|
||||
<Checkbox
|
||||
checked={completed ?? false}
|
||||
shape={CheckboxShape.Rounded}
|
||||
/>
|
||||
</StyledCheckboxContainer>
|
||||
)}
|
||||
<StyledTitleText completed={completed}>
|
||||
<OverflowingTextWithTooltip text={title ? title : '(No title)'} />
|
||||
</StyledTitleText>
|
||||
</StyledTitleContainer>
|
||||
);
|
||||
}
|
||||
@ -1,9 +1,11 @@
|
||||
import { useMemo, useRef } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { InMemoryCache, NormalizedCacheObject } from '@apollo/client';
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { tokenPairState } from '@/auth/states/tokenPairState';
|
||||
import { isDebugModeState } from '@/client-config/states/isDebugModeState';
|
||||
import { AppPath } from '@/types/AppPath';
|
||||
import { CommentThreadTarget } from '~/generated/graphql';
|
||||
import { useUpdateEffect } from '~/hooks/useUpdateEffect';
|
||||
|
||||
@ -13,6 +15,7 @@ export function useApolloFactory() {
|
||||
const apolloRef = useRef<ApolloFactory<NormalizedCacheObject> | null>(null);
|
||||
const [isDebugMode] = useRecoilState(isDebugModeState);
|
||||
|
||||
const navigate = useNavigate();
|
||||
const [tokenPair, setTokenPair] = useRecoilState(tokenPairState);
|
||||
|
||||
const apolloClient = useMemo(() => {
|
||||
@ -46,6 +49,7 @@ export function useApolloFactory() {
|
||||
},
|
||||
onUnauthenticatedError() {
|
||||
setTokenPair(null);
|
||||
navigate(AppPath.SignIn);
|
||||
},
|
||||
extraLinks: [],
|
||||
isDebugMode,
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { companyAddressFamilyState } from '@/companies/states/companyAddressFamilyState';
|
||||
@ -14,17 +15,22 @@ export function EditableCompanyAddressCell() {
|
||||
companyAddressFamilyState(currentRowEntityId ?? ''),
|
||||
);
|
||||
|
||||
const [internalValue, setInternalValue] = useState(address ?? '');
|
||||
useEffect(() => {
|
||||
setInternalValue(address ?? '');
|
||||
}, [address]);
|
||||
|
||||
return (
|
||||
<EditableCellText
|
||||
value={address || ''}
|
||||
onSubmit={(newAddress) =>
|
||||
value={internalValue}
|
||||
onSubmit={() =>
|
||||
updateCompany({
|
||||
variables: {
|
||||
where: {
|
||||
id: currentRowEntityId,
|
||||
},
|
||||
data: {
|
||||
address: newAddress,
|
||||
address: internalValue,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
@ -8,11 +8,23 @@ export enum CheckboxVariant {
|
||||
Secondary = 'secondary',
|
||||
}
|
||||
|
||||
export enum CheckboxShape {
|
||||
Squared = 'squared',
|
||||
Rounded = 'rounded',
|
||||
}
|
||||
|
||||
export enum CheckboxSize {
|
||||
Large = 'large',
|
||||
Small = 'small',
|
||||
}
|
||||
|
||||
type OwnProps = {
|
||||
checked: boolean;
|
||||
indeterminate?: boolean;
|
||||
onChange?: (value: boolean) => void;
|
||||
variant?: CheckboxVariant;
|
||||
size?: CheckboxSize;
|
||||
shape?: CheckboxShape;
|
||||
};
|
||||
|
||||
const StyledInputContainer = styled.div`
|
||||
@ -22,8 +34,10 @@ const StyledInputContainer = styled.div`
|
||||
`;
|
||||
|
||||
const StyledInput = styled.input<{
|
||||
checkboxSize: CheckboxSize;
|
||||
variant: CheckboxVariant;
|
||||
indeterminate?: boolean;
|
||||
variant?: CheckboxVariant;
|
||||
shape?: CheckboxShape;
|
||||
}>`
|
||||
cursor: pointer;
|
||||
margin: 0;
|
||||
@ -32,30 +46,37 @@ const StyledInput = styled.input<{
|
||||
z-index: 10;
|
||||
|
||||
& + label {
|
||||
--size: ${({ checkboxSize }) =>
|
||||
checkboxSize === CheckboxSize.Large ? '18px' : '12px'};
|
||||
cursor: pointer;
|
||||
height: 14px;
|
||||
height: calc(var(--size) + 2px);
|
||||
padding: 0;
|
||||
position: relative;
|
||||
width: 14px;
|
||||
width: calc(var(--size) + 2px);
|
||||
}
|
||||
|
||||
& + label:before {
|
||||
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||
--size: ${({ checkboxSize }) =>
|
||||
checkboxSize === CheckboxSize.Large ? '18px' : '12px'};
|
||||
background: ${({ theme, indeterminate }) =>
|
||||
indeterminate ? theme.color.blue : 'transparent'};
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
border-color: ${({ theme, indeterminate, variant }) =>
|
||||
indeterminate
|
||||
? theme.color.blue
|
||||
: variant === CheckboxVariant.Primary
|
||||
? theme.border.color.inverted
|
||||
: theme.border.color.secondaryInverted};
|
||||
border-radius: ${({ theme, shape }) =>
|
||||
shape === CheckboxShape.Rounded
|
||||
? theme.border.radius.rounded
|
||||
: theme.border.radius.sm};
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
content: '';
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
height: 12px;
|
||||
width: 12px;
|
||||
height: var(--size);
|
||||
width: var(--size);
|
||||
}
|
||||
|
||||
&:checked + label:before {
|
||||
@ -64,12 +85,16 @@ const StyledInput = styled.input<{
|
||||
}
|
||||
|
||||
& + label > svg {
|
||||
height: 12px;
|
||||
left: 1px;
|
||||
--padding: ${({ checkboxSize }) =>
|
||||
checkboxSize === CheckboxSize.Large ? '2px' : '1px'};
|
||||
--size: ${({ checkboxSize }) =>
|
||||
checkboxSize === CheckboxSize.Large ? '16px' : '12px'};
|
||||
height: var(--size);
|
||||
left: var(--padding);
|
||||
position: absolute;
|
||||
stroke: ${({ theme }) => theme.grayScale.gray0};
|
||||
top: 1px;
|
||||
width: 12px;
|
||||
top: var(--padding);
|
||||
width: var(--size);
|
||||
}
|
||||
`;
|
||||
|
||||
@ -78,6 +103,8 @@ export function Checkbox({
|
||||
onChange,
|
||||
indeterminate,
|
||||
variant = CheckboxVariant.Primary,
|
||||
size = CheckboxSize.Small,
|
||||
shape = CheckboxShape.Squared,
|
||||
}: OwnProps) {
|
||||
const [isInternalChecked, setIsInternalChecked] =
|
||||
React.useState<boolean>(false);
|
||||
@ -101,6 +128,8 @@ export function Checkbox({
|
||||
checked={isInternalChecked}
|
||||
indeterminate={indeterminate}
|
||||
variant={variant}
|
||||
checkboxSize={size}
|
||||
shape={shape}
|
||||
onChange={(event) => handleChange(event.target.checked)}
|
||||
/>
|
||||
<label htmlFor="checkbox">
|
||||
|
||||
@ -5,7 +5,7 @@ import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
|
||||
import { AutosizeTextInput } from '../AutosizeTextInput';
|
||||
|
||||
const meta: Meta<typeof AutosizeTextInput> = {
|
||||
title: 'UI/Inputs/AutosizeTextInput',
|
||||
title: 'UI/Input/AutosizeTextInput',
|
||||
component: AutosizeTextInput,
|
||||
decorators: [ComponentDecorator],
|
||||
};
|
||||
|
||||
@ -0,0 +1,74 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
|
||||
import { CatalogDecorator } from '~/testing/decorators/CatalogDecorator';
|
||||
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
|
||||
|
||||
import {
|
||||
Checkbox,
|
||||
CheckboxShape,
|
||||
CheckboxSize,
|
||||
CheckboxVariant,
|
||||
} from '../Checkbox';
|
||||
|
||||
const meta: Meta<typeof Checkbox> = {
|
||||
title: 'UI/Input/Checkbox',
|
||||
component: Checkbox,
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof Checkbox>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
checked: false,
|
||||
indeterminate: false,
|
||||
variant: CheckboxVariant.Primary,
|
||||
size: CheckboxSize.Small,
|
||||
shape: CheckboxShape.Squared,
|
||||
},
|
||||
decorators: [ComponentDecorator],
|
||||
};
|
||||
|
||||
export const Catalog: Story = {
|
||||
args: {},
|
||||
argTypes: {
|
||||
variant: { control: false },
|
||||
size: { control: false },
|
||||
indeterminate: { control: false },
|
||||
checked: { control: false },
|
||||
shape: { control: false },
|
||||
},
|
||||
parameters: {
|
||||
catalog: [
|
||||
{
|
||||
name: 'state',
|
||||
values: ['unchecked', 'checked', 'indeterminate'],
|
||||
props: (state: string) => {
|
||||
if (state === 'checked') {
|
||||
return { checked: true };
|
||||
}
|
||||
|
||||
if (state === 'indeterminate') {
|
||||
return { indeterminate: true };
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'shape',
|
||||
values: Object.values(CheckboxShape),
|
||||
props: (shape: CheckboxShape) => ({ shape }),
|
||||
},
|
||||
{
|
||||
name: 'variant',
|
||||
values: Object.values(CheckboxVariant),
|
||||
props: (variant: CheckboxVariant) => ({ variant }),
|
||||
},
|
||||
{
|
||||
name: 'size',
|
||||
values: Object.values(CheckboxSize),
|
||||
props: (size: CheckboxSize) => ({ size }),
|
||||
},
|
||||
],
|
||||
},
|
||||
decorators: [CatalogDecorator],
|
||||
};
|
||||
@ -11,7 +11,7 @@ import { TextInput } from '../TextInput';
|
||||
const changeJestFn = jest.fn();
|
||||
|
||||
const meta: Meta<typeof TextInput> = {
|
||||
title: 'UI/Inputs/TextInput',
|
||||
title: 'UI/Input/TextInput',
|
||||
component: TextInput,
|
||||
decorators: [ComponentDecorator],
|
||||
args: { value: '', onChange: changeJestFn, placeholder: 'Placeholder' },
|
||||
|
||||
@ -12,6 +12,7 @@ const StyledOverflowingText = styled.div<{ cursorPointer: boolean }>`
|
||||
|
||||
font-weight: inherit;
|
||||
overflow: hidden;
|
||||
text-decoration: inherit;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
white-space: nowrap;
|
||||
|
||||
Reference in New Issue
Block a user