Skip to content

Commit bfb1e49

Browse files
committed
feat: add agreement-gated feature support across files and videos pages
1 parent bde04cb commit bfb1e49

File tree

16 files changed

+506
-99
lines changed

16 files changed

+506
-99
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)