0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-01-27 21:39:16 -05:00

feat(console): add social connectors on the sie page (#2251)

This commit is contained in:
Xiao Yijun 2022-10-28 16:03:30 +08:00 committed by GitHub
parent 415c24aace
commit 305bbaad2c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 428 additions and 6 deletions

View file

@ -20,7 +20,7 @@ const SignInForm = () => {
<>
<div className={styles.title}>{t('sign_in_exp.sign_up_and_sign_in.sign_in.title')}</div>
<FormField title="sign_in_exp.sign_up_and_sign_in.sign_in.sign_in_identifier_and_auth">
<div className={styles.signInDescription}>
<div className={styles.formFieldDescription}>
{t('sign_in_exp.sign_up_and_sign_in.sign_in.description')}
</div>
<Controller

View file

@ -0,0 +1,36 @@
import { Controller, useFormContext } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import FormField from '@/components/FormField';
import type { SignInExperienceForm } from '../../types';
import SocialConnectorEditBox from './components/SocialConnectorEditBox';
import * as styles from './index.module.scss';
const SocialSignInForm = () => {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const { control } = useFormContext<SignInExperienceForm>();
return (
<>
<div className={styles.title}>
{t('sign_in_exp.sign_up_and_sign_in.social_sign_in.title')}
</div>
<FormField title="sign_in_exp.sign_up_and_sign_in.social_sign_in.social_sign_in">
<div className={styles.formFieldDescription}>
{t('sign_in_exp.sign_up_and_sign_in.social_sign_in.description')}
</div>
<Controller
control={control}
name="socialSignInConnectorTargets"
render={({ field: { value, onChange } }) => {
return <SocialConnectorEditBox value={value} onChange={onChange} />;
}}
/>
</FormField>
</>
);
};
export default SocialSignInForm;

View file

@ -0,0 +1,37 @@
@use '@/scss/underscore' as _;
.dropdown {
min-width: 208px;
}
.plusIcon {
color: var(--color-text-secondary);
}
.title {
display: flex;
align-items: center;
.logo {
margin-right: _.unit(3);
width: 20px;
height: 20px;
img {
width: 20px;
height: 20px;
}
}
.name {
font: var(--font-body-medium);
}
.icon {
width: 16px;
height: 16px;
object-fit: cover;
margin-left: _.unit(1);
color: var(--color-text-secondary);
}
}

View file

@ -0,0 +1,75 @@
import Plus from '@/assets/images/plus.svg';
import ActionMenu from '@/components/ActionMenu';
import type { Props as ButtonProps } from '@/components/Button';
import { DropdownItem } from '@/components/Dropdown';
import UnnamedTrans from '@/components/UnnamedTrans';
import ConnectorPlatformIcon from '@/icons/ConnectorPlatformIcon';
import type { ConnectorGroup } from '@/types/connector';
import * as styles from './AddButton.module.scss';
type Props = {
options: ConnectorGroup[];
onSelected: (signInIdentifier: string) => void;
hasSelectedConnectors: boolean;
};
const AddButton = ({ options, onSelected, hasSelectedConnectors }: Props) => {
if (options.length === 0) {
return null;
}
const candidates = options.map(({ target, logo, name, connectors }) => ({
value: target,
title: (
<div className={styles.title}>
<div className={styles.logo}>
<img src={logo} alt={target} />
</div>
<UnnamedTrans resource={name} className={styles.name} />
{connectors.length > 1 &&
connectors
.filter(({ enabled }) => enabled)
.map(({ platform }) => (
<div key={platform} className={styles.icon}>
{platform && <ConnectorPlatformIcon platform={platform} />}
</div>
))}
</div>
),
}));
const addSocialConnectorButtonProps: ButtonProps = {
type: 'default',
size: 'medium',
title: 'sign_in_exp.sign_up_and_sign_in.social_sign_in.add_social_connector',
icon: <Plus className={styles.plusIcon} />,
};
const addAnotherButtonProps: ButtonProps = {
type: 'text',
size: 'small',
title: 'general.add_another',
};
return (
<ActionMenu
buttonProps={hasSelectedConnectors ? addAnotherButtonProps : addSocialConnectorButtonProps}
dropdownHorizontalAlign="start"
dropDownClassName={styles.dropdown}
>
{candidates.map(({ value, title }) => (
<DropdownItem
key={value}
onClick={() => {
onSelected(value);
}}
>
{title}
</DropdownItem>
))}
</ActionMenu>
);
};
export default AddButton;

View file

@ -0,0 +1,49 @@
@use '@/scss/underscore' as _;
.item {
display: flex;
align-items: center;
margin: _.unit(2) 0;
.info {
display: flex;
align-items: center;
height: 44px;
width: 100%;
margin-right: _.unit(2);
padding: _.unit(3) _.unit(2);
background-color: var(--color-layer-2);
border-radius: 8px;
cursor: move;
color: var(--color-text);
.draggableIcon {
color: var(--color-text-secondary);
}
.logo {
margin: auto _.unit(3);
width: 20px;
height: 20px;
img {
width: 20px;
height: 20px;
}
}
.name {
font: var(--font-label-large);
}
.icon {
width: 16px;
height: 16px;
object-fit: cover;
margin-left: _.unit(1);
color: var(--color-text-secondary);
}
}
}

View file

@ -0,0 +1,44 @@
import Draggable from '@/assets/images/draggable.svg';
import Minus from '@/assets/images/minus.svg';
import IconButton from '@/components/IconButton';
import UnnamedTrans from '@/components/UnnamedTrans';
import ConnectorPlatformIcon from '@/icons/ConnectorPlatformIcon';
import type { ConnectorGroup } from '@/types/connector';
import * as styles from './SelectedConnectorItem.module.scss';
type Props = {
data: ConnectorGroup;
onDelete: (connectorTarget: string) => void;
};
const SelectedConnectorItem = ({ data: { logo, target, name, connectors }, onDelete }: Props) => {
return (
<div className={styles.item}>
<div className={styles.info}>
<Draggable className={styles.draggableIcon} />
<div className={styles.logo}>
<img src={logo} alt={target} />
</div>
<UnnamedTrans resource={name} className={styles.name} />
{connectors.length > 1 &&
connectors
.filter(({ enabled }) => enabled)
.map(({ platform }) => (
<div key={platform} className={styles.icon}>
{platform && <ConnectorPlatformIcon platform={platform} />}
</div>
))}
</div>
<IconButton
onClick={() => {
onDelete(target);
}}
>
<Minus />
</IconButton>
</div>
);
};
export default SelectedConnectorItem;

View file

@ -0,0 +1,12 @@
@use '@/scss/underscore' as _;
.setUpHint {
font: var(--font-body-medium);
color: var(--color-text-secondary);
margin-top: _.unit(2);
a {
color: var(--color-text-link);
text-decoration: none;
}
}

View file

@ -0,0 +1,94 @@
import { ConnectorType } from '@logto/schemas';
import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom';
import DragDropProvider from '@/components/Transfer/DragDropProvider';
import DraggableItem from '@/components/Transfer/DraggableItem';
import useConnectorGroups from '@/hooks/use-connector-groups';
import type { ConnectorGroup } from '@/types/connector';
import ConnectorSetupWarning from '../ConnectorSetupWarning';
import AddButton from './AddButton';
import SelectedConnectorItem from './SelectedConnectorItem';
import * as styles from './index.module.scss';
type Props = {
value: string[];
onChange: (value: string[]) => void;
};
const SocialConnectorEditBox = ({ value, onChange }: Props) => {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const { data: connectorData, error } = useConnectorGroups();
if (!connectorData || error) {
return null;
}
const onMoveItem = (dragIndex: number, hoverIndex: number) => {
const dragItem = value[dragIndex];
const hoverItem = value[hoverIndex];
if (!dragItem || !hoverItem) {
return;
}
onChange(
value.map((value_, index) => {
if (index === dragIndex) {
return hoverItem;
}
if (index === hoverIndex) {
return dragItem;
}
return value_;
})
);
};
const selectedConnectorItems = value
.map((connectorTarget) => connectorData.find(({ target }) => target === connectorTarget))
// eslint-disable-next-line unicorn/prefer-native-coercion-functions
.filter((item): item is ConnectorGroup => Boolean(item));
const connectorOptions = connectorData.filter(
({ target, type, enabled }) =>
!value.includes(target) && type === ConnectorType.Social && enabled
);
return (
<div>
<DragDropProvider>
{selectedConnectorItems.map((item, index) => (
<DraggableItem key={item.id} id={item.id} sortIndex={index} moveItem={onMoveItem}>
<SelectedConnectorItem
data={item}
onDelete={(target) => {
onChange(value.filter((connectorTarget) => connectorTarget !== target));
}}
/>
</DraggableItem>
))}
</DragDropProvider>
<AddButton
options={connectorOptions}
hasSelectedConnectors={selectedConnectorItems.length > 0}
onSelected={(target) => {
onChange([...value, target]);
}}
/>
<ConnectorSetupWarning requiredConnectors={[ConnectorType.Social]} />
<div className={styles.setUpHint}>
{t('sign_in_exp.sign_up_and_sign_in.social_sign_in.set_up_hint.not_in_list')}{' '}
<Link to="/connectors/social" target="_blank">
{t('sign_in_exp.sign_up_and_sign_in.social_sign_in.set_up_hint.set_up_more')}
</Link>{' '}
{t('sign_in_exp.sign_up_and_sign_in.social_sign_in.set_up_hint.go_to')}
</div>
</div>
);
};
export default SocialConnectorEditBox;

View file

@ -10,6 +10,12 @@
}
}
.formFieldDescription {
font: var(--font-body-medium);
color: var(--color-text-secondary);
margin-bottom: _.unit(2);
}
.socialOnlyDescription {
margin-left: _.unit(1);
color: var(--color-text-secondary);
@ -20,8 +26,3 @@
margin-top: _.unit(3);
}
}
.signInDescription {
font: var(--font-body-medium);
color: var(--color-text-secondary);
}

View file

@ -6,6 +6,7 @@ import UnsavedChangesAlertModal from '@/components/UnsavedChangesAlertModal';
import type { SignInExperienceForm } from '../../types';
import SignInForm from './SignInForm';
import SignUpForm from './SignUpForm';
import SocialSignInForm from './SocialSignInForm';
type Props = {
defaultData: SignInExperienceForm;
@ -25,6 +26,7 @@ const SignUpAndSignInTab = ({ defaultData, isDataDirty }: Props) => {
<>
<SignUpForm />
<SignInForm />
<SocialSignInForm />
<UnsavedChangesAlertModal hasUnsavedChanges={isDataDirty} />
</>
);

View file

@ -62,6 +62,18 @@ const sign_in_exp = {
verification_code_auth: 'Verification code',
auth_swap_tip: 'Swap to change the priority',
},
social_sign_in: {
title: 'SOCIAL SIGN IN',
social_sign_in: 'Social sign in',
description:
'Users may need to enter required identifier when register through social accounts. This was defined by your sign up identifier.',
add_social_connector: 'Add Social Connector',
set_up_hint: {
not_in_list: 'Not in the list?',
set_up_more: 'Set up more',
go_to: 'social connectors or go to “Connectors” section.',
},
},
},
sign_in_methods: {
title: 'SIGN-IN METHODS',

View file

@ -64,6 +64,18 @@ const sign_in_exp = {
verification_code_auth: 'Verification code', // UNTRANSLATED
auth_swap_tip: 'Swap to change the priority', // UNTRANSLATED
},
social_sign_in: {
title: 'SOCIAL SIGN IN', // UNTRANSLATED
social_sign_in: 'Social sign in', // UNTRANSLATED
description:
'Users may need to enter required identifier when register through social accounts. This was defined by your sign up identifier.', // UNTRANSLATED
add_social_connector: 'Add Social Connector', // UNTRANSLATED
set_up_hint: {
not_in_list: 'Not in the list?', // UNTRANSLATED
set_up_more: 'Set up more', // UNTRANSLATED
go_to: 'social connectors or go to “Connectors” section.', // UNTRANSLATED
},
},
},
sign_in_methods: {
title: 'METHODES DE CONNEXION',

View file

@ -59,6 +59,18 @@ const sign_in_exp = {
verification_code_auth: 'Verification code', // UNTRANSLATED
auth_swap_tip: 'Swap to change the priority', // UNTRANSLATED
},
social_sign_in: {
title: 'SOCIAL SIGN IN', // UNTRANSLATED
social_sign_in: 'Social sign in', // UNTRANSLATED
description:
'Users may need to enter required identifier when register through social accounts. This was defined by your sign up identifier.', // UNTRANSLATED
add_social_connector: 'Add Social Connector', // UNTRANSLATED
set_up_hint: {
not_in_list: 'Not in the list?', // UNTRANSLATED
set_up_more: 'Set up more', // UNTRANSLATED
go_to: 'social connectors or go to “Connectors” section.', // UNTRANSLATED
},
},
},
sign_in_methods: {
title: '로그인 방법',

View file

@ -62,6 +62,18 @@ const sign_in_exp = {
verification_code_auth: 'Verification code', // UNTRANSLATED
auth_swap_tip: 'Swap to change the priority', // UNTRANSLATED
},
social_sign_in: {
title: 'SOCIAL SIGN IN', // UNTRANSLATED
social_sign_in: 'Social sign in', // UNTRANSLATED
description:
'Users may need to enter required identifier when register through social accounts. This was defined by your sign up identifier.', // UNTRANSLATED
add_social_connector: 'Add Social Connector', // UNTRANSLATED
set_up_hint: {
not_in_list: 'Not in the list?', // UNTRANSLATED
set_up_more: 'Set up more', // UNTRANSLATED
go_to: 'social connectors or go to “Connectors” section.', // UNTRANSLATED
},
},
},
sign_in_methods: {
title: 'MÉTODOS DE LOGIN',

View file

@ -63,6 +63,18 @@ const sign_in_exp = {
verification_code_auth: 'Verification code', // UNTRANSLATED
auth_swap_tip: 'Swap to change the priority', // UNTRANSLATED
},
social_sign_in: {
title: 'SOCIAL SIGN IN', // UNTRANSLATED
social_sign_in: 'Social sign in', // UNTRANSLATED
description:
'Users may need to enter required identifier when register through social accounts. This was defined by your sign up identifier.', // UNTRANSLATED
add_social_connector: 'Add Social Connector', // UNTRANSLATED
set_up_hint: {
not_in_list: 'Not in the list?', // UNTRANSLATED
set_up_more: 'Set up more', // UNTRANSLATED
go_to: 'social connectors or go to “Connectors” section.', // UNTRANSLATED
},
},
},
sign_in_methods: {
title: 'OTURUM AÇMA YÖNTEMLERİ',

View file

@ -60,6 +60,18 @@ const sign_in_exp = {
verification_code_auth: 'Verification code', // UNTRANSLATED
auth_swap_tip: 'Swap to change the priority', // UNTRANSLATED
},
social_sign_in: {
title: 'SOCIAL SIGN IN', // UNTRANSLATED
social_sign_in: 'Social sign in', // UNTRANSLATED
description:
'Users may need to enter required identifier when register through social accounts. This was defined by your sign up identifier.', // UNTRANSLATED
add_social_connector: 'Add Social Connector', // UNTRANSLATED
set_up_hint: {
not_in_list: 'Not in the list?', // UNTRANSLATED
set_up_more: 'Set up more', // UNTRANSLATED
go_to: 'social connectors or go to “Connectors” section.', // UNTRANSLATED
},
},
},
sign_in_methods: {
title: '登录方式',