Skip to content

Commit 5b23100

Browse files
committed
feat: add agreement-gated feature support across files and videos pages
1 parent 97d088e commit 5b23100

File tree

11 files changed

+219
-65
lines changed

11 files changed

+219
-65
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 { AgreementGated } from '../../constants';
18+
import { AlertAgreementGatedFeature } from '../../generic/agreement-gated-feature/AlertAgreementGatedFeature';
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
@@ -207,3 +207,25 @@ export async function getPreviewModulestoreMigration(
207207
const { data } = await client.get(getPreviewModulestoreMigrationUrl(), { params });
208208
return camelCaseObject(data);
209209
}
210+
211+
const getUserAgreementRecordApi = (agreementType: string) => `${getConfig().LMS_BASE_URL}/api/agreements/v1/agreement_record/${agreementType}`;
212+
213+
export async function getUserAgreementRecord(agreementType: string) {
214+
const client = getAuthenticatedHttpClient();
215+
const { data } = await client.get(getUserAgreementRecordApi(agreementType));
216+
return camelCaseObject(data);
217+
}
218+
219+
export async function updateUserAgreementRecord(agreementType: string) {
220+
const client = getAuthenticatedHttpClient();
221+
const { data } = await client.post(getUserAgreementRecordApi(agreementType));
222+
return camelCaseObject(data);
223+
}
224+
225+
const getUserAgreementApi = (agreementType: string) => `${getConfig().LMS_BASE_URL}/api/agreements/v1/agreement/${agreementType}/`;
226+
227+
export async function getUserAgreement(agreementType: string) {
228+
const client = getAuthenticatedHttpClient();
229+
const { data } = await client.get(getUserAgreementApi(agreementType));
230+
return camelCaseObject(data);
231+
}

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+
.map(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
@@ -162,3 +162,19 @@ export type SelectionState = {
162162
sectionId?: string;
163163
subsectionId?: string;
164164
};
165+
166+
export interface UserAgreementRecord {
167+
username: string;
168+
agreementType: string;
169+
acceptedAt: string | null;
170+
isCurrent: boolean;
171+
}
172+
173+
export interface UserAgreement {
174+
type: string;
175+
name: string;
176+
summary: string;
177+
hasText: boolean;
178+
url: string;
179+
updated: string;
180+
}

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

Lines changed: 25 additions & 21 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 } from '@src/constants';
34
import {
45
addAssetFile,
56
deleteAssetFile,
@@ -20,6 +21,7 @@ import {
2021
FileTable,
2122
ThumbnailColumn,
2223
} from '@src/files-and-videos/generic';
24+
import { GatedComponentWrapper } from '@src/generic/agreement-gated-feature/GatedComponentWrapper';
2325
import { useModels } from '@src/generic/model-store';
2426
import { DeprecatedReduxState } from '@src/store';
2527
import { getFileSizeToClosestByte } from '@src/utils';
@@ -158,26 +160,28 @@ export const CourseFilesTable = () => {
158160
return null;
159161
}
160162
return (
161-
<>
162-
<FileTable
163-
{...{
164-
courseId,
165-
data,
166-
handleAddFile,
167-
handleDeleteFile,
168-
handleDownloadFile,
169-
handleLockFile,
170-
handleUsagePaths,
171-
handleErrorReset,
172-
handleFileOrder,
173-
tableColumns,
174-
maxFileSize,
175-
thumbnailPreview,
176-
infoModalSidebar,
177-
files: assets,
178-
}}
179-
/>
180-
<FileValidationModal {...{ handleFileOverwrite }} />
181-
</>
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>
182186
);
183187
};

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 { AgreementGated } from '../../constants';
14+
import { AlertAgreementGatedFeature } from '../../generic/agreement-gated-feature/AlertAgreementGatedFeature';
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/GatedComponentWrapper';
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: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
import { useEffect } from 'react';
1+
import { AgreementGated } from '@src/constants';
2+
import {
3+
AlertAgreementGatedFeature,
4+
} from '@src/generic/agreement-gated-feature/AlertAgreementGatedFeature';
5+
import React, { useEffect } from 'react';
26
import { Helmet } from 'react-helmet';
37
import { useDispatch, useSelector } from 'react-redux';
48

@@ -57,6 +61,9 @@ const VideosPage = () => {
5761
updateFileStatus={updateVideoStatus}
5862
loadingStatus={loadingStatus}
5963
/>
64+
<AlertAgreementGatedFeature
65+
gatingTypes={[AgreementGated.UPLOAD, AgreementGated.UPLOAD_VIDEOS]}
66+
/>
6067
<EditVideoAlertsSlot />
6168
<h2>{intl.formatMessage(messages.heading)}</h2>
6269
<CourseVideosSlot />

0 commit comments

Comments
 (0)