diff --git a/packages/api/src/generated/graphql.ts b/packages/api/src/generated/graphql.ts index 29c880397..435d1e2ab 100644 --- a/packages/api/src/generated/graphql.ts +++ b/packages/api/src/generated/graphql.ts @@ -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; +}; + +export enum UpdateLabelErrorCode { + BadRequest = 'BAD_REQUEST', + Forbidden = 'FORBIDDEN', + NotFound = 'NOT_FOUND', + Unauthorized = 'UNAUTHORIZED' +} + +export type UpdateLabelInput = { + color: Scalars['String']; + description?: InputMaybe; + 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; @@ -2095,6 +2127,11 @@ export type ResolversTypes = { UpdateHighlightReplySuccess: ResolverTypeWrapper; UpdateHighlightResult: ResolversTypes['UpdateHighlightError'] | ResolversTypes['UpdateHighlightSuccess']; UpdateHighlightSuccess: ResolverTypeWrapper; + UpdateLabelError: ResolverTypeWrapper; + UpdateLabelErrorCode: UpdateLabelErrorCode; + UpdateLabelInput: UpdateLabelInput; + UpdateLabelResult: ResolversTypes['UpdateLabelError'] | ResolversTypes['UpdateLabelSuccess']; + UpdateLabelSuccess: ResolverTypeWrapper; UpdateLinkShareInfoError: ResolverTypeWrapper; 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>; updateHighlight?: Resolver>; updateHighlightReply?: Resolver>; + updateLabel?: Resolver>; updateLinkShareInfo?: Resolver>; updateReminder?: Resolver>; updateSharedComment?: Resolver>; @@ -3257,6 +3299,20 @@ export type UpdateHighlightSuccessResolvers; }; +export type UpdateLabelErrorResolvers = { + errorCodes?: Resolver, ParentType, ContextType>; + __isTypeOf?: IsTypeOfResolverFn; +}; + +export type UpdateLabelResultResolvers = { + __resolveType: TypeResolveFn<'UpdateLabelError' | 'UpdateLabelSuccess', ParentType, ContextType>; +}; + +export type UpdateLabelSuccessResolvers = { + label?: Resolver; + __isTypeOf?: IsTypeOfResolverFn; +}; + export type UpdateLinkShareInfoErrorResolvers = { errorCodes?: Resolver, ParentType, ContextType>; __isTypeOf?: IsTypeOfResolverFn; @@ -3551,6 +3607,9 @@ export type Resolvers = { UpdateHighlightReplySuccess?: UpdateHighlightReplySuccessResolvers; UpdateHighlightResult?: UpdateHighlightResultResolvers; UpdateHighlightSuccess?: UpdateHighlightSuccessResolvers; + UpdateLabelError?: UpdateLabelErrorResolvers; + UpdateLabelResult?: UpdateLabelResultResolvers; + UpdateLabelSuccess?: UpdateLabelSuccessResolvers; UpdateLinkShareInfoError?: UpdateLinkShareInfoErrorResolvers; UpdateLinkShareInfoResult?: UpdateLinkShareInfoResultResolvers; UpdateLinkShareInfoSuccess?: UpdateLinkShareInfoSuccessResolvers; diff --git a/packages/api/src/generated/schema.graphql b/packages/api/src/generated/schema.graphql index 0d38c46b1..20055efb9 100644 --- a/packages/api/src/generated/schema.graphql +++ b/packages/api/src/generated/schema.graphql @@ -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!]! } diff --git a/packages/api/src/resolvers/function_resolvers.ts b/packages/api/src/resolvers/function_resolvers.ts index 0cf307a32..2186f701b 100644 --- a/packages/api/src/resolvers/function_resolvers.ts +++ b/packages/api/src/resolvers/function_resolvers.ts @@ -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, diff --git a/packages/api/src/resolvers/labels/index.ts b/packages/api/src/resolvers/labels/index.ts index 35a73e483..fd779086e 100644 --- a/packages/api/src/resolvers/labels/index.ts +++ b/packages/api/src/resolvers/labels/index.ts @@ -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, diff --git a/packages/api/src/schema.ts b/packages/api/src/schema.ts index dc0561077..c31bf5838 100755 --- a/packages/api/src/schema.ts +++ b/packages/api/src/schema.ts @@ -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! diff --git a/packages/web/components/elements/DropdownElements.tsx b/packages/web/components/elements/DropdownElements.tsx index f3b4f5e04..f2c1a3ed1 100644 --- a/packages/web/components/elements/DropdownElements.tsx +++ b/packages/web/components/elements/DropdownElements.tsx @@ -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 { {props.title ?? props.children} + {props.hideSeparator ? null : } ) } @@ -101,12 +106,14 @@ export function Dropdown({ triggerElement, labelText, showArrow = true, + css }: DropdownProps): JSX.Element { return ( {triggerElement} { + css={css} + onInteractOutside={(event) => { // remove focus from dropdown ;(document.activeElement as HTMLElement).blur() }} diff --git a/packages/web/components/elements/LabelColorDropdown.tsx b/packages/web/components/elements/LabelColorDropdown.tsx new file mode 100644 index 000000000..ece04e6ee --- /dev/null +++ b/packages/web/components/elements/LabelColorDropdown.tsx @@ -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(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 ( + + + + + + + {Object.keys(labelColorObjects) + .filter((labelColor) => labelColor !== 'custom color') + .map((labelColor) => ( + + setLabelColorHex({ + rowId: labelId, + value: labelColor as LabelColor, + }) + } + > + + + ))} + + + null}> + + + + + + + + + + ) +} + +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 ( + + + + + + {colorNameText} + + + {colorNameText === 'custom color' ? '' : colorHex} + + {isDropdownOption ? : null} + + ) + // colorName, + // color: hexCode, + // icon: , +} + +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 ( + + ) +} diff --git a/packages/web/components/elements/images/PlusIcon.tsx b/packages/web/components/elements/images/PlusIcon.tsx new file mode 100644 index 000000000..fac451e8c --- /dev/null +++ b/packages/web/components/elements/images/PlusIcon.tsx @@ -0,0 +1,21 @@ +type PlusIconProps = { + size: number + strokeColor: string +} + +export function PlusIcon(props: PlusIconProps): JSX.Element { + return ( + + + + ) +} diff --git a/packages/web/components/tokens/stitches.config.ts b/packages/web/components/tokens/stitches.config.ts index 461a244d8..3a09eff08 100644 --- a/packages/web/components/tokens/stitches.config.ts +++ b/packages/web/components/tokens/stitches.config.ts @@ -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)' diff --git a/packages/web/lib/networking/mutations/updateLabelMutation.ts b/packages/web/lib/networking/mutations/updateLabelMutation.ts new file mode 100644 index 000000000..8a2603838 --- /dev/null +++ b/packages/web/lib/networking/mutations/updateLabelMutation.ts @@ -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 { + 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 + } +} diff --git a/packages/web/lib/networking/queries/useGetLabelsQuery.tsx b/packages/web/lib/networking/queries/useGetLabelsQuery.tsx index 211c53178..6cab8ef62 100644 --- a/packages/web/lib/networking/queries/useGetLabelsQuery.tsx +++ b/packages/web/lib/networking/queries/useGetLabelsQuery.tsx @@ -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 { diff --git a/packages/web/package.json b/packages/web/package.json index 8990886d0..d772fa49f 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -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", diff --git a/packages/web/pages/settings/labels.tsx b/packages/web/pages/settings/labels.tsx index 64f0916ad..bdcdd7f78 100644 --- a/packages/web/pages/settings/labels.tsx +++ b/packages/web/pages/settings/labels.tsx @@ -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({ + rowId: '', + value: 'custom color', + }) + console.log('LabelsPage ~ labelColorHex', labelColorHex) + const [editingLabelId, setEditingLabelId] = useState(null) + const [nameInputText, setNameInputText] = useState('') + const [descriptionInputText, setDescriptionInputText] = useState('') + const [isCreateMode, setIsCreateMode] = useState(false) + const [windowWidth, setWindowWidth] = useState(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 { - 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 { + 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 { 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 ( - - -

Create a new label

-
=> { - e.preventDefault() - await createLabel() - }} - > - { - setName(event.target.value) - }} - /> - { - setColor(event.target.value) - }} - /> - { - setDescription(event.target.value) - }} - /> - -
- -

Labels

- {labels && - labels.map((label) => { - return ( - -
=> { - e.preventDefault() - await deleteLabel(label.id) - }} - > - - - - -
-
+ + + + + )} +
+ + + + + Name + + + + + Description + + + {/* + + Uses + + */} + + + Color + + + + + Actions + + + + + <> + {isCreateMode ? ( + windowWidth > breakpoint ? ( + + ) : ( + ) - })} + ) : null} + + {labels + ? labels.map((label, i) => { + const isLastChild = i === labels.length - 1 + const isFirstChild = i === 0 + + return ( + + ) + }) + : null} ) } + +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 ( + + + null}> + + + + + ) + } + + return ( + + + + {(showInput && !label) ? null : ( + + + + + {labelName} + + + + )} + {(showInput && !label) ? ( + setNameInputText(event.target.value)} + required + autoFocus + /> + ) : null} + + + + {showInput ? ( + setDescriptionInputText(event.target.value)} + autoFocus={!!label} + /> + ) : ( + + {editingLabelId === label?.id ? descriptionInputText : label?.description || ''} + + )} + + + {/* + + {isCreateMode && !label ? '-' : 536} + + */} + + + + + + handleGenerateRandomColor(label?.id)} + disabled={!(isCreateMode && !label) && !(editingLabelId === label?.id)} + > + + + + + + {moreActionsButton()} + + + + + {editingLabelId === label?.id || !label ? ( + <> + + + + ) : ( + + onEditPress(label)} + disabled={isCreateMode} + > + + + deleteLabel(label.id)} + disabled={isCreateMode} + > + + + {moreActionsButton()} + + )} + + {/* + + {label?.description} + + + {536} Uses + + */} + + + ) +} + +function MobileEditCard(props: any) { + const { + label, + editingLabelId, + labelColorHex, + isCreateMode, + nameInputText, + descriptionInputText, + setLabelColorHex, + setEditingLabelId, + setNameInputText, + setDescriptionInputText, + createLabel, + resetState + } = props + return ( + + + {editingLabelId ? 'Edit Label' : 'New Label'} + setNameInputText(event.target.value)} + autoFocus + /> + +