feat(approval-domain): add UI for approval domains (#10480)
This commit is contained in:
@ -13,6 +13,10 @@ button {
|
|||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
form {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
/* https://stackoverflow.com/questions/44543157/how-to-hide-the-google-invisible-recaptcha-badge */
|
/* https://stackoverflow.com/questions/44543157/how-to-hide-the-google-invisible-recaptcha-badge */
|
||||||
.grecaptcha-badge {
|
.grecaptcha-badge {
|
||||||
visibility: hidden !important;
|
visibility: hidden !important;
|
||||||
|
|||||||
@ -36,6 +36,7 @@ const StyledButton = styled.button`
|
|||||||
type SettingsListCardProps<ListItem extends { id: string }> = {
|
type SettingsListCardProps<ListItem extends { id: string }> = {
|
||||||
items: ListItem[];
|
items: ListItem[];
|
||||||
getItemLabel: (item: ListItem) => string;
|
getItemLabel: (item: ListItem) => string;
|
||||||
|
getItemDescription?: (item: ListItem) => string;
|
||||||
hasFooter?: boolean;
|
hasFooter?: boolean;
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
onRowClick?: (item: ListItem) => void;
|
onRowClick?: (item: ListItem) => void;
|
||||||
@ -54,6 +55,7 @@ export const SettingsListCard = <
|
|||||||
>({
|
>({
|
||||||
items,
|
items,
|
||||||
getItemLabel,
|
getItemLabel,
|
||||||
|
getItemDescription,
|
||||||
hasFooter,
|
hasFooter,
|
||||||
isLoading,
|
isLoading,
|
||||||
onRowClick,
|
onRowClick,
|
||||||
@ -75,6 +77,7 @@ export const SettingsListCard = <
|
|||||||
key={item.id}
|
key={item.id}
|
||||||
LeftIcon={RowIconFn ? RowIconFn(item) : RowIcon}
|
LeftIcon={RowIconFn ? RowIconFn(item) : RowIcon}
|
||||||
label={getItemLabel(item)}
|
label={getItemLabel(item)}
|
||||||
|
description={getItemDescription?.(item)}
|
||||||
rightComponent={<RowRightComponent item={item} />}
|
rightComponent={<RowRightComponent item={item} />}
|
||||||
divider={index < items.length - 1}
|
divider={index < items.length - 1}
|
||||||
onClick={() => onRowClick?.(item)}
|
onClick={() => onRowClick?.(item)}
|
||||||
|
|||||||
@ -17,8 +17,16 @@ const StyledRow = styled(CardContent)`
|
|||||||
min-height: ${({ theme }) => theme.spacing(6)};
|
min-height: ${({ theme }) => theme.spacing(6)};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledLabel = styled.span`
|
const StyledContent = styled.div`
|
||||||
flex: 1 0 auto;
|
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)`
|
const StyledLink = styled(Link)`
|
||||||
@ -32,6 +40,7 @@ const StyledLink = styled(Link)`
|
|||||||
|
|
||||||
type SettingsListItemCardContentProps = {
|
type SettingsListItemCardContentProps = {
|
||||||
label: string;
|
label: string;
|
||||||
|
description?: string;
|
||||||
divider?: boolean;
|
divider?: boolean;
|
||||||
LeftIcon?: IconComponent;
|
LeftIcon?: IconComponent;
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
@ -41,6 +50,7 @@ type SettingsListItemCardContentProps = {
|
|||||||
|
|
||||||
export const SettingsListItemCardContent = ({
|
export const SettingsListItemCardContent = ({
|
||||||
label,
|
label,
|
||||||
|
description,
|
||||||
divider,
|
divider,
|
||||||
LeftIcon,
|
LeftIcon,
|
||||||
onClick,
|
onClick,
|
||||||
@ -52,7 +62,10 @@ export const SettingsListItemCardContent = ({
|
|||||||
const content = (
|
const content = (
|
||||||
<StyledRow onClick={onClick} divider={divider}>
|
<StyledRow onClick={onClick} divider={divider}>
|
||||||
{!!LeftIcon && <LeftIcon size={theme.icon.size.md} />}
|
{!!LeftIcon && <LeftIcon size={theme.icon.size.md} />}
|
||||||
<StyledLabel>{label}</StyledLabel>
|
<StyledContent>
|
||||||
|
{label}
|
||||||
|
{!!description && <StyledDescription>{description}</StyledDescription>}
|
||||||
|
</StyledContent>
|
||||||
{rightComponent}
|
{rightComponent}
|
||||||
</StyledRow>
|
</StyledRow>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -15,6 +15,7 @@ import { approvedAccessDomainsState } from '@/settings/security/states/ApprovedA
|
|||||||
import { SettingsSecurityApprovedAccessDomainRowDropdownMenu } from '@/settings/security/components/approvedAccessDomains/SettingsSecurityApprovedAccessDomainRowDropdownMenu';
|
import { SettingsSecurityApprovedAccessDomainRowDropdownMenu } from '@/settings/security/components/approvedAccessDomains/SettingsSecurityApprovedAccessDomainRowDropdownMenu';
|
||||||
import { SettingsSecurityApprovedAccessDomainValidationEffect } from '@/settings/security/components/approvedAccessDomains/SettingsSecurityApprovedAccessDomainValidationEffect';
|
import { SettingsSecurityApprovedAccessDomainValidationEffect } from '@/settings/security/components/approvedAccessDomains/SettingsSecurityApprovedAccessDomainValidationEffect';
|
||||||
import { useGetApprovedAccessDomainsQuery } from '~/generated/graphql';
|
import { useGetApprovedAccessDomainsQuery } from '~/generated/graphql';
|
||||||
|
import { beautifyPastDateRelativeToNow } from '~/utils/date-utils';
|
||||||
|
|
||||||
const StyledLink = styled(Link)`
|
const StyledLink = styled(Link)`
|
||||||
text-decoration: none;
|
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 ? (
|
return loading || !approvedAccessDomains.length ? (
|
||||||
<StyledLink to={getSettingsPath(SettingsPath.NewApprovedAccessDomain)}>
|
<StyledLink to={getSettingsPath(SettingsPath.NewApprovedAccessDomain)}>
|
||||||
<SettingsCard
|
<SettingsCard
|
||||||
@ -53,9 +59,8 @@ export const SettingsApprovedAccessDomainsListCard = () => {
|
|||||||
<SettingsSecurityApprovedAccessDomainValidationEffect />
|
<SettingsSecurityApprovedAccessDomainValidationEffect />
|
||||||
<SettingsListCard
|
<SettingsListCard
|
||||||
items={approvedAccessDomains}
|
items={approvedAccessDomains}
|
||||||
getItemLabel={(approvedAccessDomain) =>
|
getItemLabel={({ domain }) => domain}
|
||||||
`${approvedAccessDomain.domain} - ${approvedAccessDomain.createdAt}`
|
getItemDescription={({ createdAt }) => getItemDescription(createdAt)}
|
||||||
}
|
|
||||||
RowIcon={IconAt}
|
RowIcon={IconAt}
|
||||||
RowRightComponent={({ item: approvedAccessDomain }) => (
|
RowRightComponent={({ item: approvedAccessDomain }) => (
|
||||||
<SettingsSecurityApprovedAccessDomainRowDropdownMenu
|
<SettingsSecurityApprovedAccessDomainRowDropdownMenu
|
||||||
|
|||||||
@ -31,12 +31,42 @@ const StyledContainer = styled.div<
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledInputContainer = styled.div`
|
const StyledInputContainer = styled.div`
|
||||||
|
align-items: center;
|
||||||
background-color: inherit;
|
background-color: inherit;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
position: relative;
|
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<
|
const StyledInput = styled.input<
|
||||||
Pick<
|
Pick<
|
||||||
TextInputV2ComponentProps,
|
TextInputV2ComponentProps,
|
||||||
@ -46,13 +76,21 @@ const StyledInput = styled.input<
|
|||||||
| 'width'
|
| 'width'
|
||||||
| 'inheritFontStyles'
|
| 'inheritFontStyles'
|
||||||
| 'autoGrow'
|
| 'autoGrow'
|
||||||
|
| 'rightAdornment'
|
||||||
|
| 'leftAdornment'
|
||||||
>
|
>
|
||||||
>`
|
>`
|
||||||
background-color: ${({ theme }) => theme.background.transparent.lighter};
|
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
|
border: 1px solid
|
||||||
${({ theme, error }) =>
|
${({ theme, error }) =>
|
||||||
error ? theme.border.color.danger : theme.border.color.medium};
|
error ? theme.border.color.danger : theme.border.color.medium};
|
||||||
border-radius: ${({ theme }) => theme.border.radius.sm};
|
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
color: ${({ theme }) => theme.font.color.primary};
|
color: ${({ theme }) => theme.font.color.primary};
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -165,6 +203,8 @@ export type TextInputV2ComponentProps = Omit<
|
|||||||
sizeVariant?: TextInputV2Size;
|
sizeVariant?: TextInputV2Size;
|
||||||
inheritFontStyles?: boolean;
|
inheritFontStyles?: boolean;
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
|
rightAdornment?: string;
|
||||||
|
leftAdornment?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type TextInputV2WithAutoGrowWrapperProps = TextInputV2ComponentProps;
|
type TextInputV2WithAutoGrowWrapperProps = TextInputV2ComponentProps;
|
||||||
@ -201,6 +241,8 @@ const TextInputV2Component = forwardRef<
|
|||||||
dataTestId,
|
dataTestId,
|
||||||
autoGrow = false,
|
autoGrow = false,
|
||||||
loading = false,
|
loading = false,
|
||||||
|
rightAdornment,
|
||||||
|
leftAdornment,
|
||||||
},
|
},
|
||||||
ref,
|
ref,
|
||||||
) => {
|
) => {
|
||||||
@ -235,6 +277,12 @@ const TextInputV2Component = forwardRef<
|
|||||||
</InputLabel>
|
</InputLabel>
|
||||||
)}
|
)}
|
||||||
<StyledInputContainer>
|
<StyledInputContainer>
|
||||||
|
{leftAdornment && (
|
||||||
|
<StyledAdornmentContainer sizeVariant={sizeVariant} position="left">
|
||||||
|
{leftAdornment}
|
||||||
|
</StyledAdornmentContainer>
|
||||||
|
)}
|
||||||
|
|
||||||
{!!LeftIcon && (
|
{!!LeftIcon && (
|
||||||
<StyledLeftIconContainer sizeVariant={sizeVariant}>
|
<StyledLeftIconContainer sizeVariant={sizeVariant}>
|
||||||
<StyledTrailingIcon isFocused={isFocused}>
|
<StyledTrailingIcon isFocused={isFocused}>
|
||||||
@ -271,9 +319,18 @@ const TextInputV2Component = forwardRef<
|
|||||||
sizeVariant,
|
sizeVariant,
|
||||||
inheritFontStyles,
|
inheritFontStyles,
|
||||||
autoGrow,
|
autoGrow,
|
||||||
|
leftAdornment,
|
||||||
|
rightAdornment,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
{rightAdornment && (
|
||||||
|
<StyledAdornmentContainer
|
||||||
|
sizeVariant={sizeVariant}
|
||||||
|
position="right"
|
||||||
|
>
|
||||||
|
{rightAdornment}
|
||||||
|
</StyledAdornmentContainer>
|
||||||
|
)}
|
||||||
<StyledTrailingIconContainer {...{ error }}>
|
<StyledTrailingIconContainer {...{ error }}>
|
||||||
{!error && type === INPUT_TYPE_PASSWORD && (
|
{!error && type === INPUT_TYPE_PASSWORD && (
|
||||||
<StyledTrailingIcon
|
<StyledTrailingIcon
|
||||||
|
|||||||
@ -56,3 +56,15 @@ export const Small: Story = {
|
|||||||
export const AutoGrowSmall: Story = {
|
export const AutoGrowSmall: Story = {
|
||||||
args: { autoGrow: true, sizeVariant: 'sm', value: 'Tim' },
|
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 && (
|
{IsApprovedAccessDomainsEnabled && (
|
||||||
<StyledSection>
|
<StyledSection>
|
||||||
<H2Title
|
<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.`}
|
description={t`Anyone with an email address at these domains is allowed to sign up for this workspace.`}
|
||||||
/>
|
/>
|
||||||
<SettingsApprovedAccessDomainsListCard />
|
<SettingsApprovedAccessDomainsListCard />
|
||||||
|
|||||||
@ -8,11 +8,11 @@ import { Controller, useForm } from 'react-hook-form';
|
|||||||
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
|
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
|
||||||
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
|
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
|
||||||
import { Trans, useLingui } from '@lingui/react/macro';
|
import { Trans, useLingui } from '@lingui/react/macro';
|
||||||
import { TextInput } from '@/ui/input/components/TextInput';
|
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { H2Title, Section } from 'twenty-ui';
|
import { H2Title, Section } from 'twenty-ui';
|
||||||
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
||||||
import { useCreateApprovedAccessDomainMutation } from '~/generated/graphql';
|
import { useCreateApprovedAccessDomainMutation } from '~/generated/graphql';
|
||||||
|
import { TextInputV2 } from '@/ui/input/components/TextInputV2';
|
||||||
|
|
||||||
export const SettingsSecurityApprovedAccessDomain = () => {
|
export const SettingsSecurityApprovedAccessDomain = () => {
|
||||||
const navigate = useNavigateSettings();
|
const navigate = useNavigateSettings();
|
||||||
@ -31,14 +31,14 @@ export const SettingsSecurityApprovedAccessDomain = () => {
|
|||||||
domain: z
|
domain: z
|
||||||
.string()
|
.string()
|
||||||
.regex(
|
.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),
|
.max(256),
|
||||||
email: z.string().min(1, {
|
email: z.string().min(1, {
|
||||||
message: t`Email can not be empty`,
|
message: t`Email cannot be empty`,
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
.strict(),
|
.strict(),
|
||||||
@ -53,9 +53,6 @@ export const SettingsSecurityApprovedAccessDomain = () => {
|
|||||||
|
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
try {
|
try {
|
||||||
if (!form.formState.isValid || !form.formState.isSubmitting) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
createApprovedAccessDomain({
|
createApprovedAccessDomain({
|
||||||
variables: {
|
variables: {
|
||||||
input: {
|
input: {
|
||||||
@ -117,7 +114,7 @@ export const SettingsSecurityApprovedAccessDomain = () => {
|
|||||||
field: { onChange, value },
|
field: { onChange, value },
|
||||||
fieldState: { error },
|
fieldState: { error },
|
||||||
}) => (
|
}) => (
|
||||||
<TextInput
|
<TextInputV2
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
value={value}
|
value={value}
|
||||||
onChange={(domain: string) => {
|
onChange={(domain: string) => {
|
||||||
@ -126,6 +123,7 @@ export const SettingsSecurityApprovedAccessDomain = () => {
|
|||||||
fullWidth
|
fullWidth
|
||||||
placeholder="yourdomain.com"
|
placeholder="yourdomain.com"
|
||||||
error={error?.message}
|
error={error?.message}
|
||||||
|
leftAdornment="https://"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@ -142,16 +140,16 @@ export const SettingsSecurityApprovedAccessDomain = () => {
|
|||||||
field: { onChange, value },
|
field: { onChange, value },
|
||||||
fieldState: { error },
|
fieldState: { error },
|
||||||
}) => (
|
}) => (
|
||||||
<TextInput
|
<TextInputV2
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
value={value.split('@')[0]}
|
value={value.split('@')[0]}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
fullWidth
|
fullWidth
|
||||||
error={error?.message}
|
error={error?.message}
|
||||||
|
rightAdornment={`@${domain.length !== 0 ? domain : 'your-domain.com'}`}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
{domain}
|
|
||||||
</Section>
|
</Section>
|
||||||
</SettingsPageContainer>
|
</SettingsPageContainer>
|
||||||
</SubMenuTopBarContainer>
|
</SubMenuTopBarContainer>
|
||||||
|
|||||||
Reference in New Issue
Block a user