0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2024-12-16 20:26:19 -05:00

Merge branch 'master' into gao-test-native-esm-2

This commit is contained in:
Gao Sun 2022-12-13 14:51:52 +08:00
commit 7b87a3d2e6
No known key found for this signature in database
GPG key ID: 13EBE123E4773688
42 changed files with 147 additions and 92 deletions

View file

@ -1,5 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M8.00017 1.33325C6.68162 1.33325 5.39269 1.72425 4.29636 2.45679C3.20004 3.18933 2.34555 4.23052 1.84097 5.4487C1.33638 6.66687 1.20436 8.00731 1.4616 9.30052C1.71883 10.5937 2.35377 11.7816 3.28612 12.714C4.21847 13.6463 5.40636 14.2813 6.69956 14.5385C7.99277 14.7957 9.33321 14.6637 10.5514 14.1591C11.7696 13.6545 12.8108 12.8 13.5433 11.7037C14.2758 10.6074 14.6668 9.31846 14.6668 7.99992C14.6668 7.12444 14.4944 6.25753 14.1594 5.4487C13.8243 4.63986 13.3333 3.90493 12.7142 3.28587C12.0952 2.66682 11.3602 2.17575 10.5514 1.84072C9.74255 1.50569 8.87564 1.33325 8.00017 1.33325ZM8.00017 13.3333C6.94533 13.3333 5.91419 13.0205 5.03712 12.4344C4.16006 11.8484 3.47648 11.0154 3.07281 10.0409C2.66914 9.06636 2.56352 7.994 2.76931 6.95944C2.9751 5.92487 3.48305 4.97456 4.22893 4.22868C4.97481 3.4828 5.92512 2.97485 6.95968 2.76906C7.99425 2.56328 9.0666 2.66889 10.0411 3.07256C11.0157 3.47623 11.8486 4.15982 12.4347 5.03688C13.0207 5.91394 13.3335 6.94509 13.3335 7.99992C13.3335 9.41441 12.7716 10.771 11.7714 11.7712C10.7712 12.7713 9.41465 13.3333 8.00017 13.3333ZM10.6668 7.33325H8.66683V5.33325C8.66683 5.15644 8.59659 4.98687 8.47157 4.86185C8.34655 4.73682 8.17698 4.66659 8.00017 4.66659C7.82335 4.66659 7.65379 4.73682 7.52876 4.86185C7.40374 4.98687 7.3335 5.15644 7.3335 5.33325V7.33325H5.3335C5.15669 7.33325 4.98712 7.40349 4.86209 7.52851C4.73707 7.65354 4.66683 7.82311 4.66683 7.99992C4.66683 8.17673 4.73707 8.3463 4.86209 8.47132C4.98712 8.59635 5.15669 8.66659 5.3335 8.66659H7.3335V10.6666C7.3335 10.8434 7.40374 11.013 7.52876 11.138C7.65379 11.263 7.82335 11.3333 8.00017 11.3333C8.17698 11.3333 8.34655 11.263 8.47157 11.138C8.59659 11.013 8.66683 10.8434 8.66683 10.6666V8.66659H10.6668C10.8436 8.66659 11.0132 8.59635 11.1382 8.47132C11.2633 8.3463 11.3335 8.17673 11.3335 7.99992C11.3335 7.82311 11.2633 7.65354 11.1382 7.52851C11.0132 7.40349 10.8436 7.33325 10.6668 7.33325Z"
fill="currentColor" />
<path d="M7.99998 1.33325C6.68144 1.33325 5.39251 1.72425 4.29618 2.45679C3.19985 3.18933 2.34537 4.23052 1.84079 5.4487C1.3362 6.66687 1.20418 8.00731 1.46141 9.30052C1.71865 10.5937 2.35359 11.7816 3.28594 12.714C4.21829 13.6463 5.40617 14.2813 6.69938 14.5385C7.99259 14.7957 9.33303 14.6637 10.5512 14.1591C11.7694 13.6545 12.8106 12.8 13.5431 11.7037C14.2757 10.6074 14.6666 9.31846 14.6666 7.99992C14.6666 7.12444 14.4942 6.25753 14.1592 5.4487C13.8241 4.63986 13.3331 3.90493 12.714 3.28587C12.095 2.66682 11.36 2.17575 10.5512 1.84072C9.74237 1.50569 8.87546 1.33325 7.99998 1.33325V1.33325ZM7.99998 13.3333C6.94515 13.3333 5.914 13.0205 5.03694 12.4344C4.15988 11.8484 3.47629 11.0154 3.07263 10.0409C2.66896 9.06636 2.56334 7.994 2.76913 6.95944C2.97492 5.92487 3.48287 4.97456 4.22875 4.22868C4.97463 3.4828 5.92494 2.97485 6.9595 2.76906C7.99407 2.56328 9.06642 2.66889 10.041 3.07256C11.0155 3.47623 11.8485 4.15982 12.4345 5.03688C13.0205 5.91394 13.3333 6.94509 13.3333 7.99992C13.3333 9.41441 12.7714 10.771 11.7712 11.7712C10.771 12.7713 9.41447 13.3333 7.99998 13.3333V13.3333ZM10.6666 7.33325H8.66665V5.33325C8.66665 5.15644 8.59641 4.98687 8.47139 4.86185C8.34636 4.73682 8.17679 4.66659 7.99998 4.66659C7.82317 4.66659 7.6536 4.73682 7.52858 4.86185C7.40355 4.98687 7.33332 5.15644 7.33332 5.33325V7.33325H5.33332C5.1565 7.33325 4.98694 7.40349 4.86191 7.52851C4.73689 7.65354 4.66665 7.82311 4.66665 7.99992C4.66665 8.17673 4.73689 8.3463 4.86191 8.47132C4.98694 8.59635 5.1565 8.66659 5.33332 8.66659H7.33332V10.6666C7.33332 10.8434 7.40355 11.013 7.52858 11.138C7.6536 11.263 7.82317 11.3333 7.99998 11.3333C8.17679 11.3333 8.34636 11.263 8.47139 11.138C8.59641 11.013 8.66665 10.8434 8.66665 10.6666V8.66659H10.6666C10.8435 8.66659 11.013 8.59635 11.1381 8.47132C11.2631 8.3463 11.3333 8.17673 11.3333 7.99992C11.3333 7.82311 11.2631 7.65354 11.1381 7.52851C11.013 7.40349 10.8435 7.33325 10.6666 7.33325Z" fill="currentColor"/>
</svg>

Before

Width:  |  Height:  |  Size: 2 KiB

After

Width:  |  Height:  |  Size: 2 KiB

View file

@ -40,9 +40,8 @@
}
.icon {
display: block;
width: 20px;
height: 20px;
display: flex;
align-items: center;
&:not(:last-child) {
margin-right: _.unit(2);
@ -62,6 +61,12 @@
&.text {
height: 24px;
}
.icon {
&:not(:last-child) {
margin-right: _.unit(1);
}
}
}
&.medium {

View file

@ -5,6 +5,7 @@ import type { KeyboardEvent } from 'react';
import { useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import CirclePlus from '@/assets/images/circle-plus.svg';
import Minus from '@/assets/images/minus.svg';
import Button from '../Button';
@ -106,6 +107,7 @@ const MultiTextInput = ({
type="text"
title="general.add_another"
className={styles.addAnother}
icon={<CirclePlus />}
onClick={handleAdd}
/>
<ConfirmModal

View file

@ -67,10 +67,13 @@ const ConnectorContent = ({ isDeleted, connectorData, onConnectorUpdated }: Prop
return;
}
const payload = {
config: result.data,
syncProfile: syncProfile === SyncProfileMode.EachSignIn,
};
const payload =
connectorData.type === ConnectorType.Social
? {
config: result.data,
syncProfile: syncProfile === SyncProfileMode.EachSignIn,
}
: { config: result.data };
const standardConnectorPayload = {
...payload,
metadata: { ...metadata, name: { en: metadata.name } },

View file

@ -12,6 +12,7 @@ import FormField from '@/components/FormField';
import TextInput from '@/components/TextInput';
import { Tooltip } from '@/components/Tip';
import useApi from '@/hooks/use-api';
import { onKeyDownHandler } from '@/utilities/a11y';
import { safeParseJson } from '@/utilities/json';
import * as styles from './index.module.scss';
@ -77,7 +78,6 @@ const SenderTester = ({ connectorId, connectorType, config, className }: Props)
<div className={className}>
<div className={styles.fields}>
<FormField
isRequired
title={
isSms ? 'connector_details.test_sms_sender' : 'connector_details.test_email_sender'
}
@ -91,6 +91,7 @@ const SenderTester = ({ connectorId, connectorType, config, className }: Props)
? t('connector_details.test_sms_placeholder')
: t('connector_details.test_email_placeholder')
}
onKeyDown={onKeyDownHandler({ Enter: onSubmit })}
{...register('sendTo', {
required: true,
pattern: {

View file

@ -1,4 +1,5 @@
import type { ConnectorFactoryResponse } from '@logto/schemas';
import { ConnectorType } from '@logto/schemas';
import { useState } from 'react';
import { Controller, useFormContext } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
@ -91,7 +92,7 @@ const ConnectorForm = ({ connector, isAllowEditTarget }: Props) => {
</FormField>
</>
)}
<FormField isRequired title="connectors.guide.config">
<FormField title="connectors.guide.config">
<Controller
name="config"
control={control}
@ -102,16 +103,18 @@ const ConnectorForm = ({ connector, isAllowEditTarget }: Props) => {
)}
/>
</FormField>
<FormField isRequired title="connectors.guide.sync_profile">
<Controller
name="syncProfile"
control={control}
rules={{ required: true }}
render={({ field: { onChange, value } }) => (
<Select options={syncProfileOptions} value={value} onChange={onChange} />
)}
/>
</FormField>
{connector.type === ConnectorType.Social && (
<FormField title="connectors.guide.sync_profile">
<Controller
name="syncProfile"
control={control}
rules={{ required: true }}
render={({ field: { onChange, value } }) => (
<Select options={syncProfileOptions} value={value} onChange={onChange} />
)}
/>
</FormField>
)}
</div>
);
};

View file

@ -6,6 +6,7 @@ import i18next from 'i18next';
import { FormProvider, useForm } from 'react-hook-form';
import { toast } from 'react-hot-toast';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
import Close from '@/assets/images/close.svg';
import Button from '@/components/Button';
@ -30,6 +31,7 @@ type Props = {
const Guide = ({ connector, onClose }: Props) => {
const api = useApi();
const navigate = useNavigate();
const { updateSettings } = useSettings();
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const { id: connectorId, type: connectorType, name, readme, isStandard } = connector;
@ -65,19 +67,24 @@ const Guide = ({ connector, onClose }: Props) => {
const { id: connectorId } = connector;
await api
const basePayload = {
config: result.data,
connectorId,
metadata: conditional(
isStandard && {
...otherData,
name: { en: name },
}
),
};
const payload = isSocialConnector
? { ...basePayload, syncProfile: syncProfile === SyncProfileMode.EachSignIn }
: basePayload;
const createdConnector = await api
.post('/api/connectors', {
json: {
config: result.data,
connectorId,
syncProfile: syncProfile === SyncProfileMode.EachSignIn,
metadata: conditional(
isStandard && {
...otherData,
name: { en: name },
}
),
},
json: payload,
})
.json<ConnectorResponse>();
@ -88,6 +95,7 @@ const Guide = ({ connector, onClose }: Props) => {
onClose();
toast.success(t('general.saved'));
navigate(`/connectors/${createdConnector.id}`);
});
return (

View file

@ -194,7 +194,7 @@ const Preview = ({ signInExperience, className }: Props) => {
style={conditional(
platform === 'desktopWeb' && {
// Set background color to match iframe's background color on both dark and light mode.
backgroundColor: mode === AppearanceMode.DarkMode ? '#2A2C31' : '#e5e1ec',
backgroundColor: mode === AppearanceMode.DarkMode ? '#000' : '#e5e1ec',
}
)}
>

View file

@ -3,6 +3,7 @@ import classNames from 'classnames';
import { useTranslation } from 'react-i18next';
import { snakeCase } from 'snake-case';
import CirclePlus from '@/assets/images/circle-plus.svg';
import Plus from '@/assets/images/plus.svg';
import ActionMenu from '@/components/ActionMenu';
import type { Props as ButtonProps } from '@/components/Button';
@ -33,6 +34,7 @@ const AddButton = ({ options, onSelected, hasSelectedIdentifiers }: Props) => {
type: 'text',
size: 'small',
title: 'general.add_another',
icon: <CirclePlus />,
};
return (

View file

@ -1,5 +1,6 @@
import classNames from 'classnames';
import CirclePlus from '@/assets/images/circle-plus.svg';
import Plus from '@/assets/images/plus.svg';
import ActionMenu from '@/components/ActionMenu';
import type { Props as ButtonProps } from '@/components/Button';
@ -32,6 +33,7 @@ const AddButton = ({ options, onSelected, hasSelectedConnectors }: Props) => {
type: 'text',
size: 'small',
title: 'general.add_another',
icon: <CirclePlus />,
};
return (

View file

@ -15,7 +15,11 @@ export const onKeyDownHandler =
}
if (typeof callback === 'object') {
callback[key]?.(event);
event.preventDefault();
const handler = callback[key];
if (handler) {
handler(event);
event.preventDefault();
}
}
};

View file

@ -307,6 +307,7 @@ describe('user query', () => {
where ${fields.primaryEmail} ilike $1 or ${fields.primaryPhone} ilike $2 or ${
fields.username
} ilike $3 or ${fields.name} ilike $4
order by "created_at" desc
limit $5
offset $6
`;
@ -339,6 +340,7 @@ describe('user query', () => {
and (${fields.primaryEmail} ilike $2 or ${fields.primaryPhone} ilike $3 or ${
fields.username
} ilike $4 or ${fields.name} ilike $5)
order by "created_at" desc
limit $6
offset $7
`;
@ -371,6 +373,7 @@ describe('user query', () => {
where ${fields.primaryEmail} like $1 or ${fields.primaryPhone} like $2 or ${
fields.username
} like $3 or ${fields.name} like $4
order by "created_at" desc
limit $5
offset $6
`;

View file

@ -145,6 +145,7 @@ export const findUsers = async (
select ${sql.join(Object.values(fields), sql`,`)}
from ${table}
${buildUserConditions(search, hideAdminUser, isCaseSensitive)}
order by ${fields.createdAt} desc
limit ${limit}
offset ${offset}
`

View file

@ -188,7 +188,6 @@ describe('connector route', () => {
metadata: { ...mockConnectorFactory.metadata, id: 'connectorId' },
},
]);
countConnectorByConnectorId.mockResolvedValueOnce({ count: 0 });
const response = await connectorRequest.post('/connectors').send({
connectorId: 'id0',
config: { cliend_id: 'client_id', client_secret: 'client_secret' },
@ -255,7 +254,7 @@ describe('connector route', () => {
{
...mockConnectorFactory,
type: ConnectorType.Sms,
metadata: { ...mockConnectorFactory.metadata, id: 'id0', isStandard: true },
metadata: { ...mockConnectorFactory.metadata, id: 'id1' },
},
]);
getLogtoConnectors.mockResolvedValueOnce([
@ -266,20 +265,19 @@ describe('connector route', () => {
...mockLogtoConnector,
},
]);
countConnectorByConnectorId.mockResolvedValueOnce({ count: 0 });
const response = await connectorRequest.post('/connectors').send({
connectorId: 'id0',
connectorId: 'id1',
config: { cliend_id: 'client_id', client_secret: 'client_secret' },
metadata: { target: 'target', name: { en: '' }, logo: '', logoDark: null },
});
expect(response).toHaveProperty('statusCode', 200);
expect(response.body).toMatchObject(
expect.objectContaining({
connectorId: 'id0',
connectorId: 'id1',
config: {
cliend_id: 'client_id',
client_secret: 'client_secret',
},
metadata: { target: 'target' },
})
);
expect(deleteConnectorByIds).toHaveBeenCalledWith(['id']);
@ -347,6 +345,7 @@ describe('connector route', () => {
]);
const response = await connectorRequest.post('/connectors').send({
connectorId: 'id0',
metadata: { target: 'target' },
});
expect(response).toHaveProperty('statusCode', 422);
});

View file

@ -3,7 +3,6 @@ import { emailRegEx, phoneRegEx } from '@logto/core-kit';
import type { ConnectorFactoryResponse, ConnectorResponse } from '@logto/schemas';
import { arbitraryObjectGuard, Connectors, ConnectorType } from '@logto/schemas';
import { buildIdGenerator } from '@logto/shared';
import { conditional } from '@silverhand/essentials';
import cleanDeep from 'clean-deep';
import { object, string } from 'zod';
@ -114,9 +113,9 @@ export default function connectorRoutes<T extends AuthedRouter>(router: T) {
// eslint-disable-next-line complexity
async (ctx, next) => {
const {
body: { connectorId },
body,
body: { connectorId, metadata, config, syncProfile },
} = ctx.guard;
const connectorFactories = await loadConnectorFactories();
const connectorFactory = connectorFactories.find(
({ metadata: { id } }) => id === connectorId
@ -129,6 +128,15 @@ export default function connectorRoutes<T extends AuthedRouter>(router: T) {
});
}
assertThat(
connectorFactory.metadata.isStandard !== true || metadata?.target,
'connector.can_not_modify_target'
);
assertThat(
connectorFactory.metadata.isStandard === true || metadata === undefined,
'connector.cannot_change_metadata_for_non_standard_connector'
);
const { count } = await countConnectorByConnectorId(connectorId);
assertThat(
count === 0 || connectorFactory.metadata.isStandard === true,
@ -140,25 +148,23 @@ export default function connectorRoutes<T extends AuthedRouter>(router: T) {
if (connectorFactory.type === ConnectorType.Social) {
const connectors = await getLogtoConnectors();
const connectorTarget = body.metadata?.target ?? connectorFactory.metadata.target;
assertThat(
!connectors
.filter(({ type }) => type === ConnectorType.Social)
.some(
({ metadata: { target, platform } }) =>
target === connectorTarget && platform === connectorFactory.metadata.platform
target === cleanDeep(metadata)?.target &&
platform === connectorFactory.metadata.platform
),
new RequestError({ code: 'connector.multiple_target_with_same_platform', status: 422 })
);
}
const insertConnectorId = generateConnectorId();
const { metadata, ...rest } = body;
ctx.body = await insertConnector({
id: insertConnectorId,
...conditional(metadata && { metadata: cleanDeep(metadata) }),
...rest,
connectorId,
...cleanDeep({ syncProfile, config, metadata }),
});
/**
@ -194,17 +200,25 @@ export default function connectorRoutes<T extends AuthedRouter>(router: T) {
.pick({ config: true, metadata: true, syncProfile: true })
.partial(),
}),
async (ctx, next) => {
const {
params: { id },
body: { config },
body,
body: { config, metadata, syncProfile },
} = ctx.guard;
const { type, validateConfig } = await getLogtoConnectorById(id);
const { type, validateConfig, metadata: originalMetadata } = await getLogtoConnectorById(id);
if (body.syncProfile) {
assertThat(
originalMetadata.isStandard !== true || metadata?.target === originalMetadata.target,
'connector.can_not_modify_target'
);
assertThat(
originalMetadata.isStandard === true || metadata === undefined,
'connector.cannot_change_metadata_for_non_standard_connector'
);
if (syncProfile) {
assertThat(
type === ConnectorType.Social,
new RequestError({ code: 'connector.invalid_type_for_syncing_profile', status: 422 })
@ -214,12 +228,9 @@ export default function connectorRoutes<T extends AuthedRouter>(router: T) {
if (config) {
validateConfig(config);
}
// Once created, target can not be modified.
assertThat(body.metadata?.target === undefined, 'connector.can_not_modify_target');
const { metadata: databaseMetadata, ...rest } = body;
await updateConnector({
set: databaseMetadata ? { metadata: cleanDeep(databaseMetadata), ...rest } : rest,
set: cleanDeep({ config, metadata, syncProfile }),
where: { id },
jsonbMode: 'replace',
});

View file

@ -120,7 +120,7 @@ describe('connector PATCH routes', () => {
updateConnector.mockResolvedValueOnce({
...mockConnector,
metadata: {
target: 'target',
target: 'connector',
name: { en: 'connector_name', fr: 'connector_name' },
logo: 'new_logo.png',
},
@ -131,6 +131,7 @@ describe('connector PATCH routes', () => {
name: { en: 'connector_name', fr: 'connector_name' },
logo: 'new_logo.png',
logoDark: null,
target: 'connector',
},
});
expect(updateConnector).toHaveBeenCalledWith(
@ -141,6 +142,7 @@ describe('connector PATCH routes', () => {
metadata: {
name: { en: 'connector_name', fr: 'connector_name' },
logo: 'new_logo.png',
target: 'connector',
},
},
jsonbMode: 'replace',
@ -161,24 +163,20 @@ describe('connector PATCH routes', () => {
updateConnector.mockResolvedValueOnce({
...mockConnector,
metadata: {
target: '',
target: 'connector',
name: { en: '' },
logo: '',
logoDark: '',
},
});
const response = await connectorRequest.patch('/connectors/id').send({
metadata: {
name: { en: '' },
logo: '',
logoDark: '',
},
metadata: { target: 'connector', name: { en: '' }, logo: '', logoDark: '' },
});
expect(updateConnector).toHaveBeenCalledWith(
expect.objectContaining({
where: { id: 'id' },
set: {
metadata: {},
metadata: { target: 'connector' },
},
jsonbMode: 'replace',
})

View file

@ -9,22 +9,26 @@ export const getConnector = async (connectorId: string) =>
authedAdminApi.get(`connectors/${connectorId}`).json<ConnectorResponse>();
// FIXME @Darcy: correct use of `id` and `connectorId`.
export const postConnector = async (connectorId: string) =>
export const postConnector = async (connectorId: string, metadata?: Record<string, unknown>) =>
authedAdminApi
.post({
url: `connectors`,
json: { connectorId },
json: { connectorId, metadata },
})
.json<Connector>();
export const deleteConnectorById = async (id: string) =>
authedAdminApi.delete({ url: `connectors/${id}` }).json();
export const updateConnectorConfig = async (connectorId: string, config: Record<string, unknown>) =>
export const updateConnectorConfig = async (
connectorId: string,
config: Record<string, unknown>,
metadata?: Record<string, unknown>
) =>
authedAdminApi
.patch({
url: `connectors/${connectorId}`,
json: { config },
json: { config, metadata },
})
.json<ConnectorResponse>();

View file

@ -71,13 +71,18 @@ test('connector set-up flow', async () => {
/*
* Change to another SMS/Email connector
*/
const { id } = await postConnector(mockStandardEmailConnectorId);
await updateConnectorConfig(id, mockStandardEmailConnectorConfig);
const { id } = await postConnector(mockStandardEmailConnectorId, {
target: 'mock-standard-mail',
}); // TODO [LOG-4862]: update mock connector
await updateConnectorConfig(id, mockStandardEmailConnectorConfig, {
target: 'mock-standard-mail',
}); // TODO [LOG-4862]: update mock connector
connectorIdMap.set(mockStandardEmailConnectorId, id);
const currentConnectors = await listConnectors();
expect(
currentConnectors.some((connector) => connector.connectorId === mockEmailConnectorId)
).toBeFalsy();
connectorIdMap.delete(mockEmailConnectorId);
expect(
currentConnectors.some((connector) => connector.connectorId === mockStandardEmailConnectorId)
).toBeTruthy();
@ -85,7 +90,6 @@ test('connector set-up flow', async () => {
currentConnectors.find((connector) => connector.connectorId === mockStandardEmailConnectorId)
?.config
).toEqual(mockStandardEmailConnectorConfig);
connectorIdMap.delete(mockEmailConnectorId);
/*
* Delete (i.e. disable) a connector

View file

@ -112,6 +112,8 @@ const errors = {
can_not_modify_target: 'The connector target can not be modified.',
multiple_target_with_same_platform:
'You can not have multiple social connectors that have same target and platform.',
cannot_change_metadata_for_non_standard_connector:
"This connector's `metadata` cannot be changed.",
},
passcode: {
phone_email_empty: 'Telefonnummer oder E-Mail darf nicht leer sein.',

View file

@ -33,7 +33,6 @@ const application_details = {
cors_allowed_origins_placeholder: 'https://your.website.de',
cors_allowed_origins_tip:
'Es sind standardmäßig alle Umleitungs-URI Origins erlaubt. Normalerweise ist dieses Feld nicht erforderlich. See the <a>MDN doc</a> for detailed info.', // UNTRANSLATED
add_another: 'Weitere hinzufügen',
id_token_expiration: 'ID Token Ablaufzeit',
refresh_token_expiration: 'Refresh Token Ablaufzeit',
token_endpoint: 'Token Endpoint',

View file

@ -30,7 +30,7 @@ const general = {
copying: 'Kopiere',
copied: 'Kopiert',
required: 'Erforderlich',
add_another: '+ Weitere hinzufügen',
add_another: 'Weitere hinzufügen',
deletion_confirmation: 'Willst du {{title}} wirklich löschen?',
settings_nav: 'Einstellungen',
unsaved_changes_warning:

View file

@ -111,6 +111,8 @@ const errors = {
can_not_modify_target: 'The connector target can not be modified.',
multiple_target_with_same_platform:
'You can not have multiple social connectors that have same target and platform.',
cannot_change_metadata_for_non_standard_connector:
"This connector's `metadata` cannot be changed.",
},
passcode: {
phone_email_empty: 'Both phone and email are empty.',

View file

@ -33,7 +33,6 @@ const application_details = {
cors_allowed_origins_placeholder: 'https://your.website.com',
cors_allowed_origins_tip:
'By default, all the origins of Redirect URIs will be allowed. Usually no action is required for this field. See the <a>MDN doc</a> for detailed info.',
add_another: 'Add Another',
id_token_expiration: 'ID Token expiration',
refresh_token_expiration: 'Refresh Token expiration',
token_endpoint: 'Token Endpoint',

View file

@ -30,7 +30,7 @@ const general = {
copying: 'Copying',
copied: 'Copied',
required: 'Required',
add_another: '+ Add Another',
add_another: 'Add Another',
deletion_confirmation: 'Are you sure you want to delete this {{title}}?',
settings_nav: 'Settings',
unsaved_changes_warning: 'You have made some changes. Are you sure you want to leave this page?',

View file

@ -118,6 +118,8 @@ const errors = {
can_not_modify_target: 'The connector target can not be modified.', // UNTRANSLATED
multiple_target_with_same_platform:
'You can not have multiple social connectors that have same target and platform.', // UNTRANSLATED
cannot_change_metadata_for_non_standard_connector:
"This connector's `metadata` cannot be changed.", // UNTRANSLATED
},
passcode: {
phone_email_empty: "Le téléphone et l'email sont vides.",

View file

@ -33,7 +33,6 @@ const application_details = {
cors_allowed_origins_placeholder: 'https://votre.site.com',
cors_allowed_origins_tip:
"Par défaut, toutes les origines des URI de redirection seront autorisées. En général, aucune action n'est requise pour ce champ. See the <a>MDN doc</a> for detailed info.", // UNTRANSLATED
add_another: 'Ajouter un autre',
id_token_expiration: "Expiration du jeton d'identification",
refresh_token_expiration: "Rafraîchir l'expiration du jeton",
token_endpoint: 'Token Endpoint',

View file

@ -30,7 +30,7 @@ const general = {
copying: 'Copie',
copied: 'Copié',
required: 'Requis',
add_another: '+ Ajouter un autre',
add_another: 'Ajouter un autre',
deletion_confirmation: 'Êtes-vous sûr de vouloir supprimer ce {{title}} ?',
settings_nav: 'Paramètres',
unsaved_changes_warning:

View file

@ -110,6 +110,8 @@ const errors = {
can_not_modify_target: 'The connector target can not be modified.', // UNTRANSLATED
multiple_target_with_same_platform:
'You can not have multiple social connectors that have same target and platform.', // UNTRANSLATED
cannot_change_metadata_for_non_standard_connector:
"This connector's `metadata` cannot be changed.", // UNTRANSLATED
},
passcode: {
phone_email_empty: '휴대전화번호 그리고 이메일이 비어있어요.',

View file

@ -33,7 +33,6 @@ const application_details = {
cors_allowed_origins_placeholder: 'https://your.website.com',
cors_allowed_origins_tip:
'기본으로 모든 리다이렉트의 오리진들은 허용되요. 대체적으로 이 값을 건들 필요는 없어요. See the <a>MDN doc</a> for detailed info.', // UNTRANSLATED
add_another: '새로 추가',
id_token_expiration: 'ID 토큰 만료',
refresh_token_expiration: 'Refresh 토큰 만료',
token_endpoint: '토큰 End-Point',

View file

@ -30,7 +30,7 @@ const general = {
copying: '복사 중',
copied: '복사됨',
required: '필수',
add_another: '+ 새로 추가',
add_another: '새로 추가',
deletion_confirmation: '정말로 {{title}}을/를 삭제할까요?',
settings_nav: '설정',
unsaved_changes_warning: '수정된 내용이 있어요. 정말로 현재 페이지를 벗어날까요?',

View file

@ -115,6 +115,8 @@ const errors = {
can_not_modify_target: 'O destino do conector não pode ser modificado.',
multiple_target_with_same_platform:
'Você não pode ter vários conectores sociais com o mesmo destino e plataforma.',
cannot_change_metadata_for_non_standard_connector:
"This connector's `metadata` cannot be changed.", // UNTRANSLATED
},
passcode: {
phone_email_empty: 'Telefone e e-mail estão vazios.',

View file

@ -33,7 +33,6 @@ const application_details = {
cors_allowed_origins_placeholder: 'https://your.website.com',
cors_allowed_origins_tip:
'Por padrão, todas as origens de URIs de redirecionamento serão permitidas. Normalmente, nenhuma ação é necessária para este campo. See the <a>MDN doc</a> for detailed info.', // UNTRANSLATED
add_another: 'Adicionar outro',
id_token_expiration: 'Expiração do token de ID',
refresh_token_expiration: 'Expiração Refresh Token',
token_endpoint: 'Token Endpoint',

View file

@ -30,7 +30,7 @@ const general = {
copying: 'Copiando',
copied: 'Copiado',
required: 'Obrigatório',
add_another: '+ Adicionar outro',
add_another: 'Adicionar outro',
deletion_confirmation: 'Tem certeza de que deseja excluir este {{title}}?',
settings_nav: 'Configurações',
unsaved_changes_warning:

View file

@ -113,6 +113,8 @@ const errors = {
can_not_modify_target: 'The connector target can not be modified.', // UNTRANSLATED
multiple_target_with_same_platform:
'You can not have multiple social connectors that have same target and platform.', // UNTRANSLATED
cannot_change_metadata_for_non_standard_connector:
"This connector's `metadata` cannot be changed.", // UNTRANSLATED
},
passcode: {
phone_email_empty: 'O campos telefone e email estão vazios.',

View file

@ -33,7 +33,6 @@ const application_details = {
cors_allowed_origins_placeholder: 'https://your.website.com',
cors_allowed_origins_tip:
'Por padrão, todas as origens de redirecionamento serão permitidas. Recomenda-se restringir isto. See the <a>MDN doc</a> for detailed info.', // UNTRANSLATED
add_another: 'Adicionar outro',
id_token_expiration: 'Expiração do token de ID',
refresh_token_expiration: 'Expiração do token de atualização',
token_endpoint: 'Endpoint Token',

View file

@ -30,7 +30,7 @@ const general = {
copying: 'Copiando',
copied: 'Copiado',
required: 'Necessário',
add_another: '+ Adicionar outro',
add_another: 'Adicionar outro',
deletion_confirmation: 'Tem a certeza que deseja eliminar isso {{title}}?',
settings_nav: 'Definições',
unsaved_changes_warning: 'Fez algumas alterações. Tem a certeza que deseja sair desta página?',

View file

@ -112,6 +112,8 @@ const errors = {
can_not_modify_target: 'The connector target can not be modified.', // UNTRANSLATED
multiple_target_with_same_platform:
'You can not have multiple social connectors that have same target and platform.', // UNTRANSLATED
cannot_change_metadata_for_non_standard_connector:
"This connector's `metadata` cannot be changed.", // UNTRANSLATED
},
passcode: {
phone_email_empty: 'Hem telefon hem de e-posta adresi yok.',

View file

@ -33,7 +33,6 @@ const application_details = {
cors_allowed_origins_placeholder: 'https://your.website.com',
cors_allowed_origins_tip:
'Varsayılan olarak, Yönlendirme URIlerinin tüm originlerine izin verilir. Genellikle bu alan için herhangi bir işlem gerekmez. See the <a>MDN doc</a> for detailed info.', // UNTRANSLATED
add_another: 'Bir tane daha ekle',
id_token_expiration: 'ID Token sona erme süresi',
refresh_token_expiration: 'Refresh Token sona erme süresi',
token_endpoint: 'Token bitiş noktası',

View file

@ -30,7 +30,7 @@ const general = {
copying: 'Kopyalanıyor',
copied: 'Kopyalandı',
required: 'Gerekli',
add_another: '+ Bir tane daha ekle',
add_another: 'Bir tane daha ekle',
deletion_confirmation: 'Bu dosyayı silmek istediğinize emin misiniz: {{title}}?',
settings_nav: 'Ayarlar',
unsaved_changes_warning:

View file

@ -103,6 +103,7 @@ const errors = {
invalid_type_for_syncing_profile: '只有社交连接器可以开启用户档案同步。',
can_not_modify_target: '不可修改连接器 target。',
multiple_target_with_same_platform: '不能同时存在多个有相同 target 和平台类型的社交连接器。',
cannot_change_metadata_for_non_standard_connector: '不可配置该连接器的 metadata 参数。',
},
passcode: {
phone_email_empty: '手机号与邮箱地址均为空',

View file

@ -33,7 +33,6 @@ const application_details = {
cors_allowed_origins_placeholder: 'https://your.website.com',
cors_allowed_origins_tip:
'所有 Redirect URI 的 origin 将默认被允许。通常不需要对此字段进行操作。参见 <a>MDN 文档</a>以了解更多',
add_another: '新增',
id_token_expiration: 'ID Token 过期时间',
refresh_token_expiration: 'Refresh Token 过期时间',
token_endpoint: 'Token Endpoint',

View file

@ -30,7 +30,7 @@ const general = {
copying: '复制中',
copied: '已复制',
required: '必填',
add_another: '+ 新增',
add_another: '新增',
deletion_confirmation: '你确定要删除这个 {{title}} 吗?',
settings_nav: '设置',
unsaved_changes_warning: '还有未保存的变更, 确定要离开吗?',