Refactor Checkbox (#932)

* Refactor Checkbox

* Complete note completion

* Fixes

* Fix login
This commit is contained in:
Charles Bochet
2023-07-25 21:58:57 -07:00
committed by GitHub
parent 09a019da5d
commit 66585fce9a
17 changed files with 552 additions and 258 deletions

View File

@ -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": "^_" }
]
}
};

View File

@ -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'
* },
* });
*/

View File

@ -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>,
);

View File

@ -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

View File

@ -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>
);
}

View File

@ -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

View File

@ -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
}
}
`;

View File

@ -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>
);

View File

@ -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>
</>
);
}

View File

@ -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>
);
}

View File

@ -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,

View File

@ -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,
},
},
})

View File

@ -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">

View File

@ -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],
};

View File

@ -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],
};

View File

@ -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' },

View File

@ -12,6 +12,7 @@ const StyledOverflowingText = styled.div<{ cursorPointer: boolean }>`
font-weight: inherit;
overflow: hidden;
text-decoration: inherit;
text-overflow: ellipsis;
white-space: nowrap;