feat(approval-domain): add UI for approval domains (#10480)

This commit is contained in:
Antoine Moreaux
2025-02-25 16:44:07 +01:00
committed by GitHub
parent 9997cf5a4e
commit 7c9b902cfe
8 changed files with 110 additions and 18 deletions

View File

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

View File

@ -36,6 +36,7 @@ const StyledButton = styled.button`
type SettingsListCardProps<ListItem extends { id: string }> = {
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={<RowRightComponent item={item} />}
divider={index < items.length - 1}
onClick={() => onRowClick?.(item)}

View File

@ -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 = (
<StyledRow onClick={onClick} divider={divider}>
{!!LeftIcon && <LeftIcon size={theme.icon.size.md} />}
<StyledLabel>{label}</StyledLabel>
<StyledContent>
{label}
{!!description && <StyledDescription>{description}</StyledDescription>}
</StyledContent>
{rightComponent}
</StyledRow>
);

View File

@ -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 ? (
<StyledLink to={getSettingsPath(SettingsPath.NewApprovedAccessDomain)}>
<SettingsCard
@ -53,9 +59,8 @@ export const SettingsApprovedAccessDomainsListCard = () => {
<SettingsSecurityApprovedAccessDomainValidationEffect />
<SettingsListCard
items={approvedAccessDomains}
getItemLabel={(approvedAccessDomain) =>
`${approvedAccessDomain.domain} - ${approvedAccessDomain.createdAt}`
}
getItemLabel={({ domain }) => domain}
getItemDescription={({ createdAt }) => getItemDescription(createdAt)}
RowIcon={IconAt}
RowRightComponent={({ item: approvedAccessDomain }) => (
<SettingsSecurityApprovedAccessDomainRowDropdownMenu

View File

@ -31,12 +31,42 @@ const StyledContainer = styled.div<
`;
const StyledInputContainer = styled.div`
align-items: center;
background-color: inherit;
display: flex;
flex-direction: row;
position: relative;
`;
const StyledAdornmentContainer = styled.div<{
sizeVariant: TextInputV2Size;
position: 'left' | 'right';
}>`
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<
</InputLabel>
)}
<StyledInputContainer>
{leftAdornment && (
<StyledAdornmentContainer sizeVariant={sizeVariant} position="left">
{leftAdornment}
</StyledAdornmentContainer>
)}
{!!LeftIcon && (
<StyledLeftIconContainer sizeVariant={sizeVariant}>
<StyledTrailingIcon isFocused={isFocused}>
@ -271,9 +319,18 @@ const TextInputV2Component = forwardRef<
sizeVariant,
inheritFontStyles,
autoGrow,
leftAdornment,
rightAdornment,
}}
/>
{rightAdornment && (
<StyledAdornmentContainer
sizeVariant={sizeVariant}
position="right"
>
{rightAdornment}
</StyledAdornmentContainer>
)}
<StyledTrailingIconContainer {...{ error }}>
{!error && type === INPUT_TYPE_PASSWORD && (
<StyledTrailingIcon

View File

@ -56,3 +56,15 @@ export const Small: Story = {
export const AutoGrowSmall: Story = {
args: { autoGrow: true, sizeVariant: 'sm', value: 'Tim' },
};
export const WithLeftAdornment: Story = {
args: {
leftAdornment: 'https://',
},
};
export const WithRightAdornment: Story = {
args: {
rightAdornment: '@twenty.com',
},
};

View File

@ -67,7 +67,7 @@ export const SettingsSecurity = () => {
{IsApprovedAccessDomainsEnabled && (
<StyledSection>
<H2Title
title={t`Approved Email Domain`}
title={t`Approved Domains`}
description={t`Anyone with an email address at these domains is allowed to sign up for this workspace.`}
/>
<SettingsApprovedAccessDomainsListCard />

View File

@ -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 },
}) => (
<TextInput
<TextInputV2
autoComplete="off"
value={value}
onChange={(domain: string) => {
@ -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 },
}) => (
<TextInput
<TextInputV2
autoComplete="off"
value={value.split('@')[0]}
onChange={onChange}
fullWidth
error={error?.message}
rightAdornment={`@${domain.length !== 0 ? domain : 'your-domain.com'}`}
/>
)}
/>
{domain}
</Section>
</SettingsPageContainer>
</SubMenuTopBarContainer>