[Omn-190] - [Settings View] - Labels
This commit is contained in:
@ -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'
|
||||
@ -120,6 +124,74 @@ export const createLabelResolver = 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).findOne(uid)
|
||||
if (!user) {
|
||||
return {
|
||||
errorCodes: [UpdateLabelErrorCode.Unauthorized],
|
||||
}
|
||||
}
|
||||
|
||||
const label = await getRepository(Label).findOne({
|
||||
where: {
|
||||
id: labelId,
|
||||
user,
|
||||
},
|
||||
})
|
||||
|
||||
if (!label) {
|
||||
return {
|
||||
errorCodes: [UpdateLabelErrorCode.NotFound],
|
||||
}
|
||||
}
|
||||
|
||||
const result = await getManager().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', error)
|
||||
return {
|
||||
errorCodes: [UpdateLabelErrorCode.BadRequest],
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
export const deleteLabelResolver = authorized<
|
||||
DeleteLabelSuccess,
|
||||
DeleteLabelError,
|
||||
|
||||
@ -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!
|
||||
|
||||
@ -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',
|
||||
@ -69,13 +70,16 @@ type DropdownProps = {
|
||||
labelText?: string
|
||||
showArrow?: boolean
|
||||
triggerElement: React.ReactNode
|
||||
children: React.ReactNode
|
||||
align?: DropdownAlignment
|
||||
children: React.ReactNode,
|
||||
styledArrow?: boolean
|
||||
align?: DropdownAlignment
|
||||
css?: CSS
|
||||
}
|
||||
|
||||
export const DropdownSeparator = styled(Separator, {
|
||||
height: 0,
|
||||
height: '1px',
|
||||
margin: 0,
|
||||
backgroundColor: '$grayBorder',
|
||||
})
|
||||
|
||||
type DropdownOptionProps = {
|
||||
@ -91,6 +95,7 @@ export function DropdownOption(props: DropdownOptionProps): JSX.Element {
|
||||
<StyledItem onSelect={props.onSelect}>
|
||||
{props.title ?? props.children}
|
||||
</StyledItem>
|
||||
{props.hideSeparator ? null : <DropdownSeparator />}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@ -101,12 +106,14 @@ export function Dropdown({
|
||||
triggerElement,
|
||||
labelText,
|
||||
showArrow = true,
|
||||
css
|
||||
}: DropdownProps): JSX.Element {
|
||||
return (
|
||||
<Root modal={false}>
|
||||
<DropdownTrigger>{triggerElement}</DropdownTrigger>
|
||||
<DropdownContent
|
||||
onInteractOutside={() => {
|
||||
css={css}
|
||||
onInteractOutside={(event) => {
|
||||
// remove focus from dropdown
|
||||
;(document.activeElement as HTMLElement).blur()
|
||||
}}
|
||||
|
||||
312
packages/web/components/elements/LabelColorDropdown.tsx
Normal file
312
packages/web/components/elements/LabelColorDropdown.tsx
Normal file
@ -0,0 +1,312 @@
|
||||
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',
|
||||
'&[data-state="open"]': {
|
||||
border: '2px solid $omnivoreYellow',
|
||||
borderRadius: 6,
|
||||
},
|
||||
})
|
||||
const Box = styled('div', {})
|
||||
|
||||
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'
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
style="ctaWhite"
|
||||
css={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
backgroundColor: '$grayBg',
|
||||
borderColor: '$grayBorder',
|
||||
borderWidth: '1px',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<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" />
|
||||
</Button>
|
||||
</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>
|
||||
<DropdownOption onSelect={() => null}>
|
||||
<LabelOption
|
||||
isCreateMode={isCreateMode}
|
||||
labelId={labelId}
|
||||
color="custom color"
|
||||
isDropdownOption
|
||||
/>
|
||||
</DropdownOption>
|
||||
</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 textDisplay = !isCreating && !isDropdownOption ? 'none' : 'unset'
|
||||
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',
|
||||
display: textDisplay,
|
||||
fontSize: '$3',
|
||||
whiteSpace: 'nowrap',
|
||||
textTransform: 'capitalize',
|
||||
'@md': {
|
||||
display: 'unset',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{colorNameText}
|
||||
</StyledText>
|
||||
<StyledText
|
||||
css={{
|
||||
m: '$1',
|
||||
color: '$grayText',
|
||||
display: textDisplay,
|
||||
fontSize: '$3',
|
||||
whiteSpace: 'nowrap',
|
||||
'@md': {
|
||||
display: 'unset',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{colorNameText === 'custom color' ? '' : colorHex}
|
||||
</StyledText>
|
||||
{isDropdownOption ? <Box css={{ pr: '$2' }} /> : null}
|
||||
</HStack>
|
||||
)
|
||||
// colorName,
|
||||
// color: hexCode,
|
||||
// icon: <LabelColorIcon fillColor={fillColor} strokeColor={strokeColor} />,
|
||||
}
|
||||
|
||||
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,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
21
packages/web/components/elements/images/PlusIcon.tsx
Normal file
21
packages/web/components/elements/images/PlusIcon.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
type PlusIconProps = {
|
||||
size: number
|
||||
strokeColor: string
|
||||
}
|
||||
|
||||
export function PlusIcon(props: PlusIconProps): JSX.Element {
|
||||
return (
|
||||
<svg
|
||||
width={props.size}
|
||||
height={props.size}
|
||||
viewBox={`0 0 ${props.size} ${props.size}`}
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M2.5 7.25C2.08579 7.25 1.75 7.58579 1.75 8C1.75 8.41421 2.08579 8.75 2.5 8.75V7.25ZM13.5 8.75C13.9142 8.75 14.25 8.41421 14.25 8C14.25 7.58579 13.9142 7.25 13.5 7.25V8.75ZM8.75 2.5C8.75 2.08579 8.41421 1.75 8 1.75C7.58579 1.75 7.25 2.08579 7.25 2.5H8.75ZM7.25 13.5C7.25 13.9142 7.58579 14.25 8 14.25C8.41421 14.25 8.75 13.9142 8.75 13.5H7.25ZM2.5 8.75H13.5V7.25H2.5V8.75ZM7.25 2.5V13.5H8.75V2.5H7.25Z"
|
||||
fill={props.strokeColor}
|
||||
fillOpacity="0.8"/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
@ -161,6 +161,8 @@ export const { styled, css, theme, getCssText, globalCss, keyframes, config } =
|
||||
// Avatar Fallback color
|
||||
avatarBg: '#FFFFFF',
|
||||
avatarFont: '#0A0806',
|
||||
|
||||
labelButtonsBg: '#F5F5F4',
|
||||
tooltipIcons: '#FDFAEC'
|
||||
},
|
||||
},
|
||||
@ -211,6 +213,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)'
|
||||
|
||||
52
packages/web/lib/networking/mutations/updateLabelMutation.ts
Normal file
52
packages/web/lib/networking/mutations/updateLabelMutation.ts
Normal file
@ -0,0 +1,52 @@
|
||||
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 {
|
||||
console.log('here', input)
|
||||
const data = await gqlFetcher(mutation)
|
||||
console.log('here 3')
|
||||
console.log(input, data);
|
||||
const output = data as any
|
||||
console.log(output)
|
||||
return output?.updatedLabel
|
||||
} catch (err) {
|
||||
console.log('here 3', err)
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,6 @@
|
||||
import { gql } from 'graphql-request'
|
||||
import useSWR from 'swr'
|
||||
import { LabelColor } from '../../../utils/settings-page/labels/types';
|
||||
import { Label, labelFragment } from '../fragments/labelFragment'
|
||||
import { publicGqlFetcher } from '../networkHelpers'
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -36,6 +36,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",
|
||||
|
||||
@ -1,32 +1,198 @@
|
||||
import { PrimaryLayout } from '../../components/templates/PrimaryLayout'
|
||||
import { Button } from '../../components/elements/Button'
|
||||
import { Box, VStack } from '../../components/elements/LayoutPrimitives'
|
||||
import { PlusIcon } from '../../components/elements/images/PlusIcon'
|
||||
import { styled } 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 { Label } from '../../lib/networking/queries/useGetLabelsQuery'
|
||||
import { isDarkTheme } from '../../lib/themeUpdater'
|
||||
import { showErrorToast, showSuccessToast } from '../../lib/toastHelpers'
|
||||
|
||||
export default function LabelsPage(): JSX.Element {
|
||||
const { labels, revalidate, isValidating } = useGetLabelsQuery()
|
||||
const [name, setName] = useState('')
|
||||
const [color, setColor] = useState('')
|
||||
const [description, setDescription] = useState('')
|
||||
import { useEffect, useState } from 'react'
|
||||
import { StyledText } from '../../components/elements/StyledText'
|
||||
import {
|
||||
ArrowClockwise,
|
||||
DotsThree,
|
||||
PencilSimple,
|
||||
Trash,
|
||||
Plus,
|
||||
DotsSixVertical,
|
||||
} 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'
|
||||
|
||||
applyStoredTheme(false)
|
||||
const HeaderWrapper = styled(Box, {
|
||||
width: '100%',
|
||||
})
|
||||
|
||||
const TableCard = styled(Box, {
|
||||
backgroundColor: '$grayBg',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
padding: '8px 12px',
|
||||
border: '0.3px solid $grayBgActive',
|
||||
width: '100%',
|
||||
|
||||
'&:hover': {
|
||||
border: '0.3px solid #FFD234',
|
||||
backgroundColor: '#FFFDF4',
|
||||
},
|
||||
'@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 TableHeading = styled(Box, {
|
||||
backgroundColor: '$grayBgActive',
|
||||
// gridTemplateColumns: '20% 30% 1fr 230px 1fr',
|
||||
gridTemplateColumns: '20% 30% 1fr 1fr',
|
||||
alignItems: 'center',
|
||||
padding: '12px 0px',
|
||||
borderRadius: '5px 5px 0px 0px',
|
||||
width: '100%',
|
||||
textTransform: 'uppercase',
|
||||
display: 'none',
|
||||
'@md': {
|
||||
display: 'grid',
|
||||
},
|
||||
})
|
||||
|
||||
const inputStyles = {
|
||||
backgroundColor: 'transparent',
|
||||
color: '$grayTextContrast',
|
||||
padding: '13px 6px',
|
||||
margin: '$2 0',
|
||||
border: '1px solid $grayBorder',
|
||||
borderRadius: '6px',
|
||||
fontSize: '13px',
|
||||
FontFamily: '$fontFamily',
|
||||
width: '100%',
|
||||
'@md': {
|
||||
width: 'auto',
|
||||
minWidth: '180px',
|
||||
},
|
||||
'&[disabled]': {
|
||||
border: 'none',
|
||||
},
|
||||
'&:focus': {
|
||||
outlineColor: '$omnivoreYellow',
|
||||
outlineStyle: 'solid',
|
||||
},
|
||||
}
|
||||
|
||||
const IconButton = styled(Button, {
|
||||
variants: {
|
||||
style: {
|
||||
ctaWhite: {
|
||||
color: 'red',
|
||||
padding: '14px',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
border: '1px solid $grayBorder',
|
||||
boxSizing: 'border-box',
|
||||
borderRadius: 6,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const Input = styled('input', { ...inputStyles })
|
||||
|
||||
const TextArea = styled('textarea', { ...inputStyles })
|
||||
|
||||
export default function LabelsPage(): JSX.Element {
|
||||
const { labels, revalidate } = useGetLabelsQuery()
|
||||
const [labelColorHex, setLabelColorHex] = useState<LabelColorHex>({
|
||||
rowId: '',
|
||||
value: 'custom color',
|
||||
})
|
||||
console.log('LabelsPage ~ labelColorHex', labelColorHex)
|
||||
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 breakpoint = 768
|
||||
|
||||
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('')
|
||||
resetLabelState()
|
||||
revalidate()
|
||||
}
|
||||
} else {
|
||||
@ -34,78 +200,667 @@ export default function LabelsPage(): JSX.Element {
|
||||
}
|
||||
}
|
||||
|
||||
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 deleteLabel(id: string): Promise<void> {
|
||||
await deleteLabelMutation(id)
|
||||
revalidate()
|
||||
}
|
||||
|
||||
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)
|
||||
}}
|
||||
/>
|
||||
<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
|
||||
<Toaster
|
||||
containerStyle={{
|
||||
top: '5rem',
|
||||
}}
|
||||
/>
|
||||
<VStack
|
||||
css={{
|
||||
mx: '10px',
|
||||
color: '$grayText',
|
||||
}}
|
||||
>
|
||||
<HeaderWrapper>
|
||||
<Box style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<Box style={{ flex: '1' }}>
|
||||
<StyledText style="fixedHeadline">Labels</StyledText>
|
||||
</Box>
|
||||
<Box css={{ minWidth: '178px', display: 'flex', justifyContent: 'flex-end' }}>
|
||||
{editingLabelId || isCreateMode ? null : (
|
||||
<>
|
||||
<Button
|
||||
onClick={() => {
|
||||
resetLabelState()
|
||||
setIsCreateMode(true)
|
||||
}}
|
||||
style="ctaDarkYellow"
|
||||
css={{
|
||||
display: 'none',
|
||||
'@md': {
|
||||
display: 'flex',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<PlusIcon size={20} strokeColor={'#0A0806'} />
|
||||
<SpanBox>Create New Label</SpanBox>
|
||||
</Button>
|
||||
</form>
|
||||
</Box>
|
||||
<Box
|
||||
css={{
|
||||
position: 'fixed',
|
||||
bottom: '58px',
|
||||
right: '16px',
|
||||
zIndex: '1',
|
||||
'@md': {
|
||||
display: 'none',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
style="ctaDarkYellow"
|
||||
onClick={() => {
|
||||
setIsCreateMode(true)
|
||||
}}
|
||||
>
|
||||
<Plus size={24} />
|
||||
</Button>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
<TableHeading>
|
||||
<Box>
|
||||
<StyledText
|
||||
style="highlightTitle"
|
||||
css={{
|
||||
color: '$grayTextContrast',
|
||||
padding: '0 5px 0 60px',
|
||||
}}
|
||||
>
|
||||
Name
|
||||
</StyledText>
|
||||
</Box>
|
||||
<Box style={{ flex: '35%' }}>
|
||||
<StyledText
|
||||
style="highlightTitle"
|
||||
css={{
|
||||
color: '$grayTextContrast',
|
||||
}}
|
||||
>
|
||||
Description
|
||||
</StyledText>
|
||||
</Box>
|
||||
{/* <Box>
|
||||
<StyledText
|
||||
style="highlightTitle"
|
||||
css={{
|
||||
color: '$grayTextContrast',
|
||||
textAlign: 'right',
|
||||
paddingRight: '40px',
|
||||
}}
|
||||
>
|
||||
Uses
|
||||
</StyledText>
|
||||
</Box> */}
|
||||
<Box style={{ flex: '30%' }}>
|
||||
<StyledText
|
||||
style="highlightTitle"
|
||||
css={{
|
||||
color: '$grayTextContrast',
|
||||
paddingLeft: '15px',
|
||||
}}
|
||||
>
|
||||
Color
|
||||
</StyledText>
|
||||
</Box>
|
||||
<Box>
|
||||
<StyledText
|
||||
style="highlightTitle"
|
||||
css={{
|
||||
color: '$grayTextContrast',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
Actions
|
||||
</StyledText>
|
||||
</Box>
|
||||
</TableHeading>
|
||||
</HeaderWrapper>
|
||||
<>
|
||||
{isCreateMode ? (
|
||||
windowWidth > breakpoint ? (
|
||||
<GenericTableCard
|
||||
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}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
) : null}
|
||||
</>
|
||||
{labels
|
||||
? labels.map((label, i) => {
|
||||
const isLastChild = i === labels.length - 1
|
||||
const isFirstChild = i === 0
|
||||
|
||||
return (
|
||||
<GenericTableCard
|
||||
key={label.id}
|
||||
isLastChild={isLastChild}
|
||||
isFirstChild={isFirstChild}
|
||||
label={label as unknown as Label}
|
||||
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}
|
||||
/>
|
||||
)
|
||||
})
|
||||
: null}
|
||||
</VStack>
|
||||
</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 colorObject =
|
||||
labelColorObjects[label?.color || ''] || labelColorObjects['custom color']
|
||||
const { text, border, background } = colorObject
|
||||
const showInput =
|
||||
editingLabelId === label?.id || (isCreateMode && !label)
|
||||
const labelName = label?.name || nameInputText
|
||||
|
||||
const isDarkMode = isDarkTheme()
|
||||
const iconColor = isDarkMode ? '#D8D7D5': '#5F5E58'
|
||||
|
||||
const handleEdit = () => {
|
||||
editingLabelId && updateLabel(editingLabelId)
|
||||
setEditingLabelId(null)
|
||||
}
|
||||
|
||||
const moreActionsButton = () => {
|
||||
return (
|
||||
<Button
|
||||
style="plainIcon"
|
||||
css={{
|
||||
mr: '$1',
|
||||
display: 'flex',
|
||||
background: 'transparent',
|
||||
border: 'none',
|
||||
}}
|
||||
onClick={() => true}
|
||||
disabled={isCreateMode}
|
||||
>
|
||||
<Dropdown
|
||||
triggerElement={<DotsThree size={24} color={iconColor} />}
|
||||
>
|
||||
<DropdownOption onSelect={() => null}>
|
||||
<Button
|
||||
style="plainIcon"
|
||||
css={{
|
||||
mr: '$1',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
backgroundColor: 'transparent',
|
||||
border: 0,
|
||||
}}
|
||||
onClick={() => onEditPress(label)}
|
||||
disabled={isCreateMode}
|
||||
>
|
||||
<PencilSimple size={24} color={iconColor} />
|
||||
<StyledText
|
||||
color="$grayText"
|
||||
css={{ 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={{ fontSize: '$5', marginLeft: '$2', color: '#AA2D11' }}
|
||||
>
|
||||
Delete
|
||||
</StyledText>
|
||||
</Button>
|
||||
</DropdownOption>
|
||||
</Dropdown>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<TableCard
|
||||
css={{
|
||||
'&:hover': {
|
||||
background: 'rgba(255, 234, 159, 0.12)',
|
||||
},
|
||||
'@mdDown': {
|
||||
borderTopLeftRadius: isFirstChild ? '5px' : '',
|
||||
borderTopRightRadius: isFirstChild ? '5px' : '',
|
||||
|
||||
},
|
||||
borderBottomLeftRadius: isLastChild ? '5px' : '',
|
||||
borderBottomRightRadius: isLastChild ? '5px' : '',
|
||||
}}>
|
||||
<TableCardBox
|
||||
css={{
|
||||
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',
|
||||
gridTemplateColumns: '20% 30% 1fr 1fr',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<HStack
|
||||
distribution="start"
|
||||
alignment="center"
|
||||
css={{
|
||||
padding: '0 5px',
|
||||
}}
|
||||
>
|
||||
{(showInput && !label) ? null : (
|
||||
<HStack alignment="center">
|
||||
<Button style="plainIcon" css={{marginRight: '10px'}}>
|
||||
<DotsSixVertical size={16} />
|
||||
</Button>
|
||||
<StyledText
|
||||
style="body"
|
||||
css={{
|
||||
color: text,
|
||||
fontSize: '14px',
|
||||
whiteSpace: 'nowrap',
|
||||
textAlign: 'left',
|
||||
}}
|
||||
>
|
||||
<SpanBox
|
||||
css={{
|
||||
border: '1px solid',
|
||||
borderColor: border,
|
||||
backgroundColor: background,
|
||||
borderRadius: '32px',
|
||||
padding: '4px 8px',
|
||||
}}
|
||||
>
|
||||
{labelName}
|
||||
</SpanBox>
|
||||
</StyledText>
|
||||
</HStack>
|
||||
)}
|
||||
{(showInput && !label) ? (
|
||||
<Input
|
||||
type="text"
|
||||
value={nameInputText}
|
||||
onChange={(event) => setNameInputText(event.target.value)}
|
||||
required
|
||||
autoFocus
|
||||
/>
|
||||
) : null}
|
||||
</HStack>
|
||||
|
||||
<HStack
|
||||
distribution="start"
|
||||
alignment="center"
|
||||
css={{
|
||||
display: 'none',
|
||||
'@md': {
|
||||
display: 'flex',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{showInput ? (
|
||||
<Input
|
||||
type="text"
|
||||
placeholder='What this label is about...'
|
||||
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="end"
|
||||
alignment="center"
|
||||
css={{
|
||||
display: 'none',
|
||||
'@md': {
|
||||
display: 'flex',
|
||||
paddingRight: '30px',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<StyledText css={{ fontSize: '$2' }}>
|
||||
{isCreateMode && !label ? '-' : 536}
|
||||
</StyledText>
|
||||
</HStack> */}
|
||||
|
||||
<HStack
|
||||
distribution="start"
|
||||
css={{
|
||||
padding: '4px 8px',
|
||||
paddingLeft: '10px',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<LabelColorDropdown
|
||||
isCreateMode={isCreateMode && !label}
|
||||
canEdit={editingLabelId === label?.id}
|
||||
labelColorHexRowId={labelColorHex.rowId}
|
||||
labelColorHexValue={labelColorHex.value}
|
||||
labelId={label?.id || ''}
|
||||
labelColor={label?.color || 'custom color'}
|
||||
setLabelColorHex={setLabelColorHex}
|
||||
/>
|
||||
<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: 46,
|
||||
height: 46,
|
||||
background: '$labelButtonsBg',
|
||||
}}
|
||||
onClick={() => handleGenerateRandomColor(label?.id)}
|
||||
disabled={!(isCreateMode && !label) && !(editingLabelId === label?.id)}
|
||||
>
|
||||
<ArrowClockwise size={16} color={iconColor} />
|
||||
</IconButton>
|
||||
</Box>
|
||||
</TooltipWrapped>
|
||||
<Box css={{'@md': { display: 'none' }}}>
|
||||
{moreActionsButton()}
|
||||
</Box>
|
||||
</HStack>
|
||||
|
||||
<HStack
|
||||
distribution="start"
|
||||
alignment="center"
|
||||
css={{
|
||||
padding: '4px 8px',
|
||||
'@md': {
|
||||
justifyContent: 'center',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{editingLabelId === label?.id || !label ? (
|
||||
<>
|
||||
<Button
|
||||
style="ctaDarkYellow"
|
||||
css={{ mr: '$1' }}
|
||||
onClick={() =>
|
||||
label ? handleEdit() : createLabel()
|
||||
}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
<Button
|
||||
style="plainIcon"
|
||||
css={{ mr: '$1' }}
|
||||
onClick={() => {
|
||||
resetState()
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<HStack
|
||||
distribution="end"
|
||||
alignment="center"
|
||||
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>
|
||||
{moreActionsButton()}
|
||||
</HStack>
|
||||
)}
|
||||
</HStack>
|
||||
{/* <Box className="showHidden">
|
||||
<StyledText
|
||||
style="body"
|
||||
css={{
|
||||
color: '$grayTextContrast',
|
||||
fontSize: '14px',
|
||||
marginBottom: '$2',
|
||||
}}
|
||||
>
|
||||
{label?.description}
|
||||
</StyledText>
|
||||
<StyledText
|
||||
css={{ fontSize: '$2', textAlign: 'right', color: '$grayText' }}
|
||||
>
|
||||
{536} Uses
|
||||
</StyledText>
|
||||
</Box> */}
|
||||
</TableCardBox>
|
||||
</TableCard>
|
||||
)
|
||||
}
|
||||
|
||||
function MobileEditCard(props: any) {
|
||||
const {
|
||||
label,
|
||||
editingLabelId,
|
||||
labelColorHex,
|
||||
isCreateMode,
|
||||
nameInputText,
|
||||
descriptionInputText,
|
||||
setLabelColorHex,
|
||||
setEditingLabelId,
|
||||
setNameInputText,
|
||||
setDescriptionInputText,
|
||||
createLabel,
|
||||
resetState
|
||||
} = props
|
||||
return (
|
||||
<TableCard>
|
||||
<VStack distribution="center" css={{ width: '100%' }}>
|
||||
<StyledText>{editingLabelId ? 'Edit Label' : 'New Label'}</StyledText>
|
||||
<Input
|
||||
type="text"
|
||||
value={nameInputText || label?.name}
|
||||
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
|
||||
value={descriptionInputText || label?.description}
|
||||
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 ? setEditingLabelId(null) : createLabel())}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</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;
|
||||
};
|
||||
Reference in New Issue
Block a user