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

feat(console,phrases): implement application permissions assignment 4-4 (#5230)

feat(console): implement application permission assignment modal

implement application permission assignment modal
This commit is contained in:
simeng-li 2024-01-22 10:59:43 +08:00 committed by GitHub
parent beef9c0657
commit 6b49a2e2e3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 697 additions and 76 deletions

View file

@ -28,6 +28,7 @@ type Props<TFieldValues extends FieldValues, TName extends FieldPath<TFieldValue
};
isLoading?: boolean;
onAdd?: () => void;
errorMessage?: string;
};
export const pageSize = 10;
@ -48,6 +49,7 @@ function TemplateTable<
pagination,
isLoading,
onAdd,
errorMessage,
}: Props<TFieldValues, TName>) {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
@ -63,7 +65,7 @@ function TemplateTable<
<DynamicT forKey={name} interpolation={{ count: 2 }} />
</header>
)}
{onAdd && noData && (
{onAdd && noData && !errorMessage && (
<>
{name && (
<div className={styles.empty}>
@ -73,6 +75,7 @@ function TemplateTable<
<Button icon={<Plus />} title="general.add" onClick={onAdd} />
</>
)}
{noData && errorMessage && <div className={styles.empty}>{errorMessage}</div>}
{!noData && (
<Table
hasBorder

View file

@ -22,7 +22,7 @@ import { type DataEntry, type DataGroup, type SelectedDataEntry } from './type';
* @param availableDataGroups - The list of available data groups. (Single level tree form)
*/
type Props<TEntry extends DataEntry> = {
export type Props<TEntry extends DataEntry> = {
title: AdminConsoleKey | ReactElement<typeof DangerousRaw>;
selectedData: Array<SelectedDataEntry<TEntry>>;
setSelectedData: (dataList: Array<SelectedDataEntry<TEntry>>) => void;
@ -61,5 +61,4 @@ function DataTransferBox<TEntry extends DataEntry = DataEntry>({
);
}
// eslint-disable-next-line import/no-unused-modules -- will be used in the following PR
export default DataTransferBox;

View file

@ -0,0 +1,22 @@
import { type AdminConsoleKey } from '@logto/phrases';
import { ApplicationUserConsentScopeType } from '@logto/schemas';
export const permissionTabs = Object.freeze({
[ApplicationUserConsentScopeType.UserScopes]: {
title: 'application_details.permissions.user_profile',
key: ApplicationUserConsentScopeType.UserScopes,
},
[ApplicationUserConsentScopeType.ResourceScopes]: {
title: 'application_details.permissions.api_resource',
key: ApplicationUserConsentScopeType.ResourceScopes,
},
[ApplicationUserConsentScopeType.OrganizationScopes]: {
title: 'application_details.permissions.organization',
key: ApplicationUserConsentScopeType.OrganizationScopes,
},
}) satisfies {
[key in ApplicationUserConsentScopeType]: {
title: AdminConsoleKey;
key: key;
};
};

View file

@ -0,0 +1,99 @@
import { type AdminConsoleKey } from '@logto/phrases';
import { ApplicationUserConsentScopeType } from '@logto/schemas';
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import ConfirmModal from '@/ds-components/ConfirmModal';
import DataTransferBox from '@/ds-components/DataTransferBox';
import TabNav, { TabNavItem } from '@/ds-components/TabNav';
import TabWrapper from '@/ds-components/TabWrapper';
import { permissionTabs } from './constants';
import useApplicationScopesAssignment from './use-application-scopes-assignment';
const modalText = Object.freeze({
title: 'application_details.permissions.table_name',
subtitle: 'application_details.permissions.permissions_assignment_description',
saveBtn: 'general.save',
}) satisfies Record<string, AdminConsoleKey>;
type Props = {
isOpen: boolean;
onClose: () => void;
applicationId: string;
};
function ApplicationScopesAssignmentModal({ isOpen, onClose, applicationId }: Props) {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const { activeTab, setActiveTab, scopesAssignment, clearSelectedData, onSubmit, isLoading } =
useApplicationScopesAssignment(applicationId);
const onCloseHandler = useCallback(() => {
onClose();
clearSelectedData();
setActiveTab(ApplicationUserConsentScopeType.UserScopes);
}, [clearSelectedData, onClose, setActiveTab]);
const onSubmitHandler = useCallback(async () => {
await onSubmit();
onCloseHandler();
}, [onCloseHandler, onSubmit]);
// If any of the tabs has selected scopes, the modal is dirty
const isDirty = Object.values(scopesAssignment).some(
({ selectedData }) => selectedData.length > 0
);
return (
<ConfirmModal
isOpen={isOpen}
isLoading={isLoading}
title={modalText.title}
subtitle={modalText.subtitle}
isConfirmButtonDisabled={!isDirty}
confirmButtonType="primary"
confirmButtonText={modalText.saveBtn}
isCancelButtonVisible={false}
size="large"
onCancel={onCloseHandler}
onConfirm={onSubmitHandler}
>
<TabNav>
{Object.values(permissionTabs).map(({ title, key }) => (
<TabNavItem
key={key}
isActive={key === activeTab}
onClick={() => {
setActiveTab(key);
}}
>
{`${t(title)}(${scopesAssignment[key].selectedData.length})`}
</TabNavItem>
))}
</TabNav>
<TabWrapper
key={ApplicationUserConsentScopeType.UserScopes}
isActive={ApplicationUserConsentScopeType.UserScopes === activeTab}
>
<DataTransferBox {...scopesAssignment[ApplicationUserConsentScopeType.UserScopes]} />
</TabWrapper>
<TabWrapper
key={ApplicationUserConsentScopeType.ResourceScopes}
isActive={ApplicationUserConsentScopeType.ResourceScopes === activeTab}
>
<DataTransferBox {...scopesAssignment[ApplicationUserConsentScopeType.ResourceScopes]} />
</TabWrapper>
<TabWrapper
key={ApplicationUserConsentScopeType.OrganizationScopes}
isActive={ApplicationUserConsentScopeType.OrganizationScopes === activeTab}
>
<DataTransferBox
{...scopesAssignment[ApplicationUserConsentScopeType.OrganizationScopes]}
/>
</TabWrapper>
</ConfirmModal>
);
}
export default ApplicationScopesAssignmentModal;

View file

@ -0,0 +1,29 @@
import {
type ApplicationUserConsentScopesResponse,
type ApplicationUserConsentScopeType,
} from '@logto/schemas';
import { type Props as DataTransferBoxProps } from '@/ds-components/DataTransferBox';
import { type DataEntry } from '@/ds-components/DataTransferBox/type';
type DataTransferBoxComponentProps<V extends DataEntry> = Pick<
DataTransferBoxProps<V>,
'selectedData' | 'setSelectedData' | 'availableDataList' | 'availableDataGroups' | 'title'
>;
type ScopeAssignmentHookReturnType<
T extends ApplicationUserConsentScopeType,
V extends DataEntry,
> = {
scopeType: T;
} & DataTransferBoxComponentProps<V>;
// This is used to parse the ApplicationUserConsentScopeType to the response key
type CamelCase<T> = T extends `${infer A}-${infer B}` ? `${A}${Capitalize<CamelCase<B>>}` : T;
export type ScopeAssignmentHook<
T extends ApplicationUserConsentScopeType,
V extends DataEntry = DataEntry,
> = (
assignedScopes?: ApplicationUserConsentScopesResponse[CamelCase<T>]
) => ScopeAssignmentHookReturnType<T, V>;

View file

@ -0,0 +1,89 @@
import {
ApplicationUserConsentScopeType,
type ApplicationUserConsentScopesResponse,
} from '@logto/schemas';
import { conditional } from '@silverhand/essentials';
import { useState, useCallback, useMemo } from 'react';
import useSWR from 'swr';
import useApi, { type RequestError } from '@/hooks/use-api';
import useOrganizationScopesAssignment from './use-organization-scopes-assignment';
import useResourceScopesAssignment from './use-resource-scopes-assignment';
import useUserScopesAssignment from './use-user-scopes-assignment';
const useApplicationScopesAssignment = (applicationId: string) => {
const [activeTab, setActiveTab] = useState<ApplicationUserConsentScopeType>(
ApplicationUserConsentScopeType.UserScopes
);
const [isLoading, setIsLoading] = useState(false);
const api = useApi();
const { data, mutate } = useSWR<ApplicationUserConsentScopesResponse, RequestError>(
`api/applications/${applicationId}/user-consent-scopes`
);
const userScopesAssignment = useUserScopesAssignment(data?.userScopes);
const organizationScopesAssignment = useOrganizationScopesAssignment(data?.organizationScopes);
const resourceScopesAssignment = useResourceScopesAssignment(data?.resourceScopes);
const clearSelectedData = useCallback(() => {
userScopesAssignment.setSelectedData([]);
organizationScopesAssignment.setSelectedData([]);
resourceScopesAssignment.setSelectedData([]);
}, [organizationScopesAssignment, resourceScopesAssignment, userScopesAssignment]);
const onSubmit = useCallback(async () => {
setIsLoading(true);
const newUserScopes = userScopesAssignment.selectedData.map(({ id }) => id);
const newOrganizationScopes = organizationScopesAssignment.selectedData.map(({ id }) => id);
const newResourceScopes = resourceScopesAssignment.selectedData.map(({ id }) => id);
await api
.post(`api/applications/${applicationId}/user-consent-scopes`, {
json: {
...conditional(newUserScopes.length > 0 && { userScopes: newUserScopes }),
...conditional(
newOrganizationScopes.length > 0 && {
organizationScopes: newOrganizationScopes,
}
),
...conditional(newResourceScopes.length > 0 && { resourceScopes: newResourceScopes }),
},
})
.finally(() => {
setIsLoading(false);
});
void mutate();
}, [
api,
applicationId,
mutate,
organizationScopesAssignment.selectedData,
resourceScopesAssignment.selectedData,
userScopesAssignment.selectedData,
]);
// Return selectedScopes and setSelectedScopes based on the active tab
const scopesAssignment = useMemo(
() => ({
[ApplicationUserConsentScopeType.UserScopes]: userScopesAssignment,
[ApplicationUserConsentScopeType.OrganizationScopes]: organizationScopesAssignment,
[ApplicationUserConsentScopeType.ResourceScopes]: resourceScopesAssignment,
}),
[organizationScopesAssignment, resourceScopesAssignment, userScopesAssignment]
);
return {
isLoading,
activeTab,
setActiveTab,
scopesAssignment,
clearSelectedData,
onSubmit,
};
};
export default useApplicationScopesAssignment;

View file

@ -0,0 +1,44 @@
import {
ApplicationUserConsentScopeType,
type OrganizationScope,
type ApplicationUserConsentScopesResponse,
} from '@logto/schemas';
import { useState, useMemo } from 'react';
import useSWR from 'swr';
import { type RequestError } from '@/hooks/use-api';
import { type ScopeAssignmentHook } from './type';
type HookType = ScopeAssignmentHook<
ApplicationUserConsentScopeType.OrganizationScopes,
ApplicationUserConsentScopesResponse['organizationScopes'][number]
>;
type SelectedDataType = ReturnType<HookType>['selectedData'][number];
const useOrganizationScopesAssignment: HookType = (assignedOrganizationScopes = []) => {
const [selectedData, setSelectedData] = useState<SelectedDataType[]>([]);
const { data: organizationScopes } = useSWR<OrganizationScope[], RequestError>(
'api/organization-scopes'
);
const availableDataList = useMemo(
() =>
(organizationScopes ?? []).filter(
({ id }) => !assignedOrganizationScopes.some((scope) => scope.id === id)
),
[organizationScopes, assignedOrganizationScopes]
);
return {
scopeType: ApplicationUserConsentScopeType.OrganizationScopes,
selectedData,
setSelectedData,
availableDataList,
title: 'application_details.permissions.organization_permissions_assignment_form_title',
};
};
export default useOrganizationScopesAssignment;

View file

@ -0,0 +1,69 @@
import {
type ResourceResponse,
ApplicationUserConsentScopeType,
type ApplicationUserConsentScopesResponse,
} from '@logto/schemas';
import { isManagementApi } from '@logto/schemas';
import { useState, useMemo } from 'react';
import useSWR from 'swr';
import { type RequestError } from '@/hooks/use-api';
import { type ScopeAssignmentHook } from './type';
type HookType = ScopeAssignmentHook<
ApplicationUserConsentScopeType.ResourceScopes,
ApplicationUserConsentScopesResponse['resourceScopes'][number]['scopes'][number]
>;
type SelectedDataType = ReturnType<HookType>['selectedData'][number];
const useResourceScopesAssignment: HookType = (assignedResourceScopes) => {
const [selectedData, setSelectedData] = useState<SelectedDataType[]>([]);
const { data: allResources } = useSWR<ResourceResponse[], RequestError>(
'api/resources?includeScopes=true'
);
const availableDataGroups = useMemo(() => {
if (!allResources) {
return [];
}
const resourcesWithScopes: ReturnType<HookType>['availableDataGroups'] = allResources
// Filter out the management APIs
.filter((resource) => !isManagementApi(resource.indicator))
.map(({ name, scopes, id }) => {
const assignedResource = assignedResourceScopes?.find(({ resource }) => resource.id === id);
return {
groupId: id,
groupName: name,
dataList: scopes
// Filter out the scopes that have been assigned
.filter(({ id: scopeId }) => {
if (!assignedResourceScopes) {
return true;
}
return assignedResource
? !assignedResource.scopes.some((scope) => scope.id === scopeId)
: true;
}),
};
});
// Filter out the resources that have no scopes
return resourcesWithScopes.filter(({ dataList }) => dataList.length > 0);
}, [allResources, assignedResourceScopes]);
return {
scopeType: ApplicationUserConsentScopeType.ResourceScopes,
selectedData,
setSelectedData,
availableDataGroups,
title: 'application_details.permissions.api_resource_permissions_assignment_form_title',
};
};
export default useResourceScopesAssignment;

View file

@ -0,0 +1,34 @@
import { UserScope } from '@logto/core-kit';
import { ApplicationUserConsentScopeType } from '@logto/schemas';
import { useState, useMemo } from 'react';
import { type ScopeAssignmentHook } from './type';
type HookType = ScopeAssignmentHook<ApplicationUserConsentScopeType.UserScopes>;
type SelectedDataType = ReturnType<HookType>['selectedData'][number];
const useUserScopesAssignment: HookType = (assignedUserScopes = []) => {
const [selectedData, setSelectedData] = useState<SelectedDataType[]>([]);
const availableDataList = useMemo(
() =>
Object.values(UserScope)
.map((name) => ({
name,
id: name,
}))
// Filter out the scopes that have been assigned
.filter(({ id }) => !assignedUserScopes.includes(id)),
[assignedUserScopes]
);
return {
scopeType: ApplicationUserConsentScopeType.UserScopes,
selectedData,
setSelectedData,
availableDataList,
title: 'application_details.permissions.user_permissions_assignment_form_title',
};
};
export default useUserScopesAssignment;

View file

@ -10,8 +10,9 @@ import TemplateTable from '@/components/TemplateTable';
import Tag from '@/ds-components/Tag';
import { type RequestError } from '@/hooks/use-api';
import ApplicationScopesAssignmentModal from './ApplicationScopesAssignmentModal';
import * as styles from './index.module.scss';
import usePermissionsTable from './use-permissions-table';
import useScopesTable from './use-scopes-table';
type Props = {
application: Application;
@ -21,71 +22,85 @@ function Permissions({ application }: Props) {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const [isAssignScopesModalOpen, setIsAssignScopesModalOpen] = useState(false);
const { parseRowGroup, deletePermission } = usePermissionsTable();
const { parseRowGroup, deleteScope } = useScopesTable();
const { data, mutate, isLoading } = useSWR<ApplicationUserConsentScopesResponse, RequestError>(
`api/applications/${application.id}/user-consent-scopes`
);
const { data, error, mutate, isLoading } = useSWR<
ApplicationUserConsentScopesResponse,
RequestError
>(`api/applications/${application.id}/user-consent-scopes`);
const rowGroups = useMemo(() => parseRowGroup(data), [data, parseRowGroup]);
return (
<FormCard
title="application_details.permissions.name"
description="application_details.permissions.description"
>
<TemplateTable
className={styles.permissionsModal}
name="application_details.permissions.table_name"
rowIndexKey="id"
isLoading={isLoading}
rowGroups={rowGroups}
columns={[
{
title: t('application_details.permissions.field_name'),
dataIndex: 'name',
colSpan: 5,
render: ({ name }) => (
<Tag variant="cell">
<Breakable>{name}</Breakable>
</Tag>
),
},
{
title: `${t('general.description')} (${t(
'application_details.permissions.field_description'
)})`,
dataIndex: 'description',
colSpan: 5,
render: ({ description }) => <Breakable>{description ?? '-'}</Breakable>,
},
{
title: null,
dataIndex: 'delete',
render: (data) => (
<ActionsButton
fieldName="application_details.permissions.name"
deleteConfirmation="application_details.permissions.permission_delete_confirm"
textOverrides={{
delete: 'application_details.permissions.delete_text',
deleteConfirmation: 'general.remove',
}}
onEdit={() => {
// TODO: Implement edit permission
}}
onDelete={async () => {
await deletePermission(data, application.id);
void mutate();
}}
/>
),
},
]}
onAdd={() => {
setIsAssignScopesModalOpen(true);
}}
/>
</FormCard>
<>
<FormCard
title="application_details.permissions.name"
description="application_details.permissions.description"
>
<TemplateTable
className={styles.permissionsModal}
name="application_details.permissions.table_name"
rowIndexKey="id"
errorMessage={error?.body?.message ?? error?.message}
isLoading={isLoading}
rowGroups={rowGroups}
columns={[
{
title: t('application_details.permissions.field_name'),
dataIndex: 'name',
colSpan: 5,
render: ({ name }) => (
<Tag variant="cell">
<Breakable>{name}</Breakable>
</Tag>
),
},
{
title: `${t('general.description')} (${t(
'application_details.permissions.field_description'
)})`,
dataIndex: 'description',
colSpan: 5,
render: ({ description }) => <Breakable>{description ?? '-'}</Breakable>,
},
{
title: null,
dataIndex: 'delete',
render: (data) => (
<ActionsButton
fieldName="application_details.permissions.name"
deleteConfirmation="application_details.permissions.permission_delete_confirm"
textOverrides={{
delete: 'application_details.permissions.delete_text',
deleteConfirmation: 'general.remove',
}}
onEdit={() => {
// TODO: Implement edit permission
}}
onDelete={async () => {
await deleteScope(data, application.id);
void mutate();
}}
/>
),
},
]}
onAdd={() => {
setIsAssignScopesModalOpen(true);
}}
/>
</FormCard>
{/* Render the permissions assignment modal only if the data is fetched properly */}
{data && (
<ApplicationScopesAssignmentModal
applicationId={application.id}
isOpen={isAssignScopesModalOpen}
onClose={() => {
setIsAssignScopesModalOpen(false);
}}
/>
)}
</>
);
}

View file

@ -9,36 +9,36 @@ import useApi from '@/hooks/use-api';
import * as styles from './index.module.scss';
type PermissionsTableRowDataType = {
type ScopesTableRowDataType = {
type: ApplicationUserConsentScopeType;
id: string;
name: string;
description?: string;
};
type PermissionsTableFieldGroupType = {
type ScopesTableRowGroupType = {
key: string;
label: string;
labelRowClassName?: string;
data: PermissionsTableRowDataType[];
data: ScopesTableRowDataType[];
};
/**
* - parseRowGroup: parse the application user consent scopes response data to table field group data
*/
const usePermissionsTable = () => {
const useScopesTable = () => {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const api = useApi();
const parseRowGroup = useCallback(
(data?: ApplicationUserConsentScopesResponse): PermissionsTableFieldGroupType[] => {
(data?: ApplicationUserConsentScopesResponse): ScopesTableRowGroupType[] => {
if (!data) {
return [];
}
const { organizationScopes, userScopes, resourceScopes } = data;
const userScopesGroup: PermissionsTableFieldGroupType = {
const userScopesGroup: ScopesTableRowGroupType = {
key: ApplicationUserConsentScopeType.UserScopes,
label: t('application_details.permissions.user_permissions'),
labelRowClassName: styles.sectionTitleRow,
@ -50,7 +50,7 @@ const usePermissionsTable = () => {
})),
};
const organizationScopesGroup: PermissionsTableFieldGroupType = {
const organizationScopesGroup: ScopesTableRowGroupType = {
key: ApplicationUserConsentScopeType.OrganizationScopes,
label: t('application_details.permissions.organization_permissions'),
labelRowClassName: styles.sectionTitleRow,
@ -62,7 +62,7 @@ const usePermissionsTable = () => {
})),
};
const resourceScopesGroups = resourceScopes.map<PermissionsTableFieldGroupType>(
const resourceScopesGroups = resourceScopes.map<ScopesTableRowGroupType>(
({ resource, scopes }) => ({
key: resource.indicator,
label: resource.name,
@ -81,16 +81,16 @@ const usePermissionsTable = () => {
[t]
);
const deletePermission = useCallback(
async (scope: PermissionsTableRowDataType, applicationId: string) =>
const deleteScope = useCallback(
async (scope: ScopesTableRowDataType, applicationId: string) =>
api.delete(`api/applications/${applicationId}/user-consent-scopes/${scope.type}/${scope.id}`),
[api]
);
return {
parseRowGroup,
deletePermission,
deleteScope,
};
};
export default usePermissionsTable;
export default useScopesTable;

View file

@ -151,6 +151,21 @@ const application_details = {
/** UNTRANSLATED */
permission_delete_confirm:
'This action will withdraw the permissions granted to the third-party app, preventing it from requesting user authorization for specific data types. Are you sure you want to continue?',
/** UNTRANSLATED */
permissions_assignment_description:
'Select the permissions the third-party application requests for user authorization to access specific data types.',
/** UNTRANSLATED */
user_profile: 'User profile',
/** UNTRANSLATED */
api_resource: 'API resource',
/** UNTRANSLATED */
organization: 'Organization',
/** UNTRANSLATED */
user_permissions_assignment_form_title: 'Add the user profile permissions',
/** UNTRANSLATED */
organization_permissions_assignment_form_title: 'Add the organization permissions',
/** UNTRANSLATED */
api_resource_permissions_assignment_form_title: 'Add the API resource permissions',
},
roles: {
name_column: 'Rolle',

View file

@ -109,6 +109,14 @@ const application_details = {
delete_text: 'Remove permission',
permission_delete_confirm:
'This action will withdraw the permissions granted to the third-party app, preventing it from requesting user authorization for specific data types. Are you sure you want to continue?',
permissions_assignment_description:
'Select the permissions the third-party application requests for user authorization to access specific data types.',
user_profile: 'User profile',
api_resource: 'API resource',
organization: 'Organization',
user_permissions_assignment_form_title: 'Add the user profile permissions',
organization_permissions_assignment_form_title: 'Add the organization permissions',
api_resource_permissions_assignment_form_title: 'Add the API resource permissions',
},
roles: {
name_column: 'Role',

View file

@ -151,6 +151,21 @@ const application_details = {
/** UNTRANSLATED */
permission_delete_confirm:
'This action will withdraw the permissions granted to the third-party app, preventing it from requesting user authorization for specific data types. Are you sure you want to continue?',
/** UNTRANSLATED */
permissions_assignment_description:
'Select the permissions the third-party application requests for user authorization to access specific data types.',
/** UNTRANSLATED */
user_profile: 'User profile',
/** UNTRANSLATED */
api_resource: 'API resource',
/** UNTRANSLATED */
organization: 'Organization',
/** UNTRANSLATED */
user_permissions_assignment_form_title: 'Add the user profile permissions',
/** UNTRANSLATED */
organization_permissions_assignment_form_title: 'Add the organization permissions',
/** UNTRANSLATED */
api_resource_permissions_assignment_form_title: 'Add the API resource permissions',
},
roles: {
name_column: 'Rol',

View file

@ -151,6 +151,21 @@ const application_details = {
/** UNTRANSLATED */
permission_delete_confirm:
'This action will withdraw the permissions granted to the third-party app, preventing it from requesting user authorization for specific data types. Are you sure you want to continue?',
/** UNTRANSLATED */
permissions_assignment_description:
'Select the permissions the third-party application requests for user authorization to access specific data types.',
/** UNTRANSLATED */
user_profile: 'User profile',
/** UNTRANSLATED */
api_resource: 'API resource',
/** UNTRANSLATED */
organization: 'Organization',
/** UNTRANSLATED */
user_permissions_assignment_form_title: 'Add the user profile permissions',
/** UNTRANSLATED */
organization_permissions_assignment_form_title: 'Add the organization permissions',
/** UNTRANSLATED */
api_resource_permissions_assignment_form_title: 'Add the API resource permissions',
},
roles: {
name_column: 'Rôle',

View file

@ -151,6 +151,21 @@ const application_details = {
/** UNTRANSLATED */
permission_delete_confirm:
'This action will withdraw the permissions granted to the third-party app, preventing it from requesting user authorization for specific data types. Are you sure you want to continue?',
/** UNTRANSLATED */
permissions_assignment_description:
'Select the permissions the third-party application requests for user authorization to access specific data types.',
/** UNTRANSLATED */
user_profile: 'User profile',
/** UNTRANSLATED */
api_resource: 'API resource',
/** UNTRANSLATED */
organization: 'Organization',
/** UNTRANSLATED */
user_permissions_assignment_form_title: 'Add the user profile permissions',
/** UNTRANSLATED */
organization_permissions_assignment_form_title: 'Add the organization permissions',
/** UNTRANSLATED */
api_resource_permissions_assignment_form_title: 'Add the API resource permissions',
},
roles: {
name_column: 'Ruolo',

View file

@ -151,6 +151,21 @@ const application_details = {
/** UNTRANSLATED */
permission_delete_confirm:
'This action will withdraw the permissions granted to the third-party app, preventing it from requesting user authorization for specific data types. Are you sure you want to continue?',
/** UNTRANSLATED */
permissions_assignment_description:
'Select the permissions the third-party application requests for user authorization to access specific data types.',
/** UNTRANSLATED */
user_profile: 'User profile',
/** UNTRANSLATED */
api_resource: 'API resource',
/** UNTRANSLATED */
organization: 'Organization',
/** UNTRANSLATED */
user_permissions_assignment_form_title: 'Add the user profile permissions',
/** UNTRANSLATED */
organization_permissions_assignment_form_title: 'Add the organization permissions',
/** UNTRANSLATED */
api_resource_permissions_assignment_form_title: 'Add the API resource permissions',
},
roles: {
name_column: '役割',

View file

@ -151,6 +151,21 @@ const application_details = {
/** UNTRANSLATED */
permission_delete_confirm:
'This action will withdraw the permissions granted to the third-party app, preventing it from requesting user authorization for specific data types. Are you sure you want to continue?',
/** UNTRANSLATED */
permissions_assignment_description:
'Select the permissions the third-party application requests for user authorization to access specific data types.',
/** UNTRANSLATED */
user_profile: 'User profile',
/** UNTRANSLATED */
api_resource: 'API resource',
/** UNTRANSLATED */
organization: 'Organization',
/** UNTRANSLATED */
user_permissions_assignment_form_title: 'Add the user profile permissions',
/** UNTRANSLATED */
organization_permissions_assignment_form_title: 'Add the organization permissions',
/** UNTRANSLATED */
api_resource_permissions_assignment_form_title: 'Add the API resource permissions',
},
roles: {
name_column: '역할',

View file

@ -151,6 +151,21 @@ const application_details = {
/** UNTRANSLATED */
permission_delete_confirm:
'This action will withdraw the permissions granted to the third-party app, preventing it from requesting user authorization for specific data types. Are you sure you want to continue?',
/** UNTRANSLATED */
permissions_assignment_description:
'Select the permissions the third-party application requests for user authorization to access specific data types.',
/** UNTRANSLATED */
user_profile: 'User profile',
/** UNTRANSLATED */
api_resource: 'API resource',
/** UNTRANSLATED */
organization: 'Organization',
/** UNTRANSLATED */
user_permissions_assignment_form_title: 'Add the user profile permissions',
/** UNTRANSLATED */
organization_permissions_assignment_form_title: 'Add the organization permissions',
/** UNTRANSLATED */
api_resource_permissions_assignment_form_title: 'Add the API resource permissions',
},
roles: {
name_column: 'Role',

View file

@ -151,6 +151,21 @@ const application_details = {
/** UNTRANSLATED */
permission_delete_confirm:
'This action will withdraw the permissions granted to the third-party app, preventing it from requesting user authorization for specific data types. Are you sure you want to continue?',
/** UNTRANSLATED */
permissions_assignment_description:
'Select the permissions the third-party application requests for user authorization to access specific data types.',
/** UNTRANSLATED */
user_profile: 'User profile',
/** UNTRANSLATED */
api_resource: 'API resource',
/** UNTRANSLATED */
organization: 'Organization',
/** UNTRANSLATED */
user_permissions_assignment_form_title: 'Add the user profile permissions',
/** UNTRANSLATED */
organization_permissions_assignment_form_title: 'Add the organization permissions',
/** UNTRANSLATED */
api_resource_permissions_assignment_form_title: 'Add the API resource permissions',
},
roles: {
name_column: 'Função',

View file

@ -151,6 +151,21 @@ const application_details = {
/** UNTRANSLATED */
permission_delete_confirm:
'This action will withdraw the permissions granted to the third-party app, preventing it from requesting user authorization for specific data types. Are you sure you want to continue?',
/** UNTRANSLATED */
permissions_assignment_description:
'Select the permissions the third-party application requests for user authorization to access specific data types.',
/** UNTRANSLATED */
user_profile: 'User profile',
/** UNTRANSLATED */
api_resource: 'API resource',
/** UNTRANSLATED */
organization: 'Organization',
/** UNTRANSLATED */
user_permissions_assignment_form_title: 'Add the user profile permissions',
/** UNTRANSLATED */
organization_permissions_assignment_form_title: 'Add the organization permissions',
/** UNTRANSLATED */
api_resource_permissions_assignment_form_title: 'Add the API resource permissions',
},
roles: {
name_column: 'Nome da função',

View file

@ -151,6 +151,21 @@ const application_details = {
/** UNTRANSLATED */
permission_delete_confirm:
'This action will withdraw the permissions granted to the third-party app, preventing it from requesting user authorization for specific data types. Are you sure you want to continue?',
/** UNTRANSLATED */
permissions_assignment_description:
'Select the permissions the third-party application requests for user authorization to access specific data types.',
/** UNTRANSLATED */
user_profile: 'User profile',
/** UNTRANSLATED */
api_resource: 'API resource',
/** UNTRANSLATED */
organization: 'Organization',
/** UNTRANSLATED */
user_permissions_assignment_form_title: 'Add the user profile permissions',
/** UNTRANSLATED */
organization_permissions_assignment_form_title: 'Add the organization permissions',
/** UNTRANSLATED */
api_resource_permissions_assignment_form_title: 'Add the API resource permissions',
},
roles: {
name_column: 'Роль',

View file

@ -151,6 +151,21 @@ const application_details = {
/** UNTRANSLATED */
permission_delete_confirm:
'This action will withdraw the permissions granted to the third-party app, preventing it from requesting user authorization for specific data types. Are you sure you want to continue?',
/** UNTRANSLATED */
permissions_assignment_description:
'Select the permissions the third-party application requests for user authorization to access specific data types.',
/** UNTRANSLATED */
user_profile: 'User profile',
/** UNTRANSLATED */
api_resource: 'API resource',
/** UNTRANSLATED */
organization: 'Organization',
/** UNTRANSLATED */
user_permissions_assignment_form_title: 'Add the user profile permissions',
/** UNTRANSLATED */
organization_permissions_assignment_form_title: 'Add the organization permissions',
/** UNTRANSLATED */
api_resource_permissions_assignment_form_title: 'Add the API resource permissions',
},
roles: {
name_column: 'Rol',

View file

@ -148,6 +148,21 @@ const application_details = {
/** UNTRANSLATED */
permission_delete_confirm:
'This action will withdraw the permissions granted to the third-party app, preventing it from requesting user authorization for specific data types. Are you sure you want to continue?',
/** UNTRANSLATED */
permissions_assignment_description:
'Select the permissions the third-party application requests for user authorization to access specific data types.',
/** UNTRANSLATED */
user_profile: 'User profile',
/** UNTRANSLATED */
api_resource: 'API resource',
/** UNTRANSLATED */
organization: 'Organization',
/** UNTRANSLATED */
user_permissions_assignment_form_title: 'Add the user profile permissions',
/** UNTRANSLATED */
organization_permissions_assignment_form_title: 'Add the organization permissions',
/** UNTRANSLATED */
api_resource_permissions_assignment_form_title: 'Add the API resource permissions',
},
roles: {
name_column: '角色',

View file

@ -148,6 +148,21 @@ const application_details = {
/** UNTRANSLATED */
permission_delete_confirm:
'This action will withdraw the permissions granted to the third-party app, preventing it from requesting user authorization for specific data types. Are you sure you want to continue?',
/** UNTRANSLATED */
permissions_assignment_description:
'Select the permissions the third-party application requests for user authorization to access specific data types.',
/** UNTRANSLATED */
user_profile: 'User profile',
/** UNTRANSLATED */
api_resource: 'API resource',
/** UNTRANSLATED */
organization: 'Organization',
/** UNTRANSLATED */
user_permissions_assignment_form_title: 'Add the user profile permissions',
/** UNTRANSLATED */
organization_permissions_assignment_form_title: 'Add the organization permissions',
/** UNTRANSLATED */
api_resource_permissions_assignment_form_title: 'Add the API resource permissions',
},
roles: {
name_column: '角色',

View file

@ -149,6 +149,21 @@ const application_details = {
/** UNTRANSLATED */
permission_delete_confirm:
'This action will withdraw the permissions granted to the third-party app, preventing it from requesting user authorization for specific data types. Are you sure you want to continue?',
/** UNTRANSLATED */
permissions_assignment_description:
'Select the permissions the third-party application requests for user authorization to access specific data types.',
/** UNTRANSLATED */
user_profile: 'User profile',
/** UNTRANSLATED */
api_resource: 'API resource',
/** UNTRANSLATED */
organization: 'Organization',
/** UNTRANSLATED */
user_permissions_assignment_form_title: 'Add the user profile permissions',
/** UNTRANSLATED */
organization_permissions_assignment_form_title: 'Add the organization permissions',
/** UNTRANSLATED */
api_resource_permissions_assignment_form_title: 'Add the API resource permissions',
},
roles: {
name_column: '角色',