feat(approval-domain): add UI for approval domains (#10480)
This commit is contained in:
@ -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;
|
||||
|
||||
@ -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)}
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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',
|
||||
},
|
||||
};
|
||||
|
||||
@ -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 />
|
||||
|
||||
@ -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>
|
||||
|
||||
Reference in New Issue
Block a user