0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-03-31 22:51:25 -05:00

refactor(console): refactor assign role modal (#4503)

This commit is contained in:
Darcy Ye 2023-09-18 10:34:13 +08:00 committed by GitHub
parent 7a3d4fb58c
commit 19e7292f49
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
37 changed files with 695 additions and 253 deletions

View file

@ -1,4 +1,5 @@
import type { User } from '@logto/schemas';
import type { Application, User } from '@logto/schemas';
import { ApplicationType, RoleType } from '@logto/schemas';
import { conditional } from '@silverhand/essentials';
import classNames from 'classnames';
import type { ChangeEvent } from 'react';
@ -16,32 +17,48 @@ import useDebounce from '@/hooks/use-debounce';
import * as transferLayout from '@/scss/transfer.module.scss';
import { buildUrl, formatSearchKeyword } from '@/utils/url';
import SourceUserItem from '../SourceUserItem';
import SourceEntityItem from '../SourceEntityItem';
import * as styles from './index.module.scss';
type Props = {
type Props<T> = {
roleId: string;
onChange: (value: User[]) => void;
selectedUsers: User[];
roleType: RoleType;
onChange: (value: T[]) => void;
selectedEntities: T[];
};
const pageSize = defaultPageSize;
function SourceUsersBox({ roleId, selectedUsers, onChange }: Props) {
function SourceEntitiesBox<T extends User | Application>({
roleId,
roleType,
selectedEntities,
onChange,
}: Props<T>) {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const [page, setPage] = useState(1);
const [keyword, setKeyword] = useState('');
const debounce = useDebounce();
const url = buildUrl('api/users', {
excludeRoleId: roleId,
const commonSearchParams = {
page: String(page),
page_size: String(pageSize),
...conditional(keyword && { search: formatSearchKeyword(keyword) }),
});
};
const { data, error } = useSWR<[User[], number], RequestError>(url);
const { data, error } = useSWR<[Props<T>['selectedEntities'], number], RequestError>(
roleType === RoleType.User
? buildUrl('api/users', {
excludeRoleId: roleId,
...commonSearchParams,
})
: buildUrl(`api/applications`, {
...commonSearchParams,
'search.type': ApplicationType.MachineToMachine,
'mode.type': 'exact',
})
);
const isLoading = !data && !error;
@ -54,7 +71,8 @@ function SourceUsersBox({ roleId, selectedUsers, onChange }: Props) {
});
};
const isUserAdded = (user: User) => selectedUsers.findIndex(({ id }) => id === user.id) >= 0;
const isEntityAdded = (entity: User | Application) =>
selectedEntities.findIndex(({ id }) => id === entity.id) >= 0;
const isEmpty = !isLoading && !error && dataSource.length === 0;
@ -72,21 +90,28 @@ function SourceUsersBox({ roleId, selectedUsers, onChange }: Props) {
className={classNames(transferLayout.boxContent, isEmpty && transferLayout.emptyBoxContent)}
>
{isEmpty ? (
<EmptyDataPlaceholder size="small" title={t('role_details.users.empty')} />
<EmptyDataPlaceholder
size="small"
title={t(
roleType === RoleType.User
? 'role_details.users.empty'
: 'role_details.applications.empty'
)}
/>
) : (
dataSource.map((user) => {
const isSelected = isUserAdded(user);
dataSource.map((entity) => {
const isSelected = isEntityAdded(entity);
return (
<SourceUserItem
key={user.id}
user={user}
<SourceEntityItem
key={entity.id}
entity={entity}
isSelected={isSelected}
onSelect={() => {
onChange(
isSelected
? selectedUsers.filter(({ id }) => user.id !== id)
: [user, ...selectedUsers]
? selectedEntities.filter(({ id }) => entity.id !== id)
: [entity, ...selectedEntities]
);
}}
/>
@ -107,4 +132,4 @@ function SourceUsersBox({ roleId, selectedUsers, onChange }: Props) {
</div>
);
}
export default SourceUsersBox;
export default SourceEntitiesBox;

View file

@ -7,6 +7,12 @@
cursor: pointer;
user-select: none;
.icon {
width: 20px;
height: 20px;
border-radius: 6px;
}
.title {
flex: 1 1 0;
font: var(--font-body-2);

View file

@ -0,0 +1,55 @@
import type { User, Application } from '@logto/schemas';
import { ApplicationType } from '@logto/schemas';
import ApplicationIcon from '@/components/ApplicationIcon';
import UserAvatar from '@/components/UserAvatar';
import Checkbox from '@/ds-components/Checkbox';
import SuspendedTag from '@/pages/Users/components/SuspendedTag';
import { onKeyDownHandler } from '@/utils/a11y';
import { getUserTitle } from '@/utils/user';
import { isUser } from '../../utils';
import * as styles from './index.module.scss';
type Props<T> = {
entity: T;
isSelected: boolean;
onSelect: () => void;
};
function SourceEntityItem<T extends User | Application>({
entity,
isSelected,
onSelect,
}: Props<T>) {
return (
<div
role="button"
tabIndex={0}
className={styles.item}
onKeyDown={onKeyDownHandler(() => {
onSelect();
})}
onClick={() => {
onSelect();
}}
>
<Checkbox
checked={isSelected}
onChange={() => {
onSelect();
}}
/>
{isUser(entity) ? (
<UserAvatar hasTooltip user={entity} size="micro" />
) : (
<ApplicationIcon type={ApplicationType.MachineToMachine} className={styles.icon} />
)}
<div className={styles.title}>{isUser(entity) ? getUserTitle(entity) : entity.name}</div>
{isUser(entity) && entity.isSuspended && <SuspendedTag className={styles.suspended} />}
</div>
);
}
export default SourceEntityItem;

View file

@ -1,35 +1,35 @@
import type { User } from '@logto/schemas';
import type { User, Application } from '@logto/schemas';
import { useTranslation } from 'react-i18next';
import * as transferLayout from '@/scss/transfer.module.scss';
import TargetUserItem from '../TargetUserItem';
import TargetEntityItem from '../TargetEntityItem';
import * as styles from './index.module.scss';
type Props = {
selectedUsers: User[];
onChange: (value: User[]) => void;
type Props<T> = {
selectedEntities: T[];
onChange: (value: T[]) => void;
};
function TargetUsersBox({ selectedUsers, onChange }: Props) {
function TargetEntitiesBox<T extends User | Application>({ selectedEntities, onChange }: Props<T>) {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
return (
<div className={transferLayout.box}>
<div className={transferLayout.boxTopBar}>
<span className={styles.added}>
{`${selectedUsers.length} `}
{`${selectedEntities.length} `}
{t('general.added')}
</span>
</div>
<div className={transferLayout.boxContent}>
{selectedUsers.map((user) => (
<TargetUserItem
key={user.id}
user={user}
{selectedEntities.map((entity) => (
<TargetEntityItem
key={entity.id}
entity={entity}
onDelete={() => {
onChange(selectedUsers.filter(({ id }) => id !== user.id));
onChange(selectedEntities.filter(({ id }) => id !== entity.id));
}}
/>
))}
@ -38,4 +38,4 @@ function TargetUsersBox({ selectedUsers, onChange }: Props) {
);
}
export default TargetUsersBox;
export default TargetEntitiesBox;

View file

@ -6,6 +6,13 @@
padding: _.unit(2.5) _.unit(4);
user-select: none;
.icon {
width: 20px;
height: 20px;
border-radius: 6px;
color: var(--color-text-secondary);
}
.meta {
flex: 1;
display: flex;
@ -25,10 +32,6 @@
}
}
.icon {
color: var(--color-text-secondary);
}
&:hover {
background: var(--color-hover);
}

View file

@ -0,0 +1,45 @@
import type { User, Application } from '@logto/schemas';
import { ApplicationType } from '@logto/schemas';
import Close from '@/assets/icons/close.svg';
import ApplicationIcon from '@/components/ApplicationIcon';
import UserAvatar from '@/components/UserAvatar';
import IconButton from '@/ds-components/IconButton';
import SuspendedTag from '@/pages/Users/components/SuspendedTag';
import { getUserTitle } from '@/utils/user';
import { isUser } from '../../utils';
import * as styles from './index.module.scss';
type Props<T> = {
entity: T;
onDelete: () => void;
};
function TargetEntityItem<T extends User | Application>({ entity, onDelete }: Props<T>) {
return (
<div className={styles.item}>
<div className={styles.meta}>
{isUser(entity) ? (
<UserAvatar hasTooltip user={entity} size="micro" />
) : (
<ApplicationIcon type={ApplicationType.MachineToMachine} className={styles.icon} />
)}
<div className={styles.title}>{isUser(entity) ? getUserTitle(entity) : entity.name}</div>
{isUser(entity) && entity.isSuspended && <SuspendedTag className={styles.suspended} />}
</div>
<IconButton
size="small"
iconClassName={styles.icon}
onClick={() => {
onDelete();
}}
>
<Close />
</IconButton>
</div>
);
}
export default TargetEntityItem;

View file

@ -1,5 +1,5 @@
@use '@/scss/underscore' as _;
.roleUsersTransfer {
.rolesTransfer {
height: 360px;
}

View file

@ -0,0 +1,37 @@
import type { Application, User, RoleType } from '@logto/schemas';
import classNames from 'classnames';
import * as transferLayout from '@/scss/transfer.module.scss';
import SourceEntitiesBox from './components/SourceEntitiesBox';
import TargetEntitiesBox from './components/TargetEntitiesBox';
import * as styles from './index.module.scss';
type Props<T> = {
roleId: string;
roleType: RoleType;
value: T[];
onChange: (value: T[]) => void;
};
function RoleEntitiesTransfer<T extends User | Application>({
roleId,
roleType,
value,
onChange,
}: Props<T>) {
return (
<div className={classNames(transferLayout.container, styles.rolesTransfer)}>
<SourceEntitiesBox
roleId={roleId}
roleType={roleType}
selectedEntities={value}
onChange={onChange}
/>
<div className={transferLayout.verticalBar} />
<TargetEntitiesBox selectedEntities={value} onChange={onChange} />
</div>
);
}
export default RoleEntitiesTransfer;

View file

@ -0,0 +1,4 @@
import type { User, Application } from '@logto/schemas';
export const isUser = (entity: User | Application): entity is User =>
'customData' in entity || 'identities' in entity;

View file

@ -10,7 +10,7 @@ import useSWR from 'swr';
import Search from '@/assets/icons/search.svg';
import EmptyDataPlaceholder from '@/components/EmptyDataPlaceholder';
import type { DetailedResourceResponse } from '@/components/RoleScopesTransfer/types';
import { isProduction } from '@/consts/env';
import { isDevFeaturesEnabled } from '@/consts/env';
import TextInput from '@/ds-components/TextInput';
import type { RequestError } from '@/hooks/use-api';
import * as transferLayout from '@/scss/transfer.module.scss';
@ -88,7 +88,7 @@ function SourceScopesBox({ roleId, roleType, selectedScopes, onChange }: Props)
.filter(
({ indicator, scopes }) =>
/** Should show management API scopes for machine-to-machine roles */
((!isProduction && roleType === RoleType.MachineToMachine) ||
((isDevFeaturesEnabled && roleType === RoleType.MachineToMachine) ||
!isManagementApi(indicator)) &&
scopes.some(({ id }) => !excludeScopeIds.has(id))
)

View file

@ -1,43 +0,0 @@
import type { User } from '@logto/schemas';
import UserAvatar from '@/components/UserAvatar';
import Checkbox from '@/ds-components/Checkbox';
import SuspendedTag from '@/pages/Users/components/SuspendedTag';
import { onKeyDownHandler } from '@/utils/a11y';
import { getUserTitle } from '@/utils/user';
import * as styles from './index.module.scss';
type Props = {
user: User;
isSelected: boolean;
onSelect: () => void;
};
function SourceUserItem({ user, isSelected, onSelect }: Props) {
return (
<div
role="button"
tabIndex={0}
className={styles.item}
onKeyDown={onKeyDownHandler(() => {
onSelect();
})}
onClick={() => {
onSelect();
}}
>
<Checkbox
checked={isSelected}
onChange={() => {
onSelect();
}}
/>
<UserAvatar hasTooltip user={user} size="micro" />
<div className={styles.title}>{getUserTitle(user)}</div>
{user.isSuspended && <SuspendedTag className={styles.suspended} />}
</div>
);
}
export default SourceUserItem;

View file

@ -1,37 +0,0 @@
import type { User } from '@logto/schemas';
import Close from '@/assets/icons/close.svg';
import UserAvatar from '@/components/UserAvatar';
import IconButton from '@/ds-components/IconButton';
import SuspendedTag from '@/pages/Users/components/SuspendedTag';
import { getUserTitle } from '@/utils/user';
import * as styles from './index.module.scss';
type Props = {
user: User;
onDelete: () => void;
};
function TargetUserItem({ user, onDelete }: Props) {
return (
<div className={styles.item}>
<div className={styles.meta}>
<UserAvatar hasTooltip user={user} size="micro" />
<div className={styles.title}>{getUserTitle(user)}</div>
{user.isSuspended && <SuspendedTag className={styles.suspended} />}
</div>
<IconButton
size="small"
iconClassName={styles.icon}
onClick={() => {
onDelete();
}}
>
<Close />
</IconButton>
</div>
);
}
export default TargetUserItem;

View file

@ -1,26 +0,0 @@
import type { User } from '@logto/schemas';
import classNames from 'classnames';
import * as transferLayout from '@/scss/transfer.module.scss';
import SourceUsersBox from './components/SourceUsersBox';
import TargetUsersBox from './components/TargetUsersBox';
import * as styles from './index.module.scss';
type Props = {
roleId: string;
value: User[];
onChange: (value: User[]) => void;
};
function RoleUsersTransfer({ roleId, value, onChange }: Props) {
return (
<div className={classNames(transferLayout.container, styles.roleUsersTransfer)}>
<SourceUsersBox roleId={roleId} selectedUsers={value} onChange={onChange} />
<div className={transferLayout.verticalBar} />
<TargetUsersBox selectedUsers={value} onChange={onChange} />
</div>
);
}
export default RoleUsersTransfer;

View file

@ -1,5 +1,6 @@
import { yes } from '@silverhand/essentials';
// eslint-disable-next-line import/no-unused-modules
export const isProduction = process.env.NODE_ENV === 'production';
export const isCloud = yes(process.env.IS_CLOUD);
export const adminEndpoint = process.env.ADMIN_ENDPOINT;

View file

@ -1,98 +0,0 @@
import type { User } from '@logto/schemas';
import { useState } from 'react';
import { toast } from 'react-hot-toast';
import { useTranslation } from 'react-i18next';
import ReactModal from 'react-modal';
import RoleUsersTransfer from '@/components/RoleUsersTransfer';
import Button from '@/ds-components/Button';
import FormField from '@/ds-components/FormField';
import ModalLayout from '@/ds-components/ModalLayout';
import useApi from '@/hooks/use-api';
import * as modalStyles from '@/scss/modal.module.scss';
type Props = {
roleId: string;
isRemindSkip?: boolean;
onClose: (success?: boolean) => void;
};
function AssignUsersModal({ roleId, isRemindSkip = false, onClose }: Props) {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const [isLoading, setIsLoading] = useState(false);
const [users, setUsers] = useState<User[]>([]);
const api = useApi();
const handleAssign = async () => {
if (isLoading || users.length === 0) {
return;
}
setIsLoading(true);
try {
await api.post(`api/roles/${roleId}/users`, {
json: { userIds: users.map(({ id }) => id) },
});
toast.success(t('role_details.users.users_assigned'));
onClose(true);
} finally {
setIsLoading(false);
}
};
return (
<ReactModal
isOpen
shouldCloseOnEsc
className={modalStyles.content}
overlayClassName={modalStyles.overlay}
onRequestClose={() => {
onClose();
}}
>
<ModalLayout
title="role_details.users.assign_title"
subtitle="role_details.users.assign_subtitle"
size="large"
footer={
<>
<Button
isLoading={isLoading}
htmlType="submit"
title={isRemindSkip ? 'general.skip_for_now' : 'general.cancel'}
size="large"
type="default"
onClick={() => {
onClose();
}}
/>
<Button
isLoading={isLoading}
disabled={users.length === 0}
htmlType="submit"
title="role_details.users.confirm_assign"
size="large"
type="primary"
onClick={handleAssign}
/>
</>
}
onClose={onClose}
>
<FormField title="role_details.users.assign_users_field">
<RoleUsersTransfer
roleId={roleId}
value={users}
onChange={(value) => {
setUsers(value);
}}
/>
</FormField>
</ModalLayout>
</ReactModal>
);
}
export default AssignUsersModal;

View file

@ -1,4 +1,4 @@
import type { User } from '@logto/schemas';
import { RoleType, type User } from '@logto/schemas';
import { conditional } from '@silverhand/essentials';
import { useState } from 'react';
import { toast } from 'react-hot-toast';
@ -23,13 +23,13 @@ import { Tooltip } from '@/ds-components/Tip';
import type { RequestError } from '@/hooks/use-api';
import useApi from '@/hooks/use-api';
import useSearchParametersWatcher from '@/hooks/use-search-parameters-watcher';
import AssignRoleModal from '@/pages/Roles/components/AssignRoleModal';
import SuspendedTag from '@/pages/Users/components/SuspendedTag';
import { buildUrl, formatSearchKeyword } from '@/utils/url';
import { getUserTitle, getUserSubtitle } from '@/utils/user';
import type { RoleDetailsOutletContext } from '../types';
import AssignUsersModal from './components/AssignUsersModal';
import * as styles from './index.module.scss';
const pageSize = defaultPageSize;
@ -185,8 +185,9 @@ function RoleUsers() {
</ConfirmModal>
)}
{isAssignModalOpen && (
<AssignUsersModal
<AssignRoleModal
roleId={roleId}
roleType={RoleType.User}
onClose={(success) => {
if (success) {
void mutate();

View file

@ -0,0 +1,133 @@
import { type Application, RoleType, type User } from '@logto/schemas';
import { useState } from 'react';
import { toast } from 'react-hot-toast';
import { useTranslation } from 'react-i18next';
import ReactModal from 'react-modal';
import RoleEntitiesTransfer from '@/components/RoleEntitiesTransfer';
import Button from '@/ds-components/Button';
import FormField from '@/ds-components/FormField';
import ModalLayout from '@/ds-components/ModalLayout';
import useApi from '@/hooks/use-api';
import * as modalStyles from '@/scss/modal.module.scss';
type Props = {
roleId: string;
roleType: RoleType;
isRemindSkip?: boolean;
onClose: (success?: boolean) => void;
};
function AssignRoleModal<T extends Application | User>({
roleId,
roleType,
isRemindSkip = false,
onClose,
}: Props) {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const [isLoading, setIsLoading] = useState(false);
const [entities, setEntities] = useState<T[]>([]);
const api = useApi();
const handleAssign = async () => {
if (isLoading || entities.length === 0) {
return;
}
setIsLoading(true);
try {
await (roleType === RoleType.User
? api.post(`api/roles/${roleId}/users`, {
json: { userIds: entities.map(({ id }) => id) },
})
: api.post(`api/roles/${roleId}/applications`, {
json: { applicationIds: entities.map(({ id }) => id) },
}));
toast.success(
t(
roleType === RoleType.User
? 'role_details.users.users_assigned'
: 'role_details.applications.applications_assigned'
)
);
onClose(true);
} finally {
setIsLoading(false);
}
};
return (
<ReactModal
isOpen
shouldCloseOnEsc
className={modalStyles.content}
overlayClassName={modalStyles.overlay}
onRequestClose={() => {
onClose();
}}
>
<ModalLayout
title={
roleType === RoleType.User
? 'role_details.users.assign_title'
: 'role_details.applications.assign_title'
}
subtitle={
roleType === RoleType.User
? 'role_details.users.assign_subtitle'
: 'role_details.applications.assign_subtitle'
}
size="large"
footer={
<>
<Button
isLoading={isLoading}
htmlType="submit"
title={isRemindSkip ? 'general.skip_for_now' : 'general.cancel'}
size="large"
type="default"
onClick={() => {
onClose();
}}
/>
<Button
isLoading={isLoading}
disabled={entities.length === 0}
htmlType="submit"
title={
roleType === RoleType.User
? 'role_details.users.confirm_assign'
: 'role_details.applications.confirm_assign'
}
size="large"
type="primary"
onClick={handleAssign}
/>
</>
}
onClose={onClose}
>
<FormField
title={
roleType === RoleType.User
? 'role_details.users.assign_users_field'
: 'role_details.applications.assign_applications_field'
}
>
<RoleEntitiesTransfer
roleId={roleId}
roleType={roleType}
value={entities}
onChange={(value) => {
setEntities(value);
}}
/>
</FormField>
</ModalLayout>
</ReactModal>
);
}
export default AssignRoleModal;

View file

@ -10,7 +10,7 @@ import ContactUsPhraseLink from '@/components/ContactUsPhraseLink';
import PlanName from '@/components/PlanName';
import QuotaGuardFooter from '@/components/QuotaGuardFooter';
import RoleScopesTransfer from '@/components/RoleScopesTransfer';
import { isProduction } from '@/consts/env';
import { isDevFeaturesEnabled } from '@/consts/env';
import { TenantsContext } from '@/contexts/TenantsProvider';
import Button from '@/ds-components/Button';
import DynamicT from '@/ds-components/DynamicT';
@ -158,7 +158,7 @@ function CreateRoleForm({ totalRoleCount, onClose }: Props) {
error={errors.name?.message}
/>
</FormField>
{!isProduction && (
{isDevFeaturesEnabled && (
<FormField title="roles.role_type">
<Controller
name="type"

View file

@ -1,13 +1,13 @@
import type { Role } from '@logto/schemas';
import { type Role } from '@logto/schemas';
import { useState } from 'react';
import { toast } from 'react-hot-toast';
import { useTranslation } from 'react-i18next';
import ReactModal from 'react-modal';
import useTenantPathname from '@/hooks/use-tenant-pathname';
import AssignUsersModal from '@/pages/RoleDetails/RoleUsers/components/AssignUsersModal';
import * as modalStyles from '@/scss/modal.module.scss';
import AssignRoleModal from '../AssignRoleModal';
import type { Props as CreateRoleFormProps } from '../CreateRoleForm';
import CreateRoleForm from '../CreateRoleForm';
@ -42,9 +42,10 @@ function CreateRoleModal({ totalRoleCount, onClose }: Props) {
onRequestClose={onClose}
>
{createdRole ? (
<AssignUsersModal
<AssignRoleModal
isRemindSkip
roleId={createdRole.id}
roleType={createdRole.type}
onClose={() => {
navigate(`/roles/${createdRole.id}`, { replace: true });
}}

View file

@ -246,7 +246,7 @@ describe('RBAC', () => {
await expectModalWithTitle(page, 'Assign users');
await expect(page).toClick(
'.ReactModalPortal div[class$=roleUsersTransfer] div[class$=item] div[class$=title]',
'.ReactModalPortal div[class$=rolesTransfer] div[class$=item] div[class$=title]',
{
text: rbacTestUsername,
}

View file

@ -46,6 +46,29 @@ const role_details = {
users_assigned: 'Die ausgewählten Benutzer wurden dieser Rolle erfolgreich zugewiesen',
empty: 'Kein Benutzer verfügbar',
},
applications: {
/** UNTRANSLATED */
assign_button: 'Assign applications',
/** UNTRANSLATED */
name_column: 'Application',
/** UNTRANSLATED */
app_column: 'App',
/** UNTRANSLATED */
deleted: '{{name}} was successfully removed from this role',
/** UNTRANSLATED */
assign_title: 'Assign applications',
/** UNTRANSLATED */
assign_subtitle:
'Assign applications to this role. Find appropriate applications by searching name, description or app ID.',
/** UNTRANSLATED */
assign_applications_field: 'Assign applications',
/** UNTRANSLATED */
confirm_assign: 'Assign applications',
/** UNTRANSLATED */
applications_assigned: 'The selected applications were successfully assigned to this role',
/** UNTRANSLATED */
empty: 'No application available',
},
};
export default Object.freeze(role_details);

View file

@ -45,6 +45,19 @@ const role_details = {
users_assigned: 'The selected users were successfully assigned to this role',
empty: 'No user available',
},
applications: {
assign_button: 'Assign applications',
name_column: 'Application',
app_column: 'App',
deleted: '{{name}} was successfully removed from this role',
assign_title: 'Assign applications',
assign_subtitle:
'Assign applications to this role. Find appropriate applications by searching name, description or app ID.',
assign_applications_field: 'Assign applications',
confirm_assign: 'Assign applications',
applications_assigned: 'The selected applications were successfully assigned to this role',
empty: 'No application available',
},
};
export default Object.freeze(role_details);

View file

@ -45,6 +45,29 @@ const role_details = {
users_assigned: 'Los usuarios seleccionados se asignaron correctamente a este rol',
empty: 'No hay usuarios disponibles',
},
applications: {
/** UNTRANSLATED */
assign_button: 'Assign applications',
/** UNTRANSLATED */
name_column: 'Application',
/** UNTRANSLATED */
app_column: 'App',
/** UNTRANSLATED */
deleted: '{{name}} was successfully removed from this role',
/** UNTRANSLATED */
assign_title: 'Assign applications',
/** UNTRANSLATED */
assign_subtitle:
'Assign applications to this role. Find appropriate applications by searching name, description or app ID.',
/** UNTRANSLATED */
assign_applications_field: 'Assign applications',
/** UNTRANSLATED */
confirm_assign: 'Assign applications',
/** UNTRANSLATED */
applications_assigned: 'The selected applications were successfully assigned to this role',
/** UNTRANSLATED */
empty: 'No application available',
},
};
export default Object.freeze(role_details);

View file

@ -45,6 +45,29 @@ const role_details = {
users_assigned: 'Les utilisateurs sélectionnés ont été attribués avec succès à ce rôle',
empty: 'Aucun utilisateur disponible',
},
applications: {
/** UNTRANSLATED */
assign_button: 'Assign applications',
/** UNTRANSLATED */
name_column: 'Application',
/** UNTRANSLATED */
app_column: 'App',
/** UNTRANSLATED */
deleted: '{{name}} was successfully removed from this role',
/** UNTRANSLATED */
assign_title: 'Assign applications',
/** UNTRANSLATED */
assign_subtitle:
'Assign applications to this role. Find appropriate applications by searching name, description or app ID.',
/** UNTRANSLATED */
assign_applications_field: 'Assign applications',
/** UNTRANSLATED */
confirm_assign: 'Assign applications',
/** UNTRANSLATED */
applications_assigned: 'The selected applications were successfully assigned to this role',
/** UNTRANSLATED */
empty: 'No application available',
},
};
export default Object.freeze(role_details);

View file

@ -46,6 +46,29 @@ const role_details = {
users_assigned: 'Gli utenti selezionati sono stati assegnati con successo a questo ruolo',
empty: 'Nessun utente disponibile',
},
applications: {
/** UNTRANSLATED */
assign_button: 'Assign applications',
/** UNTRANSLATED */
name_column: 'Application',
/** UNTRANSLATED */
app_column: 'App',
/** UNTRANSLATED */
deleted: '{{name}} was successfully removed from this role',
/** UNTRANSLATED */
assign_title: 'Assign applications',
/** UNTRANSLATED */
assign_subtitle:
'Assign applications to this role. Find appropriate applications by searching name, description or app ID.',
/** UNTRANSLATED */
assign_applications_field: 'Assign applications',
/** UNTRANSLATED */
confirm_assign: 'Assign applications',
/** UNTRANSLATED */
applications_assigned: 'The selected applications were successfully assigned to this role',
/** UNTRANSLATED */
empty: 'No application available',
},
};
export default Object.freeze(role_details);

View file

@ -44,6 +44,29 @@ const role_details = {
users_assigned: '選択したユーザーがこのロールに正常に割り当てられました',
empty: '使用可能なユーザーはありません',
},
applications: {
/** UNTRANSLATED */
assign_button: 'Assign applications',
/** UNTRANSLATED */
name_column: 'Application',
/** UNTRANSLATED */
app_column: 'App',
/** UNTRANSLATED */
deleted: '{{name}} was successfully removed from this role',
/** UNTRANSLATED */
assign_title: 'Assign applications',
/** UNTRANSLATED */
assign_subtitle:
'Assign applications to this role. Find appropriate applications by searching name, description or app ID.',
/** UNTRANSLATED */
assign_applications_field: 'Assign applications',
/** UNTRANSLATED */
confirm_assign: 'Assign applications',
/** UNTRANSLATED */
applications_assigned: 'The selected applications were successfully assigned to this role',
/** UNTRANSLATED */
empty: 'No application available',
},
};
export default Object.freeze(role_details);

View file

@ -44,6 +44,29 @@ const role_details = {
users_assigned: '선택된 이용자들이 이 역할에 성공적으로 할당되었어요.',
empty: '사용자 없음',
},
applications: {
/** UNTRANSLATED */
assign_button: 'Assign applications',
/** UNTRANSLATED */
name_column: 'Application',
/** UNTRANSLATED */
app_column: 'App',
/** UNTRANSLATED */
deleted: '{{name}} was successfully removed from this role',
/** UNTRANSLATED */
assign_title: 'Assign applications',
/** UNTRANSLATED */
assign_subtitle:
'Assign applications to this role. Find appropriate applications by searching name, description or app ID.',
/** UNTRANSLATED */
assign_applications_field: 'Assign applications',
/** UNTRANSLATED */
confirm_assign: 'Assign applications',
/** UNTRANSLATED */
applications_assigned: 'The selected applications were successfully assigned to this role',
/** UNTRANSLATED */
empty: 'No application available',
},
};
export default Object.freeze(role_details);

View file

@ -45,6 +45,29 @@ const role_details = {
users_assigned: 'Wybrani użytkownicy zostali pomyślnie przypisani do tej roli',
empty: 'Brak dostępnych użytkowników',
},
applications: {
/** UNTRANSLATED */
assign_button: 'Assign applications',
/** UNTRANSLATED */
name_column: 'Application',
/** UNTRANSLATED */
app_column: 'App',
/** UNTRANSLATED */
deleted: '{{name}} was successfully removed from this role',
/** UNTRANSLATED */
assign_title: 'Assign applications',
/** UNTRANSLATED */
assign_subtitle:
'Assign applications to this role. Find appropriate applications by searching name, description or app ID.',
/** UNTRANSLATED */
assign_applications_field: 'Assign applications',
/** UNTRANSLATED */
confirm_assign: 'Assign applications',
/** UNTRANSLATED */
applications_assigned: 'The selected applications were successfully assigned to this role',
/** UNTRANSLATED */
empty: 'No application available',
},
};
export default Object.freeze(role_details);

View file

@ -45,6 +45,29 @@ const role_details = {
users_assigned: 'Os usuários selecionados foram atribuídos com sucesso a este papel',
empty: 'Nenhum usuário disponível',
},
applications: {
/** UNTRANSLATED */
assign_button: 'Assign applications',
/** UNTRANSLATED */
name_column: 'Application',
/** UNTRANSLATED */
app_column: 'App',
/** UNTRANSLATED */
deleted: '{{name}} was successfully removed from this role',
/** UNTRANSLATED */
assign_title: 'Assign applications',
/** UNTRANSLATED */
assign_subtitle:
'Assign applications to this role. Find appropriate applications by searching name, description or app ID.',
/** UNTRANSLATED */
assign_applications_field: 'Assign applications',
/** UNTRANSLATED */
confirm_assign: 'Assign applications',
/** UNTRANSLATED */
applications_assigned: 'The selected applications were successfully assigned to this role',
/** UNTRANSLATED */
empty: 'No application available',
},
};
export default Object.freeze(role_details);

View file

@ -45,6 +45,29 @@ const role_details = {
users_assigned: 'Os utilizadores selecionados foram atribuídos com sucesso a esta função',
empty: 'Nenhum utilizador disponível',
},
applications: {
/** UNTRANSLATED */
assign_button: 'Assign applications',
/** UNTRANSLATED */
name_column: 'Application',
/** UNTRANSLATED */
app_column: 'App',
/** UNTRANSLATED */
deleted: '{{name}} was successfully removed from this role',
/** UNTRANSLATED */
assign_title: 'Assign applications',
/** UNTRANSLATED */
assign_subtitle:
'Assign applications to this role. Find appropriate applications by searching name, description or app ID.',
/** UNTRANSLATED */
assign_applications_field: 'Assign applications',
/** UNTRANSLATED */
confirm_assign: 'Assign applications',
/** UNTRANSLATED */
applications_assigned: 'The selected applications were successfully assigned to this role',
/** UNTRANSLATED */
empty: 'No application available',
},
};
export default Object.freeze(role_details);

View file

@ -45,6 +45,29 @@ const role_details = {
users_assigned: 'Выбранные пользователи были успешно назначены на эту роль',
empty: 'Нет доступных пользователей',
},
applications: {
/** UNTRANSLATED */
assign_button: 'Assign applications',
/** UNTRANSLATED */
name_column: 'Application',
/** UNTRANSLATED */
app_column: 'App',
/** UNTRANSLATED */
deleted: '{{name}} was successfully removed from this role',
/** UNTRANSLATED */
assign_title: 'Assign applications',
/** UNTRANSLATED */
assign_subtitle:
'Assign applications to this role. Find appropriate applications by searching name, description or app ID.',
/** UNTRANSLATED */
assign_applications_field: 'Assign applications',
/** UNTRANSLATED */
confirm_assign: 'Assign applications',
/** UNTRANSLATED */
applications_assigned: 'The selected applications were successfully assigned to this role',
/** UNTRANSLATED */
empty: 'No application available',
},
};
export default Object.freeze(role_details);

View file

@ -44,6 +44,29 @@ const role_details = {
users_assigned: 'Seçilen kullanıcılar bu role başarıyla atandı',
empty: 'Mevcut kullanıcı yok',
},
applications: {
/** UNTRANSLATED */
assign_button: 'Assign applications',
/** UNTRANSLATED */
name_column: 'Application',
/** UNTRANSLATED */
app_column: 'App',
/** UNTRANSLATED */
deleted: '{{name}} was successfully removed from this role',
/** UNTRANSLATED */
assign_title: 'Assign applications',
/** UNTRANSLATED */
assign_subtitle:
'Assign applications to this role. Find appropriate applications by searching name, description or app ID.',
/** UNTRANSLATED */
assign_applications_field: 'Assign applications',
/** UNTRANSLATED */
confirm_assign: 'Assign applications',
/** UNTRANSLATED */
applications_assigned: 'The selected applications were successfully assigned to this role',
/** UNTRANSLATED */
empty: 'No application available',
},
};
export default Object.freeze(role_details);

View file

@ -41,6 +41,29 @@ const role_details = {
users_assigned: '所选的用户已成功分配给此角色',
empty: '无可用用户',
},
applications: {
/** UNTRANSLATED */
assign_button: 'Assign applications',
/** UNTRANSLATED */
name_column: 'Application',
/** UNTRANSLATED */
app_column: 'App',
/** UNTRANSLATED */
deleted: '{{name}} was successfully removed from this role',
/** UNTRANSLATED */
assign_title: 'Assign applications',
/** UNTRANSLATED */
assign_subtitle:
'Assign applications to this role. Find appropriate applications by searching name, description or app ID.',
/** UNTRANSLATED */
assign_applications_field: 'Assign applications',
/** UNTRANSLATED */
confirm_assign: 'Assign applications',
/** UNTRANSLATED */
applications_assigned: 'The selected applications were successfully assigned to this role',
/** UNTRANSLATED */
empty: 'No application available',
},
};
export default Object.freeze(role_details);

View file

@ -41,6 +41,29 @@ const role_details = {
users_assigned: '所選的用戶已成功分配給此角色',
empty: '無可用用戶',
},
applications: {
/** UNTRANSLATED */
assign_button: 'Assign applications',
/** UNTRANSLATED */
name_column: 'Application',
/** UNTRANSLATED */
app_column: 'App',
/** UNTRANSLATED */
deleted: '{{name}} was successfully removed from this role',
/** UNTRANSLATED */
assign_title: 'Assign applications',
/** UNTRANSLATED */
assign_subtitle:
'Assign applications to this role. Find appropriate applications by searching name, description or app ID.',
/** UNTRANSLATED */
assign_applications_field: 'Assign applications',
/** UNTRANSLATED */
confirm_assign: 'Assign applications',
/** UNTRANSLATED */
applications_assigned: 'The selected applications were successfully assigned to this role',
/** UNTRANSLATED */
empty: 'No application available',
},
};
export default Object.freeze(role_details);

View file

@ -41,6 +41,29 @@ const role_details = {
users_assigned: '所選的用戶已成功分配給此角色',
empty: '無可用用戶',
},
applications: {
/** UNTRANSLATED */
assign_button: 'Assign applications',
/** UNTRANSLATED */
name_column: 'Application',
/** UNTRANSLATED */
app_column: 'App',
/** UNTRANSLATED */
deleted: '{{name}} was successfully removed from this role',
/** UNTRANSLATED */
assign_title: 'Assign applications',
/** UNTRANSLATED */
assign_subtitle:
'Assign applications to this role. Find appropriate applications by searching name, description or app ID.',
/** UNTRANSLATED */
assign_applications_field: 'Assign applications',
/** UNTRANSLATED */
confirm_assign: 'Assign applications',
/** UNTRANSLATED */
applications_assigned: 'The selected applications were successfully assigned to this role',
/** UNTRANSLATED */
empty: 'No application available',
},
};
export default Object.freeze(role_details);