@openedx/frontend-base 1.0.0-alpha.44 → 1.0.0-alpha.46

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.
Files changed (75) hide show
  1. package/dist/runtime/config/index.js +1 -0
  2. package/dist/runtime/config/index.js.map +1 -1
  3. package/dist/shell/header/Header.js +1 -1
  4. package/dist/shell/header/Header.js.map +1 -1
  5. package/dist/shell/header/app.js +13 -3
  6. package/dist/shell/header/app.js.map +1 -1
  7. package/dist/shell/header/constants.d.ts +2 -1
  8. package/dist/shell/header/constants.js +2 -1
  9. package/dist/shell/header/constants.js.map +1 -1
  10. package/dist/shell/header/{course-navigation-bar → course-bar}/data/service.d.ts +2 -0
  11. package/dist/shell/header/{course-navigation-bar → course-bar}/data/service.js +22 -0
  12. package/dist/shell/header/course-bar/data/service.js.map +1 -0
  13. package/dist/shell/header/course-bar/masquerade/MasqueradeBar.d.ts +1 -0
  14. package/dist/shell/header/course-bar/masquerade/MasqueradeBar.js +23 -0
  15. package/dist/shell/header/course-bar/masquerade/MasqueradeBar.js.map +1 -0
  16. package/dist/shell/header/course-bar/masquerade/MasqueradeContext.d.ts +3 -0
  17. package/dist/shell/header/course-bar/masquerade/MasqueradeContext.js +10 -0
  18. package/dist/shell/header/course-bar/masquerade/MasqueradeContext.js.map +1 -0
  19. package/dist/shell/header/course-bar/masquerade/StudioLink.d.ts +6 -0
  20. package/dist/shell/header/course-bar/masquerade/StudioLink.js +27 -0
  21. package/dist/shell/header/course-bar/masquerade/StudioLink.js.map +1 -0
  22. package/dist/shell/header/course-bar/masquerade/data/api.d.ts +29 -0
  23. package/dist/shell/header/course-bar/masquerade/data/api.js +25 -0
  24. package/dist/shell/header/course-bar/masquerade/data/api.js.map +1 -0
  25. package/dist/shell/header/course-bar/masquerade/hooks.d.ts +21 -0
  26. package/dist/shell/header/course-bar/masquerade/hooks.js +212 -0
  27. package/dist/shell/header/course-bar/masquerade/hooks.js.map +1 -0
  28. package/dist/shell/header/course-bar/masquerade/index.d.ts +1 -0
  29. package/dist/shell/header/course-bar/masquerade/index.js +2 -0
  30. package/dist/shell/header/course-bar/masquerade/index.js.map +1 -0
  31. package/dist/shell/header/course-bar/masquerade/masquerade-widget/MasqueradeUserNameInput.d.ts +7 -0
  32. package/dist/shell/header/course-bar/masquerade/masquerade-widget/MasqueradeUserNameInput.js +18 -0
  33. package/dist/shell/header/course-bar/masquerade/masquerade-widget/MasqueradeUserNameInput.js.map +1 -0
  34. package/dist/shell/header/course-bar/masquerade/masquerade-widget/MasqueradeWidget.d.ts +1 -0
  35. package/dist/shell/header/course-bar/masquerade/masquerade-widget/MasqueradeWidget.js +16 -0
  36. package/dist/shell/header/course-bar/masquerade/masquerade-widget/MasqueradeWidget.js.map +1 -0
  37. package/dist/shell/header/course-bar/masquerade/masquerade-widget/MasqueradeWidgetOption.d.ts +6 -0
  38. package/dist/shell/header/course-bar/masquerade/masquerade-widget/MasqueradeWidgetOption.js +18 -0
  39. package/dist/shell/header/course-bar/masquerade/masquerade-widget/MasqueradeWidgetOption.js.map +1 -0
  40. package/dist/shell/header/course-bar/masquerade/masquerade-widget/index.d.ts +2 -0
  41. package/dist/shell/header/course-bar/masquerade/masquerade-widget/index.js +3 -0
  42. package/dist/shell/header/course-bar/masquerade/masquerade-widget/index.js.map +1 -0
  43. package/dist/shell/header/course-bar/masquerade/masquerade-widget/messages.d.ts +28 -0
  44. package/dist/shell/header/course-bar/masquerade/masquerade-widget/messages.js +30 -0
  45. package/dist/shell/header/course-bar/masquerade/masquerade-widget/messages.js.map +1 -0
  46. package/dist/shell/header/course-bar/masquerade/messages.d.ts +33 -0
  47. package/dist/shell/header/course-bar/masquerade/messages.js +35 -0
  48. package/dist/shell/header/course-bar/masquerade/messages.js.map +1 -0
  49. package/dist/shell/header/{course-navigation-bar → course-bar/navigation}/CourseTabsNavigation.js +6 -29
  50. package/dist/shell/header/course-bar/navigation/CourseTabsNavigation.js.map +1 -0
  51. package/dist/shell/header/{course-navigation-bar → course-bar/navigation}/messages.js +1 -1
  52. package/dist/shell/header/course-bar/navigation/messages.js.map +1 -0
  53. package/dist/shell/header/course-bar/utils.d.ts +3 -0
  54. package/dist/shell/header/course-bar/utils.js +54 -0
  55. package/dist/shell/header/course-bar/utils.js.map +1 -0
  56. package/dist/shell/header/index.d.ts +1 -1
  57. package/dist/shell/header/index.js +1 -1
  58. package/dist/shell/header/index.js.map +1 -1
  59. package/dist/shell/index.d.ts +2 -2
  60. package/dist/shell/index.js +2 -2
  61. package/dist/shell/index.js.map +1 -1
  62. package/dist/shell/site.config.dev.js +1 -0
  63. package/dist/shell/site.config.dev.js.map +1 -1
  64. package/dist/types.d.ts +1 -0
  65. package/dist/types.js.map +1 -1
  66. package/package.json +1 -1
  67. package/dist/shell/header/course-navigation-bar/CourseTabsNavigation.js.map +0 -1
  68. package/dist/shell/header/course-navigation-bar/data/service.js.map +0 -1
  69. package/dist/shell/header/course-navigation-bar/messages.js.map +0 -1
  70. package/dist/shell/header/course-navigation-bar/utils.d.ts +0 -2
  71. package/dist/shell/header/course-navigation-bar/utils.js +0 -27
  72. package/dist/shell/header/course-navigation-bar/utils.js.map +0 -1
  73. /package/dist/shell/header/{course-navigation-bar → course-bar/navigation}/CourseTabsNavigation.d.ts +0 -0
  74. /package/dist/shell/header/{course-navigation-bar → course-bar/navigation}/course-tabs-navigation.scss +0 -0
  75. /package/dist/shell/header/{course-navigation-bar → course-bar/navigation}/messages.d.ts +0 -0
@@ -0,0 +1,212 @@
1
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
+ return new (P || (P = Promise))(function (resolve, reject) {
4
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
8
+ });
9
+ };
10
+ import { useEffect, useRef, useState } from 'react';
11
+ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
12
+ import { useLocation, useNavigate } from 'react-router-dom';
13
+ import { getMasqueradeOptions, postMasqueradeOptions, } from './data/api';
14
+ import { courseHomeCourseMetadataQueryKey, findActiveTab, getCourseHomeCourseMetadata, } from '../data/service';
15
+ import { isClientRoute } from '../utils';
16
+ import messages from './messages';
17
+ const defaultActive = {
18
+ role: 'staff',
19
+ groupId: null,
20
+ groupName: null,
21
+ userName: null,
22
+ userPartitionId: null,
23
+ };
24
+ function toMasqueradePayload(option) {
25
+ const payload = { role: option.role };
26
+ if (option.groupId !== undefined && option.userPartitionId !== undefined) {
27
+ payload.group_id = option.groupId;
28
+ payload.user_partition_id = option.userPartitionId;
29
+ }
30
+ return payload;
31
+ }
32
+ /*
33
+ * Whether `option` represents the active masquerade. Three shapes:
34
+ * - userName defined: the "Specific Student..." prompt; active when the
35
+ * server says we're masquerading as some specific user.
36
+ * - groupId defined: a group option; active when the group ids match.
37
+ * - neither: a plain role option (e.g. Staff); active when no group/user
38
+ * is set.
39
+ */
40
+ export function isOptionSelected(option, active) {
41
+ if (option.role !== active.role) {
42
+ return false;
43
+ }
44
+ if (option.userName !== undefined) {
45
+ return active.userName !== null;
46
+ }
47
+ if (option.groupId !== undefined) {
48
+ return option.groupId === active.groupId
49
+ && option.userPartitionId === active.userPartitionId;
50
+ }
51
+ return active.userName === null && active.groupId === null;
52
+ }
53
+ function getHttpStatus(error) {
54
+ var _a;
55
+ return (_a = error === null || error === void 0 ? void 0 : error.customAttributes) === null || _a === void 0 ? void 0 : _a.httpErrorStatus;
56
+ }
57
+ /*
58
+ * The server tells us "no, you can't masquerade here" via either HTTP 403 or
59
+ * a 200 with `success: false`. Both mean: hide the bar entirely. Other
60
+ * failures (network, 5xx) are treated as "couldn't load" and surface as an
61
+ * alert.
62
+ */
63
+ function isQueryDenied(query) {
64
+ if (query.isError) {
65
+ return getHttpStatus(query.error) === 403;
66
+ }
67
+ return query.data !== undefined && !query.data.success;
68
+ }
69
+ export function formatErrorMessage(formatMessage, error) {
70
+ return typeof error === 'string' ? error : formatMessage(error);
71
+ }
72
+ function pickErrorMessage(query, mutation) {
73
+ /* Denial is handled by hiding the bar; only surface truly-failed loads here. */
74
+ if (query.isError && getHttpStatus(query.error) !== 403) {
75
+ return messages.failedToLoadOptions;
76
+ }
77
+ if (mutation.isError) {
78
+ return getHttpStatus(mutation.error) === 404
79
+ ? messages.noStudentFound
80
+ : messages.genericSubmitError;
81
+ }
82
+ if (mutation.data && !mutation.data.success) {
83
+ return mutation.data.error || messages.genericSubmitError;
84
+ }
85
+ return null;
86
+ }
87
+ export function useMasqueradeState(courseId) {
88
+ var _a, _b;
89
+ const queryClient = useQueryClient();
90
+ const navigate = useNavigate();
91
+ const location = useLocation();
92
+ /*
93
+ * Tracks the option the user just clicked, so the toggle and active-marker
94
+ * can update before the server confirms. Cleared once a mutation succeeds
95
+ * (the server's active state becomes the truth) or replaced when the user
96
+ * picks something else.
97
+ */
98
+ const [pendingOption, setPendingOption] = useState(null);
99
+ const [userName, setUserName] = useState('');
100
+ const query = useQuery({
101
+ queryKey: ['masquerade', courseId],
102
+ queryFn: () => getMasqueradeOptions(courseId),
103
+ });
104
+ const mutation = useMutation({
105
+ mutationFn: (payload) => postMasqueradeOptions(courseId, payload),
106
+ onSuccess: (data, payload) => __awaiter(this, void 0, void 0, function* () {
107
+ if (!(data === null || data === void 0 ? void 0 : data.success)) {
108
+ /* Server rejected the pick. Clear the optimistic pending state so the
109
+ * UI snaps back to the real active option, except for userName
110
+ * submissions where the input must stay open for retry. */
111
+ if (payload.user_name === undefined) {
112
+ setPendingOption(null);
113
+ }
114
+ return;
115
+ }
116
+ queryClient.invalidateQueries();
117
+ /* The course-tabs metadata is the source of truth for "what pages this
118
+ * user can see in this course." Fetch the post-masquerade tabs and
119
+ * redirect to the first one if the current path no longer appears. */
120
+ const meta = yield queryClient.fetchQuery({
121
+ queryKey: courseHomeCourseMetadataQueryKey(courseId),
122
+ queryFn: () => getCourseHomeCourseMetadata(courseId),
123
+ });
124
+ const fallback = meta.tabs[0];
125
+ if (!fallback) {
126
+ return;
127
+ }
128
+ const stillVisible = findActiveTab(meta.tabs, location.pathname);
129
+ if (stillVisible) {
130
+ return;
131
+ }
132
+ const targetPath = new URL(fallback.url).pathname;
133
+ if (isClientRoute(targetPath)) {
134
+ navigate(targetPath, { replace: true });
135
+ }
136
+ else {
137
+ window.location.assign(fallback.url);
138
+ }
139
+ }),
140
+ onError: (_error, payload) => {
141
+ /* Same as the !success branch: revert the optimistic pick, but keep the
142
+ * userName input open so the user can fix a typo and retry. */
143
+ if (payload.user_name === undefined) {
144
+ setPendingOption(null);
145
+ }
146
+ },
147
+ });
148
+ const active = (((_a = query.data) === null || _a === void 0 ? void 0 : _a.success) && query.data.active) || defaultActive;
149
+ const available = (((_b = query.data) === null || _b === void 0 ? void 0 : _b.success) && query.data.available) || [];
150
+ /*
151
+ * Seed the input from active.userName once, on the first successful query.
152
+ * After that the input belongs to the user — refetches (e.g. triggered by
153
+ * a masquerade change in another tab) must not clobber what they typed.
154
+ */
155
+ const hasSeededRef = useRef(false);
156
+ useEffect(() => {
157
+ if (hasSeededRef.current || !query.isSuccess) {
158
+ return;
159
+ }
160
+ hasSeededRef.current = true;
161
+ if (active.userName) {
162
+ setUserName(active.userName);
163
+ }
164
+ }, [query.isSuccess, active.userName]);
165
+ /*
166
+ * Clear pendingOption once the refetched active state actually reflects it.
167
+ * Clearing in onSuccess instead would briefly flash the old active state
168
+ * between mutation success and the refetch landing.
169
+ */
170
+ useEffect(() => {
171
+ if (pendingOption && isOptionSelected(pendingOption, active)) {
172
+ setPendingOption(null);
173
+ }
174
+ }, [pendingOption, active]);
175
+ const showUserNameInput = active.userName !== null
176
+ || (pendingOption === null || pendingOption === void 0 ? void 0 : pendingOption.userName) !== undefined;
177
+ const errorMessage = pickErrorMessage(query, mutation);
178
+ const isDenied = isQueryDenied(query);
179
+ /* Query landed but we couldn't load options — bar shows but widget hides. */
180
+ const isUnreachable = query.isError && getHttpStatus(query.error) !== 403;
181
+ /* Loading until the first response (success or failure) lands. */
182
+ const isLoading = query.isLoading;
183
+ const select = (option) => {
184
+ /* Clear any prior mutation error so it doesn't leak into the new selection. */
185
+ mutation.reset();
186
+ setPendingOption(option);
187
+ if (option.userName !== undefined) {
188
+ setUserName(option.userName);
189
+ return;
190
+ }
191
+ mutation.mutate(toMasqueradePayload(option));
192
+ };
193
+ const submitUserName = () => {
194
+ mutation.mutate({ role: 'student', user_name: userName });
195
+ };
196
+ return {
197
+ active,
198
+ available,
199
+ pendingOption,
200
+ showUserNameInput,
201
+ userName,
202
+ setUserName,
203
+ select,
204
+ submitUserName,
205
+ errorMessage,
206
+ isSubmitting: mutation.isPending,
207
+ isLoading,
208
+ isDenied,
209
+ isUnreachable,
210
+ };
211
+ }
212
+ //# sourceMappingURL=hooks.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hooks.js","sourceRoot":"","sources":["../../../../../shell/header/course-bar/masquerade/hooks.ts"],"names":[],"mappings":"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","sourcesContent":["import { useEffect, useRef, useState } from 'react';\nimport { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';\nimport type { MessageDescriptor } from 'react-intl';\nimport { useLocation, useNavigate } from 'react-router-dom';\n\nimport {\n ActiveMasqueradeData,\n MasqueradeOption,\n MasqueradeStatus,\n MasqueradePayload,\n getMasqueradeOptions,\n postMasqueradeOptions,\n} from './data/api';\nimport {\n CourseHomeCourseMetadata,\n courseHomeCourseMetadataQueryKey,\n findActiveTab,\n getCourseHomeCourseMetadata,\n} from '../data/service';\nimport { isClientRoute } from '../utils';\nimport messages from './messages';\n\nconst defaultActive: ActiveMasqueradeData = {\n role: 'staff',\n groupId: null,\n groupName: null,\n userName: null,\n userPartitionId: null,\n};\n\nfunction toMasqueradePayload(option: MasqueradeOption): MasqueradePayload {\n const payload: MasqueradePayload = { role: option.role };\n if (option.groupId !== undefined && option.userPartitionId !== undefined) {\n payload.group_id = option.groupId;\n payload.user_partition_id = option.userPartitionId;\n }\n return payload;\n}\n\n/*\n * Whether `option` represents the active masquerade. Three shapes:\n * - userName defined: the \"Specific Student...\" prompt; active when the\n * server says we're masquerading as some specific user.\n * - groupId defined: a group option; active when the group ids match.\n * - neither: a plain role option (e.g. Staff); active when no group/user\n * is set.\n */\nexport function isOptionSelected(option: MasqueradeOption, active: ActiveMasqueradeData): boolean {\n if (option.role !== active.role) {\n return false;\n }\n if (option.userName !== undefined) {\n return active.userName !== null;\n }\n if (option.groupId !== undefined) {\n return option.groupId === active.groupId\n && option.userPartitionId === active.userPartitionId;\n }\n return active.userName === null && active.groupId === null;\n}\n\ninterface HttpishError {\n customAttributes?: { httpErrorStatus?: number },\n}\n\nfunction getHttpStatus(error: unknown): number | undefined {\n return (error as HttpishError | null)?.customAttributes?.httpErrorStatus;\n}\n\n/*\n * The server tells us \"no, you can't masquerade here\" via either HTTP 403 or\n * a 200 with `success: false`. Both mean: hide the bar entirely. Other\n * failures (network, 5xx) are treated as \"couldn't load\" and surface as an\n * alert.\n */\nfunction isQueryDenied(query: { isError: boolean, error: unknown, data: MasqueradeStatus | undefined }): boolean {\n if (query.isError) {\n return getHttpStatus(query.error) === 403;\n }\n return query.data !== undefined && !query.data.success;\n}\n\n/*\n * Errors are returned as MessageDescriptors when the frontend can pick the\n * message, or as a raw string when the server's `data.error` is more specific\n * than anything we'd hand-pick. formatErrorMessage handles both at the\n * render site.\n */\nexport type MasqueradeErrorMessage = MessageDescriptor | string;\n\nexport function formatErrorMessage(\n formatMessage: (descriptor: MessageDescriptor) => string,\n error: MasqueradeErrorMessage,\n): string {\n return typeof error === 'string' ? error : formatMessage(error);\n}\n\nfunction pickErrorMessage(\n query: { isError: boolean, error: unknown, data: MasqueradeStatus | undefined },\n mutation: { isError: boolean, error: unknown, data: MasqueradeStatus | undefined },\n): MasqueradeErrorMessage | null {\n /* Denial is handled by hiding the bar; only surface truly-failed loads here. */\n if (query.isError && getHttpStatus(query.error) !== 403) {\n return messages.failedToLoadOptions;\n }\n if (mutation.isError) {\n return getHttpStatus(mutation.error) === 404\n ? messages.noStudentFound\n : messages.genericSubmitError;\n }\n if (mutation.data && !mutation.data.success) {\n return mutation.data.error || messages.genericSubmitError;\n }\n return null;\n}\n\nexport interface MasqueradeState {\n active: ActiveMasqueradeData,\n available: MasqueradeOption[],\n pendingOption: MasqueradeOption | null,\n showUserNameInput: boolean,\n userName: string,\n setUserName: (value: string) => void,\n select: (option: MasqueradeOption) => void,\n submitUserName: () => void,\n errorMessage: MasqueradeErrorMessage | null,\n isSubmitting: boolean,\n isLoading: boolean,\n isDenied: boolean,\n isUnreachable: boolean,\n}\n\nexport function useMasqueradeState(courseId: string): MasqueradeState {\n const queryClient = useQueryClient();\n const navigate = useNavigate();\n const location = useLocation();\n /*\n * Tracks the option the user just clicked, so the toggle and active-marker\n * can update before the server confirms. Cleared once a mutation succeeds\n * (the server's active state becomes the truth) or replaced when the user\n * picks something else.\n */\n const [pendingOption, setPendingOption] = useState<MasqueradeOption | null>(null);\n const [userName, setUserName] = useState('');\n\n const query = useQuery({\n queryKey: ['masquerade', courseId],\n queryFn: () => getMasqueradeOptions(courseId),\n });\n\n const mutation = useMutation({\n mutationFn: (payload: MasqueradePayload) => postMasqueradeOptions(courseId, payload),\n onSuccess: async (data, payload) => {\n if (!data?.success) {\n /* Server rejected the pick. Clear the optimistic pending state so the\n * UI snaps back to the real active option, except for userName\n * submissions where the input must stay open for retry. */\n if (payload.user_name === undefined) {\n setPendingOption(null);\n }\n return;\n }\n\n queryClient.invalidateQueries();\n\n /* The course-tabs metadata is the source of truth for \"what pages this\n * user can see in this course.\" Fetch the post-masquerade tabs and\n * redirect to the first one if the current path no longer appears. */\n const meta = await queryClient.fetchQuery<CourseHomeCourseMetadata>({\n queryKey: courseHomeCourseMetadataQueryKey(courseId),\n queryFn: () => getCourseHomeCourseMetadata(courseId),\n });\n const fallback = meta.tabs[0];\n if (!fallback) {\n return;\n }\n const stillVisible = findActiveTab(meta.tabs, location.pathname);\n if (stillVisible) {\n return;\n }\n const targetPath = new URL(fallback.url).pathname;\n if (isClientRoute(targetPath)) {\n navigate(targetPath, { replace: true });\n } else {\n window.location.assign(fallback.url);\n }\n },\n onError: (_error, payload) => {\n /* Same as the !success branch: revert the optimistic pick, but keep the\n * userName input open so the user can fix a typo and retry. */\n if (payload.user_name === undefined) {\n setPendingOption(null);\n }\n },\n });\n\n const active = (query.data?.success && query.data.active) || defaultActive;\n const available = (query.data?.success && query.data.available) || [];\n\n /*\n * Seed the input from active.userName once, on the first successful query.\n * After that the input belongs to the user — refetches (e.g. triggered by\n * a masquerade change in another tab) must not clobber what they typed.\n */\n const hasSeededRef = useRef(false);\n useEffect(() => {\n if (hasSeededRef.current || !query.isSuccess) {\n return;\n }\n hasSeededRef.current = true;\n if (active.userName) {\n setUserName(active.userName);\n }\n }, [query.isSuccess, active.userName]);\n\n /*\n * Clear pendingOption once the refetched active state actually reflects it.\n * Clearing in onSuccess instead would briefly flash the old active state\n * between mutation success and the refetch landing.\n */\n useEffect(() => {\n if (pendingOption && isOptionSelected(pendingOption, active)) {\n setPendingOption(null);\n }\n }, [pendingOption, active]);\n\n const showUserNameInput = active.userName !== null\n || pendingOption?.userName !== undefined;\n\n const errorMessage = pickErrorMessage(query, mutation);\n const isDenied = isQueryDenied(query);\n /* Query landed but we couldn't load options — bar shows but widget hides. */\n const isUnreachable = query.isError && getHttpStatus(query.error) !== 403;\n /* Loading until the first response (success or failure) lands. */\n const isLoading = query.isLoading;\n\n const select = (option: MasqueradeOption) => {\n /* Clear any prior mutation error so it doesn't leak into the new selection. */\n mutation.reset();\n setPendingOption(option);\n if (option.userName !== undefined) {\n setUserName(option.userName);\n return;\n }\n mutation.mutate(toMasqueradePayload(option));\n };\n\n const submitUserName = () => {\n mutation.mutate({ role: 'student', user_name: userName });\n };\n\n return {\n active,\n available,\n pendingOption,\n showUserNameInput,\n userName,\n setUserName,\n select,\n submitUserName,\n errorMessage,\n isSubmitting: mutation.isPending,\n isLoading,\n isDenied,\n isUnreachable,\n };\n}\n"]}
@@ -0,0 +1 @@
1
+ export { default } from './MasqueradeBar';
@@ -0,0 +1,2 @@
1
+ export { default } from './MasqueradeBar';
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../../shell/header/course-bar/masquerade/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC","sourcesContent":["export { default } from './MasqueradeBar';\n"]}
@@ -0,0 +1,7 @@
1
+ interface Props {
2
+ id?: string;
3
+ className?: string;
4
+ autoFocus?: boolean;
5
+ }
6
+ export declare function MasqueradeUserNameInput({ id, className, autoFocus }: Props): import("react/jsx-runtime").JSX.Element;
7
+ export {};
@@ -0,0 +1,18 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useIntl } from '@openedx/frontend-base';
3
+ import { Form, FormControl, FormGroup, StatefulButton, } from '@openedx/paragon';
4
+ import { useMasqueradeContext } from '../MasqueradeContext';
5
+ import messages from './messages';
6
+ export function MasqueradeUserNameInput({ id, className, autoFocus }) {
7
+ const { formatMessage } = useIntl();
8
+ const { userName, setUserName, submitUserName, isSubmitting, } = useMasqueradeContext();
9
+ const handleSubmit = (event) => {
10
+ event.preventDefault();
11
+ submitUserName();
12
+ };
13
+ return (_jsxs(Form, { className: `d-flex align-items-center ${className !== null && className !== void 0 ? className : ''}`, onSubmit: handleSubmit, children: [_jsx(FormGroup, { controlId: id, className: "flex-grow-1 mb-0 mr-2", children: _jsx(FormControl, { autoFocus: autoFocus, value: userName, onChange: (event) => setUserName(event.target.value) }) }), _jsx(StatefulButton, { type: "submit", variant: "brand", state: isSubmitting ? 'pending' : 'default', disabled: !userName, labels: {
14
+ default: formatMessage(messages.submit),
15
+ pending: formatMessage(messages.submitting),
16
+ } })] }));
17
+ }
18
+ //# sourceMappingURL=MasqueradeUserNameInput.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MasqueradeUserNameInput.js","sourceRoot":"","sources":["../../../../../../shell/header/course-bar/masquerade/masquerade-widget/MasqueradeUserNameInput.tsx"],"names":[],"mappings":";AACA,OAAO,EAAE,OAAO,EAAE,MAAM,wBAAwB,CAAC;AACjD,OAAO,EACL,IAAI,EAAE,WAAW,EAAE,SAAS,EAAE,cAAc,GAC7C,MAAM,kBAAkB,CAAC;AAE1B,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAC5D,OAAO,QAAQ,MAAM,YAAY,CAAC;AAQlC,MAAM,UAAU,uBAAuB,CAAC,EAAE,EAAE,EAAE,SAAS,EAAE,SAAS,EAAS;IACzE,MAAM,EAAE,aAAa,EAAE,GAAG,OAAO,EAAE,CAAC;IACpC,MAAM,EACJ,QAAQ,EAAE,WAAW,EAAE,cAAc,EAAE,YAAY,GACpD,GAAG,oBAAoB,EAAE,CAAC;IAE3B,MAAM,YAAY,GAAG,CAAC,KAAiC,EAAE,EAAE;QACzD,KAAK,CAAC,cAAc,EAAE,CAAC;QACvB,cAAc,EAAE,CAAC;IACnB,CAAC,CAAC;IAEF,OAAO,CACL,MAAC,IAAI,IAAC,SAAS,EAAE,6BAA6B,SAAS,aAAT,SAAS,cAAT,SAAS,GAAI,EAAE,EAAE,EAAE,QAAQ,EAAE,YAAY,aACrF,KAAC,SAAS,IAAC,SAAS,EAAE,EAAE,EAAE,SAAS,EAAC,uBAAuB,YACzD,KAAC,WAAW,IACV,SAAS,EAAE,SAAS,EACpB,KAAK,EAAE,QAAQ,EACf,QAAQ,EAAE,CAAC,KAA0C,EAAE,EAAE,CAAC,WAAW,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,GACzF,GACQ,EACZ,KAAC,cAAc,IACb,IAAI,EAAC,QAAQ,EACb,OAAO,EAAC,OAAO,EACf,KAAK,EAAE,YAAY,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,EAC3C,QAAQ,EAAE,CAAC,QAAQ,EACnB,MAAM,EAAE;oBACN,OAAO,EAAE,aAAa,CAAC,QAAQ,CAAC,MAAM,CAAC;oBACvC,OAAO,EAAE,aAAa,CAAC,QAAQ,CAAC,UAAU,CAAC;iBAC5C,GACD,IACG,CACR,CAAC;AACJ,CAAC","sourcesContent":["import { FormEvent } from 'react';\nimport { useIntl } from '@openedx/frontend-base';\nimport {\n Form, FormControl, FormGroup, StatefulButton,\n} from '@openedx/paragon';\n\nimport { useMasqueradeContext } from '../MasqueradeContext';\nimport messages from './messages';\n\ninterface Props {\n id?: string,\n className?: string,\n autoFocus?: boolean,\n}\n\nexport function MasqueradeUserNameInput({ id, className, autoFocus }: Props) {\n const { formatMessage } = useIntl();\n const {\n userName, setUserName, submitUserName, isSubmitting,\n } = useMasqueradeContext();\n\n const handleSubmit = (event: FormEvent<HTMLFormElement>) => {\n event.preventDefault();\n submitUserName();\n };\n\n return (\n <Form className={`d-flex align-items-center ${className ?? ''}`} onSubmit={handleSubmit}>\n <FormGroup controlId={id} className=\"flex-grow-1 mb-0 mr-2\">\n <FormControl\n autoFocus={autoFocus}\n value={userName}\n onChange={(event: React.ChangeEvent<HTMLInputElement>) => setUserName(event.target.value)}\n />\n </FormGroup>\n <StatefulButton\n type=\"submit\"\n variant=\"brand\"\n state={isSubmitting ? 'pending' : 'default'}\n disabled={!userName}\n labels={{\n default: formatMessage(messages.submit),\n pending: formatMessage(messages.submitting),\n }}\n />\n </Form>\n );\n}\n"]}
@@ -0,0 +1 @@
1
+ export declare function MasqueradeWidget(): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,16 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useId } from 'react';
3
+ import { useIntl, FormattedMessage } from '@openedx/frontend-base';
4
+ import { Dropdown } from '@openedx/paragon';
5
+ import { MasqueradeUserNameInput } from './MasqueradeUserNameInput';
6
+ import { MasqueradeWidgetOption } from './MasqueradeWidgetOption';
7
+ import { useMasqueradeContext } from '../MasqueradeContext';
8
+ import messages from './messages';
9
+ export function MasqueradeWidget() {
10
+ var _a, _b, _c;
11
+ const { formatMessage } = useIntl();
12
+ const inputId = useId();
13
+ const { active, available, pendingOption, showUserNameInput, } = useMasqueradeContext();
14
+ return (_jsxs("div", { className: "flex-grow-1", children: [_jsxs("div", { className: "row", children: [_jsx("span", { className: "col-auto col-form-label pl-3", children: _jsx(FormattedMessage, Object.assign({}, messages.titleViewAs)) }), _jsxs(Dropdown, { className: "flex-shrink-1 mx-1", children: [_jsx(Dropdown.Toggle, { id: "masquerade-widget-toggle", variant: "inverse-outline-primary", children: (_c = (_b = (_a = pendingOption === null || pendingOption === void 0 ? void 0 : pendingOption.name) !== null && _a !== void 0 ? _a : active.groupName) !== null && _b !== void 0 ? _b : active.userName) !== null && _c !== void 0 ? _c : formatMessage(messages.titleStaff) }), _jsx(Dropdown.Menu, { children: available.map(option => (_jsx(MasqueradeWidgetOption, { option: option }, option.name))) })] })] }), showUserNameInput && (_jsxs("div", { className: "row mt-2", children: [_jsx("label", { htmlFor: inputId, className: "col-auto col-form-label pl-3 mb-0", children: `${formatMessage(messages.userNameLabel)}:` }), _jsx(MasqueradeUserNameInput, { id: inputId, className: "col-4", autoFocus: true })] }))] }));
15
+ }
16
+ //# sourceMappingURL=MasqueradeWidget.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MasqueradeWidget.js","sourceRoot":"","sources":["../../../../../../shell/header/course-bar/masquerade/masquerade-widget/MasqueradeWidget.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,OAAO,CAAC;AAC9B,OAAO,EAAE,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AACnE,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAE5C,OAAO,EAAE,uBAAuB,EAAE,MAAM,2BAA2B,CAAC;AACpE,OAAO,EAAE,sBAAsB,EAAE,MAAM,0BAA0B,CAAC;AAClE,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAC5D,OAAO,QAAQ,MAAM,YAAY,CAAC;AAElC,MAAM,UAAU,gBAAgB;;IAC9B,MAAM,EAAE,aAAa,EAAE,GAAG,OAAO,EAAE,CAAC;IACpC,MAAM,OAAO,GAAG,KAAK,EAAE,CAAC;IACxB,MAAM,EACJ,MAAM,EAAE,SAAS,EAAE,aAAa,EAAE,iBAAiB,GACpD,GAAG,oBAAoB,EAAE,CAAC;IAE3B,OAAO,CACL,eAAK,SAAS,EAAC,aAAa,aAC1B,eAAK,SAAS,EAAC,KAAK,aAClB,eAAM,SAAS,EAAC,8BAA8B,YAC5C,KAAC,gBAAgB,oBAAK,QAAQ,CAAC,WAAW,EAAI,GACzC,EACP,MAAC,QAAQ,IAAC,SAAS,EAAC,oBAAoB,aACtC,KAAC,QAAQ,CAAC,MAAM,IAAC,EAAE,EAAC,0BAA0B,EAAC,OAAO,EAAC,yBAAyB,YAC7E,MAAA,MAAA,MAAA,aAAa,aAAb,aAAa,uBAAb,aAAa,CAAE,IAAI,mCACjB,MAAM,CAAC,SAAS,mCAChB,MAAM,CAAC,QAAQ,mCACf,aAAa,CAAC,QAAQ,CAAC,UAAU,CAAC,GACrB,EAClB,KAAC,QAAQ,CAAC,IAAI,cACX,SAAS,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,CACvB,KAAC,sBAAsB,IAAmB,MAAM,EAAE,MAAM,IAA3B,MAAM,CAAC,IAAI,CAAoB,CAC7D,CAAC,GACY,IACP,IACP,EACL,iBAAiB,IAAI,CACpB,eAAK,SAAS,EAAC,UAAU,aACvB,gBAAO,OAAO,EAAE,OAAO,EAAE,SAAS,EAAC,mCAAmC,YACnE,GAAG,aAAa,CAAC,QAAQ,CAAC,aAAa,CAAC,GAAG,GACtC,EACR,KAAC,uBAAuB,IAAC,EAAE,EAAE,OAAO,EAAE,SAAS,EAAC,OAAO,EAAC,SAAS,SAAG,IAChE,CACP,IACG,CACP,CAAC;AACJ,CAAC","sourcesContent":["import { useId } from 'react';\nimport { useIntl, FormattedMessage } from '@openedx/frontend-base';\nimport { Dropdown } from '@openedx/paragon';\n\nimport { MasqueradeUserNameInput } from './MasqueradeUserNameInput';\nimport { MasqueradeWidgetOption } from './MasqueradeWidgetOption';\nimport { useMasqueradeContext } from '../MasqueradeContext';\nimport messages from './messages';\n\nexport function MasqueradeWidget() {\n const { formatMessage } = useIntl();\n const inputId = useId();\n const {\n active, available, pendingOption, showUserNameInput,\n } = useMasqueradeContext();\n\n return (\n <div className=\"flex-grow-1\">\n <div className=\"row\">\n <span className=\"col-auto col-form-label pl-3\">\n <FormattedMessage {...messages.titleViewAs} />\n </span>\n <Dropdown className=\"flex-shrink-1 mx-1\">\n <Dropdown.Toggle id=\"masquerade-widget-toggle\" variant=\"inverse-outline-primary\">\n {pendingOption?.name\n ?? active.groupName\n ?? active.userName\n ?? formatMessage(messages.titleStaff)}\n </Dropdown.Toggle>\n <Dropdown.Menu>\n {available.map(option => (\n <MasqueradeWidgetOption key={option.name} option={option} />\n ))}\n </Dropdown.Menu>\n </Dropdown>\n </div>\n {showUserNameInput && (\n <div className=\"row mt-2\">\n <label htmlFor={inputId} className=\"col-auto col-form-label pl-3 mb-0\">\n {`${formatMessage(messages.userNameLabel)}:`}\n </label>\n <MasqueradeUserNameInput id={inputId} className=\"col-4\" autoFocus />\n </div>\n )}\n </div>\n );\n}\n"]}
@@ -0,0 +1,6 @@
1
+ import type { MasqueradeOption } from '../data/api';
2
+ interface Props {
3
+ option: MasqueradeOption;
4
+ }
5
+ export declare function MasqueradeWidgetOption({ option }: Props): import("react/jsx-runtime").JSX.Element | null;
6
+ export {};
@@ -0,0 +1,18 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Dropdown } from '@openedx/paragon';
3
+ import { useMasqueradeContext } from '../MasqueradeContext';
4
+ import { isOptionSelected } from '../hooks';
5
+ export function MasqueradeWidgetOption({ option }) {
6
+ const { active, pendingOption, select } = useMasqueradeContext();
7
+ if (!option.name) {
8
+ return null;
9
+ }
10
+ /* While a click is pending, the menu mirrors the toggle: only that option
11
+ * is highlighted. Once the server confirms (pendingOption clears), the
12
+ * active-server-state logic takes over again. */
13
+ const isHighlighted = pendingOption !== null
14
+ ? option.name === pendingOption.name
15
+ : isOptionSelected(option, active);
16
+ return (_jsx(Dropdown.Item, { className: isHighlighted ? 'active' : '', href: "#", onClick: () => select(option), children: option.name }));
17
+ }
18
+ //# sourceMappingURL=MasqueradeWidgetOption.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MasqueradeWidgetOption.js","sourceRoot":"","sources":["../../../../../../shell/header/course-bar/masquerade/masquerade-widget/MasqueradeWidgetOption.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAE5C,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAC5D,OAAO,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AAO5C,MAAM,UAAU,sBAAsB,CAAC,EAAE,MAAM,EAAS;IACtD,MAAM,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,EAAE,GAAG,oBAAoB,EAAE,CAAC;IAEjE,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;QACjB,OAAO,IAAI,CAAC;IACd,CAAC;IAED;;qDAEiD;IACjD,MAAM,aAAa,GAAG,aAAa,KAAK,IAAI;QAC1C,CAAC,CAAC,MAAM,CAAC,IAAI,KAAK,aAAa,CAAC,IAAI;QACpC,CAAC,CAAC,gBAAgB,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAErC,OAAO,CACL,KAAC,QAAQ,CAAC,IAAI,IACZ,SAAS,EAAE,aAAa,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,EACxC,IAAI,EAAC,GAAG,EACR,OAAO,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,YAE5B,MAAM,CAAC,IAAI,GACE,CACjB,CAAC;AACJ,CAAC","sourcesContent":["import { Dropdown } from '@openedx/paragon';\n\nimport { useMasqueradeContext } from '../MasqueradeContext';\nimport { isOptionSelected } from '../hooks';\nimport type { MasqueradeOption } from '../data/api';\n\ninterface Props {\n option: MasqueradeOption,\n}\n\nexport function MasqueradeWidgetOption({ option }: Props) {\n const { active, pendingOption, select } = useMasqueradeContext();\n\n if (!option.name) {\n return null;\n }\n\n /* While a click is pending, the menu mirrors the toggle: only that option\n * is highlighted. Once the server confirms (pendingOption clears), the\n * active-server-state logic takes over again. */\n const isHighlighted = pendingOption !== null\n ? option.name === pendingOption.name\n : isOptionSelected(option, active);\n\n return (\n <Dropdown.Item\n className={isHighlighted ? 'active' : ''}\n href=\"#\"\n onClick={() => select(option)}\n >\n {option.name}\n </Dropdown.Item>\n );\n}\n"]}
@@ -0,0 +1,2 @@
1
+ import { MasqueradeWidget } from './MasqueradeWidget';
2
+ export default MasqueradeWidget;
@@ -0,0 +1,3 @@
1
+ import { MasqueradeWidget } from './MasqueradeWidget';
2
+ export default MasqueradeWidget;
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../../../shell/header/course-bar/masquerade/masquerade-widget/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAEtD,eAAe,gBAAgB,CAAC","sourcesContent":["import { MasqueradeWidget } from './MasqueradeWidget';\n\nexport default MasqueradeWidget;\n"]}
@@ -0,0 +1,28 @@
1
+ declare const messages: {
2
+ userNameLabel: {
3
+ id: string;
4
+ defaultMessage: string;
5
+ description: string;
6
+ };
7
+ submit: {
8
+ id: string;
9
+ defaultMessage: string;
10
+ description: string;
11
+ };
12
+ submitting: {
13
+ id: string;
14
+ defaultMessage: string;
15
+ description: string;
16
+ };
17
+ titleViewAs: {
18
+ id: string;
19
+ defaultMessage: string;
20
+ description: string;
21
+ };
22
+ titleStaff: {
23
+ id: string;
24
+ defaultMessage: string;
25
+ description: string;
26
+ };
27
+ };
28
+ export default messages;
@@ -0,0 +1,30 @@
1
+ import { defineMessages } from '../../../../../runtime';
2
+ const messages = defineMessages({
3
+ userNameLabel: {
4
+ id: 'masquerade-widget.userName.input.label',
5
+ defaultMessage: 'Username or email',
6
+ description: 'Label for the masquerade username/email input.',
7
+ },
8
+ submit: {
9
+ id: 'masquerade-widget.userName.submit',
10
+ defaultMessage: 'Submit',
11
+ description: 'Label for the masquerade username submit button.',
12
+ },
13
+ submitting: {
14
+ id: 'masquerade-widget.userName.submitting',
15
+ defaultMessage: 'Submitting',
16
+ description: 'Pending label for the masquerade username submit button.',
17
+ },
18
+ titleViewAs: {
19
+ id: 'masquerade-widget.view.as',
20
+ defaultMessage: 'View this course as:',
21
+ description: 'Button to view this course as',
22
+ },
23
+ titleStaff: {
24
+ id: 'masquerade-widget.staff',
25
+ defaultMessage: 'Staff',
26
+ description: 'Button Staff',
27
+ },
28
+ });
29
+ export default messages;
30
+ //# sourceMappingURL=messages.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"messages.js","sourceRoot":"","sources":["../../../../../../shell/header/course-bar/masquerade/masquerade-widget/messages.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAExD,MAAM,QAAQ,GAAG,cAAc,CAAC;IAC9B,aAAa,EAAE;QACb,EAAE,EAAE,wCAAwC;QAC5C,cAAc,EAAE,mBAAmB;QACnC,WAAW,EAAE,gDAAgD;KAC9D;IACD,MAAM,EAAE;QACN,EAAE,EAAE,mCAAmC;QACvC,cAAc,EAAE,QAAQ;QACxB,WAAW,EAAE,kDAAkD;KAChE;IACD,UAAU,EAAE;QACV,EAAE,EAAE,uCAAuC;QAC3C,cAAc,EAAE,YAAY;QAC5B,WAAW,EAAE,0DAA0D;KACxE;IACD,WAAW,EAAE;QACX,EAAE,EAAE,2BAA2B;QAC/B,cAAc,EAAE,sBAAsB;QACtC,WAAW,EAAE,+BAA+B;KAC7C;IACD,UAAU,EAAE;QACV,EAAE,EAAE,yBAAyB;QAC7B,cAAc,EAAE,OAAO;QACvB,WAAW,EAAE,cAAc;KAC5B;CACF,CAAC,CAAC;AAEH,eAAe,QAAQ,CAAC","sourcesContent":["import { defineMessages } from '../../../../../runtime';\n\nconst messages = defineMessages({\n userNameLabel: {\n id: 'masquerade-widget.userName.input.label',\n defaultMessage: 'Username or email',\n description: 'Label for the masquerade username/email input.',\n },\n submit: {\n id: 'masquerade-widget.userName.submit',\n defaultMessage: 'Submit',\n description: 'Label for the masquerade username submit button.',\n },\n submitting: {\n id: 'masquerade-widget.userName.submitting',\n defaultMessage: 'Submitting',\n description: 'Pending label for the masquerade username submit button.',\n },\n titleViewAs: {\n id: 'masquerade-widget.view.as',\n defaultMessage: 'View this course as:',\n description: 'Button to view this course as',\n },\n titleStaff: {\n id: 'masquerade-widget.staff',\n defaultMessage: 'Staff',\n description: 'Button Staff',\n },\n});\n\nexport default messages;\n"]}
@@ -0,0 +1,33 @@
1
+ declare const messages: {
2
+ ariaLabel: {
3
+ id: string;
4
+ defaultMessage: string;
5
+ description: string;
6
+ };
7
+ titleViewCourseIn: {
8
+ id: string;
9
+ defaultMessage: string;
10
+ description: string;
11
+ };
12
+ titleStudio: {
13
+ id: string;
14
+ defaultMessage: string;
15
+ description: string;
16
+ };
17
+ failedToLoadOptions: {
18
+ id: string;
19
+ defaultMessage: string;
20
+ description: string;
21
+ };
22
+ noStudentFound: {
23
+ id: string;
24
+ defaultMessage: string;
25
+ description: string;
26
+ };
27
+ genericSubmitError: {
28
+ id: string;
29
+ defaultMessage: string;
30
+ description: string;
31
+ };
32
+ };
33
+ export default messages;
@@ -0,0 +1,35 @@
1
+ import { defineMessages } from '../../../../runtime';
2
+ const messages = defineMessages({
3
+ ariaLabel: {
4
+ id: 'masqueradeBar.ariaLabel',
5
+ defaultMessage: 'Masquerade bar',
6
+ description: 'Accessible label identifying the masquerade bar region.',
7
+ },
8
+ titleViewCourseIn: {
9
+ id: 'masqueradeBar.viewCourse',
10
+ defaultMessage: 'View course in:',
11
+ description: 'Button to view the course in the studio',
12
+ },
13
+ titleStudio: {
14
+ id: 'masqueradeBar.studio',
15
+ defaultMessage: 'Studio',
16
+ description: 'Button to view in studio',
17
+ },
18
+ failedToLoadOptions: {
19
+ id: 'masqueradeBar.error.failedToLoadOptions',
20
+ defaultMessage: 'Unable to load masquerade options.',
21
+ description: 'Error shown when masquerade options cannot be retrieved from the server.',
22
+ },
23
+ noStudentFound: {
24
+ id: 'masqueradeBar.error.noStudentFound',
25
+ defaultMessage: 'No student with this username or email could be found.',
26
+ description: 'Error shown when masquerading by username and the user does not exist.',
27
+ },
28
+ genericSubmitError: {
29
+ id: 'masqueradeBar.error.genericSubmit',
30
+ defaultMessage: 'An error has occurred; please try again.',
31
+ description: 'Generic error shown when the masquerade submission fails.',
32
+ },
33
+ });
34
+ export default messages;
35
+ //# sourceMappingURL=messages.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"messages.js","sourceRoot":"","sources":["../../../../../shell/header/course-bar/masquerade/messages.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAErD,MAAM,QAAQ,GAAG,cAAc,CAAC;IAC9B,SAAS,EAAE;QACT,EAAE,EAAE,yBAAyB;QAC7B,cAAc,EAAE,gBAAgB;QAChC,WAAW,EAAE,yDAAyD;KACvE;IACD,iBAAiB,EAAE;QACjB,EAAE,EAAE,0BAA0B;QAC9B,cAAc,EAAE,iBAAiB;QACjC,WAAW,EAAE,yCAAyC;KACvD;IACD,WAAW,EAAE;QACX,EAAE,EAAE,sBAAsB;QAC1B,cAAc,EAAE,QAAQ;QACxB,WAAW,EAAE,0BAA0B;KACxC;IACD,mBAAmB,EAAE;QACnB,EAAE,EAAE,yCAAyC;QAC7C,cAAc,EAAE,oCAAoC;QACpD,WAAW,EAAE,0EAA0E;KACxF;IACD,cAAc,EAAE;QACd,EAAE,EAAE,oCAAoC;QACxC,cAAc,EAAE,wDAAwD;QACxE,WAAW,EAAE,wEAAwE;KACtF;IACD,kBAAkB,EAAE;QAClB,EAAE,EAAE,mCAAmC;QACvC,cAAc,EAAE,0CAA0C;QAC1D,WAAW,EAAE,2DAA2D;KACzE;CACF,CAAC,CAAC;AAEH,eAAe,QAAQ,CAAC","sourcesContent":["import { defineMessages } from '../../../../runtime';\n\nconst messages = defineMessages({\n ariaLabel: {\n id: 'masqueradeBar.ariaLabel',\n defaultMessage: 'Masquerade bar',\n description: 'Accessible label identifying the masquerade bar region.',\n },\n titleViewCourseIn: {\n id: 'masqueradeBar.viewCourse',\n defaultMessage: 'View course in:',\n description: 'Button to view the course in the studio',\n },\n titleStudio: {\n id: 'masqueradeBar.studio',\n defaultMessage: 'Studio',\n description: 'Button to view in studio',\n },\n failedToLoadOptions: {\n id: 'masqueradeBar.error.failedToLoadOptions',\n defaultMessage: 'Unable to load masquerade options.',\n description: 'Error shown when masquerade options cannot be retrieved from the server.',\n },\n noStudentFound: {\n id: 'masqueradeBar.error.noStudentFound',\n defaultMessage: 'No student with this username or email could be found.',\n description: 'Error shown when masquerading by username and the user does not exist.',\n },\n genericSubmitError: {\n id: 'masqueradeBar.error.genericSubmit',\n defaultMessage: 'An error has occurred; please try again.',\n description: 'Generic error shown when the masquerade submission fails.',\n },\n});\n\nexport default messages;\n"]}
@@ -1,42 +1,19 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useMemo } from 'react';
3
- import { Link, matchPath, useLocation, useParams } from 'react-router-dom';
3
+ import { Link, useLocation, useParams } from 'react-router-dom';
4
4
  import { useQuery } from '@tanstack/react-query';
5
- import { Slot, useIntl } from '../../../runtime';
6
- import { getCourseHomeCourseMetadata } from './data/service';
5
+ import { Slot, useIntl } from '../../../../runtime';
6
+ import { courseHomeCourseMetadataQueryKey, findActiveTab, getCourseHomeCourseMetadata, } from '../data/service';
7
7
  import { Container, Nav, Navbar, Skeleton } from '@openedx/paragon';
8
8
  import messages from './messages';
9
- import { isClientRoute } from './utils';
9
+ import { isClientRoute } from '../utils';
10
10
  import './course-tabs-navigation.scss';
11
- /*
12
- * Returns the tabId of the tab whose pathname is the longest prefix match
13
- * against the current path. Uses react-router's matchPath for segment-aware
14
- * matching.
15
- *
16
- * For example, given tabs with paths /course/ (tabId: "outline")
17
- * and /course/dates/ (tabId: "dates"):
18
- *
19
- * /course/dates/foo -> "dates" (longest prefix match)
20
- * /course/outline -> "outline"
21
- * /courseware -> null (not a segment boundary)
22
- */
23
- const getActiveTabId = (currentPath, tabs) => {
24
- var _a;
25
- let best = null;
26
- for (const tab of tabs) {
27
- const match = matchPath({ path: `${tab.pathname}/*`, end: false }, currentPath);
28
- if (match && (!best || tab.pathname.length > best.pathname.length)) {
29
- best = tab;
30
- }
31
- }
32
- return (_a = best === null || best === void 0 ? void 0 : best.tabId) !== null && _a !== void 0 ? _a : null;
33
- };
34
11
  const CourseTabsNavigation = () => {
35
12
  const location = useLocation();
36
13
  const { courseId = '' } = useParams();
37
14
  const intl = useIntl();
38
15
  const { data = { tabs: [] }, isLoading } = useQuery({
39
- queryKey: ['org.openedx.frontend.app.header.course-meta', courseId],
16
+ queryKey: courseHomeCourseMetadataQueryKey(courseId),
40
17
  queryFn: () => getCourseHomeCourseMetadata(courseId),
41
18
  retry: 2,
42
19
  enabled: !!courseId,
@@ -47,7 +24,7 @@ const CourseTabsNavigation = () => {
47
24
  const pathname = new URL(tab.url).pathname;
48
25
  return Object.assign(Object.assign({}, tab), { pathname, clientPath: isClientRoute(pathname) ? pathname : null });
49
26
  }), [tabs]);
50
- const currentTab = useMemo(() => resolvedTabs.length > 0 ? getActiveTabId(location.pathname, resolvedTabs) : null, [location.pathname, resolvedTabs]);
27
+ const currentTab = useMemo(() => { var _a, _b; return (resolvedTabs.length > 0 ? (_b = (_a = findActiveTab(resolvedTabs, location.pathname)) === null || _a === void 0 ? void 0 : _a.tabId) !== null && _b !== void 0 ? _b : null : null); }, [location.pathname, resolvedTabs]);
51
28
  if (isLoading) {
52
29
  return _jsx(Skeleton, { className: "lead mt-3" });
53
30
  }
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CourseTabsNavigation.js","sourceRoot":"","sources":["../../../../../shell/header/course-bar/navigation/CourseTabsNavigation.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useMemo } from 'react';\nimport { Link, useLocation, useParams } from 'react-router-dom';\nimport { useQuery } from '@tanstack/react-query';\nimport { Slot, useIntl } from '../../../../runtime';\nimport {\n CourseTab,\n courseHomeCourseMetadataQueryKey,\n findActiveTab,\n getCourseHomeCourseMetadata,\n} from '../data/service';\nimport { Container, Nav, Navbar, Skeleton } from '@openedx/paragon';\nimport messages from './messages';\nimport { isClientRoute } from '../utils';\nimport './course-tabs-navigation.scss';\n\ninterface ResolvedTab extends CourseTab {\n pathname: string,\n clientPath: string | null,\n}\n\nconst CourseTabsNavigation = () => {\n const location = useLocation();\n const { courseId = '' } = useParams();\n const intl = useIntl();\n\n const { data = { tabs: [] }, isLoading } = useQuery({\n queryKey: courseHomeCourseMetadataQueryKey(courseId),\n queryFn: () => getCourseHomeCourseMetadata(courseId),\n retry: 2,\n enabled: !!courseId,\n });\n\n const { tabs } = data;\n\n const resolvedTabs: ResolvedTab[] = useMemo(\n () => tabs.map(tab => {\n // Tab URLs from the course_home API are always absolute.\n const pathname = new URL(tab.url).pathname;\n return { ...tab, pathname, clientPath: isClientRoute(pathname) ? pathname : null };\n }),\n [tabs]\n );\n\n const currentTab = useMemo(\n () => (resolvedTabs.length > 0 ? findActiveTab(resolvedTabs, location.pathname)?.tabId ?? null : null),\n [location.pathname, resolvedTabs]\n );\n\n if (isLoading) {\n return <Skeleton className=\"lead mt-3\" />;\n }\n\n if (!courseId || resolvedTabs.length === 0) {\n return null;\n }\n\n return (\n <Navbar expand=\"sm\" className=\"course-tabs-navigation pb-0\" aria-label={intl.formatMessage(messages.courseMaterial)}>\n <Container fluid size=\"xl\">\n <Nav\n variant=\"tabs\"\n activeKey={currentTab}\n >\n <Navbar.Toggle aria-controls=\"course-nav\" />\n <Navbar.Collapse id=\"course-nav\">\n {\n resolvedTabs.map(tab => (\n <Nav.Item key={tab.tabId}>\n <Nav.Link\n {...(tab.clientPath\n ? { to: tab.clientPath, as: Link }\n : { href: tab.url }\n )}\n active={tab.tabId === currentTab}\n >\n {tab.title}\n </Nav.Link>\n </Nav.Item>\n ))\n }\n </Navbar.Collapse>\n <Slot id=\"org.openedx.frontend.slot.header.courseNavigationBar.extraContent.v1\" />\n </Nav>\n </Container>\n </Navbar>\n );\n};\n\nexport default CourseTabsNavigation;\n"]}
@@ -1,4 +1,4 @@
1
- import { defineMessages } from '../../../runtime';
1
+ import { defineMessages } from '../../../../runtime';
2
2
  const messages = defineMessages({
3
3
  courseMaterial: {
4
4
  id: 'org.openedx.frontend.slot.header.courseNavigationBar.tabs.label',
@@ -0,0 +1 @@
1
+ {"version":3,"file":"messages.js","sourceRoot":"","sources":["../../../../../shell/header/course-bar/navigation/messages.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAErD,MAAM,QAAQ,GAAG,cAAc,CAAC;IAC9B,cAAc,EAAE;QACd,EAAE,EAAE,iEAAiE;QACrE,cAAc,EAAE,uBAAuB;QACvC,WAAW,EAAE,iDAAiD;KAC/D;CACF,CAAC,CAAC;AAEH,eAAe,QAAQ,CAAC","sourcesContent":["import { defineMessages } from '../../../../runtime';\n\nconst messages = defineMessages({\n courseMaterial: {\n id: 'org.openedx.frontend.slot.header.courseNavigationBar.tabs.label',\n defaultMessage: 'Course Navigation Bar',\n description: 'The accessible label for course tabs navigation',\n },\n});\n\nexport default messages;\n"]}
@@ -0,0 +1,3 @@
1
+ export declare function isCourseBarRoute(): boolean;
2
+ export declare function isCourseBarMasqueradeRoute(): boolean;
3
+ export declare function isClientRoute(pathname: string): boolean;