@strapi/plugin-users-permissions 4.0.0-next.7 → 4.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/admin/src/components/BoundRoute/getMethodColor.js +41 -0
- package/admin/src/components/BoundRoute/index.js +40 -24
- package/admin/src/components/FormModal/Input/index.js +121 -0
- package/admin/src/components/FormModal/index.js +123 -0
- package/admin/src/components/Permissions/PermissionRow/CheckboxWrapper.js +19 -26
- package/admin/src/components/Permissions/PermissionRow/SubCategory.js +118 -0
- package/admin/src/components/Permissions/PermissionRow/index.js +9 -48
- package/admin/src/components/Permissions/index.js +36 -24
- package/admin/src/components/Permissions/init.js +1 -6
- package/admin/src/components/Policies/index.js +46 -47
- package/admin/src/components/UsersPermissions/index.js +29 -26
- package/admin/src/components/UsersPermissions/init.js +1 -2
- package/admin/src/hooks/useFetchRole/index.js +17 -7
- package/admin/src/hooks/useForm/index.js +3 -29
- package/admin/src/hooks/useForm/reducer.js +2 -21
- package/admin/src/hooks/usePlugins/index.js +12 -21
- package/admin/src/hooks/usePlugins/reducer.js +0 -3
- package/admin/src/index.js +29 -34
- package/admin/src/pages/AdvancedSettings/index.js +210 -193
- package/admin/src/pages/AdvancedSettings/utils/api.js +13 -0
- package/admin/src/pages/AdvancedSettings/utils/layout.js +96 -0
- package/admin/src/pages/AdvancedSettings/utils/schema.js +21 -0
- package/admin/src/pages/EmailTemplates/components/EmailForm.js +173 -0
- package/admin/src/pages/EmailTemplates/components/EmailTable.js +116 -0
- package/admin/src/pages/EmailTemplates/index.js +125 -198
- package/admin/src/pages/EmailTemplates/utils/api.js +13 -0
- package/admin/src/pages/Providers/index.js +208 -216
- package/admin/src/pages/Providers/utils/api.js +21 -0
- package/admin/src/pages/Providers/utils/forms.js +168 -126
- package/admin/src/pages/Roles/CreatePage/index.js +155 -147
- package/admin/src/pages/Roles/EditPage/index.js +162 -134
- package/admin/src/pages/Roles/ListPage/components/TableBody.js +96 -0
- package/admin/src/pages/Roles/ListPage/index.js +176 -156
- package/admin/src/pages/Roles/ListPage/utils/api.js +28 -0
- package/admin/src/pages/Roles/index.js +14 -8
- package/admin/src/translations/ar.json +0 -8
- package/admin/src/translations/cs.json +0 -8
- package/admin/src/translations/de.json +0 -8
- package/admin/src/translations/dk.json +0 -8
- package/admin/src/translations/en.json +33 -12
- package/admin/src/translations/es.json +0 -8
- package/admin/src/translations/fr.json +0 -8
- package/admin/src/translations/id.json +0 -8
- package/admin/src/translations/it.json +0 -8
- package/admin/src/translations/ja.json +0 -8
- package/admin/src/translations/ko.json +93 -54
- package/admin/src/translations/ms.json +0 -8
- package/admin/src/translations/nl.json +0 -8
- package/admin/src/translations/pl.json +0 -8
- package/admin/src/translations/pt-BR.json +0 -8
- package/admin/src/translations/pt.json +0 -8
- package/admin/src/translations/ru.json +0 -8
- package/admin/src/translations/sk.json +0 -8
- package/admin/src/translations/sv.json +0 -8
- package/admin/src/translations/th.json +0 -8
- package/admin/src/translations/tr.json +0 -8
- package/admin/src/translations/uk.json +0 -8
- package/admin/src/translations/vi.json +0 -8
- package/admin/src/translations/zh-Hans.json +5 -14
- package/admin/src/translations/zh.json +0 -8
- package/admin/src/utils/axiosInstance.js +36 -0
- package/admin/src/utils/formatPluginName.js +26 -0
- package/admin/src/utils/index.js +1 -0
- package/documentation/1.0.0/overrides/users-permissions-Role.json +6 -6
- package/documentation/1.0.0/overrides/users-permissions-User.json +7 -7
- package/jest.config.front.js +10 -0
- package/package.json +35 -32
- package/server/bootstrap/index.js +20 -25
- package/server/config.js +3 -3
- package/server/content-types/index.js +3 -3
- package/server/content-types/permission/index.js +30 -3
- package/server/content-types/role/index.js +47 -3
- package/server/content-types/user/index.js +65 -4
- package/server/controllers/auth.js +85 -237
- package/server/controllers/content-manager-user.js +183 -0
- package/server/controllers/index.js +12 -6
- package/server/controllers/permissions.js +26 -0
- package/server/controllers/role.js +77 -0
- package/server/controllers/settings.js +85 -0
- package/server/controllers/user.js +119 -45
- package/server/controllers/validation/auth.js +29 -0
- package/server/controllers/validation/user.js +38 -0
- package/server/graphql/index.js +44 -0
- package/server/graphql/mutations/auth/email-confirmation.js +39 -0
- package/server/graphql/mutations/auth/forgot-password.js +38 -0
- package/server/graphql/mutations/auth/login.js +38 -0
- package/server/graphql/mutations/auth/register.js +39 -0
- package/server/graphql/mutations/auth/reset-password.js +41 -0
- package/server/graphql/mutations/crud/role/create-role.js +37 -0
- package/server/graphql/mutations/crud/role/delete-role.js +28 -0
- package/server/graphql/mutations/crud/role/update-role.js +38 -0
- package/server/graphql/mutations/crud/user/create-user.js +48 -0
- package/server/graphql/mutations/crud/user/delete-user.js +42 -0
- package/server/graphql/mutations/crud/user/update-user.js +49 -0
- package/server/graphql/mutations/index.js +42 -0
- package/server/graphql/queries/index.js +13 -0
- package/server/graphql/queries/me.js +17 -0
- package/server/graphql/resolvers-configs.js +37 -0
- package/server/graphql/types/create-role-payload.js +11 -0
- package/server/graphql/types/delete-role-payload.js +11 -0
- package/server/graphql/types/index.js +21 -0
- package/server/graphql/types/login-input.js +13 -0
- package/server/graphql/types/login-payload.js +12 -0
- package/server/graphql/types/me-role.js +14 -0
- package/server/graphql/types/me.js +16 -0
- package/server/graphql/types/password-payload.js +11 -0
- package/server/graphql/types/register-input.js +13 -0
- package/server/graphql/types/update-role-payload.js +11 -0
- package/server/graphql/utils.js +27 -0
- package/server/index.js +21 -0
- package/server/middlewares/index.js +2 -2
- package/server/{policies → middlewares}/rateLimit.js +3 -7
- package/server/register.js +11 -0
- package/server/routes/admin/index.js +10 -0
- package/server/routes/admin/permissions.js +20 -0
- package/server/routes/admin/role.js +79 -0
- package/server/routes/admin/settings.js +95 -0
- package/server/routes/content-api/auth.js +73 -0
- package/server/routes/content-api/index.js +11 -0
- package/server/routes/content-api/permissions.js +9 -0
- package/server/routes/content-api/role.js +29 -0
- package/server/routes/content-api/user.js +61 -0
- package/server/routes/index.js +4 -3
- package/server/services/index.js +10 -8
- package/server/services/jwt.js +9 -17
- package/server/services/providers.js +32 -33
- package/server/services/role.js +177 -0
- package/server/services/user.js +9 -15
- package/server/services/users-permissions.js +140 -338
- package/server/strategies/users-permissions.js +123 -0
- package/server/utils/index.d.ts +2 -0
- package/strapi-admin.js +3 -0
- package/strapi-server.js +1 -19
- package/admin/src/assets/images/logo.svg +0 -1
- package/admin/src/components/BaselineAlignement/index.js +0 -33
- package/admin/src/components/Bloc/index.js +0 -10
- package/admin/src/components/BoundRoute/Components.js +0 -78
- package/admin/src/components/ContainerFluid/index.js +0 -13
- package/admin/src/components/FormBloc/index.js +0 -61
- package/admin/src/components/IntlInput/index.js +0 -38
- package/admin/src/components/ListBaselineAlignment/index.js +0 -8
- package/admin/src/components/ListRow/Components.js +0 -74
- package/admin/src/components/ListRow/index.js +0 -35
- package/admin/src/components/ModalForm/Wrapper.js +0 -12
- package/admin/src/components/ModalForm/index.js +0 -59
- package/admin/src/components/Permissions/ListWrapper.js +0 -9
- package/admin/src/components/Permissions/PermissionRow/BaselineAlignment.js +0 -7
- package/admin/src/components/Permissions/PermissionRow/RowStyle.js +0 -28
- package/admin/src/components/Permissions/PermissionRow/SubCategory/ConditionsButtonWrapper.js +0 -13
- package/admin/src/components/Permissions/PermissionRow/SubCategory/PolicyWrapper.js +0 -8
- package/admin/src/components/Permissions/PermissionRow/SubCategory/SubCategoryWrapper.js +0 -26
- package/admin/src/components/Permissions/PermissionRow/SubCategory/index.js +0 -116
- package/admin/src/components/Policies/Components.js +0 -26
- package/admin/src/components/PrefixedIcon/index.js +0 -27
- package/admin/src/components/Roles/EmptyRole/BaselineAlignment.js +0 -7
- package/admin/src/components/Roles/EmptyRole/index.js +0 -27
- package/admin/src/components/Roles/RoleListWrapper/index.js +0 -17
- package/admin/src/components/Roles/RoleRow/RoleDescription.js +0 -9
- package/admin/src/components/Roles/RoleRow/index.js +0 -45
- package/admin/src/components/Roles/index.js +0 -3
- package/admin/src/components/SizedInput/index.js +0 -24
- package/admin/src/pages/AdvancedSettings/reducer.js +0 -65
- package/admin/src/pages/AdvancedSettings/utils/form.js +0 -52
- package/admin/src/pages/EmailTemplates/CustomTextInput.js +0 -105
- package/admin/src/pages/EmailTemplates/Wrapper.js +0 -36
- package/admin/src/pages/EmailTemplates/reducer.js +0 -58
- package/admin/src/pages/EmailTemplates/utils/forms.js +0 -81
- package/admin/src/pages/Roles/ListPage/BaselineAlignment.js +0 -8
- package/server/content-types/permission/schema.json +0 -48
- package/server/content-types/role/schema.json +0 -46
- package/server/content-types/user/schema.json +0 -66
- package/server/controllers/user/admin.js +0 -230
- package/server/controllers/user/api.js +0 -174
- package/server/controllers/users-permissions.js +0 -271
- package/server/middlewares/users-permissions.js +0 -36
- package/server/policies/index.js +0 -11
- package/server/policies/isAuthenticated.js +0 -9
- package/server/policies/permissions.js +0 -94
- package/server/routes/routes.json +0 -381
- package/server/schema.graphql.js +0 -317
|
@@ -1,195 +1,215 @@
|
|
|
1
|
-
import React, {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
1
|
+
import React, { useMemo, useState } from 'react';
|
|
2
|
+
import { Button } from '@strapi/design-system/Button';
|
|
3
|
+
import { HeaderLayout, Layout, ContentLayout, ActionLayout } from '@strapi/design-system/Layout';
|
|
4
|
+
import { Main } from '@strapi/design-system/Main';
|
|
5
|
+
import { Table, Tr, Thead, Th } from '@strapi/design-system/Table';
|
|
6
|
+
import { VisuallyHidden } from '@strapi/design-system/VisuallyHidden';
|
|
7
|
+
import { Typography } from '@strapi/design-system/Typography';
|
|
8
|
+
import { useNotifyAT } from '@strapi/design-system/LiveRegions';
|
|
9
|
+
import Plus from '@strapi/icons/Plus';
|
|
7
10
|
import {
|
|
8
|
-
useRBAC,
|
|
9
|
-
PopUpWarning,
|
|
10
|
-
request,
|
|
11
11
|
useTracking,
|
|
12
|
+
SettingsPageTitle,
|
|
13
|
+
CheckPermissions,
|
|
12
14
|
useNotification,
|
|
13
|
-
|
|
15
|
+
useRBAC,
|
|
16
|
+
NoPermissions,
|
|
17
|
+
LoadingIndicatorPage,
|
|
18
|
+
SearchURLQuery,
|
|
19
|
+
useFocusWhenNavigate,
|
|
20
|
+
useQueryParams,
|
|
21
|
+
EmptyStateLayout,
|
|
22
|
+
ConfirmDialog,
|
|
14
23
|
} from '@strapi/helper-plugin';
|
|
24
|
+
import { useIntl } from 'react-intl';
|
|
25
|
+
import { useHistory } from 'react-router-dom';
|
|
26
|
+
import { useMutation, useQuery, useQueryClient } from 'react-query';
|
|
27
|
+
import matchSorter from 'match-sorter';
|
|
15
28
|
|
|
16
|
-
import
|
|
17
|
-
import { EmptyRole, RoleListWrapper, RoleRow } from '../../../components/Roles';
|
|
18
|
-
import { useRolesList } from '../../../hooks';
|
|
19
|
-
import BaselineAlignment from './BaselineAlignment';
|
|
20
|
-
import pluginId from '../../../pluginId';
|
|
29
|
+
import { fetchData, deleteData } from './utils/api';
|
|
21
30
|
import { getTrad } from '../../../utils';
|
|
31
|
+
import pluginId from '../../../pluginId';
|
|
32
|
+
import permissions from '../../../permissions';
|
|
33
|
+
import TableBody from './components/TableBody';
|
|
22
34
|
|
|
23
35
|
const RoleListPage = () => {
|
|
24
|
-
const { formatMessage } = useIntl();
|
|
25
36
|
const { trackUsage } = useTracking();
|
|
37
|
+
const { formatMessage } = useIntl();
|
|
26
38
|
const { push } = useHistory();
|
|
27
39
|
const toggleNotification = useNotification();
|
|
28
|
-
const {
|
|
29
|
-
const [
|
|
30
|
-
const
|
|
31
|
-
const [
|
|
40
|
+
const { notifyStatus } = useNotifyAT();
|
|
41
|
+
const [{ query }] = useQueryParams();
|
|
42
|
+
const _q = query?._q || '';
|
|
43
|
+
const [showConfirmDelete, setShowConfirmDelete] = useState(false);
|
|
44
|
+
const [isConfirmButtonLoading, setIsConfirmButtonLoading] = useState(false);
|
|
45
|
+
const [roleToDelete, setRoleToDelete] = useState();
|
|
46
|
+
useFocusWhenNavigate();
|
|
47
|
+
|
|
48
|
+
const queryClient = useQueryClient();
|
|
32
49
|
|
|
33
50
|
const updatePermissions = useMemo(() => {
|
|
34
51
|
return {
|
|
35
|
-
update: permissions.updateRole,
|
|
36
52
|
create: permissions.createRole,
|
|
37
|
-
delete: permissions.deleteRole,
|
|
38
53
|
read: permissions.readRoles,
|
|
54
|
+
update: permissions.updateRole,
|
|
55
|
+
delete: permissions.deleteRole,
|
|
39
56
|
};
|
|
40
57
|
}, []);
|
|
58
|
+
|
|
41
59
|
const {
|
|
42
60
|
isLoading: isLoadingForPermissions,
|
|
43
|
-
allowedActions: {
|
|
61
|
+
allowedActions: { canRead, canDelete },
|
|
44
62
|
} = useRBAC(updatePermissions);
|
|
45
|
-
const shouldFetchData = !isLoadingForPermissions && canRead;
|
|
46
63
|
|
|
47
|
-
const {
|
|
64
|
+
const {
|
|
65
|
+
isLoading: isLoadingForData,
|
|
66
|
+
data: { roles },
|
|
67
|
+
isFetching,
|
|
68
|
+
} = useQuery('get-roles', () => fetchData(toggleNotification, notifyStatus), {
|
|
69
|
+
initialData: {},
|
|
70
|
+
enabled: canRead,
|
|
71
|
+
});
|
|
48
72
|
|
|
49
|
-
const
|
|
50
|
-
if (canUpdate) {
|
|
51
|
-
push(`/settings/${pluginId}/roles/${id}`);
|
|
52
|
-
}
|
|
53
|
-
};
|
|
73
|
+
const isLoading = isLoadingForData || isFetching;
|
|
54
74
|
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
setModalButtonLoading(true);
|
|
59
|
-
|
|
60
|
-
Promise.resolve(
|
|
61
|
-
request(`/${pluginId}/roles/${modalToDelete}`, {
|
|
62
|
-
method: 'DELETE',
|
|
63
|
-
})
|
|
64
|
-
)
|
|
65
|
-
.then(() => {
|
|
66
|
-
setShouldRefetchData(true);
|
|
67
|
-
toggleNotification({
|
|
68
|
-
type: 'success',
|
|
69
|
-
message: { id: getTrad('Settings.roles.deleted') },
|
|
70
|
-
});
|
|
71
|
-
})
|
|
72
|
-
.catch(err => {
|
|
73
|
-
console.error(err);
|
|
74
|
-
toggleNotification({
|
|
75
|
-
type: 'warning',
|
|
76
|
-
message: { id: 'notification.error' },
|
|
77
|
-
});
|
|
78
|
-
})
|
|
79
|
-
.finally(() => {
|
|
80
|
-
setModalDelete(null);
|
|
81
|
-
unlockApp();
|
|
82
|
-
});
|
|
75
|
+
const handleNewRoleClick = () => {
|
|
76
|
+
trackUsage('willCreateRole');
|
|
77
|
+
push(`/settings/${pluginId}/roles/new`);
|
|
83
78
|
};
|
|
84
79
|
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
getData();
|
|
88
|
-
}
|
|
89
|
-
setModalButtonLoading(false);
|
|
90
|
-
setShouldRefetchData(false);
|
|
80
|
+
const handleShowConfirmDelete = () => {
|
|
81
|
+
setShowConfirmDelete(!showConfirmDelete);
|
|
91
82
|
};
|
|
92
83
|
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
84
|
+
const emptyLayout = {
|
|
85
|
+
roles: {
|
|
86
|
+
id: getTrad('Roles.empty'),
|
|
87
|
+
defaultMessage: "You don't have any roles yet.",
|
|
88
|
+
},
|
|
89
|
+
search: {
|
|
90
|
+
id: getTrad('Roles.empty.search'),
|
|
91
|
+
defaultMessage: 'No roles match the search.',
|
|
92
|
+
},
|
|
96
93
|
};
|
|
97
94
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
onClick: handleNewRoleClick,
|
|
107
|
-
color: 'primary',
|
|
108
|
-
type: 'button',
|
|
109
|
-
icon: true,
|
|
110
|
-
},
|
|
111
|
-
]
|
|
112
|
-
: [];
|
|
113
|
-
/* eslint-enable indent */
|
|
114
|
-
|
|
115
|
-
const checkCanDeleteRole = useCallback(
|
|
116
|
-
role => {
|
|
117
|
-
return canDelete && !['public', 'authenticated'].includes(role.type);
|
|
95
|
+
const pageTitle = formatMessage({
|
|
96
|
+
id: getTrad('HeaderNav.link.roles'),
|
|
97
|
+
defaultMessage: 'Roles',
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
const deleteMutation = useMutation(id => deleteData(id, toggleNotification), {
|
|
101
|
+
onSuccess: async () => {
|
|
102
|
+
await queryClient.invalidateQueries('get-roles');
|
|
118
103
|
},
|
|
119
|
-
|
|
120
|
-
);
|
|
104
|
+
});
|
|
121
105
|
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
icon: <FontAwesomeIcon icon="pencil-alt" />,
|
|
128
|
-
onClick: () => handleGoTo(role.id),
|
|
129
|
-
});
|
|
130
|
-
}
|
|
131
|
-
if (checkCanDeleteRole(role)) {
|
|
132
|
-
links.push({
|
|
133
|
-
icon: <FontAwesomeIcon icon="trash-alt" />,
|
|
134
|
-
onClick: e => {
|
|
135
|
-
e.preventDefault();
|
|
136
|
-
setModalDelete(role.id);
|
|
137
|
-
e.stopPropagation();
|
|
138
|
-
},
|
|
139
|
-
});
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
return links;
|
|
106
|
+
const handleConfirmDelete = async () => {
|
|
107
|
+
setIsConfirmButtonLoading(true);
|
|
108
|
+
await deleteMutation.mutateAsync(roleToDelete);
|
|
109
|
+
setShowConfirmDelete(!showConfirmDelete);
|
|
110
|
+
setIsConfirmButtonLoading(false);
|
|
143
111
|
};
|
|
144
112
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
<Helmet title={formatMessage({ id: getTrad('page.title') })} />
|
|
113
|
+
const sortedRoles = matchSorter(roles || [], _q, { keys: ['name', 'description'] });
|
|
114
|
+
const emptyContent = _q && !sortedRoles.length ? 'search' : 'roles';
|
|
148
115
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
116
|
+
const colCount = 4;
|
|
117
|
+
const rowCount = (roles?.length || 0) + 1;
|
|
118
|
+
|
|
119
|
+
return (
|
|
120
|
+
<Layout>
|
|
121
|
+
<SettingsPageTitle name={pageTitle} />
|
|
122
|
+
<Main aria-busy={isLoading}>
|
|
123
|
+
<HeaderLayout
|
|
124
|
+
title={formatMessage({
|
|
153
125
|
id: 'Settings.roles.title',
|
|
154
|
-
defaultMessage: 'Roles
|
|
155
|
-
})
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
126
|
+
defaultMessage: 'Roles',
|
|
127
|
+
})}
|
|
128
|
+
subtitle={formatMessage({
|
|
129
|
+
id: 'Settings.roles.list.description',
|
|
130
|
+
defaultMessage: 'List of roles',
|
|
131
|
+
})}
|
|
132
|
+
primaryAction={
|
|
133
|
+
<CheckPermissions permissions={permissions.createRole}>
|
|
134
|
+
<Button onClick={handleNewRoleClick} startIcon={<Plus />} size="L">
|
|
135
|
+
{formatMessage({
|
|
136
|
+
id: getTrad('List.button.roles'),
|
|
137
|
+
defaultMessage: 'Add new role',
|
|
138
|
+
})}
|
|
139
|
+
</Button>
|
|
140
|
+
</CheckPermissions>
|
|
141
|
+
}
|
|
142
|
+
/>
|
|
143
|
+
|
|
144
|
+
<ActionLayout
|
|
145
|
+
startActions={
|
|
146
|
+
<SearchURLQuery
|
|
147
|
+
label={formatMessage({
|
|
148
|
+
id: 'app.component.search.label',
|
|
149
|
+
defaultMessage: 'Search',
|
|
150
|
+
})}
|
|
151
|
+
/>
|
|
152
|
+
}
|
|
153
|
+
/>
|
|
154
|
+
|
|
155
|
+
<ContentLayout>
|
|
156
|
+
{!canRead && <NoPermissions />}
|
|
157
|
+
{(isLoading || isLoadingForPermissions) && <LoadingIndicatorPage />}
|
|
158
|
+
{canRead && sortedRoles && sortedRoles?.length ? (
|
|
159
|
+
<Table colCount={colCount} rowCount={rowCount}>
|
|
160
|
+
<Thead>
|
|
161
|
+
<Tr>
|
|
162
|
+
<Th>
|
|
163
|
+
<Typography variant="sigma" textColor="neutral600">
|
|
164
|
+
{formatMessage({ id: getTrad('Roles.name'), defaultMessage: 'Name' })}
|
|
165
|
+
</Typography>
|
|
166
|
+
</Th>
|
|
167
|
+
<Th>
|
|
168
|
+
<Typography variant="sigma" textColor="neutral600">
|
|
169
|
+
{formatMessage({
|
|
170
|
+
id: getTrad('Roles.description'),
|
|
171
|
+
defaultMessage: 'Description',
|
|
172
|
+
})}
|
|
173
|
+
</Typography>
|
|
174
|
+
</Th>
|
|
175
|
+
<Th>
|
|
176
|
+
<Typography variant="sigma" textColor="neutral600">
|
|
177
|
+
{formatMessage({
|
|
178
|
+
id: getTrad('Roles.users'),
|
|
179
|
+
defaultMessage: 'Users',
|
|
180
|
+
})}
|
|
181
|
+
</Typography>
|
|
182
|
+
</Th>
|
|
183
|
+
<Th>
|
|
184
|
+
<VisuallyHidden>
|
|
185
|
+
{formatMessage({
|
|
186
|
+
id: 'components.TableHeader.actions-label',
|
|
187
|
+
defaultMessage: 'Actions',
|
|
188
|
+
})}
|
|
189
|
+
</VisuallyHidden>
|
|
190
|
+
</Th>
|
|
191
|
+
</Tr>
|
|
192
|
+
</Thead>
|
|
193
|
+
<TableBody
|
|
194
|
+
sortedRoles={sortedRoles}
|
|
195
|
+
canDelete={canDelete}
|
|
196
|
+
permissions={permissions}
|
|
197
|
+
setRoleToDelete={setRoleToDelete}
|
|
198
|
+
onDelete={[showConfirmDelete, setShowConfirmDelete]}
|
|
199
|
+
/>
|
|
200
|
+
</Table>
|
|
201
|
+
) : (
|
|
202
|
+
<EmptyStateLayout content={emptyLayout[emptyContent]} />
|
|
203
|
+
)}
|
|
204
|
+
</ContentLayout>
|
|
205
|
+
<ConfirmDialog
|
|
206
|
+
isConfirmButtonLoading={isConfirmButtonLoading}
|
|
207
|
+
onConfirm={handleConfirmDelete}
|
|
208
|
+
onToggleDialog={handleShowConfirmDelete}
|
|
209
|
+
isOpen={showConfirmDelete}
|
|
210
|
+
/>
|
|
211
|
+
</Main>
|
|
212
|
+
</Layout>
|
|
193
213
|
);
|
|
194
214
|
};
|
|
195
215
|
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { getRequestURL, axiosInstance } from '../../../../utils';
|
|
2
|
+
|
|
3
|
+
export const fetchData = async (toggleNotification, notifyStatus) => {
|
|
4
|
+
try {
|
|
5
|
+
const { data } = await axiosInstance.get(getRequestURL('roles'));
|
|
6
|
+
notifyStatus('The roles have loaded successfully');
|
|
7
|
+
|
|
8
|
+
return data;
|
|
9
|
+
} catch (err) {
|
|
10
|
+
toggleNotification({
|
|
11
|
+
type: 'warning',
|
|
12
|
+
message: { id: 'notification.error' },
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
throw new Error('error');
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export const deleteData = async (id, toggleNotification) => {
|
|
20
|
+
try {
|
|
21
|
+
await axiosInstance.delete(`${getRequestURL('roles')}/${id}`);
|
|
22
|
+
} catch (error) {
|
|
23
|
+
toggleNotification({
|
|
24
|
+
type: 'warning',
|
|
25
|
+
message: { id: 'notification.error', defaultMessage: 'An error occured' },
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
};
|
|
@@ -1,20 +1,26 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { Switch, Route } from 'react-router-dom';
|
|
3
|
-
import { NotFound } from '@strapi/helper-plugin';
|
|
3
|
+
import { CheckPagePermissions, NotFound } from '@strapi/helper-plugin';
|
|
4
4
|
import pluginId from '../../pluginId';
|
|
5
|
-
|
|
5
|
+
import pluginPermissions from '../../permissions';
|
|
6
6
|
import ProtectedRolesListPage from './ProtectedListPage';
|
|
7
7
|
import ProtectedRolesEditPage from './ProtectedEditPage';
|
|
8
8
|
import ProtectedRolesCreatePage from './ProtectedCreatePage';
|
|
9
9
|
|
|
10
10
|
const Roles = () => {
|
|
11
11
|
return (
|
|
12
|
-
<
|
|
13
|
-
<
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
12
|
+
<CheckPagePermissions permissions={pluginPermissions.accessRoles}>
|
|
13
|
+
<Switch>
|
|
14
|
+
<Route
|
|
15
|
+
path={`/settings/${pluginId}/roles/new`}
|
|
16
|
+
component={ProtectedRolesCreatePage}
|
|
17
|
+
exact
|
|
18
|
+
/>
|
|
19
|
+
<Route path={`/settings/${pluginId}/roles/:id`} component={ProtectedRolesEditPage} exact />
|
|
20
|
+
<Route path={`/settings/${pluginId}/roles`} component={ProtectedRolesListPage} exact />
|
|
21
|
+
<Route path="" component={NotFound} />
|
|
22
|
+
</Switch>
|
|
23
|
+
</CheckPagePermissions>
|
|
18
24
|
);
|
|
19
25
|
};
|
|
20
26
|
|
|
@@ -10,17 +10,9 @@
|
|
|
10
10
|
"HeaderNav.link.emailTemplates": "قوالب الإيميل",
|
|
11
11
|
"HeaderNav.link.providers": "مزودين",
|
|
12
12
|
"HeaderNav.link.roles": "الأدوار والأذونات",
|
|
13
|
-
"List.title.emailTemplates.plural": "يتوفر {number} نماذج للبريد الإلكتروني",
|
|
14
|
-
"List.title.emailTemplates.singular": "يتوفر {number} نموذج للبريد الإلكتروني",
|
|
15
|
-
"List.title.providers.disabled.plural": "{number} معطلين",
|
|
16
|
-
"List.title.providers.disabled.singular": "{number} معطل",
|
|
17
|
-
"List.title.providers.enabled.plural": "يتم تمكين {number} مزودين و",
|
|
18
|
-
"List.title.providers.enabled.singular": "يتم تمكين {number} مزود و",
|
|
19
13
|
"Plugin.permissions.plugins.description": "حدد جميع الإجراءات المسموح بها للإضافة {name}.",
|
|
20
14
|
"Plugins.header.description": "يتم سرد الإجراءات المحددة المرتبطة بالمسار أدناه.",
|
|
21
15
|
"Plugins.header.title": "الصلاحيات",
|
|
22
|
-
"Policies.InputSelect.empty": "لا شيء",
|
|
23
|
-
"Policies.InputSelect.label": "السماح بتنفيذ هذا الإجراء لـ:",
|
|
24
16
|
"Policies.header.hint": "حدد إجراءات التطبيق أو إجراءات الإضافة وانقر على رمز الترس لعرض المسار المرتبط",
|
|
25
17
|
"Policies.header.title": "إعدادات متقدمة",
|
|
26
18
|
"PopUpForm.Email.email_templates.inputDescription": "إذا كنت غير متأكد من كيفية استخدام المتغيرات ، {link}",
|
|
@@ -16,17 +16,9 @@
|
|
|
16
16
|
"HeaderNav.link.emailTemplates": "E-mailové šablony",
|
|
17
17
|
"HeaderNav.link.providers": "Poskytovatelé",
|
|
18
18
|
"HeaderNav.link.roles": "Role & Práva",
|
|
19
|
-
"List.title.emailTemplates.plural": "{number} e-mailových šablon je k dispozici",
|
|
20
|
-
"List.title.emailTemplates.singular": "{number} e-mailová šablona je k dispozici",
|
|
21
|
-
"List.title.providers.disabled.plural": "{number} jsou zakázána",
|
|
22
|
-
"List.title.providers.disabled.singular": "{number} je zakázán",
|
|
23
|
-
"List.title.providers.enabled.plural": "{number} poskytovatele jsou nyní povoleni a",
|
|
24
|
-
"List.title.providers.enabled.singular": "{number} poskytovatel je nyní povolen a",
|
|
25
19
|
"Plugin.permissions.plugins.description": "Nastavit všechny akce pro zásuvný modul {name}.",
|
|
26
20
|
"Plugins.header.description": "Pouze akce spojené s adresou jsou vypsány níže.",
|
|
27
21
|
"Plugins.header.title": "Povolení",
|
|
28
|
-
"Policies.InputSelect.empty": "Žádné",
|
|
29
|
-
"Policies.InputSelect.label": "Povolit vykonání této akce:",
|
|
30
22
|
"Policies.header.hint": "Vyberte akce aplikace, nebo akce zásuvného modulu a klikněte na ikonku ozubeného kolečka pro zobrazení adresy s nimi spojenou.",
|
|
31
23
|
"Policies.header.title": "Pokročilá nastavení",
|
|
32
24
|
"PopUpForm.Email.email_templates.inputDescription": "Pokud si nejste jisti jak používat proměnné, {link}",
|
|
@@ -18,17 +18,9 @@
|
|
|
18
18
|
"HeaderNav.link.emailTemplates": "E-Mail-Templates",
|
|
19
19
|
"HeaderNav.link.providers": "Methoden",
|
|
20
20
|
"HeaderNav.link.roles": "Rollen",
|
|
21
|
-
"List.title.emailTemplates.plural": "{number} E-Mail-Templates sind verfügbar",
|
|
22
|
-
"List.title.emailTemplates.singular": "{number} E-Mail-Template ist verfügbar",
|
|
23
|
-
"List.title.providers.disabled.plural": "{number} sind deaktiviert",
|
|
24
|
-
"List.title.providers.disabled.singular": "{number} ist deaktiviert",
|
|
25
|
-
"List.title.providers.enabled.plural": "{number} Methoden sind aktiviert und",
|
|
26
|
-
"List.title.providers.enabled.singular": "{number} Methode ist aktiviert und",
|
|
27
21
|
"Plugin.permissions.plugins.description": "Definiere die möglichen Aktionen des {name} Plugins.",
|
|
28
22
|
"Plugins.header.description": "Nur Aktionen, die an einen Pfad gebunden sind, werden hier gelistet.",
|
|
29
23
|
"Plugins.header.title": "Berechtigungen",
|
|
30
|
-
"Policies.InputSelect.empty": "Keine",
|
|
31
|
-
"Policies.InputSelect.label": "Diese Aktion folgenden erlauben:",
|
|
32
24
|
"Policies.header.hint": "Wähle eine Aktion aus und klicke auf das Zahnrad, um den an diese Aktion gebundenen Pfad anzuzeigen",
|
|
33
25
|
"Policies.header.title": "Fortgeschrittene Einstellungen",
|
|
34
26
|
"PopUpForm.Email.email_templates.inputDescription": "{link} für mehr Informationen",
|
|
@@ -18,17 +18,9 @@
|
|
|
18
18
|
"HeaderNav.link.emailTemplates": "E-mail skabeloner",
|
|
19
19
|
"HeaderNav.link.providers": "Udbydere",
|
|
20
20
|
"HeaderNav.link.roles": "Roller & rettigheder",
|
|
21
|
-
"List.title.emailTemplates.plural": "{number} e-mail skabeloner tilgængelige",
|
|
22
|
-
"List.title.emailTemplates.singular": "{number} e-mail skabelon tilgængelig",
|
|
23
|
-
"List.title.providers.disabled.plural": "{number} er inaktive",
|
|
24
|
-
"List.title.providers.disabled.singular": "{number} er inaktiv",
|
|
25
|
-
"List.title.providers.enabled.plural": "{number} udbydere er aktive og",
|
|
26
|
-
"List.title.providers.enabled.singular": "{number} udbyder er aktiv og",
|
|
27
21
|
"Plugin.permissions.plugins.description": "Definér alle tilladte handlinger for {name} plugin.",
|
|
28
22
|
"Plugins.header.description": "Kunne handlinger tilknyttet en rute er vist nedenfor.",
|
|
29
23
|
"Plugins.header.title": "Rettigheder",
|
|
30
|
-
"Policies.InputSelect.empty": "Ingen",
|
|
31
|
-
"Policies.InputSelect.label": "Tillad denne handling for:",
|
|
32
24
|
"Policies.header.hint": "Vælg applikationens handlinger eller plugin handlinger og klik på tandhjulet for at vise den bunde rute",
|
|
33
25
|
"Policies.header.title": "Advancerede indstillinger",
|
|
34
26
|
"PopUpForm.Email.email_templates.inputDescription": "Hvis du er usikker på brugen af variabler, {link}",
|
|
@@ -12,27 +12,26 @@
|
|
|
12
12
|
"EditForm.inputToggle.label.email-confirmation-redirection": "Redirection url",
|
|
13
13
|
"EditForm.inputToggle.label.email-reset-password": "Reset password page",
|
|
14
14
|
"EditForm.inputToggle.label.sign-up": "Enable sign-ups",
|
|
15
|
-
"EditForm.inputToggle.placeholder.email-reset-password": "ex: https://yourfrontend.com/reset-password",
|
|
16
15
|
"EditForm.inputToggle.placeholder.email-confirmation-redirection": "ex: https://yourfrontend.com/reset-password",
|
|
16
|
+
"EditForm.inputToggle.placeholder.email-reset-password": "ex: https://yourfrontend.com/reset-password",
|
|
17
17
|
"EditPage.form.roles": "Role details",
|
|
18
|
+
"Email.template.data.loaded": "Email templates has been loaded",
|
|
18
19
|
"Email.template.email_confirmation": "Email address confirmation",
|
|
20
|
+
"Email.template.form.edit.label": "Edit a template",
|
|
19
21
|
"Email.template.reset_password": "Reset password",
|
|
22
|
+
"Email.template.table.action.label": "action",
|
|
23
|
+
"Email.template.table.icon.label": "icon",
|
|
24
|
+
"Email.template.table.name.label": "name",
|
|
25
|
+
"Form.advancedSettings.data.loaded": "Advanced settings data has been loaded",
|
|
26
|
+
"Form.save": "Save",
|
|
27
|
+
"Form.title.advancedSettings": "Settings",
|
|
20
28
|
"HeaderNav.link.advancedSettings": "Advanced settings",
|
|
21
|
-
"HeaderNav.link.emailTemplates": "Email
|
|
29
|
+
"HeaderNav.link.emailTemplates": "Email templates",
|
|
22
30
|
"HeaderNav.link.providers": "Providers",
|
|
23
31
|
"HeaderNav.link.roles": "Roles",
|
|
24
|
-
"List.title.emailTemplates.plural": "{number} email templates are available",
|
|
25
|
-
"List.title.emailTemplates.singular": "{number} email template is available",
|
|
26
|
-
"List.title.providers.disabled.plural": "{number} are disabled",
|
|
27
|
-
"List.title.providers.disabled.singular": "{number} is disabled",
|
|
28
|
-
"List.title.providers.enabled.plural": "{number} providers are enabled and",
|
|
29
|
-
"List.title.providers.enabled.singular": "{number} provider is enabled and",
|
|
30
|
-
"Form.title.advancedSettings": "Settings",
|
|
31
32
|
"Plugin.permissions.plugins.description": "Define all allowed actions for the {name} plugin.",
|
|
32
33
|
"Plugins.header.description": "Only actions bound by a route are listed below.",
|
|
33
34
|
"Plugins.header.title": "Permissions",
|
|
34
|
-
"Policies.InputSelect.empty": "None",
|
|
35
|
-
"Policies.InputSelect.label": "Allow to perform this action for:",
|
|
36
35
|
"Policies.header.hint": "Select the application's actions or the plugin's actions and click on the cog icon to display the bound route",
|
|
37
36
|
"Policies.header.title": "Advanced settings",
|
|
38
37
|
"PopUpForm.Email.email_templates.inputDescription": "If you're unsure how to use variables, {link}",
|
|
@@ -56,11 +55,33 @@
|
|
|
56
55
|
"PopUpForm.Providers.secret.placeholder": "TEXT",
|
|
57
56
|
"PopUpForm.Providers.subdomain.label": "Host URI (Subdomain)",
|
|
58
57
|
"PopUpForm.Providers.subdomain.placeholder": "my.subdomain.com",
|
|
59
|
-
"PopUpForm.header.edit.email-templates": "Edit
|
|
58
|
+
"PopUpForm.header.edit.email-templates": "Edit email template",
|
|
60
59
|
"PopUpForm.header.edit.providers": "Edit Provider",
|
|
60
|
+
"Providers.data.loaded": "Providers have been loaded",
|
|
61
|
+
"Providers.disabled": "Disabled",
|
|
62
|
+
"Providers.enabled": "Enabled",
|
|
63
|
+
"Providers.image": "Image",
|
|
64
|
+
"Providers.name": "Name",
|
|
65
|
+
"Providers.settings": "Settings",
|
|
66
|
+
"Providers.status": "Status",
|
|
67
|
+
"Roles.description": "Description",
|
|
68
|
+
"Roles.empty": "You don't have any roles yet.",
|
|
69
|
+
"Roles.empty.search": "No roles match the search.",
|
|
70
|
+
"Roles.name": "Name",
|
|
71
|
+
"Roles.users": "Users",
|
|
61
72
|
"Settings.roles.deleted": "Role deleted",
|
|
62
73
|
"Settings.roles.edited": "Role edited",
|
|
63
74
|
"Settings.section-label": "Users & Permissions plugin",
|
|
75
|
+
"components.Input.error.validation.email": "This is an invalid email",
|
|
76
|
+
"components.Input.error.validation.json": "This doesn't match the JSON format",
|
|
77
|
+
"components.Input.error.validation.max": "The value is too high.",
|
|
78
|
+
"components.Input.error.validation.maxLength": "The value is too long.",
|
|
79
|
+
"components.Input.error.validation.min": "The value is too low.",
|
|
80
|
+
"components.Input.error.validation.minLength": "The value is too short.",
|
|
81
|
+
"components.Input.error.validation.minSupMax": "Can't be superior",
|
|
82
|
+
"components.Input.error.validation.regex": "The value does not match the regex.",
|
|
83
|
+
"components.Input.error.validation.required": "This value is required.",
|
|
84
|
+
"components.Input.error.validation.unique": "This value is already used.",
|
|
64
85
|
"notification.success.submit": "Settings have been updated",
|
|
65
86
|
"page.title": "Settings - Roles",
|
|
66
87
|
"plugin.description.long": "Protect your API with a full authentication process based on JWT. This plugin comes also with an ACL strategy that allows you to manage the permissions between the groups of users.",
|
|
@@ -18,17 +18,9 @@
|
|
|
18
18
|
"HeaderNav.link.emailTemplates": "Plantillas de email",
|
|
19
19
|
"HeaderNav.link.providers": "Proveedores",
|
|
20
20
|
"HeaderNav.link.roles": "Roles y Permisos",
|
|
21
|
-
"List.title.emailTemplates.plural": "{number} plantillas de email disponibles",
|
|
22
|
-
"List.title.emailTemplates.singular": "{number} plantilla de email está disponible",
|
|
23
|
-
"List.title.providers.disabled.plural": "{number} están desactivados",
|
|
24
|
-
"List.title.providers.disabled.singular": "{number} está desactivado",
|
|
25
|
-
"List.title.providers.enabled.plural": "{number} proveedores están habilitados y",
|
|
26
|
-
"List.title.providers.enabled.singular": "{number} está habilitado y",
|
|
27
21
|
"Plugin.permissions.plugins.description": "Defina todas las acciones permitidas para el plugin {name}.",
|
|
28
22
|
"Plugins.header.description": "Sólo las acciones vinculadas a una ruta se enumeran a continuación.",
|
|
29
23
|
"Plugins.header.title": "Permisos",
|
|
30
|
-
"Policies.InputSelect.empty": "Ninguno",
|
|
31
|
-
"Policies.InputSelect.label": "Permita que se realice esta acción para:",
|
|
32
24
|
"Policies.header.hint": "Seleccione las acciones de la aplicación o las acciones del plugin y haga clic en el icono del engranaje para ver la ruta vinculada",
|
|
33
25
|
"Policies.header.title": "Ajustes avanzados",
|
|
34
26
|
"PopUpForm.Email.email_templates.inputDescription": "Si no estás seguro de cómo usar las variables, {link}",
|
|
@@ -16,17 +16,9 @@
|
|
|
16
16
|
"HeaderNav.link.emailTemplates": "Templates d'e-mail",
|
|
17
17
|
"HeaderNav.link.providers": "Fournisseurs",
|
|
18
18
|
"HeaderNav.link.roles": "Rôles & Permissions",
|
|
19
|
-
"List.title.emailTemplates.plural": "{number} templates d'e-mail sont disponibles",
|
|
20
|
-
"List.title.emailTemplates.singular": "{number} template d'e-mail est disponible",
|
|
21
|
-
"List.title.providers.disabled.plural": "{number} indisponibles",
|
|
22
|
-
"List.title.providers.disabled.singular": "{number} indisponible",
|
|
23
|
-
"List.title.providers.enabled.plural": "{number} providers sont disponibles et",
|
|
24
|
-
"List.title.providers.enabled.singular": "{number} provider est disponible et",
|
|
25
19
|
"Plugin.permissions.plugins.description": "Définissez les actions autorisées dans le {name} plugin.",
|
|
26
20
|
"Plugins.header.description": "Sont listés uniquement les actions associées à une route.",
|
|
27
21
|
"Plugins.header.title": "Permissions",
|
|
28
|
-
"Policies.InputSelect.empty": "Aucune",
|
|
29
|
-
"Policies.InputSelect.label": "Autorisez cette action pour :",
|
|
30
22
|
"Policies.header.hint": "Sélectionnez les actions de l'application ou d'un plugin et cliquer sur l'icon de paramètres pour voir les routes associées à cette action",
|
|
31
23
|
"Policies.header.title": "Paramètres avancés",
|
|
32
24
|
"PopUpForm.Email.email_templates.inputDescription": "Regardez la documentation des variables, {link}",
|
|
@@ -18,17 +18,9 @@
|
|
|
18
18
|
"HeaderNav.link.emailTemplates": "Template email",
|
|
19
19
|
"HeaderNav.link.providers": "Penyedia",
|
|
20
20
|
"HeaderNav.link.roles": "Peran",
|
|
21
|
-
"List.title.emailTemplates.plural": "Template email {number} tersedia",
|
|
22
|
-
"List.title.emailTemplates.singular": "Template email {number} tersedia",
|
|
23
|
-
"List.title.providers.disabled.plural": "{number} dinonaktifkan",
|
|
24
|
-
"List.title.providers.disabled.singular": "{number} dinonaktifkan",
|
|
25
|
-
"List.title.providers.enabled.plural": "{number} penyedia diaktifkan dan",
|
|
26
|
-
"List.title.providers.enabled.singular": "{number} penyedia diaktifkan dan",
|
|
27
21
|
"Plugin.permissions.plugins.description": "Tentukan semua tindakan yang diizinkan untuk plugin {name}.",
|
|
28
22
|
"Plugins.header.description": "Hanya tindakan yang terikat oleh rute yang dicantumkan di bawah.",
|
|
29
23
|
"Plugins.header.title": "Izin",
|
|
30
|
-
"Policies.InputSelect.empty": "Tidak ada",
|
|
31
|
-
"Policies.InputSelect.label": "Izinkan untuk melakukan tindakan ini untuk:",
|
|
32
24
|
"Policies.header.hint": "Pilih tindakan aplikasi atau tindakan plugin dan klik ikon roda gigi untuk menampilkan rute terikat",
|
|
33
25
|
"Policies.header.title": "Pengaturan lanjutan",
|
|
34
26
|
"PopUpForm.Email.email_templates.inputDescription": "Jika Anda tidak yakin bagaimana menggunakan variabel, {link}",
|