Skip to content

Commit 533bf2e

Browse files
Added Admin Settings changes for super editor role (#22452)
ref #22445 This PR contains the admin-x-settings changes needed to allow the super editor role. --------- Co-authored-by: Steve Larson <[email protected]>
1 parent 36d73d2 commit 533bf2e

File tree

11 files changed

+129
-33
lines changed

11 files changed

+129
-33
lines changed

.github/workflows/ci.yml

+4
Original file line numberDiff line numberDiff line change
@@ -470,6 +470,10 @@ jobs:
470470
with:
471471
timezoneLinux: "America/New_York"
472472

473+
# Ensure all built packages are built before running tests (admin is built in job_setup)
474+
- name: Build remaining assets
475+
run: yarn nx run-many -t build --exclude=ghost-admin
476+
473477
- run: yarn nx affected -t test:unit --base=${{ needs.job_setup.outputs.BASE_COMMIT }}
474478

475479
- uses: actions/upload-artifact@v4

apps/admin-x-framework/src/api/roles.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import {Meta, createQuery} from '../utils/api/hooks';
22

3-
export type UserRoleType = 'Owner' | 'Administrator' | 'Editor' | 'Author' | 'Contributor';
3+
export type UserRoleType = 'Owner' | 'Administrator' | 'Editor' | 'Author' | 'Contributor' | 'Super Editor';
44

55
export type UserRole = {
66
id: string;

apps/admin-x-framework/src/api/users.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,9 @@ export function isAdminUser(user: User) {
148148
}
149149

150150
export function isEditorUser(user: User) {
151-
return user.roles.some(role => role.name === 'Editor');
151+
const isAnyEditor = user.roles.some(role => role.name === 'Editor')
152+
|| user.roles.some(role => role.name === 'Super Editor');
153+
return isAnyEditor;
152154
}
153155

154156
export function isAuthorUser(user: User) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { Meta } from '../utils/api/hooks';
2+
export type UserRoleType = 'Owner' | 'Administrator' | 'Editor' | 'Author' | 'Contributor' | 'Super Editor';
3+
export type UserRole = {
4+
id: string;
5+
name: UserRoleType;
6+
description: string;
7+
created_at: string;
8+
updated_at: string;
9+
};
10+
export interface RolesResponseType {
11+
meta?: Meta;
12+
roles: UserRole[];
13+
}
14+
export declare const useBrowseRoles: ({ searchParams, ...query }?: import("@tanstack/react-query").UseQueryOptions<RolesResponseType, unknown, RolesResponseType, import("@tanstack/query-core").QueryKey> & {
15+
searchParams?: Record<string, string> | undefined;
16+
defaultErrorHandler?: boolean | undefined;
17+
}) => Omit<import("@tanstack/react-query").UseQueryResult<RolesResponseType>, "data"> & {
18+
data: RolesResponseType | undefined;
19+
};

apps/admin-x-settings/src/components/settings/general/InviteUserModal.tsx

+19-4
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,11 @@ import {useAddInvite, useBrowseInvites} from '@tryghost/admin-x-framework/api/in
77
import {useBrowseRoles} from '@tryghost/admin-x-framework/api/roles';
88
import {useBrowseUsers} from '@tryghost/admin-x-framework/api/users';
99
import {useEffect, useRef, useState} from 'react';
10+
import {useGlobalData} from '../../providers/GlobalDataProvider';
1011
import {useHandleError} from '@tryghost/admin-x-framework/hooks';
1112
import {useRouting} from '@tryghost/admin-x-framework/routing';
1213

13-
type RoleType = 'administrator' | 'editor' | 'author' | 'contributor';
14+
type RoleType = 'administrator' | 'editor' | 'author' | 'contributor' | 'super editor';
1415

1516
const InviteUserModal = NiceModal.create(() => {
1617
const modal = NiceModal.useModal();
@@ -21,7 +22,8 @@ const InviteUserModal = NiceModal.create(() => {
2122
const limiter = useLimiter();
2223

2324
const {updateRoute} = useRouting();
24-
25+
const {config} = useGlobalData();
26+
const editorBeta = config.labs.superEditors;
2527
const focusRef = useRef<HTMLInputElement>(null);
2628
const [email, setEmail] = useState<string>('');
2729
const [saveState, setSaveState] = useState<'saving' | 'saved' | 'error' | ''>('');
@@ -173,10 +175,23 @@ const InviteUserModal = NiceModal.create(() => {
173175
value: 'administrator'
174176
}
175177
];
176-
178+
179+
// If the editor beta is enabled, replace the editor role option with super editor options.
180+
// This gets a little weird, because we aren't changing what is actually assigned based on the toggle.
181+
// So, a site could have the editor beta enabled, but that doesn't automatically convert their editors.
182+
// (Editors can be up/downgraded by reassigning them in this modal. For 6.0, we should decide whether
183+
// the old editors are going away or whether both roles are staying, and tidy this up then.)
184+
185+
if (editorBeta) {
186+
roleOptions[2] = {
187+
hint: 'Can invite and manage other Authors and Contributors, as well as edit and publish any posts on the site. Can manage members and moderate comments.',
188+
label: 'Editor (beta mode)',
189+
value: 'super editor'
190+
};
191+
};
177192
const allowedRoleOptions = roleOptions.filter((option) => {
178193
return assignableRoles.some((r) => {
179-
return r.name === option.label;
194+
return r.name === option.label || (r.name === 'Super Editor' && option.label === 'Editor (beta mode)');
180195
});
181196
});
182197

apps/admin-x-settings/src/components/settings/general/users/RoleSelector.tsx

+40-22
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
import {Heading, Icon, Radio} from '@tryghost/admin-x-design-system';
22
import {User, isOwnerUser} from '@tryghost/admin-x-framework/api/users';
33
import {useBrowseRoles} from '@tryghost/admin-x-framework/api/roles';
4+
import {useGlobalData} from '../../../providers/GlobalDataProvider';
45

56
const RoleSelector: React.FC<{ user: User; setUserData: (user: User) => void; }> = ({user, setUserData}) => {
67
const {data: {roles} = {}} = useBrowseRoles();
8+
const {config} = useGlobalData();
9+
const editorBeta = config.labs.superEditors;
710

811
if (isOwnerUser(user)) {
912
return (
@@ -16,32 +19,47 @@ const RoleSelector: React.FC<{ user: User; setUserData: (user: User) => void; }>
1619
</>
1720
);
1821
}
22+
let optionsArray = [
23+
{
24+
hint: 'Can create and edit their own posts, but cannot publish. An Editor needs to approve and publish for them.',
25+
label: 'Contributor',
26+
value: 'contributor'
27+
},
28+
{
29+
hint: 'A trusted user who can create, edit and publish their own posts, but can’t modify others.',
30+
label: 'Author',
31+
value: 'author'
32+
},
33+
{
34+
hint: 'Can invite and manage other Authors and Contributors, as well as edit and publish any posts on the site.',
35+
label: 'Editor',
36+
value: 'editor'
37+
},
38+
{
39+
hint: 'Trusted staff user who should be able to manage all content and users, as well as site settings and options.',
40+
label: 'Administrator',
41+
value: 'administrator'
42+
}
43+
];
44+
// if the editor beta is enabled, replace the editor role with super editor
45+
if (editorBeta) {
46+
optionsArray = optionsArray.map((option) => {
47+
if (option.value === 'editor') {
48+
return {
49+
...option,
50+
label: 'Editor (beta mode)',
51+
value: 'super editor',
52+
hint: 'Can invite and manage other Authors and Contributors, as well as edit and publish any posts on the site. Can manage members and moderate comments.'
53+
};
54+
}
55+
return option;
56+
});
57+
}
1958

2059
return (
2160
<Radio
2261
id='role'
23-
options={[
24-
{
25-
hint: 'Can create and edit their own posts, but cannot publish. An Editor needs to approve and publish for them.',
26-
label: 'Contributor',
27-
value: 'contributor'
28-
},
29-
{
30-
hint: 'A trusted user who can create, edit and publish their own posts, but can’t modify others.',
31-
label: 'Author',
32-
value: 'author'
33-
},
34-
{
35-
hint: 'Can invite and manage other Authors and Contributors, as well as edit and publish any posts on the site.',
36-
label: 'Editor',
37-
value: 'editor'
38-
},
39-
{
40-
hint: 'Trusted staff user who should be able to manage all content and users, as well as site settings and options.',
41-
label: 'Administrator',
42-
value: 'administrator'
43-
}
44-
]}
62+
options={optionsArray}
4563
selectedOption={user.roles[0].name.toLowerCase()}
4664
title="Role"
4765
onSelect={(value) => {

apps/admin-x-settings/src/hooks/useStaffUsers.tsx

+9-1
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,14 @@ function getUsersByRole(users: User[], role: string): User[] {
2727
});
2828
}
2929

30+
function getUsersByRoles(users: User[], roles: string[]): User[] {
31+
return users.filter((user) => {
32+
return user.roles.find((userRole) => {
33+
return roles.includes(userRole.name);
34+
});
35+
});
36+
}
37+
3038
function getOwnerUser(users: User[]): User {
3139
return getUsersByRole(users, 'Owner')[0];
3240
}
@@ -39,7 +47,7 @@ const useStaffUsers = (): UsersHook => {
3947

4048
const ownerUser = useMemo(() => getOwnerUser(users), [users]);
4149
const adminUsers = useMemo(() => getUsersByRole(users, 'Administrator'), [users]);
42-
const editorUsers = useMemo(() => getUsersByRole(users, 'Editor'), [users]);
50+
const editorUsers = useMemo(() => getUsersByRoles(users, ['Editor', 'Super Editor']), [users]);
4351
const authorUsers = useMemo(() => getUsersByRole(users, 'Author'), [users]);
4452
const contributorUsers = useMemo(() => getUsersByRole(users, 'Contributor'), [users]);
4553
const mappedInvites = useMemo(() => invites.map((invite) => {

ghost/core/core/server/models/invite.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -87,12 +87,12 @@ Invite = ghostBookshelf.Model.extend({
8787
if (loadedPermissions.user) {
8888
const {isOwner, isAdmin, isEitherEditor} = setIsRoles(loadedPermissions);
8989
if (isOwner || isAdmin) {
90-
allowed = ['Administrator', 'Editor', 'Author', 'Contributor'];
90+
allowed = ['Administrator', 'Editor', 'Author', 'Contributor', 'Super Editor'];
9191
} else if (isEitherEditor) {
9292
allowed = ['Author', 'Contributor'];
9393
}
9494
} else if (loadedPermissions.apiKey) {
95-
allowed = ['Editor', 'Author', 'Contributor'];
95+
allowed = ['Editor', 'Author', 'Contributor', 'Super Editor'];
9696
}
9797

9898
if (allowed.indexOf(roleToInvite.get('name')) === -1) {

ghost/core/core/shared/labs.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,8 @@ const BETA_FEATURES = [
3838
'ActivityPub',
3939
'importMemberTier',
4040
'staff2fa',
41-
'contentVisibility'
41+
'contentVisibility',
42+
'superEditors'
4243
];
4344

4445
const ALPHA_FEATURES = [

ghost/core/test/e2e-api/admin/__snapshots__/config.test.js.snap

+1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ Object {
2929
"members": true,
3030
"postsX": true,
3131
"stripeAutomaticTax": true,
32+
"superEditors": true,
3233
"themeErrorsNotification": true,
3334
"urlCache": true,
3435
"webmentions": true,

ghost/core/test/e2e-api/admin/invites.test.js

+29-1
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ describe('Invites API', function () {
116116
mailService.GhostMailer.prototype.send.called.should.be.false();
117117
});
118118
});
119+
119120
describe('As Admin Integration', function () {
120121
before(async function () {
121122
await localUtils.startGhost();
@@ -212,6 +213,33 @@ describe('Invites API', function () {
212213
res.headers.location.should.equal(`http://127.0.0.1:2369${localUtils.API.getApiQuery('invites/')}${res.body.invites[0].id}/`);
213214
});
214215

216+
it('Can add a new invite by API Key with the Super Editor Role', async function () {
217+
const roleId = testUtils.getExistingData().roles.find(role => role.name === 'Super Editor').id;
218+
const res = await request
219+
.post(localUtils.API.getApiQuery('invites/'))
220+
.set('Authorization', `Ghost ${localUtils.getValidAdminToken('/admin/')}`)
221+
.send({
222+
invites: [{email: '[email protected]', role_id: roleId}]
223+
})
224+
.expect('Content-Type', /json/)
225+
.expect('Cache-Control', testUtils.cacheRules.private)
226+
.expect(201);
227+
228+
should.not.exist(res.headers['x-cache-invalidate']);
229+
const jsonResponse = res.body;
230+
should.exist(jsonResponse);
231+
should.exist(jsonResponse.invites);
232+
jsonResponse.invites.should.have.length(1);
233+
234+
localUtils.API.checkResponse(jsonResponse.invites[0], 'invite');
235+
jsonResponse.invites[0].role_id.should.eql(roleId);
236+
237+
mailService.GhostMailer.prototype.send.called.should.be.true();
238+
239+
should.exist(res.headers.location);
240+
res.headers.location.should.equal(`http://127.0.0.1:2369${localUtils.API.getApiQuery('invites/')}${res.body.invites[0].id}/`);
241+
});
242+
215243
it('Can not add a new invite by API Key with the Administrator Role', async function () {
216244
const roleId = testUtils.getExistingData().roles.find(role => role.name === 'Administrator').id;
217245
await request
@@ -225,4 +253,4 @@ describe('Invites API', function () {
225253
.expect(403);
226254
});
227255
});
228-
});
256+
});

0 commit comments

Comments
 (0)