Skip to content

Commit b5bbe08

Browse files
committed
feat: adds agreement-gated feature with support across files and videos pages
Adds new generic components for gating certain features based on acceptance or acknowledgement of user agreements. It adds one alert that can be displayed where a feature (such as uploading) is blocked based on user agreeement, and it adds a wrapper component that disables the components inside it till the agreement has been accepted.
1 parent 5672644 commit b5bbe08

File tree

15 files changed

+506
-58
lines changed

15 files changed

+506
-58
lines changed

src/constants.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,3 +116,9 @@ export const BROKEN = 'broken';
116116
export const LOCKED = 'locked';
117117

118118
export const MANUAL = 'manual';
119+
120+
export enum AgreementGated {
121+
UPLOAD = 'upload',
122+
UPLOAD_VIDEOS = 'upload.videos',
123+
UPLOAD_FILES = 'upload.files',
124+
}

src/course-outline/page-alerts/PageAlerts.jsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import PropTypes from 'prop-types';
1414
import React, { useState } from 'react';
1515
import { useDispatch, useSelector } from 'react-redux';
1616
import { Link, useNavigate } from 'react-router-dom';
17+
import { AlertAgreementGatedFeature } from '@src/generic/agreement-gated-feature';
18+
import { AgreementGated } from '../../constants';
1719
import CourseOutlinePageAlertsSlot from '../../plugin-slots/CourseOutlinePageAlertsSlot';
1820
import advancedSettingsMessages from '../../advanced-settings/messages';
1921
import { OutOfSyncAlert } from '../../course-libraries/OutOfSyncAlert';
@@ -438,6 +440,9 @@ const PageAlerts = ({
438440
{conflictingFilesPasteAlert()}
439441
{newFilesPasteAlert()}
440442
{renderOutOfSyncAlert()}
443+
<AlertAgreementGatedFeature
444+
gatingTypes={[AgreementGated.UPLOAD, AgreementGated.UPLOAD_VIDEOS, AgreementGated.UPLOAD_FILES]}
445+
/>
441446
<CourseOutlinePageAlertsSlot />
442447
</>
443448
);

src/data/api.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,3 +208,25 @@ export async function getPreviewModulestoreMigration(
208208
const { data } = await client.get(getPreviewModulestoreMigrationUrl(), { params });
209209
return camelCaseObject(data);
210210
}
211+
212+
export const getUserAgreementRecordApi = (agreementType: string) => `${getConfig().LMS_BASE_URL}/api/agreements/v1/agreement_record/${agreementType}`;
213+
214+
export async function getUserAgreementRecord(agreementType: string) {
215+
const client = getAuthenticatedHttpClient();
216+
const { data } = await client.get(getUserAgreementRecordApi(agreementType));
217+
return camelCaseObject(data);
218+
}
219+
220+
export async function updateUserAgreementRecord(agreementType: string) {
221+
const client = getAuthenticatedHttpClient();
222+
const { data } = await client.post(getUserAgreementRecordApi(agreementType));
223+
return camelCaseObject(data);
224+
}
225+
226+
export const getUserAgreementApi = (agreementType: string) => `${getConfig().LMS_BASE_URL}/api/agreements/v1/agreement/${agreementType}/`;
227+
228+
export async function getUserAgreement(agreementType: string) {
229+
const client = getAuthenticatedHttpClient();
230+
const { data } = await client.get(getUserAgreementApi(agreementType));
231+
return camelCaseObject(data);
232+
}

src/data/apiHooks.ts

Lines changed: 55 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
1-
import {
2-
skipToken, useMutation, useQuery, useQueryClient,
3-
} from '@tanstack/react-query';
1+
import { getConfig } from '@edx/frontend-platform';
42
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
3+
import { UserAgreement, UserAgreementRecord } from '@src/data/types';
54
import { libraryAuthoringQueryKeys } from '@src/library-authoring/data/apiHooks';
65
import {
7-
getWaffleFlags,
8-
waffleFlagDefaults,
9-
bulkModulestoreMigrate,
10-
getModulestoreMigrationStatus,
6+
skipToken, useMutation, useQueries, useQuery, useQueryClient, UseQueryOptions,
7+
} from '@tanstack/react-query';
8+
import {
119
BulkMigrateRequestData,
10+
bulkModulestoreMigrate,
1211
getCourseDetails,
13-
getPreviewModulestoreMigration,
12+
getModulestoreMigrationStatus,
13+
getPreviewModulestoreMigration, getUserAgreement,
14+
getUserAgreementRecord,
15+
getWaffleFlags, updateUserAgreementRecord,
16+
waffleFlagDefaults,
1417
} from './api';
1518
import { RequestStatus, RequestStatusType } from './constants';
1619

@@ -130,3 +133,47 @@ export const useCourseDetails = (courseId: string) => {
130133
status,
131134
};
132135
};
136+
137+
export const getGatingAgreementTypes = (gatingTypes: string[]): string[] => (
138+
[...new Set(
139+
gatingTypes
140+
.flatMap(gatingType => getConfig().AGREEMENT_GATING?.[gatingType])
141+
.filter(item => Boolean(item)),
142+
)]
143+
);
144+
145+
export const useUserAgreementRecord = (agreementType:string) => (
146+
useQuery<UserAgreementRecord, Error>({
147+
queryKey: ['agreement-record', agreementType],
148+
queryFn: () => getUserAgreementRecord(agreementType),
149+
retry: false,
150+
})
151+
);
152+
153+
export const useUserAgreementRecords = (agreementTypes:string[]) => (
154+
useQueries({
155+
queries: agreementTypes.map<UseQueryOptions<UserAgreementRecord, Error>>(agreementType => ({
156+
queryKey: ['agreement-record', agreementType],
157+
queryFn: () => getUserAgreementRecord(agreementType),
158+
retry: false,
159+
})),
160+
})
161+
);
162+
163+
export const useUserAgreementRecordUpdater = (agreementType:string) => {
164+
const queryClient = useQueryClient();
165+
return useMutation({
166+
mutationFn: async () => updateUserAgreementRecord(agreementType),
167+
onSuccess: () => {
168+
queryClient.invalidateQueries({ queryKey: ['agreement-record', agreementType] });
169+
},
170+
});
171+
};
172+
173+
export const useUserAgreement = (agreementType:string) => (
174+
useQuery<UserAgreement, Error>({
175+
queryKey: ['agreements', agreementType],
176+
queryFn: () => getUserAgreement(agreementType),
177+
retry: false,
178+
})
179+
);

src/data/types.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,3 +165,19 @@ export type SelectionState = {
165165
sectionId?: string;
166166
subsectionId?: string;
167167
};
168+
169+
export interface UserAgreementRecord {
170+
username: string;
171+
agreementType: string;
172+
acceptedAt: string | null;
173+
isCurrent: boolean;
174+
}
175+
176+
export interface UserAgreement {
177+
type: string;
178+
name: string;
179+
summary: string;
180+
hasText: boolean;
181+
url: string;
182+
updated: string;
183+
}

src/files-and-videos/files-page/CourseFilesTable.tsx

Lines changed: 25 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { useIntl } from '@edx/frontend-platform/i18n';
22
import { CheckboxFilter } from '@openedx/paragon';
3+
import { AgreementGated, UPLOAD_FILE_MAX_SIZE } from '@src/constants';
34
import {
45
addAssetFile,
56
deleteAssetFile,
@@ -20,13 +21,13 @@ import {
2021
FileTable,
2122
ThumbnailColumn,
2223
} from '@src/files-and-videos/generic';
24+
import { GatedComponentWrapper } from '@src/generic/agreement-gated-feature';
2325
import { useModels } from '@src/generic/model-store';
2426
import { DeprecatedReduxState } from '@src/store';
2527
import { getFileSizeToClosestByte } from '@src/utils';
2628
import React from 'react';
2729
import { useDispatch, useSelector } from 'react-redux';
2830
import { useParams } from 'react-router-dom';
29-
import { UPLOAD_FILE_MAX_SIZE } from '@src/constants';
3031

3132
export const CourseFilesTable = () => {
3233
const intl = useIntl();
@@ -159,26 +160,28 @@ export const CourseFilesTable = () => {
159160
return null;
160161
}
161162
return (
162-
<>
163-
<FileTable
164-
{...{
165-
courseId,
166-
data,
167-
handleAddFile,
168-
handleDeleteFile,
169-
handleDownloadFile,
170-
handleLockFile,
171-
handleUsagePaths,
172-
handleErrorReset,
173-
handleFileOrder,
174-
tableColumns,
175-
maxFileSize,
176-
thumbnailPreview,
177-
infoModalSidebar,
178-
files: assets,
179-
}}
180-
/>
181-
<FileValidationModal {...{ handleFileOverwrite }} />
182-
</>
163+
<GatedComponentWrapper gatingTypes={[AgreementGated.UPLOAD, AgreementGated.UPLOAD_FILES]}>
164+
<>
165+
<FileTable
166+
{...{
167+
courseId,
168+
data,
169+
handleAddFile,
170+
handleDeleteFile,
171+
handleDownloadFile,
172+
handleLockFile,
173+
handleUsagePaths,
174+
handleErrorReset,
175+
handleFileOrder,
176+
tableColumns,
177+
maxFileSize,
178+
thumbnailPreview,
179+
infoModalSidebar,
180+
files: assets,
181+
}}
182+
/>
183+
<FileValidationModal {...{ handleFileOverwrite }} />
184+
</>
185+
</GatedComponentWrapper>
183186
);
184187
};

src/files-and-videos/files-page/FilesPage.jsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { useIntl } from '@edx/frontend-platform/i18n';
22

33
import { Container } from '@openedx/paragon';
4-
import { useEffect } from 'react';
4+
import React, { useEffect } from 'react';
55
import { useDispatch, useSelector } from 'react-redux';
66

77
import { useCourseAuthoringContext } from '@src/CourseAuthoringContext';
@@ -10,6 +10,8 @@ import Placeholder from '@src/editors/Placeholder';
1010
import { RequestStatus } from '@src/data/constants';
1111
import getPageHeadTitle from '@src/generic/utils';
1212
import EditFileAlertsSlot from '@src/plugin-slots/EditFileAlertsSlot';
13+
import { AlertAgreementGatedFeature } from '@src/generic/agreement-gated-feature';
14+
import { AgreementGated } from '@src/constants';
1315

1416
import { EditFileErrors } from '../generic';
1517
import { fetchAssets, resetErrors } from './data/thunks';
@@ -55,6 +57,9 @@ const FilesPage = () => {
5557
updateFileStatus={updateAssetStatus}
5658
loadingStatus={loadingStatus}
5759
/>
60+
<AlertAgreementGatedFeature
61+
gatingTypes={[AgreementGated.UPLOAD, AgreementGated.UPLOAD_FILES]}
62+
/>
5863
<EditFileAlertsSlot />
5964
<div className="h2">
6065
{intl.formatMessage(messages.heading)}

src/files-and-videos/videos-page/CourseVideosTable.tsx

Lines changed: 30 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { useIntl } from '@edx/frontend-platform/i18n';
22
import {
33
ActionRow, Button, CheckboxFilter, useToggle,
44
} from '@openedx/paragon';
5+
import { AgreementGated } from '@src/constants';
56
import { RequestStatus } from '@src/data/constants';
67
import {
78
ActiveColumn,
@@ -29,6 +30,7 @@ import messages from '@src/files-and-videos/videos-page/messages';
2930
import TranscriptSettings from '@src/files-and-videos/videos-page/transcript-settings';
3031
import UploadModal from '@src/files-and-videos/videos-page/upload-modal';
3132
import VideoThumbnail from '@src/files-and-videos/videos-page/VideoThumbnail';
33+
import { GatedComponentWrapper } from '@src/generic/agreement-gated-feature';
3234
import { useModels } from '@src/generic/model-store';
3335
import { DeprecatedReduxState } from '@src/store';
3436
import React, { useEffect, useRef } from 'react';
@@ -224,23 +226,24 @@ export const CourseVideosTable = () => {
224226
];
225227

226228
return (
227-
<>
228-
<ActionRow>
229-
<ActionRow.Spacer />
230-
{isVideoTranscriptEnabled ? (
231-
<Button
232-
variant="link"
233-
size="sm"
234-
onClick={() => {
235-
openTranscriptSettings();
236-
handleErrorReset({ errorType: 'transcript' });
237-
}}
238-
>
239-
{intl.formatMessage(messages.transcriptSettingsButtonLabel)}
240-
</Button>
241-
) : null}
242-
</ActionRow>
243-
{
229+
<GatedComponentWrapper gatingTypes={[AgreementGated.UPLOAD, AgreementGated.UPLOAD_VIDEOS]}>
230+
<>
231+
<ActionRow>
232+
<ActionRow.Spacer />
233+
{isVideoTranscriptEnabled ? (
234+
<Button
235+
variant="link"
236+
size="sm"
237+
onClick={() => {
238+
openTranscriptSettings();
239+
handleErrorReset({ errorType: 'transcript' });
240+
}}
241+
>
242+
{intl.formatMessage(messages.transcriptSettingsButtonLabel)}
243+
</Button>
244+
) : null}
245+
</ActionRow>
246+
{
244247
loadingStatus !== RequestStatus.FAILED && (
245248
<>
246249
{isVideoTranscriptEnabled && (
@@ -275,14 +278,15 @@ export const CourseVideosTable = () => {
275278
</>
276279
)
277280
}
278-
<UploadModal
279-
{...{
280-
isUploadTrackerOpen,
281-
currentUploadingIdsRef: uploadingIdsRef.current,
282-
handleUploadCancel,
283-
addVideoStatus,
284-
}}
285-
/>
286-
</>
281+
<UploadModal
282+
{...{
283+
isUploadTrackerOpen,
284+
currentUploadingIdsRef: uploadingIdsRef.current,
285+
handleUploadCancel,
286+
addVideoStatus,
287+
}}
288+
/>
289+
</>
290+
</GatedComponentWrapper>
287291
);
288292
};

src/files-and-videos/videos-page/VideosPage.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
import { useEffect } from 'react';
1+
import { AgreementGated } from '@src/constants';
2+
import { AlertAgreementGatedFeature } from '@src/generic/agreement-gated-feature';
3+
import React, { useEffect } from 'react';
24
import { Helmet } from 'react-helmet';
35
import { useDispatch, useSelector } from 'react-redux';
46

@@ -57,6 +59,9 @@ const VideosPage = () => {
5759
updateFileStatus={updateVideoStatus}
5860
loadingStatus={loadingStatus}
5961
/>
62+
<AlertAgreementGatedFeature
63+
gatingTypes={[AgreementGated.UPLOAD, AgreementGated.UPLOAD_VIDEOS]}
64+
/>
6065
<EditVideoAlertsSlot />
6166
<h2>{intl.formatMessage(messages.heading)}</h2>
6267
<CourseVideosSlot />

0 commit comments

Comments
 (0)