From 7c9b902cfeb694111f0523a6a2729445497fa20d Mon Sep 17 00:00:00 2001 From: Antoine Moreaux Date: Tue, 25 Feb 2025 16:44:07 +0100 Subject: [PATCH] feat(approval-domain): add UI for approval domains (#10480) --- packages/twenty-front/src/index.css | 4 ++ .../settings/components/SettingsListCard.tsx | 3 + .../SettingsListItemCardContent.tsx | 17 +++++- .../SettingsApprovedAccessDomainsListCard.tsx | 11 +++- .../ui/input/components/TextInputV2.tsx | 61 ++++++++++++++++++- .../__stories__/TextInputV2.stories.tsx | 12 ++++ .../settings/security/SettingsSecurity.tsx | 2 +- .../SettingsSecurityApprovedAccessDomain.tsx | 18 +++--- 8 files changed, 110 insertions(+), 18 deletions(-) diff --git a/packages/twenty-front/src/index.css b/packages/twenty-front/src/index.css index 22b25687a..927b31a09 100644 --- a/packages/twenty-front/src/index.css +++ b/packages/twenty-front/src/index.css @@ -13,6 +13,10 @@ button { font-size: 13px; } +form { + width: 100%; +} + /* https://stackoverflow.com/questions/44543157/how-to-hide-the-google-invisible-recaptcha-badge */ .grecaptcha-badge { visibility: hidden !important; diff --git a/packages/twenty-front/src/modules/settings/components/SettingsListCard.tsx b/packages/twenty-front/src/modules/settings/components/SettingsListCard.tsx index 4d5cf9652..189c468d4 100644 --- a/packages/twenty-front/src/modules/settings/components/SettingsListCard.tsx +++ b/packages/twenty-front/src/modules/settings/components/SettingsListCard.tsx @@ -36,6 +36,7 @@ const StyledButton = styled.button` type SettingsListCardProps = { items: ListItem[]; getItemLabel: (item: ListItem) => string; + getItemDescription?: (item: ListItem) => string; hasFooter?: boolean; isLoading?: boolean; onRowClick?: (item: ListItem) => void; @@ -54,6 +55,7 @@ export const SettingsListCard = < >({ items, getItemLabel, + getItemDescription, hasFooter, isLoading, onRowClick, @@ -75,6 +77,7 @@ export const SettingsListCard = < key={item.id} LeftIcon={RowIconFn ? RowIconFn(item) : RowIcon} label={getItemLabel(item)} + description={getItemDescription?.(item)} rightComponent={} divider={index < items.length - 1} onClick={() => onRowClick?.(item)} diff --git a/packages/twenty-front/src/modules/settings/components/SettingsListItemCardContent.tsx b/packages/twenty-front/src/modules/settings/components/SettingsListItemCardContent.tsx index e07649d67..0dfb42211 100644 --- a/packages/twenty-front/src/modules/settings/components/SettingsListItemCardContent.tsx +++ b/packages/twenty-front/src/modules/settings/components/SettingsListItemCardContent.tsx @@ -17,8 +17,16 @@ const StyledRow = styled(CardContent)` min-height: ${({ theme }) => theme.spacing(6)}; `; -const StyledLabel = styled.span` +const StyledContent = styled.div` flex: 1 0 auto; + display: flex; + gap: ${({ theme }) => theme.spacing(1)}; +`; + +const StyledDescription = styled.span` + color: ${({ theme }) => theme.font.color.light}; + font-weight: ${({ theme }) => theme.font.weight.regular}; + padding-left: ${({ theme }) => theme.spacing(1)}; `; const StyledLink = styled(Link)` @@ -32,6 +40,7 @@ const StyledLink = styled(Link)` type SettingsListItemCardContentProps = { label: string; + description?: string; divider?: boolean; LeftIcon?: IconComponent; onClick?: () => void; @@ -41,6 +50,7 @@ type SettingsListItemCardContentProps = { export const SettingsListItemCardContent = ({ label, + description, divider, LeftIcon, onClick, @@ -52,7 +62,10 @@ export const SettingsListItemCardContent = ({ const content = ( {!!LeftIcon && } - {label} + + {label} + {!!description && {description}} + {rightComponent} ); diff --git a/packages/twenty-front/src/modules/settings/security/components/approvedAccessDomains/SettingsApprovedAccessDomainsListCard.tsx b/packages/twenty-front/src/modules/settings/security/components/approvedAccessDomains/SettingsApprovedAccessDomainsListCard.tsx index c2de573b2..ecc02d058 100644 --- a/packages/twenty-front/src/modules/settings/security/components/approvedAccessDomains/SettingsApprovedAccessDomainsListCard.tsx +++ b/packages/twenty-front/src/modules/settings/security/components/approvedAccessDomains/SettingsApprovedAccessDomainsListCard.tsx @@ -15,6 +15,7 @@ import { approvedAccessDomainsState } from '@/settings/security/states/ApprovedA import { SettingsSecurityApprovedAccessDomainRowDropdownMenu } from '@/settings/security/components/approvedAccessDomains/SettingsSecurityApprovedAccessDomainRowDropdownMenu'; import { SettingsSecurityApprovedAccessDomainValidationEffect } from '@/settings/security/components/approvedAccessDomains/SettingsSecurityApprovedAccessDomainValidationEffect'; import { useGetApprovedAccessDomainsQuery } from '~/generated/graphql'; +import { beautifyPastDateRelativeToNow } from '~/utils/date-utils'; const StyledLink = styled(Link)` text-decoration: none; @@ -41,6 +42,11 @@ export const SettingsApprovedAccessDomainsListCard = () => { }, }); + const getItemDescription = (createdAt: string) => { + const beautifyPastDateRelative = beautifyPastDateRelativeToNow(createdAt); + return t`Added ${beautifyPastDateRelative}`; + }; + return loading || !approvedAccessDomains.length ? ( { - `${approvedAccessDomain.domain} - ${approvedAccessDomain.createdAt}` - } + getItemLabel={({ domain }) => domain} + getItemDescription={({ createdAt }) => getItemDescription(createdAt)} RowIcon={IconAt} RowRightComponent={({ item: approvedAccessDomain }) => ( ` + align-items: center; + background-color: ${({ theme }) => theme.background.transparent.light}; + border: 1px solid ${({ theme }) => theme.border.color.medium}; + border-radius: ${({ theme, position }) => + position === 'left' + ? `${theme.border.radius.sm} 0 0 ${theme.border.radius.sm}` + : `0 ${theme.border.radius.sm} ${theme.border.radius.sm} 0`}; + box-sizing: border-box; + color: ${({ theme }) => theme.font.color.tertiary}; + display: flex; + font-size: ${({ theme }) => theme.font.size.md}; + font-weight: ${({ theme }) => theme.font.weight.medium}; + height: ${({ sizeVariant }) => + sizeVariant === 'sm' ? '20px' : sizeVariant === 'md' ? '28px' : '32px'}; + justify-content: center; + min-width: fit-content; + padding: ${({ theme }) => theme.spacing(2)}; + width: auto; + line-height: ${({ sizeVariant }) => + sizeVariant === 'sm' ? '20px' : sizeVariant === 'md' ? '28px' : '32px'}; + + ${({ position }) => + position === 'left' ? 'border-right: none;' : 'border-left: none;'} +`; + const StyledInput = styled.input< Pick< TextInputV2ComponentProps, @@ -46,13 +76,21 @@ const StyledInput = styled.input< | 'width' | 'inheritFontStyles' | 'autoGrow' + | 'rightAdornment' + | 'leftAdornment' > >` background-color: ${({ theme }) => theme.background.transparent.lighter}; + border-radius: ${({ theme, leftAdornment, rightAdornment }) => + leftAdornment + ? `0 ${theme.border.radius.sm} ${theme.border.radius.sm} 0` + : rightAdornment + ? `${theme.border.radius.sm} 0 0 ${theme.border.radius.sm}` + : theme.border.radius.sm}; + border: 1px solid ${({ theme, error }) => error ? theme.border.color.danger : theme.border.color.medium}; - border-radius: ${({ theme }) => theme.border.radius.sm}; box-sizing: border-box; color: ${({ theme }) => theme.font.color.primary}; display: flex; @@ -165,6 +203,8 @@ export type TextInputV2ComponentProps = Omit< sizeVariant?: TextInputV2Size; inheritFontStyles?: boolean; loading?: boolean; + rightAdornment?: string; + leftAdornment?: string; }; type TextInputV2WithAutoGrowWrapperProps = TextInputV2ComponentProps; @@ -201,6 +241,8 @@ const TextInputV2Component = forwardRef< dataTestId, autoGrow = false, loading = false, + rightAdornment, + leftAdornment, }, ref, ) => { @@ -235,6 +277,12 @@ const TextInputV2Component = forwardRef< )} + {leftAdornment && ( + + {leftAdornment} + + )} + {!!LeftIcon && ( @@ -271,9 +319,18 @@ const TextInputV2Component = forwardRef< sizeVariant, inheritFontStyles, autoGrow, + leftAdornment, + rightAdornment, }} /> - + {rightAdornment && ( + + {rightAdornment} + + )} {!error && type === INPUT_TYPE_PASSWORD && ( { {IsApprovedAccessDomainsEnabled && ( diff --git a/packages/twenty-front/src/pages/settings/security/SettingsSecurityApprovedAccessDomain.tsx b/packages/twenty-front/src/pages/settings/security/SettingsSecurityApprovedAccessDomain.tsx index 531268e05..ca5f40b4a 100644 --- a/packages/twenty-front/src/pages/settings/security/SettingsSecurityApprovedAccessDomain.tsx +++ b/packages/twenty-front/src/pages/settings/security/SettingsSecurityApprovedAccessDomain.tsx @@ -8,11 +8,11 @@ import { Controller, useForm } from 'react-hook-form'; import { useNavigateSettings } from '~/hooks/useNavigateSettings'; import { getSettingsPath } from '~/utils/navigation/getSettingsPath'; import { Trans, useLingui } from '@lingui/react/macro'; -import { TextInput } from '@/ui/input/components/TextInput'; import { z } from 'zod'; import { H2Title, Section } from 'twenty-ui'; import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer'; import { useCreateApprovedAccessDomainMutation } from '~/generated/graphql'; +import { TextInputV2 } from '@/ui/input/components/TextInputV2'; export const SettingsSecurityApprovedAccessDomain = () => { const navigate = useNavigateSettings(); @@ -31,14 +31,14 @@ export const SettingsSecurityApprovedAccessDomain = () => { domain: z .string() .regex( - /^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9-]*[A-Za-z0-9])$/, + /^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9-]*[A-Za-z0-9])\.[a-zA-Z]{2,}$/, { - message: t`Invalid domain. Domains have to be smaller than 256 characters in length, cannot be IP addresses, cannot contain spaces, cannot contain any special characters such as _~\`!@#$%^*()=+{}[]|\\;:'",<>/? and cannot begin or end with a '-' character.`, + message: t`Domains have to be smaller than 256 characters, cannot contain spaces and cannot contain any special characters.`, }, ) .max(256), email: z.string().min(1, { - message: t`Email can not be empty`, + message: t`Email cannot be empty`, }), }) .strict(), @@ -53,9 +53,6 @@ export const SettingsSecurityApprovedAccessDomain = () => { const handleSave = async () => { try { - if (!form.formState.isValid || !form.formState.isSubmitting) { - return; - } createApprovedAccessDomain({ variables: { input: { @@ -117,7 +114,7 @@ export const SettingsSecurityApprovedAccessDomain = () => { field: { onChange, value }, fieldState: { error }, }) => ( - { @@ -126,6 +123,7 @@ export const SettingsSecurityApprovedAccessDomain = () => { fullWidth placeholder="yourdomain.com" error={error?.message} + leftAdornment="https://" /> )} /> @@ -142,16 +140,16 @@ export const SettingsSecurityApprovedAccessDomain = () => { field: { onChange, value }, fieldState: { error }, }) => ( - )} /> - {domain}