mirror of
https://github.com/logto-io/logto.git
synced 2025-04-07 23:01:25 -05:00
refactor(console,phrases): move permissions and roles to step 2 in org guide (#4858)
This commit is contained in:
parent
73f348af89
commit
a8b164ca54
26 changed files with 455 additions and 472 deletions
|
@ -19,7 +19,7 @@ import TabNav, { TabNavItem } from '@/ds-components/TabNav';
|
|||
import useApi, { type RequestError } from '@/hooks/use-api';
|
||||
import useTenantPathname from '@/hooks/use-tenant-pathname';
|
||||
|
||||
import IntroductionAndPermissions from '../Organizations/Guide/IntroductionAndPermissions';
|
||||
import IntroductionAndPermissions from '../Organizations/Guide/Introduction';
|
||||
|
||||
import Members from './Members';
|
||||
import Settings from './Settings';
|
||||
|
|
|
@ -1,165 +0,0 @@
|
|||
import { type OrganizationRoleWithScopes, Theme, type OrganizationRole } from '@logto/schemas';
|
||||
import classNames from 'classnames';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Controller, useFieldArray, useForm } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import useSWR from 'swr';
|
||||
|
||||
import RbacFeatureDark from '@/assets/icons/rbac-feature-dark.svg';
|
||||
import RbacFeature from '@/assets/icons/rbac-feature.svg';
|
||||
import ActionBar from '@/components/ActionBar';
|
||||
import OrganizationScopesSelect from '@/components/OrganizationScopesSelect';
|
||||
import Button from '@/ds-components/Button';
|
||||
import Card from '@/ds-components/Card';
|
||||
import FormField from '@/ds-components/FormField';
|
||||
import OverlayScrollbar from '@/ds-components/OverlayScrollbar';
|
||||
import { type Option } from '@/ds-components/Select/MultiSelect';
|
||||
import TextInput from '@/ds-components/TextInput';
|
||||
import useApi, { type RequestError } from '@/hooks/use-api';
|
||||
import useTenantPathname from '@/hooks/use-tenant-pathname';
|
||||
import useTheme from '@/hooks/use-theme';
|
||||
import { trySubmitSafe } from '@/utils/form';
|
||||
|
||||
import { organizationRolePath } from '../../RoleModal';
|
||||
import DynamicFormFields from '../DynamicFormFields';
|
||||
import { steps } from '../const';
|
||||
import styles from '../index.module.scss';
|
||||
|
||||
type RoleForm = {
|
||||
roles: Array<Omit<OrganizationRole, 'tenantId' | 'id'> & { scopes: Array<Option<string>> }>;
|
||||
};
|
||||
|
||||
const defaultRoles = { name: '', description: '', scopes: [] };
|
||||
|
||||
function CreateRoles() {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console.organizations.guide' });
|
||||
const theme = useTheme();
|
||||
const Icon = theme === Theme.Light ? RbacFeature : RbacFeatureDark;
|
||||
const { navigate } = useTenantPathname();
|
||||
const api = useApi();
|
||||
const { data, error } = useSWR<OrganizationRoleWithScopes[], RequestError>(
|
||||
'api/organization-roles'
|
||||
);
|
||||
const [keyword, setKeyword] = useState('');
|
||||
|
||||
const {
|
||||
control,
|
||||
register,
|
||||
handleSubmit,
|
||||
reset,
|
||||
formState: { errors, isSubmitting, isDirty },
|
||||
} = useForm<RoleForm>({
|
||||
defaultValues: {
|
||||
roles: [defaultRoles],
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (data?.length) {
|
||||
reset({
|
||||
roles: data.map(({ name, description, scopes }) => ({
|
||||
name,
|
||||
description,
|
||||
scopes: scopes.map(({ id, name }) => ({ value: id, title: name })),
|
||||
})),
|
||||
});
|
||||
}
|
||||
}, [data, reset]);
|
||||
|
||||
const { fields, append, remove } = useFieldArray({ control, name: 'roles' });
|
||||
|
||||
const onSubmit = handleSubmit(
|
||||
trySubmitSafe(async ({ roles }) => {
|
||||
// If user has pre-saved data and no changes, skip submit and go directly to next step
|
||||
if (data?.length && !isDirty) {
|
||||
navigate(`../${steps.createOrganization}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (data?.length) {
|
||||
await Promise.all(data.map(async ({ id }) => api.delete(`${organizationRolePath}/${id}`)));
|
||||
}
|
||||
await Promise.all(
|
||||
roles.map(async ({ name, description, scopes }) => {
|
||||
const { id } = await api
|
||||
.post(organizationRolePath, { json: { name, description } })
|
||||
.json<OrganizationRole>();
|
||||
|
||||
if (scopes.length > 0) {
|
||||
await api.put(`${organizationRolePath}/${id}/scopes`, {
|
||||
json: { organizationScopeIds: scopes.map(({ value }) => value) },
|
||||
});
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
navigate(`../${steps.createOrganization}`);
|
||||
})
|
||||
);
|
||||
|
||||
const onNavigateBack = () => {
|
||||
reset();
|
||||
setKeyword('');
|
||||
navigate(`../${steps.createPermissions}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<OverlayScrollbar className={styles.stepContainer}>
|
||||
<div className={classNames(styles.content)}>
|
||||
<Card className={styles.card}>
|
||||
<Icon className={styles.icon} />
|
||||
<div className={styles.section}>
|
||||
<div className={styles.title}>{t('step_2')}</div>
|
||||
<div className={styles.description}>{t('step_2_description')}</div>
|
||||
</div>
|
||||
<form>
|
||||
<DynamicFormFields
|
||||
isLoading={!data && !error}
|
||||
title="organizations.guide.organization_roles"
|
||||
fields={fields}
|
||||
render={(index) => (
|
||||
<div className={styles.fieldGroup}>
|
||||
<FormField isRequired title="organizations.guide.role_name">
|
||||
<TextInput
|
||||
{...register(`roles.${index}.name`, { required: true })}
|
||||
error={Boolean(errors.roles?.[index]?.name)}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField title="general.description">
|
||||
<TextInput {...register(`roles.${index}.description`)} />
|
||||
</FormField>
|
||||
<FormField title="organizations.guide.permissions">
|
||||
<Controller
|
||||
name={`roles.${index}.scopes`}
|
||||
control={control}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<OrganizationScopesSelect
|
||||
keyword={keyword}
|
||||
setKeyword={setKeyword}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</FormField>
|
||||
</div>
|
||||
)}
|
||||
onAdd={() => {
|
||||
append(defaultRoles);
|
||||
}}
|
||||
onRemove={remove}
|
||||
/>
|
||||
</form>
|
||||
</Card>
|
||||
</div>
|
||||
</OverlayScrollbar>
|
||||
<ActionBar step={2} totalSteps={3}>
|
||||
<Button isLoading={isSubmitting} title="general.next" type="primary" onClick={onSubmit} />
|
||||
<Button title="general.back" onClick={onNavigateBack} />
|
||||
</ActionBar>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default CreateRoles;
|
|
@ -3,13 +3,7 @@ import * as styles from './index.module.scss';
|
|||
|
||||
function Diagram() {
|
||||
return (
|
||||
<svg
|
||||
width="100%"
|
||||
height="auto"
|
||||
viewBox="0 0 762 630"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<svg width="100%" viewBox="0 0 762 630" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="762" height="629.523" fill="none" />
|
||||
<path d="M93.234 89.1221L279.704 89.589" stroke="#8E9192" strokeWidth="0.71719" />
|
||||
<path d="M283.131 89.561L278.11 93.0809L278.11 86.041L283.131 89.561Z" fill="#8E9192" />
|
|
@ -0,0 +1,130 @@
|
|||
import { Theme } from '@logto/schemas';
|
||||
import classNames from 'classnames';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import OrganizationFeatureDark from '@/assets/icons/organization-feature-dark.svg';
|
||||
import OrganizationFeature from '@/assets/icons/organization-feature.svg';
|
||||
import ActionBar from '@/components/ActionBar';
|
||||
import Button from '@/ds-components/Button';
|
||||
import Card from '@/ds-components/Card';
|
||||
import OverlayScrollbar from '@/ds-components/OverlayScrollbar';
|
||||
import useTenantPathname from '@/hooks/use-tenant-pathname';
|
||||
import useTheme from '@/hooks/use-theme';
|
||||
|
||||
import { steps } from '../const';
|
||||
import * as parentStyles from '../index.module.scss';
|
||||
|
||||
import Diagram from './Diagram';
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
const icons = {
|
||||
[Theme.Light]: { OrganizationIcon: OrganizationFeature },
|
||||
[Theme.Dark]: { OrganizationIcon: OrganizationFeatureDark },
|
||||
};
|
||||
|
||||
type Props = {
|
||||
/* True if the guide is in the "Check guide" drawer of organization details page */
|
||||
isReadonly?: boolean;
|
||||
};
|
||||
|
||||
function Introduction({ isReadonly }: Props) {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console.organizations.guide' });
|
||||
const { navigate } = useTenantPathname();
|
||||
const theme = useTheme();
|
||||
const { OrganizationIcon } = icons[theme];
|
||||
|
||||
return (
|
||||
<>
|
||||
<OverlayScrollbar className={parentStyles.stepContainer}>
|
||||
<div className={classNames(parentStyles.content)}>
|
||||
<Card className={parentStyles.card}>
|
||||
<OrganizationIcon className={parentStyles.icon} />
|
||||
<div className={styles.container}>
|
||||
<div className={styles.section}>
|
||||
<div className={styles.title}>{t('introduction.section_1.title')}</div>
|
||||
<div className={styles.description}>{t('introduction.section_1.description')}</div>
|
||||
</div>
|
||||
<div className={styles.title}>{t('introduction.section_2.title')}</div>
|
||||
<div className={styles.section}>
|
||||
<div className={styles.subtitle}>{t('organization_permissions')}</div>
|
||||
<div className={styles.description}>
|
||||
{t('introduction.section_2.organization_permission_description')}
|
||||
</div>
|
||||
<div className={styles.panel}>
|
||||
<div className={styles.header}>{t('organization_permissions')}</div>
|
||||
<div className={styles.body}>
|
||||
<div className={styles.cell}>
|
||||
<div className={styles.cellTitle}>{t('read_resource')}</div>
|
||||
</div>
|
||||
<div className={styles.cell}>
|
||||
<div className={styles.cellTitle}>{t('edit_resource')}</div>
|
||||
</div>
|
||||
<div className={styles.cell}>
|
||||
<div className={styles.cellTitle}>{t('delete_resource')}</div>
|
||||
</div>
|
||||
<div className={styles.cell}>
|
||||
<div className={styles.cellTitle}>{t('ellipsis')}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.section}>
|
||||
<div className={styles.subtitle}>{t('organization_roles')}</div>
|
||||
<div className={styles.description}>
|
||||
{t('introduction.section_2.organization_role_description')}
|
||||
</div>
|
||||
<div className={styles.panel}>
|
||||
<div className={styles.header}>{t('organization_roles')}</div>
|
||||
<div className={styles.body}>
|
||||
<div className={styles.cell}>
|
||||
<div className={styles.cellTitle}>{t('admin')}</div>
|
||||
<div className={styles.items}>
|
||||
<div>{t('read_resource')}</div>
|
||||
<div>{t('edit_resource')}</div>
|
||||
<div>{t('delete_resource')}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.cell}>
|
||||
<div className={styles.cellTitle}>{t('member')}</div>
|
||||
<div className={styles.items}>
|
||||
<div>{t('read_resource')}</div>
|
||||
<div>{t('edit_resource')}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.cell}>
|
||||
<div className={styles.cellTitle}>{t('guest')}</div>
|
||||
<div className={styles.items}>
|
||||
<div>{t('read_resource')}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.cell}>
|
||||
<div className={styles.cellTitle}>{t('ellipsis')}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.section}>
|
||||
<div className={styles.title}>{t('introduction.section_3.title')}</div>
|
||||
<div className={styles.description}>{t('introduction.section_3.description')}</div>
|
||||
<Diagram />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</OverlayScrollbar>
|
||||
{!isReadonly && (
|
||||
<ActionBar step={1} totalSteps={3}>
|
||||
<Button
|
||||
title="general.next"
|
||||
type="primary"
|
||||
onClick={() => {
|
||||
navigate(`../${steps.permissionsAndRoles}`);
|
||||
}}
|
||||
/>
|
||||
</ActionBar>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default Introduction;
|
|
@ -1,83 +0,0 @@
|
|||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import Diagram from './Diagram';
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
function Introduction() {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console.organizations.guide' });
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.section}>
|
||||
<div className={styles.title}>{t('introduction.section_1.title')}</div>
|
||||
<div className={styles.description}>{t('introduction.section_1.description')}</div>
|
||||
</div>
|
||||
<div className={styles.title}>{t('introduction.section_2.title')}</div>
|
||||
<div className={styles.section}>
|
||||
<div className={styles.subtitle}>{t('organization_permissions')}</div>
|
||||
<div className={styles.description}>
|
||||
{t('introduction.section_2.organization_permission_description')}
|
||||
</div>
|
||||
<div className={styles.panel}>
|
||||
<div className={styles.header}>{t('organization_permissions')}</div>
|
||||
<div className={styles.body}>
|
||||
<div className={styles.cell}>
|
||||
<div className={styles.cellTitle}>{t('read_resource')}</div>
|
||||
</div>
|
||||
<div className={styles.cell}>
|
||||
<div className={styles.cellTitle}>{t('edit_resource')}</div>
|
||||
</div>
|
||||
<div className={styles.cell}>
|
||||
<div className={styles.cellTitle}>{t('delete_resource')}</div>
|
||||
</div>
|
||||
<div className={styles.cell}>
|
||||
<div className={styles.cellTitle}>{t('ellipsis')}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.section}>
|
||||
<div className={styles.subtitle}>{t('organization_roles')}</div>
|
||||
<div className={styles.description}>
|
||||
{t('introduction.section_2.organization_role_description')}
|
||||
</div>
|
||||
<div className={styles.panel}>
|
||||
<div className={styles.header}>{t('organization_roles')}</div>
|
||||
<div className={styles.body}>
|
||||
<div className={styles.cell}>
|
||||
<div className={styles.cellTitle}>{t('admin')}</div>
|
||||
<div className={styles.items}>
|
||||
<div>{t('read_resource')}</div>
|
||||
<div>{t('edit_resource')}</div>
|
||||
<div>{t('delete_resource')}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.cell}>
|
||||
<div className={styles.cellTitle}>{t('member')}</div>
|
||||
<div className={styles.items}>
|
||||
<div>{t('read_resource')}</div>
|
||||
<div>{t('edit_resource')}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.cell}>
|
||||
<div className={styles.cellTitle}>{t('guest')}</div>
|
||||
<div className={styles.items}>
|
||||
<div>{t('read_resource')}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.cell}>
|
||||
<div className={styles.cellTitle}>{t('ellipsis')}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.section}>
|
||||
<div className={styles.title}>{t('introduction.section_3.title')}</div>
|
||||
<div className={styles.description}>{t('introduction.section_3.description')}</div>
|
||||
<Diagram />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Introduction;
|
|
@ -1,149 +0,0 @@
|
|||
import { type OrganizationScope, Theme } from '@logto/schemas';
|
||||
import classNames from 'classnames';
|
||||
import { useEffect } from 'react';
|
||||
import { useFieldArray, useForm } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import useSWR from 'swr';
|
||||
|
||||
import OrganizationFeatureDark from '@/assets/icons/organization-feature-dark.svg';
|
||||
import OrganizationFeature from '@/assets/icons/organization-feature.svg';
|
||||
import PermissionFeatureDark from '@/assets/icons/permission-feature-dark.svg';
|
||||
import PermissionFeature from '@/assets/icons/permission-feature.svg';
|
||||
import ActionBar from '@/components/ActionBar';
|
||||
import Button from '@/ds-components/Button';
|
||||
import Card from '@/ds-components/Card';
|
||||
import FormField from '@/ds-components/FormField';
|
||||
import OverlayScrollbar from '@/ds-components/OverlayScrollbar';
|
||||
import TextInput from '@/ds-components/TextInput';
|
||||
import useApi, { type RequestError } from '@/hooks/use-api';
|
||||
import useTenantPathname from '@/hooks/use-tenant-pathname';
|
||||
import useTheme from '@/hooks/use-theme';
|
||||
import { trySubmitSafe } from '@/utils/form';
|
||||
|
||||
import { organizationScopesPath } from '../../PermissionModal';
|
||||
import DynamicFormFields from '../DynamicFormFields';
|
||||
import { steps } from '../const';
|
||||
import * as styles from '../index.module.scss';
|
||||
|
||||
import Introduction from './Introduction';
|
||||
|
||||
const icons = {
|
||||
[Theme.Light]: { OrganizationIcon: OrganizationFeature, PermissionIcon: PermissionFeature },
|
||||
[Theme.Dark]: {
|
||||
OrganizationIcon: OrganizationFeatureDark,
|
||||
PermissionIcon: PermissionFeatureDark,
|
||||
},
|
||||
};
|
||||
|
||||
type PermissionForm = {
|
||||
permissions: Array<Omit<OrganizationScope, 'id' | 'tenantId'>>;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
/* True if the guide is in the "Check guide" drawer of organization details page */
|
||||
isReadonly?: boolean;
|
||||
};
|
||||
|
||||
const defaultPermission = { name: '', description: '' };
|
||||
|
||||
function IntroductionAndPermissions({ isReadonly }: Props) {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console.organizations.guide' });
|
||||
const theme = useTheme();
|
||||
const { OrganizationIcon, PermissionIcon } = icons[theme];
|
||||
const { navigate } = useTenantPathname();
|
||||
const api = useApi();
|
||||
const { data, error } = useSWR<OrganizationScope[], RequestError>('api/organization-scopes');
|
||||
|
||||
const {
|
||||
control,
|
||||
register,
|
||||
handleSubmit,
|
||||
reset,
|
||||
formState: { errors, isSubmitting, isDirty },
|
||||
} = useForm<PermissionForm>({
|
||||
defaultValues: {
|
||||
permissions: [defaultPermission],
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (data?.length) {
|
||||
reset({ permissions: data.map(({ name, description }) => ({ name, description })) });
|
||||
}
|
||||
}, [data, reset]);
|
||||
|
||||
const { fields, append, remove } = useFieldArray({ control, name: 'permissions' });
|
||||
|
||||
const onSubmit = handleSubmit(
|
||||
trySubmitSafe(async ({ permissions }) => {
|
||||
// If user has pre-saved data and no changes, skip submit and go directly to next step
|
||||
if (data?.length && !isDirty) {
|
||||
navigate(`../${steps.createRoles}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (data?.length) {
|
||||
await Promise.all(
|
||||
data.map(async ({ id }) => api.delete(`${organizationScopesPath}/${id}`))
|
||||
);
|
||||
}
|
||||
await Promise.all(
|
||||
permissions.map(async ({ name, description }) => {
|
||||
await api.post(organizationScopesPath, { json: { name, description } });
|
||||
})
|
||||
);
|
||||
|
||||
navigate(`../${steps.createRoles}`);
|
||||
})
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<OverlayScrollbar className={styles.stepContainer}>
|
||||
<div className={classNames(styles.content)}>
|
||||
<Card className={styles.card}>
|
||||
<OrganizationIcon className={styles.icon} />
|
||||
<Introduction />
|
||||
</Card>
|
||||
{!isReadonly && (
|
||||
<Card className={styles.card}>
|
||||
<PermissionIcon className={styles.icon} />
|
||||
<div className={styles.title}>{t('step_1')}</div>
|
||||
<form>
|
||||
<DynamicFormFields
|
||||
isLoading={!data && !error}
|
||||
title="organizations.guide.organization_permissions"
|
||||
fields={fields}
|
||||
render={(index) => (
|
||||
<div className={styles.fieldGroup}>
|
||||
<FormField isRequired title="organizations.guide.permission_name">
|
||||
<TextInput
|
||||
{...register(`permissions.${index}.name`, { required: true })}
|
||||
error={Boolean(errors.permissions?.[index]?.name)}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField title="general.description">
|
||||
<TextInput {...register(`permissions.${index}.description`)} />
|
||||
</FormField>
|
||||
</div>
|
||||
)}
|
||||
onAdd={() => {
|
||||
append(defaultPermission);
|
||||
}}
|
||||
onRemove={remove}
|
||||
/>
|
||||
</form>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
</OverlayScrollbar>
|
||||
{!isReadonly && (
|
||||
<ActionBar step={1} totalSteps={3}>
|
||||
<Button isLoading={isSubmitting} title="general.next" type="primary" onClick={onSubmit} />
|
||||
</ActionBar>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default IntroductionAndPermissions;
|
|
@ -25,8 +25,8 @@ type OrganizationForm = {
|
|||
name: string;
|
||||
};
|
||||
|
||||
function CreateOrganization() {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console.organizations.guide' });
|
||||
function OrganizationInfo() {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console.organizations' });
|
||||
const theme = useTheme();
|
||||
const Icon = theme === Theme.Light ? OrganizationFeature : OrganizationFeatureDark;
|
||||
const { navigate } = useTenantPathname();
|
||||
|
@ -52,7 +52,7 @@ function CreateOrganization() {
|
|||
|
||||
const onNavigateBack = () => {
|
||||
reset();
|
||||
navigate(`../${steps.createRoles}`);
|
||||
navigate(`../${steps.permissionsAndRoles}`);
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -62,19 +62,23 @@ function CreateOrganization() {
|
|||
<Card className={styles.card}>
|
||||
<Icon className={styles.icon} />
|
||||
<div className={styles.section}>
|
||||
<div className={styles.title}>{t('step_3')}</div>
|
||||
<div className={styles.description}>{t('step_3_description')}</div>
|
||||
<div className={styles.title}>{t('guide.step_3')}</div>
|
||||
<div className={styles.description}>{t('guide.step_3_description')}</div>
|
||||
</div>
|
||||
<form>
|
||||
<FormField isRequired title="organizations.guide.organization_name">
|
||||
<TextInput {...register('name', { required: true })} error={Boolean(errors.name)} />
|
||||
<TextInput
|
||||
{...register('name', { required: true })}
|
||||
error={Boolean(errors.name)}
|
||||
placeholder={t('organization_name_placeholder')}
|
||||
/>
|
||||
</FormField>
|
||||
</form>
|
||||
</Card>
|
||||
<Card className={styles.card}>
|
||||
<div className={styles.section}>
|
||||
<div className={styles.title}>{t('more_next_steps')}</div>
|
||||
<div className={styles.subtitle}>{t('add_members')}</div>
|
||||
<div className={styles.title}>{t('guide.more_next_steps')}</div>
|
||||
<div className={styles.subtitle}>{t('guide.add_members')}</div>
|
||||
{/* eslint-disable-next-line no-warning-comments */}
|
||||
{/* TODO: @charles Documentation links will be updated later */}
|
||||
<ul>
|
||||
|
@ -84,11 +88,11 @@ function CreateOrganization() {
|
|||
rel="noopener"
|
||||
href="https://docs.logto.io/docs/tutorials/"
|
||||
>
|
||||
{t('add_members_action')}
|
||||
{t('guide.add_members_action')}
|
||||
</TextLink>
|
||||
</li>
|
||||
</ul>
|
||||
<div className={styles.subtitle}>{t('add_enterprise_connector')}</div>
|
||||
<div className={styles.subtitle}>{t('guide.add_enterprise_connector')}</div>
|
||||
<ul>
|
||||
<li>
|
||||
<TextLink
|
||||
|
@ -96,7 +100,7 @@ function CreateOrganization() {
|
|||
rel="noopener"
|
||||
href="https://docs.logto.io/docs/tutorials/"
|
||||
>
|
||||
{t('add_enterprise_connector_action')}
|
||||
{t('guide.add_enterprise_connector_action')}
|
||||
</TextLink>
|
||||
</li>
|
||||
</ul>
|
||||
|
@ -112,4 +116,4 @@ function CreateOrganization() {
|
|||
);
|
||||
}
|
||||
|
||||
export default CreateOrganization;
|
||||
export default OrganizationInfo;
|
|
@ -0,0 +1,252 @@
|
|||
import {
|
||||
type OrganizationRoleWithScopes,
|
||||
Theme,
|
||||
type OrganizationRole,
|
||||
type OrganizationScope,
|
||||
} from '@logto/schemas';
|
||||
import classNames from 'classnames';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Controller, useFieldArray, useForm } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import useSWR from 'swr';
|
||||
|
||||
import PermissionFeatureDark from '@/assets/icons/permission-feature-dark.svg';
|
||||
import PermissionFeature from '@/assets/icons/permission-feature.svg';
|
||||
import RbacFeatureDark from '@/assets/icons/rbac-feature-dark.svg';
|
||||
import RbacFeature from '@/assets/icons/rbac-feature.svg';
|
||||
import ActionBar from '@/components/ActionBar';
|
||||
import OrganizationScopesSelect from '@/components/OrganizationScopesSelect';
|
||||
import Button from '@/ds-components/Button';
|
||||
import Card from '@/ds-components/Card';
|
||||
import FormField from '@/ds-components/FormField';
|
||||
import OverlayScrollbar from '@/ds-components/OverlayScrollbar';
|
||||
import { type Option } from '@/ds-components/Select/MultiSelect';
|
||||
import TextInput from '@/ds-components/TextInput';
|
||||
import useApi, { type RequestError } from '@/hooks/use-api';
|
||||
import useTenantPathname from '@/hooks/use-tenant-pathname';
|
||||
import useTheme from '@/hooks/use-theme';
|
||||
import { trySubmitSafe } from '@/utils/form';
|
||||
|
||||
import { organizationScopesPath } from '../../PermissionModal';
|
||||
import { organizationRolePath } from '../../RoleModal';
|
||||
import DynamicFormFields from '../DynamicFormFields';
|
||||
import { steps } from '../const';
|
||||
import * as styles from '../index.module.scss';
|
||||
|
||||
type Form = {
|
||||
permissions: Array<Omit<OrganizationScope, 'id' | 'tenantId'>>;
|
||||
roles: Array<Omit<OrganizationRole, 'tenantId' | 'id'> & { scopes: Array<Option<string>> }>;
|
||||
};
|
||||
|
||||
const icons = {
|
||||
[Theme.Light]: { PermissionIcon: PermissionFeature, RbacIcon: RbacFeature },
|
||||
[Theme.Dark]: { PermissionIcon: PermissionFeatureDark, RbacIcon: RbacFeatureDark },
|
||||
};
|
||||
|
||||
const defaultPermission = { name: '', description: '' };
|
||||
const defaultRoles = { name: '', description: '', scopes: [] };
|
||||
|
||||
function PermissionsAndRoles() {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console.organizations' });
|
||||
const theme = useTheme();
|
||||
const { PermissionIcon, RbacIcon } = icons[theme];
|
||||
const { navigate } = useTenantPathname();
|
||||
const api = useApi();
|
||||
const { data: permissionsData, error: permissionsError } = useSWR<
|
||||
OrganizationScope[],
|
||||
RequestError
|
||||
>('api/organization-scopes');
|
||||
const { data: rolesData, error: rolesError } = useSWR<OrganizationRoleWithScopes[], RequestError>(
|
||||
'api/organization-roles'
|
||||
);
|
||||
const [keyword, setKeyword] = useState('');
|
||||
|
||||
const {
|
||||
control,
|
||||
register,
|
||||
handleSubmit,
|
||||
reset,
|
||||
formState: { errors, isSubmitting, isDirty },
|
||||
} = useForm<Form>({
|
||||
defaultValues: {
|
||||
permissions: [defaultPermission],
|
||||
roles: [defaultRoles],
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (permissionsData?.length) {
|
||||
reset({
|
||||
permissions: permissionsData.map(({ name, description }) => ({ name, description })),
|
||||
});
|
||||
}
|
||||
}, [permissionsData, reset]);
|
||||
|
||||
useEffect(() => {
|
||||
if (rolesData?.length) {
|
||||
reset({
|
||||
roles: rolesData.map(({ name, description, scopes }) => ({
|
||||
name,
|
||||
description,
|
||||
scopes: scopes.map(({ id, name }) => ({ value: id, title: name })),
|
||||
})),
|
||||
});
|
||||
}
|
||||
}, [rolesData, reset]);
|
||||
|
||||
const permissionFields = useFieldArray({ control, name: 'permissions' });
|
||||
const roleFields = useFieldArray({ control, name: 'roles' });
|
||||
|
||||
const onSubmit = handleSubmit(
|
||||
trySubmitSafe(async ({ permissions, roles }) => {
|
||||
// If user has pre-saved data but with no changes made this time,
|
||||
// skip form submit and go directly to the next step.
|
||||
if ((Boolean(permissionsData?.length) || Boolean(rolesData?.length)) && !isDirty) {
|
||||
navigate(`../${steps.organizationInfo}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// If there's pre-saved permissions, remove them first
|
||||
if (permissionsData?.length) {
|
||||
await Promise.all(
|
||||
permissionsData.map(async ({ id }) => api.delete(`${organizationScopesPath}/${id}`))
|
||||
);
|
||||
}
|
||||
// Create new permissions
|
||||
if (permissions.length > 0) {
|
||||
await Promise.all(
|
||||
permissions.map(async ({ name, description }) => {
|
||||
await api.post(organizationScopesPath, { json: { name, description } });
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// Remove pre-saved roles
|
||||
if (rolesData?.length) {
|
||||
await Promise.all(
|
||||
rolesData.map(async ({ id }) => api.delete(`${organizationRolePath}/${id}`))
|
||||
);
|
||||
}
|
||||
// Create new roles
|
||||
if (roles.length > 0) {
|
||||
await Promise.all(
|
||||
roles.map(async ({ name, description, scopes }) => {
|
||||
const { id } = await api
|
||||
.post(organizationRolePath, { json: { name, description } })
|
||||
.json<OrganizationRole>();
|
||||
|
||||
if (scopes.length > 0) {
|
||||
await api.put(`${organizationRolePath}/${id}/scopes`, {
|
||||
json: { organizationScopeIds: scopes.map(({ value }) => value) },
|
||||
});
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
navigate(`../${steps.organizationInfo}`);
|
||||
})
|
||||
);
|
||||
|
||||
const onNavigateBack = () => {
|
||||
reset();
|
||||
setKeyword('');
|
||||
navigate(`../${steps.introduction}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<OverlayScrollbar className={styles.stepContainer}>
|
||||
<div className={classNames(styles.content)}>
|
||||
<Card className={styles.card}>
|
||||
<PermissionIcon className={styles.icon} />
|
||||
<div className={styles.title}>{t('guide.step_1')}</div>
|
||||
<form>
|
||||
<DynamicFormFields
|
||||
isLoading={!permissionsData && !permissionsError}
|
||||
title="organizations.guide.organization_permissions"
|
||||
fields={permissionFields.fields}
|
||||
render={(index) => (
|
||||
<div className={styles.fieldGroup}>
|
||||
<FormField title="organizations.guide.permission_name">
|
||||
<TextInput
|
||||
{...register(`permissions.${index}.name`)}
|
||||
error={Boolean(errors.permissions?.[index]?.name)}
|
||||
placeholder="read:appointment"
|
||||
/>
|
||||
</FormField>
|
||||
<FormField title="general.description">
|
||||
<TextInput
|
||||
{...register(`permissions.${index}.description`)}
|
||||
placeholder={t('create_permission_placeholder')}
|
||||
/>
|
||||
</FormField>
|
||||
</div>
|
||||
)}
|
||||
onAdd={() => {
|
||||
permissionFields.append(defaultPermission);
|
||||
}}
|
||||
onRemove={permissionFields.remove}
|
||||
/>
|
||||
</form>
|
||||
</Card>
|
||||
<Card className={styles.card}>
|
||||
<RbacIcon className={styles.icon} />
|
||||
<div className={styles.section}>
|
||||
<div className={styles.title}>{t('guide.step_2')}</div>
|
||||
<div className={styles.description}>{t('guide.step_2_description')}</div>
|
||||
</div>
|
||||
<form>
|
||||
<DynamicFormFields
|
||||
isLoading={!rolesData && !rolesError}
|
||||
title="organizations.guide.organization_roles"
|
||||
fields={roleFields.fields}
|
||||
render={(index) => (
|
||||
<div className={styles.fieldGroup}>
|
||||
<FormField title="organizations.guide.role_name">
|
||||
<TextInput
|
||||
{...register(`roles.${index}.name`)}
|
||||
error={Boolean(errors.roles?.[index]?.name)}
|
||||
placeholder="viewer"
|
||||
/>
|
||||
</FormField>
|
||||
<FormField title="general.description">
|
||||
<TextInput
|
||||
{...register(`roles.${index}.description`)}
|
||||
placeholder={t('create_role_placeholder')}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField title="organizations.guide.permissions">
|
||||
<Controller
|
||||
name={`roles.${index}.scopes`}
|
||||
control={control}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<OrganizationScopesSelect
|
||||
keyword={keyword}
|
||||
setKeyword={setKeyword}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</FormField>
|
||||
</div>
|
||||
)}
|
||||
onAdd={() => {
|
||||
roleFields.append(defaultRoles);
|
||||
}}
|
||||
onRemove={roleFields.remove}
|
||||
/>
|
||||
</form>
|
||||
</Card>
|
||||
</div>
|
||||
</OverlayScrollbar>
|
||||
<ActionBar step={2} totalSteps={3}>
|
||||
<Button isLoading={isSubmitting} title="general.next" type="primary" onClick={onSubmit} />
|
||||
<Button title="general.back" onClick={onNavigateBack} />
|
||||
</ActionBar>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default PermissionsAndRoles;
|
|
@ -1,5 +1,5 @@
|
|||
export const steps = Object.freeze({
|
||||
createPermissions: 'create-permissions',
|
||||
createRoles: 'create-roles',
|
||||
createOrganization: 'create-organization',
|
||||
introduction: 'introduction',
|
||||
permissionsAndRoles: 'permissions-and-roles',
|
||||
organizationInfo: 'organization-info',
|
||||
});
|
||||
|
|
|
@ -6,9 +6,9 @@ import DsModalHeader from '@/ds-components/ModalHeader';
|
|||
import useTenantPathname from '@/hooks/use-tenant-pathname';
|
||||
import * as modalStyles from '@/scss/modal.module.scss';
|
||||
|
||||
import CreateOrganization from './CreateOrganization';
|
||||
import CreateRoles from './CreateRoles';
|
||||
import IntroductionAndPermissions from './IntroductionAndPermissions';
|
||||
import Introduction from './Introduction';
|
||||
import OrganizationInfo from './OrganizationInfo';
|
||||
import PermissionsAndRoles from './PermissionsAndRoles';
|
||||
import { steps } from './const';
|
||||
import * as styles from './index.module.scss';
|
||||
|
||||
|
@ -28,10 +28,10 @@ function Guide() {
|
|||
onClose={onClose}
|
||||
/>
|
||||
<Routes>
|
||||
<Route index element={<Navigate replace to={steps.createPermissions} />} />
|
||||
<Route path={steps.createPermissions} element={<IntroductionAndPermissions />} />
|
||||
<Route path={steps.createRoles} element={<CreateRoles />} />
|
||||
<Route path={steps.createOrganization} element={<CreateOrganization />} />
|
||||
<Route index element={<Navigate replace to={steps.introduction} />} />
|
||||
<Route path={steps.introduction} element={<Introduction />} />
|
||||
<Route path={steps.permissionsAndRoles} element={<PermissionsAndRoles />} />
|
||||
<Route path={steps.organizationInfo} element={<OrganizationInfo />} />
|
||||
</Routes>
|
||||
</div>
|
||||
</Modal>
|
||||
|
|
|
@ -22,7 +22,7 @@ const organizations = {
|
|||
/** UNTRANSLATED */
|
||||
organization_name_placeholder: 'My organization',
|
||||
/** UNTRANSLATED */
|
||||
organization_description_placeholder: 'A brief description of the organization.',
|
||||
organization_description_placeholder: 'A brief description of the organization',
|
||||
/** UNTRANSLATED */
|
||||
organization_permission: 'Organization permission',
|
||||
/** UNTRANSLATED */
|
||||
|
@ -34,7 +34,7 @@ const organizations = {
|
|||
organization_permission_delete_confirm:
|
||||
'If this permission is deleted, all organization roles including this permission will lose this permission, and users who had this permission will lose the access granted by it.',
|
||||
/** UNTRANSLATED */
|
||||
create_permission_placeholder: 'Read appointment history.',
|
||||
create_permission_placeholder: 'Read appointment history',
|
||||
/** UNTRANSLATED */
|
||||
permission: 'Permission',
|
||||
/** UNTRANSLATED */
|
||||
|
@ -52,7 +52,7 @@ const organizations = {
|
|||
/** UNTRANSLATED */
|
||||
role: 'Role',
|
||||
/** UNTRANSLATED */
|
||||
create_role_placeholder: 'Users with view-only permissions.',
|
||||
create_role_placeholder: 'Users with view-only permissions',
|
||||
/** UNTRANSLATED */
|
||||
search_placeholder: 'Search by organization name or ID',
|
||||
/** UNTRANSLATED */
|
||||
|
|
|
@ -11,14 +11,14 @@ const organization = {
|
|||
organization_list_placeholder_text:
|
||||
'Organization is usually used in SaaS or SaaS-like multi-tenancy apps. The Organizations feature allows your B2B customers to better manage their partners and customers, and to customize the ways that end-users access their applications.',
|
||||
organization_name_placeholder: 'My organization',
|
||||
organization_description_placeholder: 'A brief description of the organization.',
|
||||
organization_description_placeholder: 'A brief description of the organization',
|
||||
organization_permission: 'Organization permission',
|
||||
organization_permission_other: 'Organization permissions',
|
||||
organization_permission_description:
|
||||
'Organization permission refers to the authorization to access a resource in the context of organization. An organization permission should be represented as a meaningful string, also serving as the name and unique identifier.',
|
||||
organization_permission_delete_confirm:
|
||||
'If this permission is deleted, all organization roles including this permission will lose this permission, and users who had this permission will lose the access granted by it.',
|
||||
create_permission_placeholder: 'Read appointment history.',
|
||||
create_permission_placeholder: 'Read appointment history',
|
||||
permission: 'Permission',
|
||||
permission_other: 'Permissions',
|
||||
organization_role: 'Organization role',
|
||||
|
@ -28,7 +28,7 @@ const organization = {
|
|||
organization_role_delete_confirm:
|
||||
'Doing so will remove the permissions associated with this role from the affected users and delete the relations among organization roles, members in the organization, and organization permissions.',
|
||||
role: 'Role',
|
||||
create_role_placeholder: 'Users with view-only permissions.',
|
||||
create_role_placeholder: 'Users with view-only permissions',
|
||||
search_placeholder: 'Search by organization name or ID',
|
||||
search_permission_placeholder: 'Type to search and select permissions',
|
||||
search_role_placeholder: 'Type to search and select roles',
|
||||
|
|
|
@ -22,7 +22,7 @@ const organizations = {
|
|||
/** UNTRANSLATED */
|
||||
organization_name_placeholder: 'My organization',
|
||||
/** UNTRANSLATED */
|
||||
organization_description_placeholder: 'A brief description of the organization.',
|
||||
organization_description_placeholder: 'A brief description of the organization',
|
||||
/** UNTRANSLATED */
|
||||
organization_permission: 'Organization permission',
|
||||
/** UNTRANSLATED */
|
||||
|
@ -34,7 +34,7 @@ const organizations = {
|
|||
organization_permission_delete_confirm:
|
||||
'If this permission is deleted, all organization roles including this permission will lose this permission, and users who had this permission will lose the access granted by it.',
|
||||
/** UNTRANSLATED */
|
||||
create_permission_placeholder: 'Read appointment history.',
|
||||
create_permission_placeholder: 'Read appointment history',
|
||||
/** UNTRANSLATED */
|
||||
permission: 'Permission',
|
||||
/** UNTRANSLATED */
|
||||
|
@ -52,7 +52,7 @@ const organizations = {
|
|||
/** UNTRANSLATED */
|
||||
role: 'Role',
|
||||
/** UNTRANSLATED */
|
||||
create_role_placeholder: 'Users with view-only permissions.',
|
||||
create_role_placeholder: 'Users with view-only permissions',
|
||||
/** UNTRANSLATED */
|
||||
search_placeholder: 'Search by organization name or ID',
|
||||
/** UNTRANSLATED */
|
||||
|
|
|
@ -22,7 +22,7 @@ const organizations = {
|
|||
/** UNTRANSLATED */
|
||||
organization_name_placeholder: 'My organization',
|
||||
/** UNTRANSLATED */
|
||||
organization_description_placeholder: 'A brief description of the organization.',
|
||||
organization_description_placeholder: 'A brief description of the organization',
|
||||
/** UNTRANSLATED */
|
||||
organization_permission: 'Organization permission',
|
||||
/** UNTRANSLATED */
|
||||
|
@ -34,7 +34,7 @@ const organizations = {
|
|||
organization_permission_delete_confirm:
|
||||
'If this permission is deleted, all organization roles including this permission will lose this permission, and users who had this permission will lose the access granted by it.',
|
||||
/** UNTRANSLATED */
|
||||
create_permission_placeholder: 'Read appointment history.',
|
||||
create_permission_placeholder: 'Read appointment history',
|
||||
/** UNTRANSLATED */
|
||||
permission: 'Permission',
|
||||
/** UNTRANSLATED */
|
||||
|
@ -52,7 +52,7 @@ const organizations = {
|
|||
/** UNTRANSLATED */
|
||||
role: 'Role',
|
||||
/** UNTRANSLATED */
|
||||
create_role_placeholder: 'Users with view-only permissions.',
|
||||
create_role_placeholder: 'Users with view-only permissions',
|
||||
/** UNTRANSLATED */
|
||||
search_placeholder: 'Search by organization name or ID',
|
||||
/** UNTRANSLATED */
|
||||
|
|
|
@ -22,7 +22,7 @@ const organizations = {
|
|||
/** UNTRANSLATED */
|
||||
organization_name_placeholder: 'My organization',
|
||||
/** UNTRANSLATED */
|
||||
organization_description_placeholder: 'A brief description of the organization.',
|
||||
organization_description_placeholder: 'A brief description of the organization',
|
||||
/** UNTRANSLATED */
|
||||
organization_permission: 'Organization permission',
|
||||
/** UNTRANSLATED */
|
||||
|
@ -34,7 +34,7 @@ const organizations = {
|
|||
organization_permission_delete_confirm:
|
||||
'If this permission is deleted, all organization roles including this permission will lose this permission, and users who had this permission will lose the access granted by it.',
|
||||
/** UNTRANSLATED */
|
||||
create_permission_placeholder: 'Read appointment history.',
|
||||
create_permission_placeholder: 'Read appointment history',
|
||||
/** UNTRANSLATED */
|
||||
permission: 'Permission',
|
||||
/** UNTRANSLATED */
|
||||
|
@ -52,7 +52,7 @@ const organizations = {
|
|||
/** UNTRANSLATED */
|
||||
role: 'Role',
|
||||
/** UNTRANSLATED */
|
||||
create_role_placeholder: 'Users with view-only permissions.',
|
||||
create_role_placeholder: 'Users with view-only permissions',
|
||||
/** UNTRANSLATED */
|
||||
search_placeholder: 'Search by organization name or ID',
|
||||
/** UNTRANSLATED */
|
||||
|
|
|
@ -22,7 +22,7 @@ const organizations = {
|
|||
/** UNTRANSLATED */
|
||||
organization_name_placeholder: 'My organization',
|
||||
/** UNTRANSLATED */
|
||||
organization_description_placeholder: 'A brief description of the organization.',
|
||||
organization_description_placeholder: 'A brief description of the organization',
|
||||
/** UNTRANSLATED */
|
||||
organization_permission: 'Organization permission',
|
||||
/** UNTRANSLATED */
|
||||
|
@ -34,7 +34,7 @@ const organizations = {
|
|||
organization_permission_delete_confirm:
|
||||
'If this permission is deleted, all organization roles including this permission will lose this permission, and users who had this permission will lose the access granted by it.',
|
||||
/** UNTRANSLATED */
|
||||
create_permission_placeholder: 'Read appointment history.',
|
||||
create_permission_placeholder: 'Read appointment history',
|
||||
/** UNTRANSLATED */
|
||||
permission: 'Permission',
|
||||
/** UNTRANSLATED */
|
||||
|
@ -52,7 +52,7 @@ const organizations = {
|
|||
/** UNTRANSLATED */
|
||||
role: 'Role',
|
||||
/** UNTRANSLATED */
|
||||
create_role_placeholder: 'Users with view-only permissions.',
|
||||
create_role_placeholder: 'Users with view-only permissions',
|
||||
/** UNTRANSLATED */
|
||||
search_placeholder: 'Search by organization name or ID',
|
||||
/** UNTRANSLATED */
|
||||
|
|
|
@ -22,7 +22,7 @@ const organizations = {
|
|||
/** UNTRANSLATED */
|
||||
organization_name_placeholder: 'My organization',
|
||||
/** UNTRANSLATED */
|
||||
organization_description_placeholder: 'A brief description of the organization.',
|
||||
organization_description_placeholder: 'A brief description of the organization',
|
||||
/** UNTRANSLATED */
|
||||
organization_permission: 'Organization permission',
|
||||
/** UNTRANSLATED */
|
||||
|
@ -34,7 +34,7 @@ const organizations = {
|
|||
organization_permission_delete_confirm:
|
||||
'If this permission is deleted, all organization roles including this permission will lose this permission, and users who had this permission will lose the access granted by it.',
|
||||
/** UNTRANSLATED */
|
||||
create_permission_placeholder: 'Read appointment history.',
|
||||
create_permission_placeholder: 'Read appointment history',
|
||||
/** UNTRANSLATED */
|
||||
permission: 'Permission',
|
||||
/** UNTRANSLATED */
|
||||
|
@ -52,7 +52,7 @@ const organizations = {
|
|||
/** UNTRANSLATED */
|
||||
role: 'Role',
|
||||
/** UNTRANSLATED */
|
||||
create_role_placeholder: 'Users with view-only permissions.',
|
||||
create_role_placeholder: 'Users with view-only permissions',
|
||||
/** UNTRANSLATED */
|
||||
search_placeholder: 'Search by organization name or ID',
|
||||
/** UNTRANSLATED */
|
||||
|
|
|
@ -22,7 +22,7 @@ const organizations = {
|
|||
/** UNTRANSLATED */
|
||||
organization_name_placeholder: 'My organization',
|
||||
/** UNTRANSLATED */
|
||||
organization_description_placeholder: 'A brief description of the organization.',
|
||||
organization_description_placeholder: 'A brief description of the organization',
|
||||
/** UNTRANSLATED */
|
||||
organization_permission: 'Organization permission',
|
||||
/** UNTRANSLATED */
|
||||
|
@ -34,7 +34,7 @@ const organizations = {
|
|||
organization_permission_delete_confirm:
|
||||
'If this permission is deleted, all organization roles including this permission will lose this permission, and users who had this permission will lose the access granted by it.',
|
||||
/** UNTRANSLATED */
|
||||
create_permission_placeholder: 'Read appointment history.',
|
||||
create_permission_placeholder: 'Read appointment history',
|
||||
/** UNTRANSLATED */
|
||||
permission: 'Permission',
|
||||
/** UNTRANSLATED */
|
||||
|
@ -52,7 +52,7 @@ const organizations = {
|
|||
/** UNTRANSLATED */
|
||||
role: 'Role',
|
||||
/** UNTRANSLATED */
|
||||
create_role_placeholder: 'Users with view-only permissions.',
|
||||
create_role_placeholder: 'Users with view-only permissions',
|
||||
/** UNTRANSLATED */
|
||||
search_placeholder: 'Search by organization name or ID',
|
||||
/** UNTRANSLATED */
|
||||
|
|
|
@ -22,7 +22,7 @@ const organizations = {
|
|||
/** UNTRANSLATED */
|
||||
organization_name_placeholder: 'My organization',
|
||||
/** UNTRANSLATED */
|
||||
organization_description_placeholder: 'A brief description of the organization.',
|
||||
organization_description_placeholder: 'A brief description of the organization',
|
||||
/** UNTRANSLATED */
|
||||
organization_permission: 'Organization permission',
|
||||
/** UNTRANSLATED */
|
||||
|
@ -34,7 +34,7 @@ const organizations = {
|
|||
organization_permission_delete_confirm:
|
||||
'If this permission is deleted, all organization roles including this permission will lose this permission, and users who had this permission will lose the access granted by it.',
|
||||
/** UNTRANSLATED */
|
||||
create_permission_placeholder: 'Read appointment history.',
|
||||
create_permission_placeholder: 'Read appointment history',
|
||||
/** UNTRANSLATED */
|
||||
permission: 'Permission',
|
||||
/** UNTRANSLATED */
|
||||
|
@ -52,7 +52,7 @@ const organizations = {
|
|||
/** UNTRANSLATED */
|
||||
role: 'Role',
|
||||
/** UNTRANSLATED */
|
||||
create_role_placeholder: 'Users with view-only permissions.',
|
||||
create_role_placeholder: 'Users with view-only permissions',
|
||||
/** UNTRANSLATED */
|
||||
search_placeholder: 'Search by organization name or ID',
|
||||
/** UNTRANSLATED */
|
||||
|
|
|
@ -22,7 +22,7 @@ const organizations = {
|
|||
/** UNTRANSLATED */
|
||||
organization_name_placeholder: 'My organization',
|
||||
/** UNTRANSLATED */
|
||||
organization_description_placeholder: 'A brief description of the organization.',
|
||||
organization_description_placeholder: 'A brief description of the organization',
|
||||
/** UNTRANSLATED */
|
||||
organization_permission: 'Organization permission',
|
||||
/** UNTRANSLATED */
|
||||
|
@ -34,7 +34,7 @@ const organizations = {
|
|||
organization_permission_delete_confirm:
|
||||
'If this permission is deleted, all organization roles including this permission will lose this permission, and users who had this permission will lose the access granted by it.',
|
||||
/** UNTRANSLATED */
|
||||
create_permission_placeholder: 'Read appointment history.',
|
||||
create_permission_placeholder: 'Read appointment history',
|
||||
/** UNTRANSLATED */
|
||||
permission: 'Permission',
|
||||
/** UNTRANSLATED */
|
||||
|
@ -52,7 +52,7 @@ const organizations = {
|
|||
/** UNTRANSLATED */
|
||||
role: 'Role',
|
||||
/** UNTRANSLATED */
|
||||
create_role_placeholder: 'Users with view-only permissions.',
|
||||
create_role_placeholder: 'Users with view-only permissions',
|
||||
/** UNTRANSLATED */
|
||||
search_placeholder: 'Search by organization name or ID',
|
||||
/** UNTRANSLATED */
|
||||
|
|
|
@ -22,7 +22,7 @@ const organizations = {
|
|||
/** UNTRANSLATED */
|
||||
organization_name_placeholder: 'My organization',
|
||||
/** UNTRANSLATED */
|
||||
organization_description_placeholder: 'A brief description of the organization.',
|
||||
organization_description_placeholder: 'A brief description of the organization',
|
||||
/** UNTRANSLATED */
|
||||
organization_permission: 'Organization permission',
|
||||
/** UNTRANSLATED */
|
||||
|
@ -34,7 +34,7 @@ const organizations = {
|
|||
organization_permission_delete_confirm:
|
||||
'If this permission is deleted, all organization roles including this permission will lose this permission, and users who had this permission will lose the access granted by it.',
|
||||
/** UNTRANSLATED */
|
||||
create_permission_placeholder: 'Read appointment history.',
|
||||
create_permission_placeholder: 'Read appointment history',
|
||||
/** UNTRANSLATED */
|
||||
permission: 'Permission',
|
||||
/** UNTRANSLATED */
|
||||
|
@ -52,7 +52,7 @@ const organizations = {
|
|||
/** UNTRANSLATED */
|
||||
role: 'Role',
|
||||
/** UNTRANSLATED */
|
||||
create_role_placeholder: 'Users with view-only permissions.',
|
||||
create_role_placeholder: 'Users with view-only permissions',
|
||||
/** UNTRANSLATED */
|
||||
search_placeholder: 'Search by organization name or ID',
|
||||
/** UNTRANSLATED */
|
||||
|
|
|
@ -22,7 +22,7 @@ const organizations = {
|
|||
/** UNTRANSLATED */
|
||||
organization_name_placeholder: 'My organization',
|
||||
/** UNTRANSLATED */
|
||||
organization_description_placeholder: 'A brief description of the organization.',
|
||||
organization_description_placeholder: 'A brief description of the organization',
|
||||
/** UNTRANSLATED */
|
||||
organization_permission: 'Organization permission',
|
||||
/** UNTRANSLATED */
|
||||
|
@ -34,7 +34,7 @@ const organizations = {
|
|||
organization_permission_delete_confirm:
|
||||
'If this permission is deleted, all organization roles including this permission will lose this permission, and users who had this permission will lose the access granted by it.',
|
||||
/** UNTRANSLATED */
|
||||
create_permission_placeholder: 'Read appointment history.',
|
||||
create_permission_placeholder: 'Read appointment history',
|
||||
/** UNTRANSLATED */
|
||||
permission: 'Permission',
|
||||
/** UNTRANSLATED */
|
||||
|
@ -52,7 +52,7 @@ const organizations = {
|
|||
/** UNTRANSLATED */
|
||||
role: 'Role',
|
||||
/** UNTRANSLATED */
|
||||
create_role_placeholder: 'Users with view-only permissions.',
|
||||
create_role_placeholder: 'Users with view-only permissions',
|
||||
/** UNTRANSLATED */
|
||||
search_placeholder: 'Search by organization name or ID',
|
||||
/** UNTRANSLATED */
|
||||
|
|
|
@ -22,7 +22,7 @@ const organizations = {
|
|||
/** UNTRANSLATED */
|
||||
organization_name_placeholder: 'My organization',
|
||||
/** UNTRANSLATED */
|
||||
organization_description_placeholder: 'A brief description of the organization.',
|
||||
organization_description_placeholder: 'A brief description of the organization',
|
||||
/** UNTRANSLATED */
|
||||
organization_permission: 'Organization permission',
|
||||
/** UNTRANSLATED */
|
||||
|
@ -34,7 +34,7 @@ const organizations = {
|
|||
organization_permission_delete_confirm:
|
||||
'If this permission is deleted, all organization roles including this permission will lose this permission, and users who had this permission will lose the access granted by it.',
|
||||
/** UNTRANSLATED */
|
||||
create_permission_placeholder: 'Read appointment history.',
|
||||
create_permission_placeholder: 'Read appointment history',
|
||||
/** UNTRANSLATED */
|
||||
permission: 'Permission',
|
||||
/** UNTRANSLATED */
|
||||
|
@ -52,7 +52,7 @@ const organizations = {
|
|||
/** UNTRANSLATED */
|
||||
role: 'Role',
|
||||
/** UNTRANSLATED */
|
||||
create_role_placeholder: 'Users with view-only permissions.',
|
||||
create_role_placeholder: 'Users with view-only permissions',
|
||||
/** UNTRANSLATED */
|
||||
search_placeholder: 'Search by organization name or ID',
|
||||
/** UNTRANSLATED */
|
||||
|
|
|
@ -22,7 +22,7 @@ const organizations = {
|
|||
/** UNTRANSLATED */
|
||||
organization_name_placeholder: 'My organization',
|
||||
/** UNTRANSLATED */
|
||||
organization_description_placeholder: 'A brief description of the organization.',
|
||||
organization_description_placeholder: 'A brief description of the organization',
|
||||
/** UNTRANSLATED */
|
||||
organization_permission: 'Organization permission',
|
||||
/** UNTRANSLATED */
|
||||
|
@ -34,7 +34,7 @@ const organizations = {
|
|||
organization_permission_delete_confirm:
|
||||
'If this permission is deleted, all organization roles including this permission will lose this permission, and users who had this permission will lose the access granted by it.',
|
||||
/** UNTRANSLATED */
|
||||
create_permission_placeholder: 'Read appointment history.',
|
||||
create_permission_placeholder: 'Read appointment history',
|
||||
/** UNTRANSLATED */
|
||||
permission: 'Permission',
|
||||
/** UNTRANSLATED */
|
||||
|
@ -52,7 +52,7 @@ const organizations = {
|
|||
/** UNTRANSLATED */
|
||||
role: 'Role',
|
||||
/** UNTRANSLATED */
|
||||
create_role_placeholder: 'Users with view-only permissions.',
|
||||
create_role_placeholder: 'Users with view-only permissions',
|
||||
/** UNTRANSLATED */
|
||||
search_placeholder: 'Search by organization name or ID',
|
||||
/** UNTRANSLATED */
|
||||
|
|
|
@ -22,7 +22,7 @@ const organizations = {
|
|||
/** UNTRANSLATED */
|
||||
organization_name_placeholder: 'My organization',
|
||||
/** UNTRANSLATED */
|
||||
organization_description_placeholder: 'A brief description of the organization.',
|
||||
organization_description_placeholder: 'A brief description of the organization',
|
||||
/** UNTRANSLATED */
|
||||
organization_permission: 'Organization permission',
|
||||
/** UNTRANSLATED */
|
||||
|
@ -34,7 +34,7 @@ const organizations = {
|
|||
organization_permission_delete_confirm:
|
||||
'If this permission is deleted, all organization roles including this permission will lose this permission, and users who had this permission will lose the access granted by it.',
|
||||
/** UNTRANSLATED */
|
||||
create_permission_placeholder: 'Read appointment history.',
|
||||
create_permission_placeholder: 'Read appointment history',
|
||||
/** UNTRANSLATED */
|
||||
permission: 'Permission',
|
||||
/** UNTRANSLATED */
|
||||
|
@ -52,7 +52,7 @@ const organizations = {
|
|||
/** UNTRANSLATED */
|
||||
role: 'Role',
|
||||
/** UNTRANSLATED */
|
||||
create_role_placeholder: 'Users with view-only permissions.',
|
||||
create_role_placeholder: 'Users with view-only permissions',
|
||||
/** UNTRANSLATED */
|
||||
search_placeholder: 'Search by organization name or ID',
|
||||
/** UNTRANSLATED */
|
||||
|
|
Loading…
Add table
Reference in a new issue