Skip to content

Commit 6ee1bbd

Browse files
committed
feat(marketplace): open-to-everyone mode banner on the admin allow-list page
1 parent 2b47933 commit 6ee1bbd

5 files changed

Lines changed: 285 additions & 16 deletions

File tree

client/app/api/system/Admin.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@ export default class AdminAPI extends BaseSystemAPI {
182182
* Fetches the marketplace allow-list rules.
183183
*/
184184
indexMarketplaceAllowlistRules(): Promise<
185-
AxiosResponse<{ rules: AllowlistRuleData[] }>
185+
AxiosResponse<{ rules: AllowlistRuleData[]; everyoneRuleId: number | null }>
186186
> {
187187
return this.client.get(
188188
`${AdminAPI.#urlPrefix}/marketplace_allowlist_rules`,
@@ -208,6 +208,17 @@ export default class AdminAPI extends BaseSystemAPI {
208208
);
209209
}
210210

211+
/**
212+
* Opens the marketplace to everyone by creating the single `everyone` allow-list rule.
213+
* Returns the created rule; only its `id` is consumed (to later restrict).
214+
*/
215+
openMarketplaceToEveryone(): Promise<AxiosResponse<{ id: number }>> {
216+
return this.client.post(
217+
`${AdminAPI.#urlPrefix}/marketplace_allowlist_rules`,
218+
{ allowlist_rule: { rule_type: 'everyone' } },
219+
);
220+
}
221+
211222
/**
212223
* Deletes a marketplace allow-list rule.
213224
*/
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import { useState } from 'react';
2+
import { defineMessages } from 'react-intl';
3+
import { Alert, Button } from '@mui/material';
4+
5+
import Prompt from 'lib/components/core/dialogs/Prompt';
6+
import useTranslation from 'lib/hooks/useTranslation';
7+
8+
interface Props {
9+
openToEveryone: boolean;
10+
onOpenToEveryone: () => Promise<void>;
11+
onRestrict: () => Promise<void>;
12+
}
13+
14+
const translations = defineMessages({
15+
scopedTitle: {
16+
id: 'system.admin.admin.MarketplaceAllowlistModeBanner.scopedTitle',
17+
defaultMessage: 'Access is limited to the rules below.',
18+
},
19+
everyoneTitle: {
20+
id: 'system.admin.admin.MarketplaceAllowlistModeBanner.everyoneTitle',
21+
defaultMessage:
22+
'The marketplace is open to all course managers. The rules below are preserved but inactive.',
23+
},
24+
openButton: {
25+
id: 'system.admin.admin.MarketplaceAllowlistModeBanner.openButton',
26+
defaultMessage: 'Open to everyone',
27+
},
28+
restrictButton: {
29+
id: 'system.admin.admin.MarketplaceAllowlistModeBanner.restrictButton',
30+
defaultMessage: 'Restrict to scoped rules',
31+
},
32+
openConfirmTitle: {
33+
id: 'system.admin.admin.MarketplaceAllowlistModeBanner.openConfirmTitle',
34+
defaultMessage: 'Open marketplace to everyone?',
35+
},
36+
openConfirmBody: {
37+
id: 'system.admin.admin.MarketplaceAllowlistModeBanner.openConfirmBody',
38+
defaultMessage:
39+
'This makes the marketplace visible to all course managers and owners in every instance. You can restrict it again at any time; your scoped rules are kept.',
40+
},
41+
restrictConfirmTitle: {
42+
id: 'system.admin.admin.MarketplaceAllowlistModeBanner.restrictConfirmTitle',
43+
defaultMessage: 'Restrict to scoped rules?',
44+
},
45+
restrictConfirmBody: {
46+
id: 'system.admin.admin.MarketplaceAllowlistModeBanner.restrictConfirmBody',
47+
defaultMessage:
48+
'The marketplace will again be limited to the rules below. Managers not covered by a rule will lose access.',
49+
},
50+
confirmOpen: {
51+
id: 'system.admin.admin.MarketplaceAllowlistModeBanner.confirmOpen',
52+
defaultMessage: 'Open to everyone',
53+
},
54+
confirmRestrict: {
55+
id: 'system.admin.admin.MarketplaceAllowlistModeBanner.confirmRestrict',
56+
defaultMessage: 'Restrict',
57+
},
58+
});
59+
60+
const MarketplaceAllowlistModeBanner = ({
61+
openToEveryone,
62+
onOpenToEveryone,
63+
onRestrict,
64+
}: Props): JSX.Element => {
65+
const { t } = useTranslation();
66+
const [isConfirmOpen, setIsConfirmOpen] = useState(false);
67+
const [submitting, setSubmitting] = useState(false);
68+
69+
const handleConfirm = async (): Promise<void> => {
70+
setSubmitting(true);
71+
try {
72+
await (openToEveryone ? onRestrict() : onOpenToEveryone());
73+
setIsConfirmOpen(false);
74+
} finally {
75+
setSubmitting(false);
76+
}
77+
};
78+
79+
return (
80+
<>
81+
<Alert
82+
action={
83+
<Button
84+
color="inherit"
85+
disabled={submitting}
86+
onClick={(): void => setIsConfirmOpen(true)}
87+
size="small"
88+
>
89+
{openToEveryone
90+
? t(translations.restrictButton)
91+
: t(translations.openButton)}
92+
</Button>
93+
}
94+
className="mb-4"
95+
severity={openToEveryone ? 'success' : 'info'}
96+
>
97+
{openToEveryone
98+
? t(translations.everyoneTitle)
99+
: t(translations.scopedTitle)}
100+
</Alert>
101+
102+
<Prompt
103+
onClickPrimary={handleConfirm}
104+
onClose={(): void => setIsConfirmOpen(false)}
105+
open={isConfirmOpen}
106+
primaryColor={openToEveryone ? 'error' : 'primary'}
107+
primaryDisabled={submitting}
108+
primaryLabel={
109+
openToEveryone
110+
? t(translations.confirmRestrict)
111+
: t(translations.confirmOpen)
112+
}
113+
title={
114+
openToEveryone
115+
? t(translations.restrictConfirmTitle)
116+
: t(translations.openConfirmTitle)
117+
}
118+
>
119+
{openToEveryone
120+
? t(translations.restrictConfirmBody)
121+
: t(translations.openConfirmBody)}
122+
</Prompt>
123+
</>
124+
);
125+
};
126+
127+
export default MarketplaceAllowlistModeBanner;

client/app/bundles/system/admin/admin/components/tables/MarketplaceAllowlistTable.tsx

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import useTranslation from 'lib/hooks/useTranslation';
88
interface Props {
99
rules: AllowlistRuleData[];
1010
onDelete: (id: number) => Promise<void>;
11+
disabled?: boolean;
1112
}
1213

1314
const translations = defineMessages({
@@ -46,7 +47,11 @@ const translations = defineMessages({
4647
},
4748
});
4849

49-
const MarketplaceAllowlistTable = ({ rules, onDelete }: Props): JSX.Element => {
50+
const MarketplaceAllowlistTable = ({
51+
rules,
52+
onDelete,
53+
disabled = false,
54+
}: Props): JSX.Element => {
5055
const { t } = useTranslation();
5156

5257
const typeLabels: Record<AllowlistRuleData['ruleType'], string> = {
@@ -83,20 +88,22 @@ const MarketplaceAllowlistTable = ({ rules, onDelete }: Props): JSX.Element => {
8388
cell: (rule) => (
8489
<DeleteButton
8590
confirmMessage={t(translations.deleteConfirm)}
86-
disabled={false}
91+
disabled={disabled}
8792
onClick={(): Promise<void> => onDelete(rule.id)}
8893
/>
8994
),
9095
},
9196
];
9297

9398
return (
94-
<Table
95-
columns={columns}
96-
data={rules}
97-
getRowId={(rule): string => rule.id.toString()}
98-
renderEmpty={t(translations.empty)}
99-
/>
99+
<div className={disabled ? 'opacity-50' : undefined}>
100+
<Table
101+
columns={columns}
102+
data={rules}
103+
getRowId={(rule): string => rule.id.toString()}
104+
renderEmpty={t(translations.empty)}
105+
/>
106+
</div>
100107
);
101108
};
102109

client/app/bundles/system/admin/admin/pages/MarketplaceAllowlistIndex.tsx

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import LoadingIndicator from 'lib/components/core/LoadingIndicator';
1212
import toast from 'lib/hooks/toast';
1313

1414
import MarketplaceAllowlistRuleForm from '../components/forms/MarketplaceAllowlistRuleForm';
15+
import MarketplaceAllowlistModeBanner from '../components/MarketplaceAllowlistModeBanner';
1516
import MarketplaceAllowlistTable from '../components/tables/MarketplaceAllowlistTable';
1617

1718
type Props = WrappedComponentProps;
@@ -45,21 +46,43 @@ const translations = defineMessages({
4546
id: 'system.admin.admin.MarketplaceAllowlistIndex.deleteFailure',
4647
defaultMessage: 'Failed to remove access rule.',
4748
},
49+
openSuccess: {
50+
id: 'system.admin.admin.MarketplaceAllowlistIndex.openSuccess',
51+
defaultMessage: 'Marketplace opened to all course managers.',
52+
},
53+
openFailure: {
54+
id: 'system.admin.admin.MarketplaceAllowlistIndex.openFailure',
55+
defaultMessage: 'Failed to open the marketplace to everyone.',
56+
},
57+
restrictSuccess: {
58+
id: 'system.admin.admin.MarketplaceAllowlistIndex.restrictSuccess',
59+
defaultMessage: 'Marketplace restricted to the scoped rules.',
60+
},
61+
restrictFailure: {
62+
id: 'system.admin.admin.MarketplaceAllowlistIndex.restrictFailure',
63+
defaultMessage: 'Failed to restrict the marketplace.',
64+
},
4865
});
4966

5067
const MarketplaceAllowlistIndex: FC<Props> = ({ intl }) => {
5168
const [isLoading, setIsLoading] = useState(true);
5269
const [isFormOpen, setIsFormOpen] = useState(false);
5370
const [rules, setRules] = useState<AllowlistRuleData[]>([]);
71+
const [everyoneRuleId, setEveryoneRuleId] = useState<number | null>(null);
5472

5573
useEffect(() => {
5674
SystemAPI.admin
5775
.indexMarketplaceAllowlistRules()
58-
.then((response) => setRules(response.data.rules))
76+
.then((response) => {
77+
setRules(response.data.rules);
78+
setEveryoneRuleId(response.data.everyoneRuleId ?? null);
79+
})
5980
.catch(() => toast.error(intl.formatMessage(translations.fetchFailure)))
6081
.finally(() => setIsLoading(false));
6182
}, []);
6283

84+
const openToEveryone = everyoneRuleId !== null;
85+
6386
const handleCreate = async (data: AllowlistRuleFormData): Promise<void> => {
6487
try {
6588
const response =
@@ -82,20 +105,52 @@ const MarketplaceAllowlistIndex: FC<Props> = ({ intl }) => {
82105
}
83106
};
84107

108+
const handleOpenToEveryone = async (): Promise<void> => {
109+
try {
110+
const response = await SystemAPI.admin.openMarketplaceToEveryone();
111+
setEveryoneRuleId(response.data.id);
112+
toast.success(intl.formatMessage(translations.openSuccess));
113+
} catch {
114+
toast.error(intl.formatMessage(translations.openFailure));
115+
}
116+
};
117+
118+
const handleRestrict = async (): Promise<void> => {
119+
if (everyoneRuleId === null) return;
120+
try {
121+
await SystemAPI.admin.deleteMarketplaceAllowlistRule(everyoneRuleId);
122+
setEveryoneRuleId(null);
123+
toast.success(intl.formatMessage(translations.restrictSuccess));
124+
} catch {
125+
toast.error(intl.formatMessage(translations.restrictFailure));
126+
}
127+
};
128+
85129
if (isLoading) return <LoadingIndicator />;
86130

87131
return (
88132
<Page title={intl.formatMessage(translations.header)}>
89133
<AddButton
90134
className="float-right"
135+
disabled={openToEveryone}
91136
fixed
92137
id="add-allowlist-rule-button"
93138
onClick={(): void => setIsFormOpen(true)}
94139
>
95140
{intl.formatMessage(translations.addRule)}
96141
</AddButton>
97142

98-
<MarketplaceAllowlistTable onDelete={handleDelete} rules={rules} />
143+
<MarketplaceAllowlistModeBanner
144+
onOpenToEveryone={handleOpenToEveryone}
145+
onRestrict={handleRestrict}
146+
openToEveryone={openToEveryone}
147+
/>
148+
149+
<MarketplaceAllowlistTable
150+
disabled={openToEveryone}
151+
onDelete={handleDelete}
152+
rules={rules}
153+
/>
99154

100155
<MarketplaceAllowlistRuleForm
101156
onClose={(): void => setIsFormOpen(false)}

0 commit comments

Comments
 (0)