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:
parent
415c24aace
commit
305bbaad2c
16 changed files with 428 additions and 6 deletions
|
@ -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
|
||||
|
|
|
@ -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;
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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} />
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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: '로그인 방법',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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İ',
|
||||
|
|
|
@ -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: '登录方式',
|
||||
|
|
Loading…
Add table
Reference in a new issue