@openedx/frontend-app-instructor-dashboard 1.0.0-alpha.39 → 1.0.0-alpha.40
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Main.js +2 -1
- package/dist/Main.js.map +1 -1
- package/dist/certificates/CertificatesPage.js +78 -6
- package/dist/certificates/CertificatesPage.js.map +1 -1
- package/dist/certificates/components/CertificatesToolbar.js +21 -20
- package/dist/certificates/components/CertificatesToolbar.js.map +1 -1
- package/dist/certificates/components/GenerateCertificatesModal.d.ts +9 -0
- package/dist/certificates/components/GenerateCertificatesModal.js +19 -0
- package/dist/certificates/components/GenerateCertificatesModal.js.map +1 -0
- package/dist/certificates/components/RegenerateCertificatesModal.d.ts +11 -0
- package/dist/certificates/components/RegenerateCertificatesModal.js +46 -0
- package/dist/certificates/components/RegenerateCertificatesModal.js.map +1 -0
- package/dist/certificates/data/api.d.ts +1 -1
- package/dist/certificates/data/api.js +3 -2
- package/dist/certificates/data/api.js.map +1 -1
- package/dist/certificates/data/apiHook.d.ts +4 -1
- package/dist/certificates/data/apiHook.js +1 -1
- package/dist/certificates/data/apiHook.js.map +1 -1
- package/dist/certificates/messages.d.ts +105 -0
- package/dist/certificates/messages.js +105 -0
- package/dist/certificates/messages.js.map +1 -1
- package/dist/certificates/utils/errorHandling.d.ts +3 -1
- package/dist/certificates/utils/errorHandling.js +22 -1
- package/dist/certificates/utils/errorHandling.js.map +1 -1
- package/dist/data/apiHook.js +1 -0
- package/dist/data/apiHook.js.map +1 -1
- package/dist/data/utils.d.ts +2 -0
- package/dist/data/utils.js +9 -0
- package/dist/data/utils.js.map +1 -0
- package/dist/instructorNav/InstructorNav.js +11 -4
- package/dist/instructorNav/InstructorNav.js.map +1 -1
- package/dist/pageWrapper/PageWrapper.js +3 -1
- package/dist/pageWrapper/PageWrapper.js.map +1 -1
- package/dist/providers/AccessErrorObserver.d.ts +9 -0
- package/dist/providers/AccessErrorObserver.js +35 -0
- package/dist/providers/AccessErrorObserver.js.map +1 -0
- package/dist/providers/AccessErrorProvider.d.ts +19 -0
- package/dist/providers/AccessErrorProvider.js +51 -0
- package/dist/providers/AccessErrorProvider.js.map +1 -0
- package/dist/providers/messages.d.ts +33 -0
- package/dist/providers/messages.js +35 -0
- package/dist/providers/messages.js.map +1 -0
- package/package.json +1 -1
|
@@ -4,15 +4,22 @@ import { useParams, Link } from 'react-router-dom';
|
|
|
4
4
|
import { Nav, Navbar, Skeleton } from '@openedx/paragon';
|
|
5
5
|
import { useCourseInfo } from '../data/apiHook';
|
|
6
6
|
import { useAlert } from '../providers/AlertProvider';
|
|
7
|
+
import { useAccessError } from '../providers/AccessErrorProvider';
|
|
7
8
|
import { useWidgetProps } from '../slots/SlotUtils';
|
|
8
9
|
const InstructorNav = () => {
|
|
9
10
|
const { courseId = '', tabId = '' } = useParams();
|
|
10
11
|
const { data: courseInfo, isLoading } = useCourseInfo(courseId);
|
|
11
12
|
const widgetPropsArray = useWidgetProps('org.openedx.frontend.slot.instructorDashboard.tabs.v1');
|
|
12
13
|
const { clearAlerts } = useAlert();
|
|
14
|
+
const { clearError, errorType } = useAccessError();
|
|
15
|
+
const handleTabClick = () => {
|
|
16
|
+
clearAlerts();
|
|
17
|
+
clearError();
|
|
18
|
+
};
|
|
19
|
+
const hasError = errorType !== null;
|
|
13
20
|
const sortedTabs = useMemo(() => {
|
|
14
21
|
var _a;
|
|
15
|
-
if (isLoading)
|
|
22
|
+
if (isLoading || hasError)
|
|
16
23
|
return [];
|
|
17
24
|
const apiTabs = (_a = courseInfo === null || courseInfo === void 0 ? void 0 : courseInfo.tabs) !== null && _a !== void 0 ? _a : [];
|
|
18
25
|
const tabMap = new Map();
|
|
@@ -30,17 +37,17 @@ const InstructorNav = () => {
|
|
|
30
37
|
const allTabs = Array.from(tabMap.values());
|
|
31
38
|
// Tabs are sorted by sortOrder, with a fallback to 1000 to be placed at the end for tabs that don't have sortOrder defined (to avoid NaN issues)
|
|
32
39
|
return allTabs.sort((a, b) => { var _a, _b; return ((_a = a.sortOrder) !== null && _a !== void 0 ? _a : 1000) - ((_b = b.sortOrder) !== null && _b !== void 0 ? _b : 1000); });
|
|
33
|
-
}, [courseInfo === null || courseInfo === void 0 ? void 0 : courseInfo.tabs, isLoading, widgetPropsArray]);
|
|
40
|
+
}, [courseInfo === null || courseInfo === void 0 ? void 0 : courseInfo.tabs, isLoading, widgetPropsArray, hasError]);
|
|
34
41
|
if (isLoading) {
|
|
35
42
|
return _jsx(Skeleton, { className: "lead" });
|
|
36
43
|
}
|
|
37
|
-
if (sortedTabs.length === 0)
|
|
44
|
+
if (sortedTabs.length === 0 || hasError)
|
|
38
45
|
return null;
|
|
39
46
|
return (_jsx(Navbar, { expand: "md", className: "py-0", children: _jsxs(Nav, { variant: "tabs", activeKey: tabId, children: [_jsx(Navbar.Toggle, { "aria-controls": "instructor-nav" }), _jsx(Navbar.Collapse, { id: "instructor-nav", children: sortedTabs.map((tab) => {
|
|
40
47
|
const isInternal = tab.url.startsWith('/');
|
|
41
48
|
return (_jsx(Nav.Item, { children: _jsx(Nav.Link, Object.assign({}, (isInternal
|
|
42
49
|
? { to: tab.url, as: Link }
|
|
43
|
-
: { href: tab.url }), { active: tab.tabId === tabId, onClick:
|
|
50
|
+
: { href: tab.url }), { active: tab.tabId === tabId, onClick: handleTabClick, children: tab.title })) }, tab.tabId));
|
|
44
51
|
}) })] }) }));
|
|
45
52
|
};
|
|
46
53
|
export default InstructorNav;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"InstructorNav.js","sourceRoot":"","sources":["../../src/instructorNav/InstructorNav.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,OAAO,CAAC;AAChC,OAAO,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AACnD,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AACzD,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAClD,OAAO,EAAE,QAAQ,EAAE,MAAM,8BAA8B,CAAC;AACxD,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAStD,MAAM,aAAa,GAAG,GAAG,EAAE;IACzB,MAAM,EAAE,QAAQ,GAAG,EAAE,EAAE,KAAK,GAAG,EAAE,EAAE,GAAG,SAAS,EAAwC,CAAC;IACxF,MAAM,EAAE,IAAI,EAAE,UAAU,EAAE,SAAS,EAAE,GAAG,aAAa,CAAC,QAAQ,CAAC,CAAC;IAChE,MAAM,gBAAgB,GAAG,cAAc,CAAC,uDAAuD,CAAe,CAAC;IAC/G,MAAM,EAAE,WAAW,EAAE,GAAG,QAAQ,EAAE,CAAC;
|
|
1
|
+
{"version":3,"file":"InstructorNav.js","sourceRoot":"","sources":["../../src/instructorNav/InstructorNav.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useMemo } from 'react';\nimport { useParams, Link } from 'react-router-dom';\nimport { Nav, Navbar, Skeleton } from '@openedx/paragon';\nimport { useCourseInfo } from '@src/data/apiHook';\nimport { useAlert } from '@src/providers/AlertProvider';\nimport { useAccessError } from '@src/providers/AccessErrorProvider';\nimport { useWidgetProps } from '@src/slots/SlotUtils';\n\nexport interface TabProps {\n tabId: string,\n url: string,\n title: string,\n sortOrder: number,\n}\n\nconst InstructorNav = () => {\n const { courseId = '', tabId = '' } = useParams<{ courseId: string, tabId?: string }>();\n const { data: courseInfo, isLoading } = useCourseInfo(courseId);\n const widgetPropsArray = useWidgetProps('org.openedx.frontend.slot.instructorDashboard.tabs.v1') as TabProps[];\n const { clearAlerts } = useAlert();\n const { clearError, errorType } = useAccessError();\n\n const handleTabClick = () => {\n clearAlerts();\n clearError();\n };\n\n const hasError = errorType !== null;\n\n const sortedTabs = useMemo(() => {\n if (isLoading || hasError) return [];\n const apiTabs: TabProps[] = courseInfo?.tabs ?? [];\n const tabMap = new Map<string, TabProps>();\n\n // Adding tabs from API and from slot into a map to avoid duplicates\n apiTabs.forEach(tab => {\n tabMap.set(tab.tabId, tab);\n });\n\n widgetPropsArray.forEach(slotTab => {\n // If the slotTab doesn't have a tabId or title, we can't render it properly, so we skip it.\n if (!slotTab.tabId || !slotTab.title) {\n return;\n }\n\n tabMap.set(slotTab.tabId, slotTab);\n });\n\n const allTabs = Array.from(tabMap.values());\n\n // Tabs are sorted by sortOrder, with a fallback to 1000 to be placed at the end for tabs that don't have sortOrder defined (to avoid NaN issues)\n return allTabs.sort((a, b) => (a.sortOrder ?? 1000) - (b.sortOrder ?? 1000));\n }, [courseInfo?.tabs, isLoading, widgetPropsArray, hasError]);\n\n if (isLoading) {\n return <Skeleton className=\"lead\" />;\n }\n\n if (sortedTabs.length === 0 || hasError) return null;\n\n return (\n <Navbar expand=\"md\" className=\"py-0\">\n <Nav\n variant=\"tabs\"\n activeKey={tabId}\n >\n <Navbar.Toggle aria-controls=\"instructor-nav\" />\n <Navbar.Collapse id=\"instructor-nav\">\n {\n sortedTabs.map((tab) => {\n const isInternal = tab.url.startsWith('/');\n return (\n <Nav.Item key={tab.tabId}>\n <Nav.Link\n {...(isInternal\n ? { to: tab.url, as: Link }\n : { href: tab.url }\n )}\n active={tab.tabId === tabId}\n onClick={handleTabClick}\n >\n {tab.title}\n </Nav.Link>\n </Nav.Item>\n );\n })\n }\n </Navbar.Collapse>\n </Nav>\n </Navbar>\n );\n};\n\nexport default InstructorNav;\n"]}
|
|
@@ -3,9 +3,11 @@ import { useIntl } from '@openedx/frontend-base';
|
|
|
3
3
|
import messages from './messages';
|
|
4
4
|
import { Container } from '@openedx/paragon';
|
|
5
5
|
import InstructorNav from '../instructorNav/InstructorNav';
|
|
6
|
+
import { AccessErrorGuard } from '../providers/AccessErrorProvider';
|
|
7
|
+
import AccessErrorObserver from '../providers/AccessErrorObserver';
|
|
6
8
|
const PageWrapper = ({ children }) => {
|
|
7
9
|
const { formatMessage } = useIntl();
|
|
8
|
-
return (_jsxs(Container, { size: "xl", fluid: true, children: [_jsx("h2", { className: "text-primary-700 m-4", children: formatMessage(messages.pageTitle) }), _jsx(InstructorNav, {}), _jsx("div", { className: "m-4", children: children })] }));
|
|
10
|
+
return (_jsxs(Container, { size: "xl", fluid: true, children: [_jsx(AccessErrorObserver, {}), _jsx("h2", { className: "text-primary-700 m-4", children: formatMessage(messages.pageTitle) }), _jsx(InstructorNav, {}), _jsx(AccessErrorGuard, { children: _jsx("div", { className: "m-4", children: children }) })] }));
|
|
9
11
|
};
|
|
10
12
|
export default PageWrapper;
|
|
11
13
|
//# sourceMappingURL=PageWrapper.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PageWrapper.js","sourceRoot":"","sources":["../../src/pageWrapper/PageWrapper.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,wBAAwB,CAAC;AACjD,OAAO,QAAQ,MAAM,YAAY,CAAC;AAClC,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAC7C,OAAO,aAAa,MAAM,kCAAkC,CAAC;
|
|
1
|
+
{"version":3,"file":"PageWrapper.js","sourceRoot":"","sources":["../../src/pageWrapper/PageWrapper.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,wBAAwB,CAAC;AACjD,OAAO,QAAQ,MAAM,YAAY,CAAC;AAClC,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAC7C,OAAO,aAAa,MAAM,kCAAkC,CAAC;AAC7D,OAAO,EAAE,gBAAgB,EAAE,MAAM,oCAAoC,CAAC;AACtE,OAAO,mBAAmB,MAAM,oCAAoC,CAAC;AAErE,MAAM,WAAW,GAAG,CAAC,EAAE,QAAQ,EAAiC,EAAE,EAAE;IAClE,MAAM,EAAE,aAAa,EAAE,GAAG,OAAO,EAAE,CAAC;IACpC,OAAO,CACL,MAAC,SAAS,IAAC,IAAI,EAAC,IAAI,EAAC,KAAK,mBACxB,KAAC,mBAAmB,KAAG,EACvB,aAAI,SAAS,EAAC,sBAAsB,YAAE,aAAa,CAAC,QAAQ,CAAC,SAAS,CAAC,GAAM,EAC7E,KAAC,aAAa,KAAG,EACjB,KAAC,gBAAgB,cACf,cAAK,SAAS,EAAC,KAAK,YACjB,QAAQ,GACL,GACW,IACT,CACb,CAAC;AACJ,CAAC,CAAC;AAEF,eAAe,WAAW,CAAC","sourcesContent":["import { useIntl } from '@openedx/frontend-base';\nimport messages from './messages';\nimport { Container } from '@openedx/paragon';\nimport InstructorNav from '@src/instructorNav/InstructorNav';\nimport { AccessErrorGuard } from '@src/providers/AccessErrorProvider';\nimport AccessErrorObserver from '@src/providers/AccessErrorObserver';\n\nconst PageWrapper = ({ children }: { children: React.ReactNode }) => {\n const { formatMessage } = useIntl();\n return (\n <Container size=\"xl\" fluid>\n <AccessErrorObserver />\n <h2 className=\"text-primary-700 m-4\">{formatMessage(messages.pageTitle)}</h2>\n <InstructorNav />\n <AccessErrorGuard>\n <div className=\"m-4\">\n {children}\n </div>\n </AccessErrorGuard>\n </Container>\n );\n};\n\nexport default PageWrapper;\n"]}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Observes the courseInfo query and syncs 401/403 errors with the AccessErrorProvider.
|
|
3
|
+
* This component must be rendered inside AccessErrorProvider.
|
|
4
|
+
* By keeping this logic here (instead of inside useCourseInfo), the hook stays
|
|
5
|
+
* decoupled from the provider and can be used in slots or other contexts
|
|
6
|
+
* that live outside the provider tree.
|
|
7
|
+
*/
|
|
8
|
+
declare const AccessErrorObserver: () => null;
|
|
9
|
+
export default AccessErrorObserver;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
import { useParams } from 'react-router-dom';
|
|
3
|
+
import { useCourseInfo } from '../data/apiHook';
|
|
4
|
+
import { isForbiddenError, isUnauthorizedError } from '../data/utils';
|
|
5
|
+
import { useAccessError } from '../providers/AccessErrorProvider';
|
|
6
|
+
/**
|
|
7
|
+
* Observes the courseInfo query and syncs 401/403 errors with the AccessErrorProvider.
|
|
8
|
+
* This component must be rendered inside AccessErrorProvider.
|
|
9
|
+
* By keeping this logic here (instead of inside useCourseInfo), the hook stays
|
|
10
|
+
* decoupled from the provider and can be used in slots or other contexts
|
|
11
|
+
* that live outside the provider tree.
|
|
12
|
+
*/
|
|
13
|
+
const AccessErrorObserver = () => {
|
|
14
|
+
const { courseId = '' } = useParams();
|
|
15
|
+
const { isLoading, error } = useCourseInfo(courseId);
|
|
16
|
+
const { setErrorType, setLoading } = useAccessError();
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
setLoading(isLoading);
|
|
19
|
+
if (error && isForbiddenError(error)) {
|
|
20
|
+
setErrorType('forbidden');
|
|
21
|
+
}
|
|
22
|
+
else if (error && isUnauthorizedError(error)) {
|
|
23
|
+
setErrorType('unauthorized');
|
|
24
|
+
}
|
|
25
|
+
else if (error) {
|
|
26
|
+
setErrorType('generic');
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
setErrorType(null);
|
|
30
|
+
}
|
|
31
|
+
}, [isLoading, error, setErrorType, setLoading]);
|
|
32
|
+
return null;
|
|
33
|
+
};
|
|
34
|
+
export default AccessErrorObserver;
|
|
35
|
+
//# sourceMappingURL=AccessErrorObserver.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AccessErrorObserver.js","sourceRoot":"","sources":["../../src/providers/AccessErrorObserver.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAClC,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAC7C,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAClD,OAAO,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AACxE,OAAO,EAAE,cAAc,EAAE,MAAM,oCAAoC,CAAC;AAEpE;;;;;;GAMG;AACH,MAAM,mBAAmB,GAAG,GAAG,EAAE;IAC/B,MAAM,EAAE,QAAQ,GAAG,EAAE,EAAE,GAAG,SAAS,EAAwB,CAAC;IAC5D,MAAM,EAAE,SAAS,EAAE,KAAK,EAAE,GAAG,aAAa,CAAC,QAAQ,CAAC,CAAC;IACrD,MAAM,EAAE,YAAY,EAAE,UAAU,EAAE,GAAG,cAAc,EAAE,CAAC;IAEtD,SAAS,CAAC,GAAG,EAAE;QACb,UAAU,CAAC,SAAS,CAAC,CAAC;QACtB,IAAI,KAAK,IAAI,gBAAgB,CAAC,KAAK,CAAC,EAAE,CAAC;YACrC,YAAY,CAAC,WAAW,CAAC,CAAC;QAC5B,CAAC;aAAM,IAAI,KAAK,IAAI,mBAAmB,CAAC,KAAK,CAAC,EAAE,CAAC;YAC/C,YAAY,CAAC,cAAc,CAAC,CAAC;QAC/B,CAAC;aAAM,IAAI,KAAK,EAAE,CAAC;YACjB,YAAY,CAAC,SAAS,CAAC,CAAC;QAC1B,CAAC;aAAM,CAAC;YACN,YAAY,CAAC,IAAI,CAAC,CAAC;QACrB,CAAC;IACH,CAAC,EAAE,CAAC,SAAS,EAAE,KAAK,EAAE,YAAY,EAAE,UAAU,CAAC,CAAC,CAAC;IAEjD,OAAO,IAAI,CAAC;AACd,CAAC,CAAC;AAEF,eAAe,mBAAmB,CAAC","sourcesContent":["import { useEffect } from 'react';\nimport { useParams } from 'react-router-dom';\nimport { useCourseInfo } from '@src/data/apiHook';\nimport { isForbiddenError, isUnauthorizedError } from '@src/data/utils';\nimport { useAccessError } from '@src/providers/AccessErrorProvider';\n\n/**\n * Observes the courseInfo query and syncs 401/403 errors with the AccessErrorProvider.\n * This component must be rendered inside AccessErrorProvider.\n * By keeping this logic here (instead of inside useCourseInfo), the hook stays\n * decoupled from the provider and can be used in slots or other contexts\n * that live outside the provider tree.\n */\nconst AccessErrorObserver = () => {\n const { courseId = '' } = useParams<{ courseId: string }>();\n const { isLoading, error } = useCourseInfo(courseId);\n const { setErrorType, setLoading } = useAccessError();\n\n useEffect(() => {\n setLoading(isLoading);\n if (error && isForbiddenError(error)) {\n setErrorType('forbidden');\n } else if (error && isUnauthorizedError(error)) {\n setErrorType('unauthorized');\n } else if (error) {\n setErrorType('generic');\n } else {\n setErrorType(null);\n }\n }, [isLoading, error, setErrorType, setLoading]);\n\n return null;\n};\n\nexport default AccessErrorObserver;\n"]}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { ReactNode, FC } from 'react';
|
|
2
|
+
type ErrorType = 'forbidden' | 'unauthorized' | 'generic' | null;
|
|
3
|
+
interface AccessErrorContextType {
|
|
4
|
+
errorType: ErrorType;
|
|
5
|
+
setErrorType: (error: ErrorType) => void;
|
|
6
|
+
clearError: () => void;
|
|
7
|
+
isLoading: boolean;
|
|
8
|
+
setLoading: (loading: boolean) => void;
|
|
9
|
+
}
|
|
10
|
+
interface AccessErrorProviderProps {
|
|
11
|
+
children: ReactNode;
|
|
12
|
+
}
|
|
13
|
+
export declare const AccessErrorProvider: FC<AccessErrorProviderProps>;
|
|
14
|
+
interface AccessErrorGuardProps {
|
|
15
|
+
children: ReactNode;
|
|
16
|
+
}
|
|
17
|
+
export declare const AccessErrorGuard: FC<AccessErrorGuardProps>;
|
|
18
|
+
export declare const useAccessError: () => AccessErrorContextType;
|
|
19
|
+
export {};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { createContext, useContext, useState, useCallback, useMemo } from 'react';
|
|
3
|
+
import { Alert, Skeleton } from '@openedx/paragon';
|
|
4
|
+
import { Error as ErrorIcon } from '@openedx/paragon/icons';
|
|
5
|
+
import { useIntl } from '@openedx/frontend-base';
|
|
6
|
+
import messages from '../providers/messages';
|
|
7
|
+
const AccessErrorContext = createContext(undefined);
|
|
8
|
+
export const AccessErrorProvider = ({ children }) => {
|
|
9
|
+
const [errorType, setErrorType] = useState(null);
|
|
10
|
+
const [isLoading, setLoading] = useState(false);
|
|
11
|
+
const clearError = useCallback(() => {
|
|
12
|
+
setErrorType(null);
|
|
13
|
+
}, []);
|
|
14
|
+
const value = useMemo(() => ({
|
|
15
|
+
errorType,
|
|
16
|
+
setErrorType,
|
|
17
|
+
clearError,
|
|
18
|
+
isLoading,
|
|
19
|
+
setLoading,
|
|
20
|
+
}), [errorType, clearError, isLoading]);
|
|
21
|
+
return (_jsx(AccessErrorContext.Provider, { value: value, children: children }));
|
|
22
|
+
};
|
|
23
|
+
export const AccessErrorGuard = ({ children }) => {
|
|
24
|
+
const intl = useIntl();
|
|
25
|
+
const { errorType, isLoading } = useAccessError();
|
|
26
|
+
if (isLoading) {
|
|
27
|
+
return _jsx(Skeleton, { className: "lead" });
|
|
28
|
+
}
|
|
29
|
+
if (errorType) {
|
|
30
|
+
const headingMap = {
|
|
31
|
+
unauthorized: messages.unauthorizedErrorHeading,
|
|
32
|
+
forbidden: messages.forbiddenErrorHeading,
|
|
33
|
+
generic: messages.genericErrorHeading,
|
|
34
|
+
};
|
|
35
|
+
const messageMap = {
|
|
36
|
+
unauthorized: messages.unauthorizedErrorMessage,
|
|
37
|
+
forbidden: messages.forbiddenErrorMessage,
|
|
38
|
+
generic: messages.genericErrorMessage,
|
|
39
|
+
};
|
|
40
|
+
return (_jsxs(Alert, { variant: "danger", icon: ErrorIcon, children: [_jsx(Alert.Heading, { children: intl.formatMessage(headingMap[errorType]) }), _jsx("p", { children: intl.formatMessage(messageMap[errorType]) })] }));
|
|
41
|
+
}
|
|
42
|
+
return _jsx(_Fragment, { children: children });
|
|
43
|
+
};
|
|
44
|
+
export const useAccessError = () => {
|
|
45
|
+
const context = useContext(AccessErrorContext);
|
|
46
|
+
if (context === undefined) {
|
|
47
|
+
throw new Error('useAccessError must be used within a AccessErrorProvider');
|
|
48
|
+
}
|
|
49
|
+
return context;
|
|
50
|
+
};
|
|
51
|
+
//# sourceMappingURL=AccessErrorProvider.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AccessErrorProvider.js","sourceRoot":"","sources":["../../src/providers/AccessErrorProvider.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,QAAQ,EAAa,WAAW,EAAE,OAAO,EAAM,MAAM,OAAO,CAAC;AACjG,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AACnD,OAAO,EAAE,KAAK,IAAI,SAAS,EAAE,MAAM,wBAAwB,CAAC;AAC5D,OAAO,EAAE,OAAO,EAAE,MAAM,wBAAwB,CAAC;AACjD,OAAO,QAAQ,MAAM,yBAAyB,CAAC;AAY/C,MAAM,kBAAkB,GAAG,aAAa,CAAqC,SAAS,CAAC,CAAC;AAMxF,MAAM,CAAC,MAAM,mBAAmB,GAAiC,CAAC,EAAE,QAAQ,EAAE,EAAE,EAAE;IAChF,MAAM,CAAC,SAAS,EAAE,YAAY,CAAC,GAAG,QAAQ,CAAY,IAAI,CAAC,CAAC;IAC5D,MAAM,CAAC,SAAS,EAAE,UAAU,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC;IAEhD,MAAM,UAAU,GAAG,WAAW,CAAC,GAAG,EAAE;QAClC,YAAY,CAAC,IAAI,CAAC,CAAC;IACrB,CAAC,EAAE,EAAE,CAAC,CAAC;IAEP,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC;QAC3B,SAAS;QACT,YAAY;QACZ,UAAU;QACV,SAAS;QACT,UAAU;KACX,CAAC,EAAE,CAAC,SAAS,EAAE,UAAU,EAAE,SAAS,CAAC,CAAC,CAAC;IAExC,OAAO,CACL,KAAC,kBAAkB,CAAC,QAAQ,IAAC,KAAK,EAAE,KAAK,YACtC,QAAQ,GACmB,CAC/B,CAAC;AACJ,CAAC,CAAC;AAMF,MAAM,CAAC,MAAM,gBAAgB,GAA8B,CAAC,EAAE,QAAQ,EAAE,EAAE,EAAE;IAC1E,MAAM,IAAI,GAAG,OAAO,EAAE,CAAC;IACvB,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,GAAG,cAAc,EAAE,CAAC;IAElD,IAAI,SAAS,EAAE,CAAC;QACd,OAAO,KAAC,QAAQ,IAAC,SAAS,EAAC,MAAM,GAAG,CAAC;IACvC,CAAC;IAED,IAAI,SAAS,EAAE,CAAC;QACd,MAAM,UAAU,GAAG;YACjB,YAAY,EAAE,QAAQ,CAAC,wBAAwB;YAC/C,SAAS,EAAE,QAAQ,CAAC,qBAAqB;YACzC,OAAO,EAAE,QAAQ,CAAC,mBAAmB;SACtC,CAAC;QACF,MAAM,UAAU,GAAG;YACjB,YAAY,EAAE,QAAQ,CAAC,wBAAwB;YAC/C,SAAS,EAAE,QAAQ,CAAC,qBAAqB;YACzC,OAAO,EAAE,QAAQ,CAAC,mBAAmB;SACtC,CAAC;QAEF,OAAO,CACL,MAAC,KAAK,IACJ,OAAO,EAAC,QAAQ,EAChB,IAAI,EAAE,SAAS,aAEf,KAAC,KAAK,CAAC,OAAO,cAAE,IAAI,CAAC,aAAa,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC,GAAiB,EAC1E,sBACG,IAAI,CAAC,aAAa,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC,GACxC,IACE,CACT,CAAC;IACJ,CAAC;IAED,OAAO,4BAAG,QAAQ,GAAI,CAAC;AACzB,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,cAAc,GAAG,GAA2B,EAAE;IACzD,MAAM,OAAO,GAAG,UAAU,CAAC,kBAAkB,CAAC,CAAC;IAC/C,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;QAC1B,MAAM,IAAI,KAAK,CAAC,0DAA0D,CAAC,CAAC;IAC9E,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC,CAAC","sourcesContent":["import { createContext, useContext, useState, ReactNode, useCallback, useMemo, FC } from 'react';\nimport { Alert, Skeleton } from '@openedx/paragon';\nimport { Error as ErrorIcon } from '@openedx/paragon/icons';\nimport { useIntl } from '@openedx/frontend-base';\nimport messages from '@src/providers/messages';\n\ntype ErrorType = 'forbidden' | 'unauthorized' | 'generic' | null;\n\ninterface AccessErrorContextType {\n errorType: ErrorType,\n setErrorType: (error: ErrorType) => void,\n clearError: () => void,\n isLoading: boolean,\n setLoading: (loading: boolean) => void,\n}\n\nconst AccessErrorContext = createContext<AccessErrorContextType | undefined>(undefined);\n\ninterface AccessErrorProviderProps {\n children: ReactNode,\n}\n\nexport const AccessErrorProvider: FC<AccessErrorProviderProps> = ({ children }) => {\n const [errorType, setErrorType] = useState<ErrorType>(null);\n const [isLoading, setLoading] = useState(false);\n\n const clearError = useCallback(() => {\n setErrorType(null);\n }, []);\n\n const value = useMemo(() => ({\n errorType,\n setErrorType,\n clearError,\n isLoading,\n setLoading,\n }), [errorType, clearError, isLoading]);\n\n return (\n <AccessErrorContext.Provider value={value}>\n {children}\n </AccessErrorContext.Provider>\n );\n};\n\ninterface AccessErrorGuardProps {\n children: ReactNode,\n}\n\nexport const AccessErrorGuard: FC<AccessErrorGuardProps> = ({ children }) => {\n const intl = useIntl();\n const { errorType, isLoading } = useAccessError();\n\n if (isLoading) {\n return <Skeleton className=\"lead\" />;\n }\n\n if (errorType) {\n const headingMap = {\n unauthorized: messages.unauthorizedErrorHeading,\n forbidden: messages.forbiddenErrorHeading,\n generic: messages.genericErrorHeading,\n };\n const messageMap = {\n unauthorized: messages.unauthorizedErrorMessage,\n forbidden: messages.forbiddenErrorMessage,\n generic: messages.genericErrorMessage,\n };\n\n return (\n <Alert\n variant=\"danger\"\n icon={ErrorIcon}\n >\n <Alert.Heading>{intl.formatMessage(headingMap[errorType])}</Alert.Heading>\n <p>\n {intl.formatMessage(messageMap[errorType])}\n </p>\n </Alert>\n );\n }\n\n return <>{children}</>;\n};\n\nexport const useAccessError = (): AccessErrorContextType => {\n const context = useContext(AccessErrorContext);\n if (context === undefined) {\n throw new Error('useAccessError must be used within a AccessErrorProvider');\n }\n return context;\n};\n"]}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
declare const messages: {
|
|
2
|
+
forbiddenErrorHeading: {
|
|
3
|
+
id: string;
|
|
4
|
+
defaultMessage: string;
|
|
5
|
+
description: string;
|
|
6
|
+
};
|
|
7
|
+
forbiddenErrorMessage: {
|
|
8
|
+
id: string;
|
|
9
|
+
defaultMessage: string;
|
|
10
|
+
description: string;
|
|
11
|
+
};
|
|
12
|
+
unauthorizedErrorHeading: {
|
|
13
|
+
id: string;
|
|
14
|
+
defaultMessage: string;
|
|
15
|
+
description: string;
|
|
16
|
+
};
|
|
17
|
+
unauthorizedErrorMessage: {
|
|
18
|
+
id: string;
|
|
19
|
+
defaultMessage: string;
|
|
20
|
+
description: string;
|
|
21
|
+
};
|
|
22
|
+
genericErrorHeading: {
|
|
23
|
+
id: string;
|
|
24
|
+
defaultMessage: string;
|
|
25
|
+
description: string;
|
|
26
|
+
};
|
|
27
|
+
genericErrorMessage: {
|
|
28
|
+
id: string;
|
|
29
|
+
defaultMessage: string;
|
|
30
|
+
description: string;
|
|
31
|
+
};
|
|
32
|
+
};
|
|
33
|
+
export default messages;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { defineMessages } from '@openedx/frontend-base';
|
|
2
|
+
const messages = defineMessages({
|
|
3
|
+
forbiddenErrorHeading: {
|
|
4
|
+
id: 'instructorDashboard.forbiddenError.heading',
|
|
5
|
+
defaultMessage: 'Access Denied',
|
|
6
|
+
description: 'Heading for the forbidden access error message',
|
|
7
|
+
},
|
|
8
|
+
forbiddenErrorMessage: {
|
|
9
|
+
id: 'instructorDashboard.forbiddenError.message',
|
|
10
|
+
defaultMessage: 'You do not have permission to access this content. Please contact the administrator if you believe this is an error.',
|
|
11
|
+
description: 'Message displayed when user lacks permission to access content',
|
|
12
|
+
},
|
|
13
|
+
unauthorizedErrorHeading: {
|
|
14
|
+
id: 'instructorDashboard.unauthorizedError.heading',
|
|
15
|
+
defaultMessage: 'Unauthorized',
|
|
16
|
+
description: 'Heading for the unauthorized access error message',
|
|
17
|
+
},
|
|
18
|
+
unauthorizedErrorMessage: {
|
|
19
|
+
id: 'instructorDashboard.unauthorizedError.message',
|
|
20
|
+
defaultMessage: 'You must be logged in and have instructor permissions to view this content.',
|
|
21
|
+
description: 'Message displayed when user is not authenticated or lacks instructor permissions',
|
|
22
|
+
},
|
|
23
|
+
genericErrorHeading: {
|
|
24
|
+
id: 'instructorDashboard.genericError.heading',
|
|
25
|
+
defaultMessage: 'Something went wrong',
|
|
26
|
+
description: 'Heading for a generic server error message',
|
|
27
|
+
},
|
|
28
|
+
genericErrorMessage: {
|
|
29
|
+
id: 'instructorDashboard.genericError.message',
|
|
30
|
+
defaultMessage: 'An unexpected error occurred. Please try again later.',
|
|
31
|
+
description: 'Message displayed when an unexpected server error occurs',
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
export default messages;
|
|
35
|
+
//# sourceMappingURL=messages.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"messages.js","sourceRoot":"","sources":["../../src/providers/messages.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAExD,MAAM,QAAQ,GAAG,cAAc,CAAC;IAC9B,qBAAqB,EAAE;QACrB,EAAE,EAAE,4CAA4C;QAChD,cAAc,EAAE,eAAe;QAC/B,WAAW,EAAE,gDAAgD;KAC9D;IACD,qBAAqB,EAAE;QACrB,EAAE,EAAE,4CAA4C;QAChD,cAAc,EAAE,sHAAsH;QACtI,WAAW,EAAE,gEAAgE;KAC9E;IACD,wBAAwB,EAAE;QACxB,EAAE,EAAE,+CAA+C;QACnD,cAAc,EAAE,cAAc;QAC9B,WAAW,EAAE,mDAAmD;KACjE;IACD,wBAAwB,EAAE;QACxB,EAAE,EAAE,+CAA+C;QACnD,cAAc,EAAE,6EAA6E;QAC7F,WAAW,EAAE,kFAAkF;KAChG;IACD,mBAAmB,EAAE;QACnB,EAAE,EAAE,0CAA0C;QAC9C,cAAc,EAAE,sBAAsB;QACtC,WAAW,EAAE,4CAA4C;KAC1D;IACD,mBAAmB,EAAE;QACnB,EAAE,EAAE,0CAA0C;QAC9C,cAAc,EAAE,uDAAuD;QACvE,WAAW,EAAE,0DAA0D;KACxE;CACF,CAAC,CAAC;AAEH,eAAe,QAAQ,CAAC","sourcesContent":["import { defineMessages } from '@openedx/frontend-base';\n\nconst messages = defineMessages({\n forbiddenErrorHeading: {\n id: 'instructorDashboard.forbiddenError.heading',\n defaultMessage: 'Access Denied',\n description: 'Heading for the forbidden access error message',\n },\n forbiddenErrorMessage: {\n id: 'instructorDashboard.forbiddenError.message',\n defaultMessage: 'You do not have permission to access this content. Please contact the administrator if you believe this is an error.',\n description: 'Message displayed when user lacks permission to access content',\n },\n unauthorizedErrorHeading: {\n id: 'instructorDashboard.unauthorizedError.heading',\n defaultMessage: 'Unauthorized',\n description: 'Heading for the unauthorized access error message',\n },\n unauthorizedErrorMessage: {\n id: 'instructorDashboard.unauthorizedError.message',\n defaultMessage: 'You must be logged in and have instructor permissions to view this content.',\n description: 'Message displayed when user is not authenticated or lacks instructor permissions',\n },\n genericErrorHeading: {\n id: 'instructorDashboard.genericError.heading',\n defaultMessage: 'Something went wrong',\n description: 'Heading for a generic server error message',\n },\n genericErrorMessage: {\n id: 'instructorDashboard.genericError.message',\n defaultMessage: 'An unexpected error occurred. Please try again later.',\n description: 'Message displayed when an unexpected server error occurs',\n },\n});\n\nexport default messages;\n"]}
|