mirror of
https://github.com/omnivore-app/omnivore.git
synced 2026-03-11 08:54:26 +00:00
Merge pull request #345 from omnivore-app/OMN-190
[Omn-190] - [Settings View] - Labels
This commit is contained in:
commit
589e639bb5
25 changed files with 1676 additions and 160 deletions
|
|
@ -121,6 +121,7 @@
|
|||
"node": "14.18.x"
|
||||
},
|
||||
"volta": {
|
||||
"node": "14.18.x"
|
||||
"node": "14.18.1",
|
||||
"yarn": "1.22.18"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -792,6 +792,7 @@ export type Mutation = {
|
|||
signup: SignupResult;
|
||||
updateHighlight: UpdateHighlightResult;
|
||||
updateHighlightReply: UpdateHighlightReplyResult;
|
||||
updateLabel: UpdateLabelResult;
|
||||
updateLinkShareInfo: UpdateLinkShareInfoResult;
|
||||
updateReminder: UpdateReminderResult;
|
||||
updateSharedComment: UpdateSharedCommentResult;
|
||||
|
|
@ -966,6 +967,11 @@ export type MutationUpdateHighlightReplyArgs = {
|
|||
};
|
||||
|
||||
|
||||
export type MutationUpdateLabelArgs = {
|
||||
input: UpdateLabelInput;
|
||||
};
|
||||
|
||||
|
||||
export type MutationUpdateLinkShareInfoArgs = {
|
||||
input: UpdateLinkShareInfoInput;
|
||||
};
|
||||
|
|
@ -1577,6 +1583,32 @@ export type UpdateHighlightSuccess = {
|
|||
highlight: Highlight;
|
||||
};
|
||||
|
||||
export type UpdateLabelError = {
|
||||
__typename?: 'UpdateLabelError';
|
||||
errorCodes: Array<UpdateLabelErrorCode>;
|
||||
};
|
||||
|
||||
export enum UpdateLabelErrorCode {
|
||||
BadRequest = 'BAD_REQUEST',
|
||||
Forbidden = 'FORBIDDEN',
|
||||
NotFound = 'NOT_FOUND',
|
||||
Unauthorized = 'UNAUTHORIZED'
|
||||
}
|
||||
|
||||
export type UpdateLabelInput = {
|
||||
color: Scalars['String'];
|
||||
description?: InputMaybe<Scalars['String']>;
|
||||
labelId: Scalars['ID'];
|
||||
name: Scalars['String'];
|
||||
};
|
||||
|
||||
export type UpdateLabelResult = UpdateLabelError | UpdateLabelSuccess;
|
||||
|
||||
export type UpdateLabelSuccess = {
|
||||
__typename?: 'UpdateLabelSuccess';
|
||||
label: Label;
|
||||
};
|
||||
|
||||
export type UpdateLinkShareInfoError = {
|
||||
__typename?: 'UpdateLinkShareInfoError';
|
||||
errorCodes: Array<UpdateLinkShareInfoErrorCode>;
|
||||
|
|
@ -2095,6 +2127,11 @@ export type ResolversTypes = {
|
|||
UpdateHighlightReplySuccess: ResolverTypeWrapper<UpdateHighlightReplySuccess>;
|
||||
UpdateHighlightResult: ResolversTypes['UpdateHighlightError'] | ResolversTypes['UpdateHighlightSuccess'];
|
||||
UpdateHighlightSuccess: ResolverTypeWrapper<UpdateHighlightSuccess>;
|
||||
UpdateLabelError: ResolverTypeWrapper<UpdateLabelError>;
|
||||
UpdateLabelErrorCode: UpdateLabelErrorCode;
|
||||
UpdateLabelInput: UpdateLabelInput;
|
||||
UpdateLabelResult: ResolversTypes['UpdateLabelError'] | ResolversTypes['UpdateLabelSuccess'];
|
||||
UpdateLabelSuccess: ResolverTypeWrapper<UpdateLabelSuccess>;
|
||||
UpdateLinkShareInfoError: ResolverTypeWrapper<UpdateLinkShareInfoError>;
|
||||
UpdateLinkShareInfoErrorCode: UpdateLinkShareInfoErrorCode;
|
||||
UpdateLinkShareInfoInput: UpdateLinkShareInfoInput;
|
||||
|
|
@ -2326,6 +2363,10 @@ export type ResolversParentTypes = {
|
|||
UpdateHighlightReplySuccess: UpdateHighlightReplySuccess;
|
||||
UpdateHighlightResult: ResolversParentTypes['UpdateHighlightError'] | ResolversParentTypes['UpdateHighlightSuccess'];
|
||||
UpdateHighlightSuccess: UpdateHighlightSuccess;
|
||||
UpdateLabelError: UpdateLabelError;
|
||||
UpdateLabelInput: UpdateLabelInput;
|
||||
UpdateLabelResult: ResolversParentTypes['UpdateLabelError'] | ResolversParentTypes['UpdateLabelSuccess'];
|
||||
UpdateLabelSuccess: UpdateLabelSuccess;
|
||||
UpdateLinkShareInfoError: UpdateLinkShareInfoError;
|
||||
UpdateLinkShareInfoInput: UpdateLinkShareInfoInput;
|
||||
UpdateLinkShareInfoResult: ResolversParentTypes['UpdateLinkShareInfoError'] | ResolversParentTypes['UpdateLinkShareInfoSuccess'];
|
||||
|
|
@ -2937,6 +2978,7 @@ export type MutationResolvers<ContextType = ResolverContext, ParentType extends
|
|||
signup?: Resolver<ResolversTypes['SignupResult'], ParentType, ContextType, RequireFields<MutationSignupArgs, 'input'>>;
|
||||
updateHighlight?: Resolver<ResolversTypes['UpdateHighlightResult'], ParentType, ContextType, RequireFields<MutationUpdateHighlightArgs, 'input'>>;
|
||||
updateHighlightReply?: Resolver<ResolversTypes['UpdateHighlightReplyResult'], ParentType, ContextType, RequireFields<MutationUpdateHighlightReplyArgs, 'input'>>;
|
||||
updateLabel?: Resolver<ResolversTypes['UpdateLabelResult'], ParentType, ContextType, RequireFields<MutationUpdateLabelArgs, 'input'>>;
|
||||
updateLinkShareInfo?: Resolver<ResolversTypes['UpdateLinkShareInfoResult'], ParentType, ContextType, RequireFields<MutationUpdateLinkShareInfoArgs, 'input'>>;
|
||||
updateReminder?: Resolver<ResolversTypes['UpdateReminderResult'], ParentType, ContextType, RequireFields<MutationUpdateReminderArgs, 'input'>>;
|
||||
updateSharedComment?: Resolver<ResolversTypes['UpdateSharedCommentResult'], ParentType, ContextType, RequireFields<MutationUpdateSharedCommentArgs, 'input'>>;
|
||||
|
|
@ -3257,6 +3299,20 @@ export type UpdateHighlightSuccessResolvers<ContextType = ResolverContext, Paren
|
|||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
};
|
||||
|
||||
export type UpdateLabelErrorResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['UpdateLabelError'] = ResolversParentTypes['UpdateLabelError']> = {
|
||||
errorCodes?: Resolver<Array<ResolversTypes['UpdateLabelErrorCode']>, ParentType, ContextType>;
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
};
|
||||
|
||||
export type UpdateLabelResultResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['UpdateLabelResult'] = ResolversParentTypes['UpdateLabelResult']> = {
|
||||
__resolveType: TypeResolveFn<'UpdateLabelError' | 'UpdateLabelSuccess', ParentType, ContextType>;
|
||||
};
|
||||
|
||||
export type UpdateLabelSuccessResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['UpdateLabelSuccess'] = ResolversParentTypes['UpdateLabelSuccess']> = {
|
||||
label?: Resolver<ResolversTypes['Label'], ParentType, ContextType>;
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
};
|
||||
|
||||
export type UpdateLinkShareInfoErrorResolvers<ContextType = ResolverContext, ParentType extends ResolversParentTypes['UpdateLinkShareInfoError'] = ResolversParentTypes['UpdateLinkShareInfoError']> = {
|
||||
errorCodes?: Resolver<Array<ResolversTypes['UpdateLinkShareInfoErrorCode']>, ParentType, ContextType>;
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
|
|
@ -3551,6 +3607,9 @@ export type Resolvers<ContextType = ResolverContext> = {
|
|||
UpdateHighlightReplySuccess?: UpdateHighlightReplySuccessResolvers<ContextType>;
|
||||
UpdateHighlightResult?: UpdateHighlightResultResolvers<ContextType>;
|
||||
UpdateHighlightSuccess?: UpdateHighlightSuccessResolvers<ContextType>;
|
||||
UpdateLabelError?: UpdateLabelErrorResolvers<ContextType>;
|
||||
UpdateLabelResult?: UpdateLabelResultResolvers<ContextType>;
|
||||
UpdateLabelSuccess?: UpdateLabelSuccessResolvers<ContextType>;
|
||||
UpdateLinkShareInfoError?: UpdateLinkShareInfoErrorResolvers<ContextType>;
|
||||
UpdateLinkShareInfoResult?: UpdateLinkShareInfoResultResolvers<ContextType>;
|
||||
UpdateLinkShareInfoSuccess?: UpdateLinkShareInfoSuccessResolvers<ContextType>;
|
||||
|
|
|
|||
|
|
@ -705,6 +705,7 @@ type Mutation {
|
|||
signup(input: SignupInput!): SignupResult!
|
||||
updateHighlight(input: UpdateHighlightInput!): UpdateHighlightResult!
|
||||
updateHighlightReply(input: UpdateHighlightReplyInput!): UpdateHighlightReplyResult!
|
||||
updateLabel(input: UpdateLabelInput!): UpdateLabelResult!
|
||||
updateLinkShareInfo(input: UpdateLinkShareInfoInput!): UpdateLinkShareInfoResult!
|
||||
updateReminder(input: UpdateReminderInput!): UpdateReminderResult!
|
||||
updateSharedComment(input: UpdateSharedCommentInput!): UpdateSharedCommentResult!
|
||||
|
|
@ -1194,6 +1195,30 @@ type UpdateHighlightSuccess {
|
|||
highlight: Highlight!
|
||||
}
|
||||
|
||||
type UpdateLabelError {
|
||||
errorCodes: [UpdateLabelErrorCode!]!
|
||||
}
|
||||
|
||||
enum UpdateLabelErrorCode {
|
||||
BAD_REQUEST
|
||||
FORBIDDEN
|
||||
NOT_FOUND
|
||||
UNAUTHORIZED
|
||||
}
|
||||
|
||||
input UpdateLabelInput {
|
||||
color: String!
|
||||
description: String
|
||||
labelId: ID!
|
||||
name: String!
|
||||
}
|
||||
|
||||
union UpdateLabelResult = UpdateLabelError | UpdateLabelSuccess
|
||||
|
||||
type UpdateLabelSuccess {
|
||||
label: Label!
|
||||
}
|
||||
|
||||
type UpdateLinkShareInfoError {
|
||||
errorCodes: [UpdateLinkShareInfoErrorCode!]!
|
||||
}
|
||||
|
|
|
|||
|
|
@ -71,6 +71,7 @@ import {
|
|||
updateUserResolver,
|
||||
uploadFileRequestResolver,
|
||||
validateUsernameResolver,
|
||||
updateLabelResolver,
|
||||
} from './index'
|
||||
import { getShareInfoForArticle } from '../datalayer/links/share_info'
|
||||
import {
|
||||
|
|
@ -132,6 +133,7 @@ export const functionResolvers = {
|
|||
deleteReminder: deleteReminderResolver,
|
||||
setDeviceToken: setDeviceTokenResolver,
|
||||
createLabel: createLabelResolver,
|
||||
updateLabel: updateLabelResolver,
|
||||
deleteLabel: deleteLabelResolver,
|
||||
login: loginResolver,
|
||||
signup: signupResolver,
|
||||
|
|
|
|||
|
|
@ -12,9 +12,13 @@ import {
|
|||
MutationCreateLabelArgs,
|
||||
MutationDeleteLabelArgs,
|
||||
MutationSetLabelsArgs,
|
||||
MutationUpdateLabelArgs,
|
||||
SetLabelsError,
|
||||
SetLabelsErrorCode,
|
||||
SetLabelsSuccess,
|
||||
UpdateLabelError,
|
||||
UpdateLabelErrorCode,
|
||||
UpdateLabelSuccess,
|
||||
} from '../../generated/graphql'
|
||||
import { analytics } from '../../utils/analytics'
|
||||
import { env } from '../../env'
|
||||
|
|
@ -42,6 +46,11 @@ export const labelsResolver = authorized<LabelsSuccess, LabelsError>(
|
|||
const user = await getRepository(User).findOne({
|
||||
where: { id: uid },
|
||||
relations: ['labels'],
|
||||
order: {
|
||||
labels: {
|
||||
createdAt: 'DESC',
|
||||
},
|
||||
},
|
||||
})
|
||||
if (!user) {
|
||||
return {
|
||||
|
|
@ -249,3 +258,67 @@ export const setLabelsResolver = authorized<
|
|||
}
|
||||
}
|
||||
})
|
||||
|
||||
export const updateLabelResolver = authorized<
|
||||
UpdateLabelSuccess,
|
||||
UpdateLabelError,
|
||||
MutationUpdateLabelArgs
|
||||
>(async (_, { input }, { claims: { uid }, log }) => {
|
||||
log.info('updateLabelResolver')
|
||||
|
||||
try {
|
||||
const { name, color, description, labelId } = input
|
||||
const user = await getRepository(User).findOneBy({ id: uid })
|
||||
if (!user) {
|
||||
return {
|
||||
errorCodes: [UpdateLabelErrorCode.Unauthorized],
|
||||
}
|
||||
}
|
||||
|
||||
const label = await getRepository(Label).findOne({
|
||||
where: { id: labelId },
|
||||
relations: ['user'],
|
||||
})
|
||||
if (!label) {
|
||||
return {
|
||||
errorCodes: [UpdateLabelErrorCode.NotFound],
|
||||
}
|
||||
}
|
||||
|
||||
const result = await AppDataSource.transaction(async (t) => {
|
||||
await setClaims(t, uid)
|
||||
return await t.getRepository(Label).update(
|
||||
{ id: labelId },
|
||||
{
|
||||
name: name,
|
||||
description: description || undefined,
|
||||
color: color,
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
log.info('Updating a label', {
|
||||
result,
|
||||
labels: {
|
||||
source: 'resolver',
|
||||
resolver: 'updateLabelResolver',
|
||||
},
|
||||
})
|
||||
|
||||
if (!result) {
|
||||
log.info('failed to update')
|
||||
return {
|
||||
errorCodes: [UpdateLabelErrorCode.BadRequest],
|
||||
}
|
||||
}
|
||||
|
||||
log.info('updated successfully')
|
||||
|
||||
return { label: label }
|
||||
} catch (error) {
|
||||
log.error('error updating label', error)
|
||||
return {
|
||||
errorCodes: [UpdateLabelErrorCode.BadRequest],
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1310,6 +1310,30 @@ const schema = gql`
|
|||
|
||||
union DeleteLabelResult = DeleteLabelSuccess | DeleteLabelError
|
||||
|
||||
input UpdateLabelInput {
|
||||
labelId: ID!
|
||||
color: String!
|
||||
description: String
|
||||
name: String!
|
||||
}
|
||||
|
||||
type UpdateLabelSuccess {
|
||||
label: Label!
|
||||
}
|
||||
|
||||
enum UpdateLabelErrorCode {
|
||||
UNAUTHORIZED
|
||||
BAD_REQUEST
|
||||
NOT_FOUND
|
||||
FORBIDDEN
|
||||
}
|
||||
|
||||
type UpdateLabelError {
|
||||
errorCodes: [UpdateLabelErrorCode!]!
|
||||
}
|
||||
|
||||
union UpdateLabelResult = UpdateLabelSuccess | UpdateLabelError
|
||||
|
||||
input LoginInput {
|
||||
password: String!
|
||||
email: String!
|
||||
|
|
@ -1410,6 +1434,7 @@ const schema = gql`
|
|||
deleteReminder(id: ID!): DeleteReminderResult!
|
||||
setDeviceToken(input: SetDeviceTokenInput!): SetDeviceTokenResult!
|
||||
createLabel(input: CreateLabelInput!): CreateLabelResult!
|
||||
updateLabel(input: UpdateLabelInput!): UpdateLabelResult!
|
||||
deleteLabel(id: ID!): DeleteLabelResult!
|
||||
login(input: LoginInput!): LoginResult!
|
||||
signup(input: SignupInput!): SignupResult!
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ describe('Labels API', () => {
|
|||
user: { id: user.id },
|
||||
})
|
||||
expect(res.body.data.labels.labels).to.eql(
|
||||
labels.map((label) => ({
|
||||
labels.reverse().map((label) => ({
|
||||
id: label.id,
|
||||
name: label.name,
|
||||
color: label.color,
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import {
|
|||
Arrow,
|
||||
Label,
|
||||
} from '@radix-ui/react-dropdown-menu'
|
||||
import { CSS } from '@stitches/react';
|
||||
import { styled } from './../tokens/stitches.config'
|
||||
|
||||
const itemStyles = {
|
||||
|
|
@ -43,7 +44,7 @@ const StyledTriggerItem = styled(TriggerItem, {
|
|||
...itemStyles,
|
||||
})
|
||||
|
||||
const DropdownContent = styled(Content, {
|
||||
export const DropdownContent = styled(Content, {
|
||||
minWidth: 130,
|
||||
backgroundColor: '$grayBg',
|
||||
borderRadius: '0.5em',
|
||||
|
|
@ -70,12 +71,16 @@ type DropdownProps = {
|
|||
showArrow?: boolean
|
||||
triggerElement: React.ReactNode
|
||||
children: React.ReactNode
|
||||
styledArrow?: boolean
|
||||
align?: DropdownAlignment
|
||||
disabled?: boolean
|
||||
css?: CSS
|
||||
}
|
||||
|
||||
export const DropdownSeparator = styled(Separator, {
|
||||
height: 0,
|
||||
height: '1px',
|
||||
margin: 0,
|
||||
backgroundColor: '$grayBorder',
|
||||
})
|
||||
|
||||
type DropdownOptionProps = {
|
||||
|
|
@ -91,6 +96,7 @@ export function DropdownOption(props: DropdownOptionProps): JSX.Element {
|
|||
<StyledItem onSelect={props.onSelect}>
|
||||
{props.title ?? props.children}
|
||||
</StyledItem>
|
||||
{props.hideSeparator ? null : <DropdownSeparator />}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
@ -101,12 +107,15 @@ export function Dropdown({
|
|||
triggerElement,
|
||||
labelText,
|
||||
showArrow = true,
|
||||
disabled = false,
|
||||
css
|
||||
}: DropdownProps): JSX.Element {
|
||||
return (
|
||||
<Root modal={false}>
|
||||
<DropdownTrigger>{triggerElement}</DropdownTrigger>
|
||||
<DropdownTrigger disabled={disabled}>{triggerElement}</DropdownTrigger>
|
||||
<DropdownContent
|
||||
onInteractOutside={() => {
|
||||
css={css}
|
||||
onInteractOutside={(event) => {
|
||||
// remove focus from dropdown
|
||||
;(document.activeElement as HTMLElement).blur()
|
||||
}}
|
||||
|
|
|
|||
33
packages/web/components/elements/InfoLink.tsx
Normal file
33
packages/web/components/elements/InfoLink.tsx
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import Link from 'next/link'
|
||||
import { Info } from 'phosphor-react'
|
||||
import { Box } from '../elements/LayoutPrimitives'
|
||||
import { theme } from '../tokens/stitches.config'
|
||||
import { TooltipWrapped } from './Tooltip'
|
||||
|
||||
type InfoLinkProps = {
|
||||
href: string
|
||||
}
|
||||
|
||||
const TooltipStyle = {
|
||||
backgroundColor: '#F9D354',
|
||||
color: '#0A0806',
|
||||
}
|
||||
|
||||
export function InfoLink(props: InfoLinkProps): JSX.Element {
|
||||
return (
|
||||
<Box style={{ flex: '1', marginLeft: '9px' }}>
|
||||
<Link passHref href={props.href}>
|
||||
<a style={{ textDecoration: 'none' }}>
|
||||
<TooltipWrapped
|
||||
tooltipContent="Learn More"
|
||||
tooltipSide={'top'}
|
||||
style={TooltipStyle}
|
||||
arrowStyles={{ fill: '#F9D354' }}
|
||||
>
|
||||
<Info size={24} color={theme.colors.grayText.toString()} />
|
||||
</TooltipWrapped>
|
||||
</a>
|
||||
</Link>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
|
@ -21,9 +21,11 @@ export function LabelChip(props: LabelChipProps): JSX.Element {
|
|||
margin: '4px',
|
||||
borderRadius: '32px',
|
||||
color: props.color,
|
||||
fontSize: '12px',
|
||||
fontWeight: 'bold',
|
||||
padding: '4px 8px 4px 8px',
|
||||
border: `1px solid rgba(${color[0]}, ${color[1]}, ${color[2]}, 0.40)`,
|
||||
backgroundColor: `rgba(${color[0]}, ${color[1]}, ${color[2]}, 0.20)`,
|
||||
border: `1px solid rgba(${color[0]}, ${color[1]}, ${color[2]}, 0.7)`,
|
||||
backgroundColor: `rgba(${color[0]}, ${color[1]}, ${color[2]}, 0.08)`,
|
||||
}}
|
||||
>
|
||||
{props.text}
|
||||
|
|
|
|||
327
packages/web/components/elements/LabelColorDropdown.tsx
Normal file
327
packages/web/components/elements/LabelColorDropdown.tsx
Normal file
|
|
@ -0,0 +1,327 @@
|
|||
import React, { useState } from 'react'
|
||||
import { styled } from '../tokens/stitches.config'
|
||||
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'
|
||||
import { HexColorPicker } from 'react-colorful'
|
||||
import { Button } from './Button'
|
||||
import { HStack, SpanBox } from './LayoutPrimitives'
|
||||
import { CaretDown } from 'phosphor-react'
|
||||
import { StyledText } from './StyledText'
|
||||
import {
|
||||
ColorDetailsProps,
|
||||
LabelColor,
|
||||
LabelColorDropdownProps,
|
||||
LabelColorHex,
|
||||
LabelColorObject,
|
||||
LabelOptionProps,
|
||||
} from '../../utils/settings-page/labels/types'
|
||||
import { labelColorObjects } from '../../utils/settings-page/labels/labelColorObjects'
|
||||
import { DropdownOption } from './DropdownElements'
|
||||
import { isDarkTheme } from '../../lib/themeUpdater'
|
||||
|
||||
const DropdownMenuContent = styled(DropdownMenuPrimitive.Content, {
|
||||
maxWidth: 190,
|
||||
borderRadius: 6,
|
||||
backgroundColor: '$grayBg',
|
||||
padding: 5,
|
||||
boxShadow:
|
||||
'0px 10px 38px -10px rgba(22, 23, 24, 0.35), 0px 10px 20px -15px rgba(22, 23, 24, 0.2)',
|
||||
})
|
||||
|
||||
const itemStyles = {
|
||||
all: 'unset',
|
||||
fontSize: '$3',
|
||||
lineHeight: 1,
|
||||
borderRadius: 3,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
height: 25,
|
||||
position: 'relative',
|
||||
userSelect: 'none',
|
||||
}
|
||||
|
||||
const DropdownMenuTriggerItem = styled(DropdownMenuPrimitive.TriggerItem, {
|
||||
'&[data-state="open"]': {
|
||||
outline: 'none',
|
||||
backgroundColor: '$grayBgHover',
|
||||
},
|
||||
...itemStyles,
|
||||
padding: '$2 0',
|
||||
'&:focus': {
|
||||
outline: 'none',
|
||||
backgroundColor: '$grayBgHover',
|
||||
},
|
||||
})
|
||||
|
||||
const DropdownMenu = DropdownMenuPrimitive.Root
|
||||
|
||||
const DropdownMenuTrigger = styled(DropdownMenuPrimitive.Trigger, {
|
||||
backgroundColor: 'transparent',
|
||||
border: 0,
|
||||
padding: 0,
|
||||
marginRight: '$2',
|
||||
})
|
||||
const Box = styled('div', {})
|
||||
|
||||
const MainContainer = styled(Box, {
|
||||
fontFamily: 'inter',
|
||||
fontSize: '$2',
|
||||
lineHeight: '1.25',
|
||||
color: '$grayText',
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
backgroundColor: '$grayBg',
|
||||
border: '1px solid $grayBorder',
|
||||
width: '180px',
|
||||
borderRadius: '$3',
|
||||
px: '$3',
|
||||
py: '0px',
|
||||
cursor: 'pointer',
|
||||
'&:hover': {
|
||||
border: '1px solid $grayBorderHover',
|
||||
},
|
||||
'@mdDown': {
|
||||
width: '100%'
|
||||
}
|
||||
})
|
||||
|
||||
const CustomLabelWrapper = styled(Box, {
|
||||
fontSize: 13,
|
||||
padding: '$2',
|
||||
borderRadius: 3,
|
||||
cursor: 'default',
|
||||
color: '$grayText',
|
||||
|
||||
'&:focus': {
|
||||
outline: 'none',
|
||||
backgroundColor: '$grayBgHover',
|
||||
},
|
||||
})
|
||||
|
||||
export const LabelColorDropdown = (props: LabelColorDropdownProps) => {
|
||||
const {
|
||||
isCreateMode,
|
||||
canEdit,
|
||||
labelColorHexRowId,
|
||||
labelId,
|
||||
labelColor,
|
||||
labelColorHexValue,
|
||||
setLabelColorHex,
|
||||
} = props
|
||||
|
||||
const isDarkMode = isDarkTheme()
|
||||
const iconColor = isDarkMode ? '#FFFFFF': '#0A0806'
|
||||
const [open, setOpen] = useState<boolean | undefined>(false);
|
||||
|
||||
const handleCustomColorChange = (color: string) => {
|
||||
setLabelColorHex({
|
||||
rowId: labelId,
|
||||
value: color.toUpperCase() as LabelColor,
|
||||
})
|
||||
}
|
||||
|
||||
const handleOpen = (open: boolean) => {
|
||||
if (canEdit && open) setOpen(true)
|
||||
else if((isCreateMode && !canEdit) && open) setOpen(true)
|
||||
else setOpen(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<DropdownMenu onOpenChange={handleOpen} open={open}>
|
||||
<DropdownMenuTrigger
|
||||
css={{
|
||||
minWidth: '64px',
|
||||
width: '100%',
|
||||
'@md': {
|
||||
minWidth: '170px',
|
||||
width: 'auto'
|
||||
},
|
||||
}}
|
||||
>
|
||||
<MainContainer>
|
||||
<SpanBox css={{ paddingRight: '$3' }}>
|
||||
{labelId !== '' && labelId === labelColorHexRowId ? (
|
||||
<LabelOption
|
||||
isCreateMode={isCreateMode}
|
||||
labelId={labelId}
|
||||
color={labelColorHexValue}
|
||||
isDropdownOption={false}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
{labelId ? (
|
||||
<LabelOption
|
||||
isCreateMode={isCreateMode}
|
||||
labelId={labelId}
|
||||
color={labelColor}
|
||||
isDropdownOption={false}
|
||||
/>
|
||||
) : (
|
||||
<LabelOption
|
||||
isCreateMode={isCreateMode}
|
||||
labelId={''}
|
||||
color={labelColorHexValue}
|
||||
isDropdownOption={false}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</SpanBox>
|
||||
|
||||
<CaretDown size={16} color={iconColor} weight="bold" />
|
||||
</MainContainer>
|
||||
</DropdownMenuTrigger>
|
||||
|
||||
<DropdownMenuContent sideOffset={5}>
|
||||
{Object.keys(labelColorObjects)
|
||||
.filter((labelColor) => labelColor !== 'custom color')
|
||||
.map((labelColor) => (
|
||||
<DropdownOption
|
||||
key={labelColor}
|
||||
onSelect={() =>
|
||||
setLabelColorHex({
|
||||
rowId: labelId,
|
||||
value: labelColor as LabelColor,
|
||||
})
|
||||
}
|
||||
>
|
||||
<LabelOption
|
||||
isCreateMode={isCreateMode}
|
||||
labelId={labelId}
|
||||
color={labelColor}
|
||||
isDropdownOption
|
||||
/>
|
||||
</DropdownOption>
|
||||
))}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTriggerItem>
|
||||
<CustomLabelWrapper onSelect={() => null}>
|
||||
<LabelOption
|
||||
isCreateMode={isCreateMode}
|
||||
labelId={labelId}
|
||||
color="custom color"
|
||||
isDropdownOption
|
||||
/>
|
||||
</CustomLabelWrapper>
|
||||
</DropdownMenuTriggerItem>
|
||||
<DropdownMenuContent
|
||||
sideOffset={-25}
|
||||
alignOffset={-5}
|
||||
css={{ minWidth: 'unset' }}
|
||||
>
|
||||
<HexColorPicker
|
||||
color={labelColor}
|
||||
onChange={handleCustomColorChange}
|
||||
/>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)
|
||||
}
|
||||
|
||||
function LabelOption(props: LabelOptionProps): JSX.Element {
|
||||
const { color, isDropdownOption, isCreateMode, labelId } = props
|
||||
// const colorDetails = getColorDetails(
|
||||
// color as LabelColor,
|
||||
// labelId,
|
||||
// Boolean(isCreateMode)
|
||||
// )
|
||||
const isCreating = isCreateMode && !labelId
|
||||
const { text, border, colorName, background } = getLabelColorObject(
|
||||
color as LabelColor
|
||||
)
|
||||
|
||||
let colorNameText = colorName
|
||||
if (!labelId && isCreateMode) {
|
||||
colorNameText = 'Select Color'
|
||||
colorNameText = isDropdownOption ? colorName : colorNameText
|
||||
}
|
||||
|
||||
colorNameText = color === 'custom color' ? colorNameText : colorName
|
||||
|
||||
let colorHex = !labelId && isCreateMode && !isDropdownOption ? '' : text
|
||||
|
||||
colorHex =
|
||||
!labelId && isCreateMode && !isDropdownOption && color !== 'custom color'
|
||||
? text
|
||||
: colorHex
|
||||
|
||||
return (
|
||||
<HStack
|
||||
alignment="center"
|
||||
distribution="start"
|
||||
css={{ width: '100%', padding: isDropdownOption ? '' : '$2 0' }}
|
||||
>
|
||||
<Box
|
||||
css={{
|
||||
m: '$1',
|
||||
textTransform: 'capitalize',
|
||||
fontSize: '$3',
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
<LabelColorIcon fillColor={text} strokeColor={border} />
|
||||
</Box>
|
||||
<StyledText
|
||||
css={{
|
||||
m: '$1',
|
||||
color: '$grayText',
|
||||
fontSize: '$3',
|
||||
whiteSpace: 'nowrap',
|
||||
textTransform: 'capitalize',
|
||||
'@md': {
|
||||
display: 'unset',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{colorNameText}
|
||||
</StyledText>
|
||||
<StyledText
|
||||
css={{
|
||||
m: '$1',
|
||||
color: '$grayText',
|
||||
fontSize: '$3',
|
||||
whiteSpace: 'nowrap',
|
||||
'@md': {
|
||||
display: 'unset',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{colorNameText === 'custom color' ? '' : colorHex}
|
||||
</StyledText>
|
||||
{isDropdownOption ? <Box css={{ pr: '$2' }} /> : null}
|
||||
</HStack>
|
||||
)
|
||||
}
|
||||
|
||||
function getLabelColorObject(color: LabelColor) {
|
||||
if (labelColorObjects[color]) {
|
||||
return labelColorObjects[color]
|
||||
}
|
||||
const colorObject: LabelColorObject = {
|
||||
colorName: 'Custom',
|
||||
text: color,
|
||||
border: color + '66',
|
||||
background: color + '0D',
|
||||
}
|
||||
return colorObject
|
||||
}
|
||||
|
||||
function LabelColorIcon(props: {
|
||||
fillColor: string
|
||||
strokeColor: string
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<Box
|
||||
css={{
|
||||
width: '14px',
|
||||
height: '14px',
|
||||
borderRadius: '50%',
|
||||
border: '2px solid',
|
||||
borderColor: props.strokeColor,
|
||||
backgroundColor: props.fillColor,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
@ -29,7 +29,7 @@ const textVariants = {
|
|||
},
|
||||
},
|
||||
fixedHeadline: {
|
||||
fontSize: '$6',
|
||||
fontSize: '24px',
|
||||
fontWeight: '500',
|
||||
},
|
||||
subHeadline: {
|
||||
|
|
|
|||
|
|
@ -67,6 +67,7 @@ export const TooltipWrapped: FC<TooltipWrappedProps> = ({
|
|||
active,
|
||||
tooltipContent,
|
||||
tooltipSide,
|
||||
arrowStyles,
|
||||
...props
|
||||
}) => {
|
||||
return (
|
||||
|
|
@ -74,7 +75,7 @@ export const TooltipWrapped: FC<TooltipWrappedProps> = ({
|
|||
<TooltipTrigger asChild>{children}</TooltipTrigger>
|
||||
<TooltipContent sideOffset={5} side={tooltipSide} {...props}>
|
||||
{tooltipContent}
|
||||
<TooltipArrow style={props.arrowStyles} />
|
||||
<TooltipArrow style={arrowStyles} />
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -8,11 +8,10 @@ import { Button } from '../../elements/Button'
|
|||
import { StyledText } from '../../elements/StyledText'
|
||||
import { CrossIcon } from '../../elements/images/CrossIcon'
|
||||
import { theme } from '../../tokens/stitches.config'
|
||||
import { useGetLabelsQuery } from '../../../lib/networking/queries/useGetLabelsQuery'
|
||||
import { Label, useGetLabelsQuery } from '../../../lib/networking/queries/useGetLabelsQuery'
|
||||
import { ChangeEvent, useCallback, useState } from 'react'
|
||||
import { setLabelsMutation } from '../../../lib/networking/mutations/setLabelsMutation'
|
||||
import { ArticleAttributes } from '../../../lib/networking/queries/useGetArticleQuery'
|
||||
import { Label } from '../../../lib/networking/fragments/labelFragment'
|
||||
import { LabelChip } from '../../elements/LabelChip'
|
||||
|
||||
type EditLabelsModalProps = {
|
||||
|
|
|
|||
|
|
@ -146,6 +146,8 @@ export const { styled, css, theme, getCssText, globalCss, keyframes, config } =
|
|||
// Avatar Fallback color
|
||||
avatarBg: '#FFFFFF',
|
||||
avatarFont: '#0A0806',
|
||||
|
||||
labelButtonsBg: '#F5F5F4',
|
||||
tooltipIcons: '#FDFAEC'
|
||||
},
|
||||
},
|
||||
|
|
@ -191,6 +193,8 @@ const darkThemeSpec = {
|
|||
tooltipIcons: '#5F5E58',
|
||||
avatarBg: '#000000',
|
||||
avatarFont: 'rgba(255, 255, 255, 0.8)',
|
||||
|
||||
labelButtonsBg: '#5F5E58',
|
||||
},
|
||||
shadows: {
|
||||
cardBoxShadow: '0px 0px 9px -2px rgba(32, 31, 29, 0.09), 0px 7px 12px rgba(32, 31, 29, 0.07)'
|
||||
|
|
|
|||
|
|
@ -1,6 +1,16 @@
|
|||
import { gql } from 'graphql-request'
|
||||
import { Label } from '../fragments/labelFragment'
|
||||
import { gqlFetcher } from '../networkHelpers'
|
||||
|
||||
type DeleteLabelResult = {
|
||||
deleteLabel: DeleteLabel
|
||||
errorCodes?: unknown[]
|
||||
}
|
||||
|
||||
type DeleteLabel = {
|
||||
label: Label
|
||||
}
|
||||
|
||||
export async function deleteLabelMutation(
|
||||
labelId: string
|
||||
): Promise<any | undefined> {
|
||||
|
|
@ -24,9 +34,8 @@ export async function deleteLabelMutation(
|
|||
`
|
||||
|
||||
try {
|
||||
const data = await gqlFetcher(mutation)
|
||||
console.log('deleted label', data)
|
||||
return data
|
||||
const data = await gqlFetcher(mutation) as DeleteLabelResult
|
||||
return data.errorCodes ? undefined : data.deleteLabel.label.id
|
||||
} catch (error) {
|
||||
console.log('deleteLabelMutation error', error)
|
||||
return undefined
|
||||
|
|
|
|||
49
packages/web/lib/networking/mutations/updateLabelMutation.ts
Normal file
49
packages/web/lib/networking/mutations/updateLabelMutation.ts
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
import { gql } from 'graphql-request'
|
||||
import { gqlFetcher } from '../networkHelpers'
|
||||
|
||||
export type UpdateLabelInput = {
|
||||
labelId: string
|
||||
name: string,
|
||||
color: string,
|
||||
description?: string
|
||||
}
|
||||
|
||||
export async function updateLabelMutation(
|
||||
input: UpdateLabelInput
|
||||
): Promise<string | undefined> {
|
||||
const mutation = gql`
|
||||
mutation {
|
||||
updateLabel(
|
||||
input: {
|
||||
color: "${input.color}"
|
||||
name: "${input.name}"
|
||||
description: "${input.description}"
|
||||
labelId: "${input.labelId}"
|
||||
}
|
||||
) {
|
||||
... on UpdateLabelSuccess {
|
||||
label {
|
||||
id
|
||||
name
|
||||
color
|
||||
description
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
... on UpdateLabelError {
|
||||
errorCodes
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
try {
|
||||
const data = await gqlFetcher(mutation)
|
||||
console.log(input, data);
|
||||
const output = data as any
|
||||
console.log(output)
|
||||
return output?.updatedLabel
|
||||
} catch (err) {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
import { gql } from 'graphql-request'
|
||||
import useSWR from 'swr'
|
||||
import { Label, labelFragment } from '../fragments/labelFragment'
|
||||
import { LabelColor } from '../../../utils/settings-page/labels/types';
|
||||
import { labelFragment } from '../fragments/labelFragment'
|
||||
import { publicGqlFetcher } from '../networkHelpers'
|
||||
|
||||
type LabelsQueryResponse = {
|
||||
|
|
@ -17,6 +18,14 @@ type LabelsData = {
|
|||
labels?: unknown
|
||||
}
|
||||
|
||||
export type Label = {
|
||||
id: string
|
||||
name: string
|
||||
color: LabelColor
|
||||
description?: string
|
||||
createdAt: Date
|
||||
}
|
||||
|
||||
export function useGetLabelsQuery(): LabelsQueryResponse {
|
||||
const query = gql`
|
||||
query GetLabels {
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@
|
|||
"pspdfkit": "^2021.6.0",
|
||||
"react": "^17.0.2",
|
||||
"react-apple-login": "^1.1.3",
|
||||
"react-colorful": "^5.5.1",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-hot-toast": "^2.1.1",
|
||||
"react-intl": "^5.20.12",
|
||||
|
|
|
|||
|
|
@ -124,7 +124,7 @@ export default function Home(): JSX.Element {
|
|||
articleReadingProgressMutation,
|
||||
}}
|
||||
/>
|
||||
{showLabelsModal && (
|
||||
{/* {showLabelsModal && (
|
||||
<EditLabelsModal
|
||||
labels={article.labels || []}
|
||||
article={article}
|
||||
|
|
@ -135,7 +135,7 @@ export default function Home(): JSX.Element {
|
|||
// setLabels(labels)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
)} */}
|
||||
</VStack>
|
||||
)}
|
||||
</PrimaryLayout>
|
||||
|
|
|
|||
|
|
@ -18,12 +18,13 @@ import {
|
|||
VStack,
|
||||
} from '../../components/elements/LayoutPrimitives'
|
||||
import { useCopyLink } from '../../lib/hooks/useCopyLink'
|
||||
import { toast, Toaster } from 'react-hot-toast'
|
||||
import { Toaster } from 'react-hot-toast'
|
||||
import { useCallback } from 'react'
|
||||
import { StyledText } from '../../components/elements/StyledText'
|
||||
import { applyStoredTheme } from '../../lib/themeUpdater'
|
||||
import { showErrorToast, showSuccessToast } from '../../lib/toastHelpers'
|
||||
import Link from 'next/link'
|
||||
import { InfoLink } from '../../components/elements/InfoLink'
|
||||
|
||||
enum TextType {
|
||||
EmailAddress,
|
||||
|
|
@ -36,7 +37,7 @@ type CopyTextButtonProps = {
|
|||
}
|
||||
|
||||
const HeaderWrapper = styled(Box, {
|
||||
width: '863px',
|
||||
width: '100%',
|
||||
'@md': {
|
||||
display: 'block',
|
||||
},
|
||||
|
|
@ -55,7 +56,6 @@ const TableCard = styled(Box, {
|
|||
},
|
||||
'@md': {
|
||||
paddingLeft: '0',
|
||||
width: '863px',
|
||||
},
|
||||
})
|
||||
|
||||
|
|
@ -66,7 +66,7 @@ const TableHeading = styled(Box, {
|
|||
alignItems: 'center',
|
||||
padding: '14px 0 14px 40px',
|
||||
borderRadius: '5px 5px 0px 0px',
|
||||
width: '863px',
|
||||
width: '100%',
|
||||
'@md': {
|
||||
display: 'flex',
|
||||
}
|
||||
|
|
@ -89,16 +89,6 @@ const CopyTextBtnWrapper = styled(Box, {
|
|||
border: '1px solid rgba(0, 0, 0, 0.06)',
|
||||
})
|
||||
|
||||
const MobileBtnWrapper = styled(Box, {
|
||||
display: 'flex',
|
||||
position: 'fixed',
|
||||
bottom: '16px',
|
||||
right: '25px',
|
||||
'@md': {
|
||||
display: 'none',
|
||||
},
|
||||
})
|
||||
|
||||
const InfoIcon = styled(Info, {
|
||||
marginTop: '8px',
|
||||
'&:hover': {
|
||||
|
|
@ -218,11 +208,9 @@ export default function EmailsPage(): JSX.Element {
|
|||
color: '$grayText',
|
||||
paddingBottom: '5rem',
|
||||
paddingTop: '2rem',
|
||||
|
||||
'@md': {
|
||||
m: '16px',
|
||||
alignSelf: 'center',
|
||||
maxWidth: '72%',
|
||||
mx: '42px',
|
||||
paddingTop: '0',
|
||||
},
|
||||
|
|
@ -233,49 +221,41 @@ export default function EmailsPage(): JSX.Element {
|
|||
<Box>
|
||||
<StyledText
|
||||
style="fixedHeadline"
|
||||
css={{
|
||||
fontSize: '24px',
|
||||
fontWeight: '700',
|
||||
}}
|
||||
>
|
||||
Email Addresses{' '}
|
||||
</StyledText>
|
||||
</Box>
|
||||
<Box style={{ flex: '1', marginLeft: '9px' }}>
|
||||
<Link passHref href="/help/newsletters">
|
||||
<a style={{ textDecoration: 'none' }}>
|
||||
<TooltipWrapped
|
||||
tooltipContent="Learn More"
|
||||
tooltipSide={'top'}
|
||||
style={TooltipStyle}
|
||||
arrowStyles={{ fill: '#F9D354' }}
|
||||
>
|
||||
<InfoIcon size={24} css={{ color: '$grayText' }} />
|
||||
</TooltipWrapped>
|
||||
</a>
|
||||
</Link>
|
||||
</Box>
|
||||
<Box css={{
|
||||
display: 'none',
|
||||
'@md': {
|
||||
display: 'unset',
|
||||
},
|
||||
}}>
|
||||
<Button
|
||||
onClick={createEmail}
|
||||
style="ctaDarkYellow"
|
||||
css={{
|
||||
<InfoLink href="/help/newsletters" />
|
||||
<Button
|
||||
onClick={createEmail}
|
||||
style="ctaDarkYellow"
|
||||
css={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
marginLeft: 'auto',
|
||||
}}
|
||||
>
|
||||
<SpanBox css={{
|
||||
display: 'none',
|
||||
'@md': {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<Plus size={18} style={{ marginRight: '6.5px' }} />
|
||||
},
|
||||
}}>
|
||||
<SpanBox>Add Email</SpanBox>
|
||||
</Button>
|
||||
</Box>
|
||||
</SpanBox>
|
||||
<SpanBox css={{
|
||||
p: '0',
|
||||
display: 'flex',
|
||||
'@md': {
|
||||
display: 'none',
|
||||
},
|
||||
}}>
|
||||
<Plus size={24} />
|
||||
</SpanBox>
|
||||
</Button>
|
||||
</Box>
|
||||
<TableHeading>
|
||||
<Box
|
||||
<Box
|
||||
css={{
|
||||
flex: '49%',
|
||||
}}
|
||||
|
|
@ -325,8 +305,8 @@ export default function EmailsPage(): JSX.Element {
|
|||
<Box
|
||||
css={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
width: '100%',
|
||||
flexDirection: 'column',
|
||||
'@md': {
|
||||
flexDirection: 'row',
|
||||
},
|
||||
|
|
@ -367,6 +347,7 @@ export default function EmailsPage(): JSX.Element {
|
|||
</CopyTextBtnWrapper>
|
||||
<Box
|
||||
css={{
|
||||
marginLeft: 'auto',
|
||||
textAlign: 'right',
|
||||
display: 'flex',
|
||||
'@md': {
|
||||
|
|
@ -386,6 +367,7 @@ export default function EmailsPage(): JSX.Element {
|
|||
borderRadius: '6px',
|
||||
padding: '8px 4px 4px 7px',
|
||||
'@md': {
|
||||
padding: 'unset',
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
}}
|
||||
|
|
@ -400,7 +382,9 @@ export default function EmailsPage(): JSX.Element {
|
|||
<Box>
|
||||
<CopyTextBtnWrapper
|
||||
css={{
|
||||
border: 'none',
|
||||
'@mdDown': {
|
||||
border: 'none',
|
||||
}
|
||||
}}
|
||||
>
|
||||
<CopyTextButton
|
||||
|
|
@ -413,7 +397,7 @@ export default function EmailsPage(): JSX.Element {
|
|||
</HStack>
|
||||
)}
|
||||
</Box>
|
||||
<HStack distribution={'start'}>
|
||||
<HStack distribution={'start'} css={{ marginLeft: 'auto' }}>
|
||||
<Box
|
||||
css={{
|
||||
textAlign: 'right',
|
||||
|
|
@ -429,20 +413,8 @@ export default function EmailsPage(): JSX.Element {
|
|||
</TableCard>
|
||||
)
|
||||
})}
|
||||
<MobileBtnWrapper>
|
||||
<Button
|
||||
onClick={createEmail}
|
||||
style="ctaDarkYellow"
|
||||
css={{
|
||||
display: 'flex',
|
||||
border: '1px solid $grayBorder',
|
||||
borderRadius: '8px',
|
||||
}}
|
||||
>
|
||||
<Plus size={24} />
|
||||
</Button>
|
||||
</MobileBtnWrapper>
|
||||
</VStack>
|
||||
<Box css={{ height: '120px' }} />
|
||||
</PrimaryLayout>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,32 +1,200 @@
|
|||
import { useEffect, useState } from 'react'
|
||||
import { PrimaryLayout } from '../../components/templates/PrimaryLayout'
|
||||
import { Button } from '../../components/elements/Button'
|
||||
import { Box, VStack } from '../../components/elements/LayoutPrimitives'
|
||||
import { styled, theme } from '../../components/tokens/stitches.config'
|
||||
import {
|
||||
Box,
|
||||
SpanBox,
|
||||
HStack,
|
||||
VStack,
|
||||
} from '../../components/elements/LayoutPrimitives'
|
||||
import { Toaster } from 'react-hot-toast'
|
||||
import { useGetLabelsQuery } from '../../lib/networking/queries/useGetLabelsQuery'
|
||||
import { createLabelMutation } from '../../lib/networking/mutations/createLabelMutation'
|
||||
import { updateLabelMutation } from '../../lib/networking/mutations/updateLabelMutation'
|
||||
import { deleteLabelMutation } from '../../lib/networking/mutations/deleteLabelMutation'
|
||||
import { useState } from 'react'
|
||||
import { applyStoredTheme } from '../../lib/themeUpdater'
|
||||
import { applyStoredTheme, isDarkTheme } from '../../lib/themeUpdater'
|
||||
import { showErrorToast, showSuccessToast } from '../../lib/toastHelpers'
|
||||
import { Label } from '../../lib/networking/queries/useGetLabelsQuery'
|
||||
|
||||
import { StyledText } from '../../components/elements/StyledText'
|
||||
import {
|
||||
ArrowClockwise,
|
||||
DotsThree,
|
||||
PencilSimple,
|
||||
Trash,
|
||||
Plus,
|
||||
} from 'phosphor-react'
|
||||
import {
|
||||
LabelColor,
|
||||
GenericTableCardProps,
|
||||
LabelColorHex,
|
||||
} from '../../utils/settings-page/labels/types'
|
||||
import { labelColorObjects, } from '../../utils/settings-page/labels/labelColorObjects'
|
||||
import {
|
||||
TooltipWrapped
|
||||
} from '../../components/elements/Tooltip'
|
||||
import { LabelColorDropdown } from '../../components/elements/LabelColorDropdown'
|
||||
import {
|
||||
Dropdown,
|
||||
DropdownOption,
|
||||
} from '../../components/elements/DropdownElements'
|
||||
import { LabelChip } from '../../components/elements/LabelChip'
|
||||
import { ConfirmationModal } from '../../components/patterns/ConfirmationModal'
|
||||
import { InfoLink } from '../../components/elements/InfoLink'
|
||||
|
||||
const HeaderWrapper = styled(Box, {
|
||||
width: '100%',
|
||||
})
|
||||
|
||||
const TableCard = styled(Box, {
|
||||
padding: '0px',
|
||||
backgroundColor: '$grayBg',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
border: '0.3px solid $grayBgActive',
|
||||
width: '100%',
|
||||
'@md': {
|
||||
paddingLeft: '0',
|
||||
},
|
||||
})
|
||||
|
||||
const TableCardBox = styled(Box, {
|
||||
display: 'grid',
|
||||
width: '100%',
|
||||
gridGap: '$1',
|
||||
gridTemplateColumns: '3fr 1fr',
|
||||
'.showHidden': {
|
||||
display: 'none',
|
||||
},
|
||||
'&:hover': {
|
||||
'.showHidden': {
|
||||
display: 'unset',
|
||||
gridColumn: 'span 2',
|
||||
width: '100%',
|
||||
padding: '$2 $3 0 $3',
|
||||
},
|
||||
},
|
||||
'@md': {
|
||||
gridTemplateColumns: '20% 15% 1fr 1fr 1fr',
|
||||
'&:hover': {
|
||||
'.showHidden': {
|
||||
display: 'none',
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const inputStyles = {
|
||||
backgroundColor: 'transparent',
|
||||
color: '$grayTextContrast',
|
||||
padding: '6px 6px',
|
||||
margin: '$2 0',
|
||||
border: '1px solid $grayBorder',
|
||||
borderRadius: '6px',
|
||||
fontSize: '16px',
|
||||
FontFamily: '$fontFamily',
|
||||
width: '100%',
|
||||
'@md': {
|
||||
width: 'auto',
|
||||
minWidth: '180px',
|
||||
},
|
||||
'&[disabled]': {
|
||||
border: 'none',
|
||||
},
|
||||
'&:focus': {
|
||||
outlineColor: '$omnivoreYellow',
|
||||
outlineStyle: 'solid',
|
||||
},
|
||||
}
|
||||
|
||||
const ActionsWrapper = styled(Box, {
|
||||
mr: '$1',
|
||||
display: 'flex',
|
||||
width: 40,
|
||||
height: 40,
|
||||
alignItems: 'center',
|
||||
bg: 'transparent',
|
||||
cursor: 'pointer',
|
||||
fontFamily: 'inter',
|
||||
fontSize: '$2',
|
||||
lineHeight: '1.25',
|
||||
color: '$grayText',
|
||||
'&:hover': {
|
||||
opacity: 0.8,
|
||||
},
|
||||
})
|
||||
|
||||
const IconButton = styled(Button, {
|
||||
variants: {
|
||||
style: {
|
||||
ctaWhite: {
|
||||
color: 'red',
|
||||
padding: '10px',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
border: '1px solid $grayBorder',
|
||||
boxSizing: 'border-box',
|
||||
borderRadius: 6,
|
||||
width: 40,
|
||||
height: 40,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const Input = styled('input', { ...inputStyles })
|
||||
|
||||
const TextArea = styled('textarea', { ...inputStyles })
|
||||
|
||||
export default function LabelsPage(): JSX.Element {
|
||||
const { labels, revalidate, isValidating } = useGetLabelsQuery()
|
||||
const [name, setName] = useState('')
|
||||
const [color, setColor] = useState('')
|
||||
const [description, setDescription] = useState('')
|
||||
const { labels, revalidate } = useGetLabelsQuery()
|
||||
const [labelColorHex, setLabelColorHex] = useState<LabelColorHex>({
|
||||
rowId: '',
|
||||
value: 'custom color',
|
||||
})
|
||||
const [editingLabelId, setEditingLabelId] = useState<string | null>(null)
|
||||
const [nameInputText, setNameInputText] = useState<string>('')
|
||||
const [descriptionInputText, setDescriptionInputText] = useState<string>('')
|
||||
const [isCreateMode, setIsCreateMode] = useState<boolean>(false)
|
||||
const [windowWidth, setWindowWidth] = useState<number>(0)
|
||||
const [confirmRemoveLabelId, setConfirmRemoveLabelId] = useState<string | null>(null)
|
||||
const breakpoint = 768
|
||||
|
||||
applyStoredTheme(false)
|
||||
|
||||
useEffect(() => {
|
||||
const handleResizeWindow = () => setWindowWidth(window.innerWidth)
|
||||
if (windowWidth === 0) {
|
||||
setWindowWidth(window.innerWidth)
|
||||
}
|
||||
window.addEventListener('resize', handleResizeWindow)
|
||||
return () => {
|
||||
window.removeEventListener('resize', handleResizeWindow)
|
||||
}
|
||||
}, [windowWidth])
|
||||
|
||||
const resetLabelState = () => {
|
||||
setIsCreateMode(false)
|
||||
setEditingLabelId('')
|
||||
setNameInputText('')
|
||||
setDescriptionInputText('')
|
||||
setLabelColorHex({ rowId: '', value: 'custom color' })
|
||||
}
|
||||
|
||||
async function createLabel(): Promise<void> {
|
||||
const res = await createLabelMutation(name, color, description)
|
||||
const res = await createLabelMutation(
|
||||
nameInputText,
|
||||
labelColorHex.value,
|
||||
descriptionInputText
|
||||
)
|
||||
if (res) {
|
||||
if (res.createLabel.errorCodes && res.createLabel.errorCodes.length > 0) {
|
||||
showErrorToast(res.createLabel.errorCodes[0])
|
||||
} else {
|
||||
showSuccessToast('Label created')
|
||||
setName('')
|
||||
setColor('')
|
||||
setDescription('')
|
||||
showSuccessToast('Label created', { position: 'bottom-right' })
|
||||
resetLabelState()
|
||||
revalidate()
|
||||
}
|
||||
} else {
|
||||
|
|
@ -34,78 +202,704 @@ export default function LabelsPage(): JSX.Element {
|
|||
}
|
||||
}
|
||||
|
||||
async function deleteLabel(id: string): Promise<void> {
|
||||
await deleteLabelMutation(id)
|
||||
async function updateLabel(id: string): Promise<void> {
|
||||
await updateLabelMutation({
|
||||
labelId: id,
|
||||
name: nameInputText,
|
||||
color: labelColorHex.value,
|
||||
description: descriptionInputText,
|
||||
})
|
||||
revalidate()
|
||||
}
|
||||
|
||||
const onEditPress = (label : Label | null) => {
|
||||
if (label) {
|
||||
setEditingLabelId(label.id)
|
||||
setNameInputText(label.name)
|
||||
setDescriptionInputText(label.description || '')
|
||||
setLabelColorHex({ rowId: '', value: label.color })
|
||||
}
|
||||
else {
|
||||
resetLabelState()
|
||||
}
|
||||
}
|
||||
|
||||
async function onDeleteLabel(id: string): Promise<void> {
|
||||
const result = await deleteLabelMutation(id)
|
||||
if (result) {
|
||||
showSuccessToast('Label deleted', { position: 'bottom-right' })
|
||||
} else {
|
||||
showErrorToast('Failed to delete label', { position: 'bottom-right' })
|
||||
}
|
||||
revalidate()
|
||||
}
|
||||
|
||||
async function deleteLabel(id: string): Promise<void> {
|
||||
setConfirmRemoveLabelId(id)
|
||||
}
|
||||
|
||||
const handleGenerateRandomColor = (rowId?: string) => {
|
||||
const colorHexes = Object.keys(labelColorObjects).slice(
|
||||
0,
|
||||
-1
|
||||
) as LabelColor[]
|
||||
const randomColorHex =
|
||||
colorHexes[Math.floor(Math.random() * colorHexes.length)]
|
||||
setLabelColorHex((prevState) => ({
|
||||
...prevState,
|
||||
rowId: rowId || '',
|
||||
value: randomColorHex,
|
||||
}))
|
||||
}
|
||||
|
||||
return (
|
||||
<PrimaryLayout pageTestId="settings-labels-tag">
|
||||
<Toaster />
|
||||
<VStack css={{ mx: '42px' }}>
|
||||
<h2>Create a new label</h2>
|
||||
<form
|
||||
onSubmit={async (e): Promise<void> => {
|
||||
e.preventDefault()
|
||||
await createLabel()
|
||||
}}
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
name="name"
|
||||
placeholder="Name"
|
||||
required
|
||||
value={name}
|
||||
onChange={(event) => {
|
||||
setName(event.target.value)
|
||||
}}
|
||||
<Toaster
|
||||
containerStyle={{
|
||||
top: '5rem',
|
||||
}}
|
||||
/>
|
||||
<VStack
|
||||
css={{
|
||||
mx: '10px',
|
||||
color: '$grayText',
|
||||
}}
|
||||
>
|
||||
{confirmRemoveLabelId ? (
|
||||
<ConfirmationModal
|
||||
message={'Are you sure? Deleting a label will remove it from all pages.'}
|
||||
onAccept={() => {
|
||||
onDeleteLabel(confirmRemoveLabelId)
|
||||
setConfirmRemoveLabelId(null)}
|
||||
}
|
||||
onOpenChange={() => setConfirmRemoveLabelId(null)}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
name="color"
|
||||
placeholder="Color"
|
||||
required
|
||||
value={color}
|
||||
onChange={(event) => {
|
||||
setColor(event.target.value)
|
||||
}}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
name="description"
|
||||
placeholder="Description"
|
||||
value={description}
|
||||
onChange={(event) => {
|
||||
setDescription(event.target.value)
|
||||
}}
|
||||
/>
|
||||
<Button type="submit" disabled={isValidating}>
|
||||
Create
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
<h2>Labels</h2>
|
||||
{labels &&
|
||||
labels.map((label) => {
|
||||
return (
|
||||
<Box key={label.id}>
|
||||
<form
|
||||
onSubmit={async (e): Promise<void> => {
|
||||
e.preventDefault()
|
||||
await deleteLabel(label.id)
|
||||
}}
|
||||
>
|
||||
<input type="text" value={label.name} disabled />
|
||||
<input type="text" value={label.color} disabled />
|
||||
<input type="text" value={label.description} disabled />
|
||||
<Button type="submit" disabled={isValidating}>
|
||||
Delete
|
||||
) : null}
|
||||
<HeaderWrapper>
|
||||
<Box style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<Box>
|
||||
<StyledText
|
||||
style="fixedHeadline"
|
||||
>
|
||||
Labels{' '}
|
||||
</StyledText>
|
||||
</Box>
|
||||
<InfoLink href="/help/labels" />
|
||||
<Box css={{display: 'flex', justifyContent: 'flex-end' }}>
|
||||
{isCreateMode ? null : (
|
||||
<>
|
||||
<Button
|
||||
onClick={() => {
|
||||
resetLabelState()
|
||||
handleGenerateRandomColor()
|
||||
setIsCreateMode(true)
|
||||
}}
|
||||
style="ctaDarkYellow"
|
||||
css={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<SpanBox css={{
|
||||
display: 'none',
|
||||
'@md': {
|
||||
display: 'flex',
|
||||
},
|
||||
}}>
|
||||
<SpanBox>Add Label</SpanBox>
|
||||
</SpanBox>
|
||||
<SpanBox css={{
|
||||
p: '0',
|
||||
display: 'flex',
|
||||
'@md': {
|
||||
display: 'none',
|
||||
},
|
||||
}}>
|
||||
<Plus size={24} />
|
||||
</SpanBox>
|
||||
</Button>
|
||||
</form>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</HeaderWrapper>
|
||||
<>
|
||||
{isCreateMode ? (
|
||||
windowWidth > breakpoint ? (
|
||||
<DesktopEditCard
|
||||
label={null}
|
||||
labelColorHex={labelColorHex}
|
||||
editingLabelId={editingLabelId}
|
||||
isCreateMode={isCreateMode}
|
||||
handleGenerateRandomColor={handleGenerateRandomColor}
|
||||
setEditingLabelId={setEditingLabelId}
|
||||
setLabelColorHex={setLabelColorHex}
|
||||
deleteLabel={deleteLabel}
|
||||
nameInputText={nameInputText}
|
||||
descriptionInputText={descriptionInputText}
|
||||
setNameInputText={setNameInputText}
|
||||
setDescriptionInputText={setDescriptionInputText}
|
||||
setIsCreateMode={setIsCreateMode}
|
||||
createLabel={createLabel}
|
||||
updateLabel={updateLabel}
|
||||
onEditPress={onEditPress}
|
||||
resetState={resetLabelState}
|
||||
/>
|
||||
) : (
|
||||
<MobileEditCard
|
||||
label={null}
|
||||
labelColorHex={labelColorHex}
|
||||
editingLabelId={editingLabelId}
|
||||
isCreateMode={isCreateMode}
|
||||
handleGenerateRandomColor={handleGenerateRandomColor}
|
||||
setEditingLabelId={setEditingLabelId}
|
||||
setLabelColorHex={setLabelColorHex}
|
||||
deleteLabel={deleteLabel}
|
||||
nameInputText={nameInputText}
|
||||
descriptionInputText={descriptionInputText}
|
||||
setNameInputText={setNameInputText}
|
||||
setDescriptionInputText={setDescriptionInputText}
|
||||
setIsCreateMode={setIsCreateMode}
|
||||
createLabel={createLabel}
|
||||
resetState={resetLabelState}
|
||||
updateLabel={updateLabel}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
) : null}
|
||||
</>
|
||||
{labels
|
||||
? labels.map((label, i) => {
|
||||
const isLastChild = i === labels.length - 1
|
||||
const isFirstChild = i === 0
|
||||
const cardProps = {
|
||||
label: label,
|
||||
labelColorHex: labelColorHex,
|
||||
editingLabelId: editingLabelId,
|
||||
isCreateMode: isCreateMode,
|
||||
isLastChild: isLastChild,
|
||||
isFirstChild: isFirstChild,
|
||||
handleGenerateRandomColor: handleGenerateRandomColor,
|
||||
setEditingLabelId: setEditingLabelId,
|
||||
setLabelColorHex: setLabelColorHex,
|
||||
deleteLabel: deleteLabel,
|
||||
nameInputText: nameInputText,
|
||||
descriptionInputText: descriptionInputText,
|
||||
setNameInputText: setNameInputText,
|
||||
setDescriptionInputText: setDescriptionInputText,
|
||||
setIsCreateMode: setIsCreateMode,
|
||||
createLabel: createLabel,
|
||||
resetState: resetLabelState,
|
||||
updateLabel: updateLabel,
|
||||
}
|
||||
|
||||
if (editingLabelId == label.id) {
|
||||
if (windowWidth >= breakpoint) {
|
||||
return <DesktopEditCard {...cardProps} />
|
||||
} else {
|
||||
return <MobileEditCard {...cardProps} />
|
||||
}
|
||||
}
|
||||
|
||||
return (<GenericTableCard key={label.id} {...cardProps} onEditPress={onEditPress} />)
|
||||
}) : null}
|
||||
</VStack>
|
||||
<Box css={{ height: '120px' }} />
|
||||
</PrimaryLayout>
|
||||
)
|
||||
}
|
||||
|
||||
function GenericTableCard(props: GenericTableCardProps & { isLastChild?: boolean; isFirstChild?: boolean}) {
|
||||
const {
|
||||
label,
|
||||
isLastChild,
|
||||
isFirstChild,
|
||||
editingLabelId,
|
||||
labelColorHex,
|
||||
isCreateMode,
|
||||
nameInputText,
|
||||
descriptionInputText,
|
||||
handleGenerateRandomColor,
|
||||
setLabelColorHex,
|
||||
setEditingLabelId,
|
||||
deleteLabel,
|
||||
setNameInputText,
|
||||
setDescriptionInputText,
|
||||
createLabel,
|
||||
updateLabel,
|
||||
onEditPress,
|
||||
resetState,
|
||||
} = props
|
||||
const showInput =
|
||||
editingLabelId === label?.id || (isCreateMode && !label)
|
||||
const labelColor = editingLabelId === label?.id ? labelColorHex.value : label?.color
|
||||
const iconColor = isDarkTheme() ? '#D8D7D5': '#5F5E58'
|
||||
|
||||
const handleEdit = () => {
|
||||
editingLabelId && updateLabel(editingLabelId)
|
||||
setEditingLabelId(null)
|
||||
}
|
||||
|
||||
const moreActionsButton = () => {
|
||||
return (
|
||||
<ActionsWrapper>
|
||||
<Dropdown
|
||||
disabled={isCreateMode}
|
||||
triggerElement={<DotsThree size={24} color={iconColor} />}
|
||||
>
|
||||
<DropdownOption onSelect={() => null}>
|
||||
<Button
|
||||
style="plainIcon"
|
||||
css={{
|
||||
mr: '0px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
backgroundColor: 'transparent',
|
||||
border: 0,
|
||||
}}
|
||||
onClick={() => onEditPress(label)}
|
||||
disabled={isCreateMode}
|
||||
>
|
||||
<PencilSimple size={24} color={iconColor} />
|
||||
<StyledText
|
||||
color="$grayText"
|
||||
css={{ m: '0px', fontSize: '$5', marginLeft: '$2' }}
|
||||
>
|
||||
Edit
|
||||
</StyledText>
|
||||
</Button>
|
||||
</DropdownOption>
|
||||
<DropdownOption onSelect={() => null}>
|
||||
<Button
|
||||
style="plainIcon"
|
||||
css={{
|
||||
mr: '$1',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
backgroundColor: 'transparent',
|
||||
border: 0,
|
||||
}}
|
||||
onClick={() => (label ? deleteLabel(label.id) : null)}
|
||||
disabled={isCreateMode}
|
||||
>
|
||||
<Trash size={24} color="#AA2D11" />
|
||||
<StyledText
|
||||
css={{ m: '0px', fontSize: '$5', marginLeft: '$2', color: '#AA2D11' }}
|
||||
>
|
||||
Delete
|
||||
</StyledText>
|
||||
</Button>
|
||||
</DropdownOption>
|
||||
</Dropdown>
|
||||
</ActionsWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<TableCard
|
||||
css={{
|
||||
'&:hover': {
|
||||
background: 'rgba(255, 234, 159, 0.12)',
|
||||
},
|
||||
borderTopLeftRadius: isFirstChild ? '5px' : '',
|
||||
borderTopRightRadius: isFirstChild ? '5px' : '',
|
||||
borderBottomLeftRadius: isLastChild ? '5px' : '',
|
||||
borderBottomRightRadius: isLastChild ? '5px' : '',
|
||||
}}>
|
||||
<TableCardBox
|
||||
css={{
|
||||
display: 'grid',
|
||||
width: '100%',
|
||||
gridGap: '$1',
|
||||
gridTemplateColumns: '3fr 2fr',
|
||||
height: editingLabelId == label?.id ? '120px' : '56px',
|
||||
'.showHidden': {
|
||||
display: 'none',
|
||||
},
|
||||
'&:hover': {
|
||||
'.showHidden': {
|
||||
display: 'unset',
|
||||
gridColumn: 'span 2',
|
||||
width: '100%',
|
||||
padding: '$2 $3 0 $3',
|
||||
},
|
||||
},
|
||||
'@md': {
|
||||
height: '56px',
|
||||
gridTemplateColumns: '20% 28% 1fr 1fr',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<HStack
|
||||
distribution="start"
|
||||
alignment="center"
|
||||
css={{
|
||||
padding: '0 5px',
|
||||
}}
|
||||
>
|
||||
{(showInput && !label) ? null : (
|
||||
<HStack alignment="center" css={{ ml: '16px', '@smDown': { ml: '0px' } }}>
|
||||
<LabelChip color={labelColor || ''} text={label?.name || ''} />
|
||||
</HStack>
|
||||
)}
|
||||
{(showInput && !label) ? (
|
||||
<SpanBox css={{
|
||||
'@smDown': {
|
||||
display: 'none',
|
||||
},
|
||||
}}>
|
||||
<Input
|
||||
type="text"
|
||||
value={nameInputText}
|
||||
onChange={(event) => setNameInputText(event.target.value)}
|
||||
required
|
||||
autoFocus
|
||||
/>
|
||||
</SpanBox>
|
||||
) : null}
|
||||
</HStack>
|
||||
|
||||
<HStack
|
||||
distribution="start"
|
||||
alignment="center"
|
||||
css={{
|
||||
display: 'none',
|
||||
'@md': {
|
||||
display: 'flex',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{showInput ? (
|
||||
<Input
|
||||
type="text"
|
||||
placeholder='Description (optional)'
|
||||
value={descriptionInputText}
|
||||
onChange={(event) => setDescriptionInputText(event.target.value)}
|
||||
autoFocus={!!label}
|
||||
/>
|
||||
) : (
|
||||
<StyledText
|
||||
style="body"
|
||||
css={{
|
||||
color: '$grayTextContrast',
|
||||
fontSize: '14px',
|
||||
}}
|
||||
>
|
||||
{editingLabelId === label?.id ? descriptionInputText : label?.description || ''}
|
||||
</StyledText>
|
||||
)}
|
||||
</HStack>
|
||||
|
||||
<HStack
|
||||
distribution="start"
|
||||
css={{
|
||||
padding: '4px 8px',
|
||||
paddingLeft: '10px',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
{showInput && (
|
||||
<LabelColorDropdown
|
||||
isCreateMode={isCreateMode && !label}
|
||||
canEdit={editingLabelId === label?.id}
|
||||
labelColorHexRowId={labelColorHex.rowId}
|
||||
labelColorHexValue={labelColorHex.value}
|
||||
labelId={label?.id || ''}
|
||||
labelColor={label?.color || 'custom color'}
|
||||
setLabelColorHex={setLabelColorHex}
|
||||
/>
|
||||
)}
|
||||
{showInput && (
|
||||
<TooltipWrapped
|
||||
tooltipSide={'top'}
|
||||
tooltipContent='Random Color'
|
||||
arrowStyles={{fill: '#F9D354'}}
|
||||
style={{backgroundColor: '#F9D354', color: 'black'}}
|
||||
>
|
||||
<Box css={{py: 4}}>
|
||||
<IconButton
|
||||
style="ctaWhite"
|
||||
css={{
|
||||
mr: '$1',
|
||||
width: 40,
|
||||
height: 40,
|
||||
background: '$labelButtonsBg',
|
||||
}}
|
||||
onClick={() => handleGenerateRandomColor(label?.id)}
|
||||
disabled={!(isCreateMode && !label) && !(editingLabelId === label?.id)}
|
||||
>
|
||||
<ArrowClockwise size={16} color={iconColor} />
|
||||
</IconButton>
|
||||
</Box>
|
||||
</TooltipWrapped>
|
||||
)}
|
||||
{!showInput && (
|
||||
<Box css={{ marginLeft: 'auto', '@md': { display: 'none' }}}>
|
||||
{moreActionsButton()}
|
||||
</Box>
|
||||
)}
|
||||
</HStack>
|
||||
|
||||
<HStack
|
||||
distribution="start"
|
||||
alignment="center"
|
||||
css={{
|
||||
ml: '8px',
|
||||
display: 'flex',
|
||||
'@md': {
|
||||
display: 'none',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{showInput && (
|
||||
<Input
|
||||
type="text"
|
||||
placeholder='What this label is about...'
|
||||
value={descriptionInputText}
|
||||
onChange={(event) => setDescriptionInputText(event.target.value)}
|
||||
autoFocus={!!label}
|
||||
/>
|
||||
)}
|
||||
</HStack>
|
||||
|
||||
<HStack
|
||||
distribution="end"
|
||||
alignment="center"
|
||||
css={{
|
||||
padding: '0px 8px',
|
||||
}}
|
||||
>
|
||||
{editingLabelId === label?.id || !label ? (
|
||||
<>
|
||||
<Button
|
||||
style="plainIcon"
|
||||
css={{ mr: '$1' }}
|
||||
onClick={() => {
|
||||
resetState()
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
style="ctaDarkYellow"
|
||||
css={{ my: '0px', mr: '$1' }}
|
||||
onClick={() =>
|
||||
label ? handleEdit() : createLabel()
|
||||
}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<HStack
|
||||
distribution="end"
|
||||
alignment="end"
|
||||
css={{
|
||||
display: 'none',
|
||||
'@md': {
|
||||
display: 'flex',
|
||||
width: '100%'
|
||||
},
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
style="ctaWhite"
|
||||
css={{ mr: '$1', background: '$labelButtonsBg' }}
|
||||
onClick={() => onEditPress(label)}
|
||||
disabled={isCreateMode}
|
||||
>
|
||||
<PencilSimple size={16} color={iconColor} />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
style="ctaWhite"
|
||||
css={{ mr: '$1', background: '$labelButtonsBg' }}
|
||||
onClick={() => deleteLabel(label.id)}
|
||||
disabled={isCreateMode}
|
||||
>
|
||||
<Trash size={16} color={iconColor} />
|
||||
</IconButton>
|
||||
</HStack>
|
||||
)}
|
||||
</HStack>
|
||||
</TableCardBox>
|
||||
</TableCard>
|
||||
)
|
||||
}
|
||||
|
||||
function MobileEditCard(props: any) {
|
||||
const {
|
||||
label,
|
||||
editingLabelId,
|
||||
labelColorHex,
|
||||
isCreateMode,
|
||||
nameInputText,
|
||||
descriptionInputText,
|
||||
setLabelColorHex,
|
||||
setEditingLabelId,
|
||||
setNameInputText,
|
||||
setDescriptionInputText,
|
||||
createLabel,
|
||||
resetState,
|
||||
updateLabel,
|
||||
isFirstChild,
|
||||
isLastChild,
|
||||
} = props
|
||||
|
||||
const handleEdit = () => {
|
||||
editingLabelId && updateLabel(editingLabelId)
|
||||
setEditingLabelId(null)
|
||||
}
|
||||
|
||||
return (
|
||||
<TableCard
|
||||
css={{
|
||||
borderTopLeftRadius: isFirstChild ? '5px' : '',
|
||||
borderTopRightRadius: isFirstChild ? '5px' : '',
|
||||
borderBottomLeftRadius: isLastChild ? '5px' : '',
|
||||
borderBottomRightRadius: isLastChild ? '5px' : '',
|
||||
}}>
|
||||
<VStack distribution="center" css={{ width: '100%', margin: '8px' }}>
|
||||
{nameInputText && (
|
||||
<SpanBox css={{ ml: '-2px', mt: '0px' }}>
|
||||
<LabelChip color={labelColorHex.value} text={nameInputText} />
|
||||
</SpanBox>
|
||||
)}
|
||||
<Input
|
||||
type="text"
|
||||
value={nameInputText}
|
||||
onChange={(event) => setNameInputText(event.target.value)}
|
||||
autoFocus
|
||||
/>
|
||||
<LabelColorDropdown
|
||||
isCreateMode={isCreateMode && !label}
|
||||
canEdit={editingLabelId === label?.id}
|
||||
labelColorHexRowId={labelColorHex.rowId}
|
||||
labelColorHexValue={labelColorHex.value}
|
||||
labelId={label?.id || ''}
|
||||
labelColor={label?.color || 'custom color'}
|
||||
setLabelColorHex={setLabelColorHex}
|
||||
/>
|
||||
<TextArea
|
||||
placeholder='Description (optional)'
|
||||
value={descriptionInputText}
|
||||
onChange={(event) => setDescriptionInputText(event.target.value)}
|
||||
rows={5}
|
||||
/>
|
||||
<HStack
|
||||
distribution="end"
|
||||
alignment="center"
|
||||
css={{ width: '100%', margin: '$1 0' }}
|
||||
>
|
||||
<Button
|
||||
style="plainIcon"
|
||||
css={{ mr: '$1' }}
|
||||
onClick={() => {
|
||||
resetState()
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
style="ctaDarkYellow"
|
||||
css={{ mr: '$1' }}
|
||||
onClick={() => label ? handleEdit() : createLabel()}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</HStack>
|
||||
</VStack>
|
||||
</TableCard>
|
||||
)
|
||||
}
|
||||
|
||||
function DesktopEditCard(props: any) {
|
||||
const {
|
||||
label,
|
||||
editingLabelId,
|
||||
labelColorHex,
|
||||
isCreateMode,
|
||||
nameInputText,
|
||||
descriptionInputText,
|
||||
setLabelColorHex,
|
||||
setEditingLabelId,
|
||||
setNameInputText,
|
||||
setDescriptionInputText,
|
||||
createLabel,
|
||||
resetState,
|
||||
updateLabel,
|
||||
isFirstChild,
|
||||
isLastChild,
|
||||
} = props
|
||||
|
||||
const handleEdit = () => {
|
||||
editingLabelId && updateLabel(editingLabelId)
|
||||
setEditingLabelId(null)
|
||||
}
|
||||
|
||||
return (
|
||||
<TableCard
|
||||
css={{
|
||||
width: '100%',
|
||||
borderTopLeftRadius: isFirstChild ? '5px' : '',
|
||||
borderTopRightRadius: isFirstChild ? '5px' : '',
|
||||
borderBottomLeftRadius: isLastChild ? '5px' : '',
|
||||
borderBottomRightRadius: isLastChild ? '5px' : '',
|
||||
}}>
|
||||
<VStack distribution="center" css={{ width: '100%', my: '8px', ml: '8px', mr: '0px' }}>
|
||||
{nameInputText && (
|
||||
<SpanBox css={{ px: '11px', mt: '3px' }}>
|
||||
<LabelChip color={labelColorHex.value} text={nameInputText} />
|
||||
</SpanBox>
|
||||
)}
|
||||
<HStack
|
||||
distribution="start"
|
||||
alignment="center"
|
||||
css={{ pt: '6px', px: '13px', width: '100%', gap: '16px' }}
|
||||
>
|
||||
<Input
|
||||
type="text"
|
||||
value={nameInputText}
|
||||
onChange={(event) => setNameInputText(event.target.value)}
|
||||
autoFocus
|
||||
/>
|
||||
<LabelColorDropdown
|
||||
isCreateMode={isCreateMode && !label}
|
||||
canEdit={editingLabelId === label?.id}
|
||||
labelColorHexRowId={labelColorHex.rowId}
|
||||
labelColorHexValue={labelColorHex.value}
|
||||
labelId={label?.id || ''}
|
||||
labelColor={label?.color || 'custom color'}
|
||||
setLabelColorHex={setLabelColorHex}
|
||||
/>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder='Description (optional)'
|
||||
value={descriptionInputText}
|
||||
onChange={(event) => setDescriptionInputText(event.target.value)}
|
||||
/>
|
||||
<HStack
|
||||
distribution="end"
|
||||
alignment="center"
|
||||
css={{ marginLeft: 'auto', width: '100%' }}
|
||||
>
|
||||
<Button
|
||||
style="plainIcon"
|
||||
css={{ mr: '12px' }}
|
||||
onClick={() => {
|
||||
resetState()
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
style="ctaDarkYellow"
|
||||
css={{ }}
|
||||
onClick={() => label ? handleEdit() : createLabel()}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</HStack>
|
||||
</HStack>
|
||||
</VStack>
|
||||
</TableCard>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,46 @@
|
|||
import { LabelColorObjects } from './types'
|
||||
|
||||
export const labelColorObjects: LabelColorObjects = {
|
||||
'#FF5D99': {
|
||||
colorName: 'red',
|
||||
text: '#B20042',
|
||||
border: '#FF5D9966',
|
||||
background: '#FF5D990D',
|
||||
},
|
||||
'#7CFF7B': {
|
||||
colorName: 'green',
|
||||
text: '#01A800',
|
||||
border: '#7CFF7B66',
|
||||
background: '#7CFF7B0D',
|
||||
},
|
||||
'#FFD234': {
|
||||
colorName: 'yellow',
|
||||
text: '#947300',
|
||||
border: '#FFD23466',
|
||||
background: '#FFD2340D',
|
||||
},
|
||||
'#7BE4FF': {
|
||||
colorName: 'blue',
|
||||
text: '#007E9E',
|
||||
border: '#7BE4FF66',
|
||||
background: '#7BE4FF0D',
|
||||
},
|
||||
'#CE88EF': {
|
||||
colorName: 'purple',
|
||||
text: '#B759E3',
|
||||
border: '#CE88EF66',
|
||||
background: '#CE88EF0D',
|
||||
},
|
||||
'#EF8C43': {
|
||||
colorName: 'orange',
|
||||
text: '#F37417',
|
||||
border: '#EF8C4366',
|
||||
background: '#EF8C430D',
|
||||
},
|
||||
'custom color': {
|
||||
colorName: 'custom color',
|
||||
text: '#A5A4A1',
|
||||
border: '#D8D7D566',
|
||||
background: '#D8D7D50D',
|
||||
},
|
||||
}
|
||||
71
packages/web/utils/settings-page/labels/types.ts
Normal file
71
packages/web/utils/settings-page/labels/types.ts
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
import React from "react";
|
||||
import { Label } from "../../../lib/networking/queries/useGetLabelsQuery";
|
||||
|
||||
export type LabelColor =
|
||||
| '#FF5D99'
|
||||
| '#7CFF7B'
|
||||
| '#FFD234'
|
||||
| '#7BE4FF'
|
||||
| '#CE88EF'
|
||||
| '#EF8C43'
|
||||
| 'custom color';
|
||||
|
||||
export type LabelOptionProps = {
|
||||
color: string;
|
||||
isDropdownOption?: boolean;
|
||||
isCreateMode: boolean | undefined;
|
||||
labelId: string;
|
||||
};
|
||||
|
||||
export type ColorDetailsProps = {
|
||||
colorName: string;
|
||||
color: string;
|
||||
icon: JSX.Element;
|
||||
};
|
||||
|
||||
export type LabelColorObject = {
|
||||
colorName: string;
|
||||
text: string;
|
||||
border: string;
|
||||
background: string;
|
||||
};
|
||||
|
||||
export type LabelColorObjects = {
|
||||
[key: string]: LabelColorObject;
|
||||
};
|
||||
|
||||
export type LabelColorHex = {
|
||||
rowId: string;
|
||||
value: LabelColor;
|
||||
};
|
||||
|
||||
export type GenericTableCardProps = {
|
||||
label: Label | null;
|
||||
editingLabelId: string | null;
|
||||
labelColorHex: LabelColorHex;
|
||||
isCreateMode: boolean;
|
||||
nameInputText: string,
|
||||
descriptionInputText: string,
|
||||
isMobileView?: boolean;
|
||||
handleGenerateRandomColor: (rowId?: string) => void;
|
||||
setEditingLabelId: (id: string | null) => void;
|
||||
setLabelColorHex: (color: LabelColorHex) => void;
|
||||
deleteLabel: (id: string) => void;
|
||||
setNameInputText: (text: string) => void,
|
||||
setDescriptionInputText: (text: string) => void,
|
||||
resetState: () => void,
|
||||
createLabel: () => void,
|
||||
updateLabel: (id: string) => void;
|
||||
setIsCreateMode: (isCreateMode: boolean) => void,
|
||||
onEditPress: (label: Label | null) => void,
|
||||
};
|
||||
|
||||
export type LabelColorDropdownProps = {
|
||||
isCreateMode: boolean;
|
||||
canEdit: boolean;
|
||||
labelColorHexRowId: string;
|
||||
labelColorHexValue: string;
|
||||
labelId: string;
|
||||
labelColor: LabelColor;
|
||||
setLabelColorHex: (color: LabelColorHex) => void;
|
||||
};
|
||||
|
|
@ -16535,6 +16535,11 @@ react-apple-login@^1.1.3:
|
|||
resolved "https://registry.yarnpkg.com/react-apple-login/-/react-apple-login-1.1.3.tgz#5cb047a52a251fc52d920c7b4d9151d411994997"
|
||||
integrity sha512-I6/2WQEfum9QGowHs9Maea5JhzsOjl8dMCqFP5got+jnnNZRimLE6wGambAvDXdiOuLr+gwMQx5W4JeBGKJHTA==
|
||||
|
||||
react-colorful@^5.5.1:
|
||||
version "5.5.1"
|
||||
resolved "https://registry.yarnpkg.com/react-colorful/-/react-colorful-5.5.1.tgz#29d9c4e496f2ca784dd2bb5053a3a4340cfaf784"
|
||||
integrity sha512-M1TJH2X3RXEt12sWkpa6hLc/bbYS0H6F4rIqjQZ+RxNBstpY67d9TrFXtqdZwhpmBXcCwEi7stKqFue3ZRkiOg==
|
||||
|
||||
react-dom@^17.0.2:
|
||||
version "17.0.2"
|
||||
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-17.0.2.tgz#ecffb6845e3ad8dbfcdc498f0d0a939736502c23"
|
||||
|
|
|
|||
Loading…
Reference in a new issue