@instructure/platform-modules 0.1.0

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 (81) hide show
  1. package/README.md +75 -0
  2. package/dist/adapters/canvasGraphql/index.d.ts +54 -0
  3. package/dist/adapters/canvasGraphql/index.d.ts.map +1 -0
  4. package/dist/adapters/canvasGraphql/normalize.d.ts +345 -0
  5. package/dist/adapters/canvasGraphql/normalize.d.ts.map +1 -0
  6. package/dist/adapters/canvasGraphql/queries.d.ts +69 -0
  7. package/dist/adapters/canvasGraphql/queries.d.ts.map +1 -0
  8. package/dist/adapters/inMemory.d.ts +63 -0
  9. package/dist/adapters/inMemory.d.ts.map +1 -0
  10. package/dist/adapters/index.d.ts +3 -0
  11. package/dist/adapters/index.d.ts.map +1 -0
  12. package/dist/adapters/limits.d.ts +86 -0
  13. package/dist/adapters/limits.d.ts.map +1 -0
  14. package/dist/adapters/types.d.ts +87 -0
  15. package/dist/adapters/types.d.ts.map +1 -0
  16. package/dist/cache/index.d.ts +3 -0
  17. package/dist/cache/index.d.ts.map +1 -0
  18. package/dist/cache/invalidate.d.ts +52 -0
  19. package/dist/cache/invalidate.d.ts.map +1 -0
  20. package/dist/cache/keys.d.ts +86 -0
  21. package/dist/cache/keys.d.ts.map +1 -0
  22. package/dist/components/FilterChips.d.ts +19 -0
  23. package/dist/components/FilterChips.d.ts.map +1 -0
  24. package/dist/components/ModuleCard.d.ts +32 -0
  25. package/dist/components/ModuleCard.d.ts.map +1 -0
  26. package/dist/components/ModuleItemRow.d.ts +17 -0
  27. package/dist/components/ModuleItemRow.d.ts.map +1 -0
  28. package/dist/components/ModulesList.d.ts +69 -0
  29. package/dist/components/ModulesList.d.ts.map +1 -0
  30. package/dist/components/ModulesList.stories.d.ts +138 -0
  31. package/dist/components/ModulesList.stories.d.ts.map +1 -0
  32. package/dist/components/StatusPill.d.ts +18 -0
  33. package/dist/components/StatusPill.d.ts.map +1 -0
  34. package/dist/components/index.d.ts +7 -0
  35. package/dist/components/index.d.ts.map +1 -0
  36. package/dist/components/labels.d.ts +89 -0
  37. package/dist/components/labels.d.ts.map +1 -0
  38. package/dist/config/index.d.ts +3 -0
  39. package/dist/config/index.d.ts.map +1 -0
  40. package/dist/config/registry.d.ts +38 -0
  41. package/dist/config/registry.d.ts.map +1 -0
  42. package/dist/config/schemas.d.ts +16 -0
  43. package/dist/config/schemas.d.ts.map +1 -0
  44. package/dist/fixtures/generate.d.ts +48 -0
  45. package/dist/fixtures/generate.d.ts.map +1 -0
  46. package/dist/fixtures/index.d.ts +3 -0
  47. package/dist/fixtures/index.d.ts.map +1 -0
  48. package/dist/fixtures/scenarios.d.ts +42 -0
  49. package/dist/fixtures/scenarios.d.ts.map +1 -0
  50. package/dist/hooks/constants.d.ts +49 -0
  51. package/dist/hooks/constants.d.ts.map +1 -0
  52. package/dist/hooks/index.d.ts +7 -0
  53. package/dist/hooks/index.d.ts.map +1 -0
  54. package/dist/hooks/queryOptions.d.ts +58 -0
  55. package/dist/hooks/queryOptions.d.ts.map +1 -0
  56. package/dist/hooks/useItemsMode.d.ts +114 -0
  57. package/dist/hooks/useItemsMode.d.ts.map +1 -0
  58. package/dist/hooks/useModuleItemQueries.d.ts +56 -0
  59. package/dist/hooks/useModuleItemQueries.d.ts.map +1 -0
  60. package/dist/hooks/useModuleItems.d.ts +41 -0
  61. package/dist/hooks/useModuleItems.d.ts.map +1 -0
  62. package/dist/hooks/useModules.d.ts +50 -0
  63. package/dist/hooks/useModules.d.ts.map +1 -0
  64. package/dist/hooks/useModulesPageData.d.ts +78 -0
  65. package/dist/hooks/useModulesPageData.d.ts.map +1 -0
  66. package/dist/i18n.d.ts +92 -0
  67. package/dist/i18n.d.ts.map +1 -0
  68. package/dist/index.d.ts +19 -0
  69. package/dist/index.d.ts.map +1 -0
  70. package/dist/index.js +1966 -0
  71. package/dist/types/domain.d.ts +147 -0
  72. package/dist/types/domain.d.ts.map +1 -0
  73. package/dist/types/enums.d.ts +93 -0
  74. package/dist/types/enums.d.ts.map +1 -0
  75. package/dist/types/extensions.d.ts +48 -0
  76. package/dist/types/extensions.d.ts.map +1 -0
  77. package/dist/types/index.d.ts +4 -0
  78. package/dist/types/index.d.ts.map +1 -0
  79. package/dist/types/schemas.d.ts +38 -0
  80. package/dist/types/schemas.d.ts.map +1 -0
  81. package/package.json +80 -0
package/README.md ADDED
@@ -0,0 +1,75 @@
1
+ # @instructure/platform-modules
2
+
3
+ The platform modules experience. One data layer and one set of components, driven by a
4
+ host-supplied adapter.
5
+
6
+ ## Installation
7
+
8
+ Add the workspace dependency:
9
+
10
+ ```json
11
+ {
12
+ "dependencies": {
13
+ "@instructure/platform-modules": "workspace:*"
14
+ }
15
+ }
16
+ ```
17
+
18
+ ### Peer dependencies
19
+
20
+ The host provides React 18, `@tanstack/react-query` 5, `graphql` 16, `zod` 3, and the InstUI 11
21
+ packages listed under `peerDependencies` in `package.json`.
22
+
23
+ ## Usage
24
+
25
+ Build an adapter once, outside render. Wrap the page in the i18n provider. Call the hook and pass
26
+ its whole result to the list.
27
+
28
+ ```tsx
29
+ import {
30
+ createCanvasGraphqlAdapter,
31
+ ModulesI18nProvider,
32
+ ModulesList,
33
+ useModulesPageData,
34
+ } from '@instructure/platform-modules'
35
+
36
+ const adapter = createCanvasGraphqlAdapter({ executeQuery })
37
+
38
+ function ModulesPage({ courseId }: { courseId: string }) {
39
+ const data = useModulesPageData({ courseId, adapter, view: 'student' })
40
+ return (
41
+ <ModulesI18nProvider t={t} locale={locale}>
42
+ <ModulesList data={data} courseName={courseName} />
43
+ </ModulesI18nProvider>
44
+ )
45
+ }
46
+ ```
47
+
48
+ Keep the adapter referentially stable. A new adapter identity on every render rebuilds every item
49
+ observer's options on every render.
50
+
51
+ ## Adapters
52
+
53
+ | Adapter | Use |
54
+ |---|---|
55
+ | `createCanvasGraphqlAdapter({ executeQuery })` | Canvas GraphQL. The host injects `executeQuery` |
56
+ | `createInMemoryAdapter({ fixture })` | Tests, stories, and the demo app |
57
+
58
+ `executeQuery` is a constructor argument, not a context read, so each host supplies its own
59
+ endpoint and auth for the same documents.
60
+
61
+ ## Development
62
+
63
+ ```bash
64
+ pnpm --filter @instructure/platform-modules test
65
+ pnpm --filter @instructure/platform-modules type-check
66
+ pnpm --filter @instructure/platform-modules build
67
+ pnpm storybook
68
+ ```
69
+
70
+ ## Documentation
71
+
72
+ - `CLAUDE.md` describes what is true in `src/` right now.
73
+ - `docs/adr/` holds the decision records. They are append-only: supersede a record, never edit it.
74
+ - Reasoning local to one file stays in a doc comment there. Start with `adapters/types.ts`,
75
+ `adapters/limits.ts`, `hooks/constants.ts`, and `cache/invalidate.ts`.
@@ -0,0 +1,54 @@
1
+ import { ModulesDataAdapter } from '../types';
2
+ import { UnsupportedModuleItem } from './normalize';
3
+ /**
4
+ * The Canvas GraphQL implementation of `ModulesDataAdapter`.
5
+ *
6
+ * `executeQuery` is injected rather than pulled from `usePlatformUi()`. Two
7
+ * reasons, both load-bearing: the adapter stays a plain function that a test
8
+ * can drive with `vi.fn()` and no React, and it stays host-agnostic — Canvas
9
+ * Career supplies a different `executeQuery` (different endpoint, different
10
+ * auth) for the identical documents.
11
+ */
12
+ export type ExecuteQuery = <TResult, TVariables>(query: unknown, variables: TVariables) => Promise<TResult>;
13
+ export { INLINE_ITEM_LIMIT, INLINE_QUERY_NODE_BUDGET } from '../limits';
14
+ export interface CanvasGraphqlAdapterOptions {
15
+ executeQuery: ExecuteQuery;
16
+ /** @see INLINE_ITEM_LIMIT */
17
+ inlineItemLimit?: number;
18
+ /** @see INLINE_QUERY_NODE_BUDGET */
19
+ nodeBudget?: number;
20
+ /**
21
+ * Notified when an item is dropped for having a content type outside the
22
+ * closed domain enum. Unset by default: dropping is the deliberate policy,
23
+ * this is the seam for making it visible.
24
+ */
25
+ onUnsupportedItem?: (item: UnsupportedModuleItem) => void;
26
+ /**
27
+ * The student whose submissions to select, when there is one.
28
+ *
29
+ * Omit for a teacher, an admin, an observer, or any viewer without a single
30
+ * subject. Omitting it is *safe by construction*: the submission field set is
31
+ * then not requested at all, so `ModuleItem.status` comes back undefined —
32
+ * already the contract's "not fetched" — rather than filled with somebody
33
+ * else's data.
34
+ *
35
+ * Supplying it is what makes per-item status available, and it must be the
36
+ * viewer's own id. Canvas's `submissionsConnection` returns *every* visible
37
+ * student's submissions to a viewer holding `manage_grades` or
38
+ * `view_all_grades`, ordered by `user_id`. @see the `SUBMISSION_FIELDS`
39
+ * docstring in `./queries`.
40
+ */
41
+ studentId?: string | null;
42
+ /**
43
+ * Validate normalised output against the Zod schemas before returning.
44
+ *
45
+ * On by default. This is the check that keeps the GraphQL and REST adapters
46
+ * from drifting, and it is the only place the contract is enforced at
47
+ * runtime — turn it off only if a profile says it costs something real.
48
+ */
49
+ validate?: boolean;
50
+ }
51
+ export declare function createCanvasGraphqlAdapter({ executeQuery, inlineItemLimit, nodeBudget, onUnsupportedItem, studentId, validate, }: CanvasGraphqlAdapterOptions): ModulesDataAdapter;
52
+ export { GET_MODULE_ITEMS, GET_MODULES, type GetModuleItemsVariables, type GetModulesVariables, } from './queries';
53
+ export { buildStudentContext, deriveModuleItemStatus, deriveModuleProgress, deriveModuleStatus, firstSubmission, type GetModuleItemsResult, type GetModulesResult, type GraphqlCompletionRequirement, type GraphqlModuleConnection, type GraphqlModuleItemConnection, type GraphqlModuleItemContent, type GraphqlModuleItemNode, type GraphqlModuleNode, type GraphqlModuleProgression, type GraphqlModuleStatistics, type GraphqlPageInfo, type GraphqlRequirement, type GraphqlSubmission, type GraphqlSubmissionConnection, type ModuleStudentContext, normalizeItemType, normalizeModule, normalizeModuleItem, normalizeModuleItemsPage, normalizeModulesPage, normalizePageInfo, type NormalizeOptions, PartialCanvasResponseError, type PartialResponseSubject, parseDurationMinutes, type UnsupportedModuleItem, } from './normalize';
54
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/adapters/canvasGraphql/index.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAwC,kBAAkB,EAAE,MAAM,UAAU,CAAA;AACxF,OAAO,EAKL,KAAK,qBAAqB,EAC3B,MAAM,aAAa,CAAA;AAQpB;;;;;;;;GAQG;AAEH,MAAM,MAAM,YAAY,GAAG,CAAC,OAAO,EAAE,UAAU,EAC7C,KAAK,EAAE,OAAO,EACd,SAAS,EAAE,UAAU,KAClB,OAAO,CAAC,OAAO,CAAC,CAAA;AAUrB,OAAO,EAAE,iBAAiB,EAAE,wBAAwB,EAAE,MAAM,WAAW,CAAA;AAEvE,MAAM,WAAW,2BAA2B;IAC1C,YAAY,EAAE,YAAY,CAAA;IAC1B,6BAA6B;IAC7B,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,oCAAoC;IACpC,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB;;;;OAIG;IACH,iBAAiB,CAAC,EAAE,CAAC,IAAI,EAAE,qBAAqB,KAAK,IAAI,CAAA;IACzD;;;;;;;;;;;;;;OAcG;IACH,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACzB;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB;AA6BD,wBAAgB,0BAA0B,CAAC,EACzC,YAAY,EACZ,eAAmC,EACnC,UAAqC,EACrC,iBAAiB,EACjB,SAAS,EACT,QAAe,GAChB,EAAE,2BAA2B,GAAG,kBAAkB,CAgFlD;AAED,OAAO,EACL,gBAAgB,EAChB,WAAW,EACX,KAAK,uBAAuB,EAC5B,KAAK,mBAAmB,GACzB,MAAM,WAAW,CAAA;AAClB,OAAO,EACL,mBAAmB,EACnB,sBAAsB,EACtB,oBAAoB,EACpB,kBAAkB,EAClB,eAAe,EACf,KAAK,oBAAoB,EACzB,KAAK,gBAAgB,EACrB,KAAK,4BAA4B,EACjC,KAAK,uBAAuB,EAC5B,KAAK,2BAA2B,EAChC,KAAK,wBAAwB,EAC7B,KAAK,qBAAqB,EAC1B,KAAK,iBAAiB,EACtB,KAAK,wBAAwB,EAC7B,KAAK,uBAAuB,EAC5B,KAAK,eAAe,EACpB,KAAK,kBAAkB,EACvB,KAAK,iBAAiB,EACtB,KAAK,2BAA2B,EAChC,KAAK,oBAAoB,EACzB,iBAAiB,EACjB,eAAe,EACf,mBAAmB,EACnB,wBAAwB,EACxB,oBAAoB,EACpB,iBAAiB,EACjB,KAAK,gBAAgB,EACrB,0BAA0B,EAC1B,KAAK,sBAAsB,EAC3B,oBAAoB,EACpB,KAAK,qBAAqB,GAC3B,MAAM,aAAa,CAAA"}
@@ -0,0 +1,345 @@
1
+ import { CompletionRequirement, CompletionRequirementType, Module, ModuleItem, ModuleItemsPage, ModuleItemStatus, ModuleItemType, ModuleProgress, ModulesPage, ModuleStatus, PageInfo } from '../../types/domain';
2
+ /**
3
+ * GraphQL node → domain model.
4
+ *
5
+ * Everything transport-shaped stops here. The three normalisation rules from
6
+ * `types/domain.ts` are enforced in this file and nowhere else:
7
+ *
8
+ * 1. **Ids are strings.** Canvas hands back `ID`, which the JSON transport
9
+ * may render as a number; every id goes through `String()`.
10
+ * 2. **`published` is always populated.** On this transport Canvas's resolver
11
+ * cannot return null for any viewer, so a null is a partial response and
12
+ * throws — see `resolvePublished`, which explains why the REST rule about
13
+ * absence meaning "published" does not apply here.
14
+ * 3. **One closed item enum.** `__typename` is the discriminator, with
15
+ * `Assignment { isNewQuiz: true }` collapsing to `'quiz'`.
16
+ */
17
+ export interface GraphqlPageInfo {
18
+ hasNextPage: boolean;
19
+ endCursor: string | null;
20
+ }
21
+ export interface GraphqlCompletionRequirement {
22
+ id: string | number;
23
+ type: string;
24
+ minScore?: number | null;
25
+ minPercentage?: number | null;
26
+ }
27
+ /** One row of `submissionsConnection`. Present only for a student viewer. */
28
+ export interface GraphqlSubmission {
29
+ _id?: string | number | null;
30
+ /** `SubmissionState`: unsubmitted | submitted | pending_review | graded | ungraded | deleted. */
31
+ state?: string | null;
32
+ /** `SubmissionGradingStatus`: needs_grading | needs_review | graded | excused. */
33
+ gradingStatus?: string | null;
34
+ submittedAt?: string | null;
35
+ attempt?: number | null;
36
+ missing?: boolean | null;
37
+ /** Per-student, override-applied due date, denormalised onto the submission. */
38
+ cachedDueDate?: string | null;
39
+ }
40
+ export interface GraphqlSubmissionConnection {
41
+ nodes?: (GraphqlSubmission | null)[] | null;
42
+ }
43
+ export interface GraphqlModuleItemContent {
44
+ __typename?: string | null;
45
+ isNewQuiz?: boolean | null;
46
+ dueAt?: string | null;
47
+ /** Declared on `ModuleItemInterface`; `null` for content that cannot be graded. */
48
+ pointsPossible?: number | null;
49
+ submissionsConnection?: GraphqlSubmissionConnection | null;
50
+ }
51
+ /** A `Requirement` as it appears in `ModuleProgression.requirementsMet`. */
52
+ export interface GraphqlRequirement {
53
+ id: string | number;
54
+ type: string;
55
+ minScore?: number | null;
56
+ minPercentage?: number | null;
57
+ }
58
+ export interface GraphqlModuleProgression {
59
+ /** locked | unlocked | started | completed. */
60
+ workflowState?: string | null;
61
+ completed?: boolean | null;
62
+ locked?: boolean | null;
63
+ started?: boolean | null;
64
+ requirementsMet?: (GraphqlRequirement | null)[] | null;
65
+ }
66
+ /** `Module.submissionStatistics`, i.e. Canvas's `ModuleStatistics`. */
67
+ export interface GraphqlModuleStatistics {
68
+ latestDueAt?: string | null;
69
+ missingAssignmentCount?: number | null;
70
+ }
71
+ export interface GraphqlModuleItemNode {
72
+ _id: string | number;
73
+ title?: string | null;
74
+ url?: string | null;
75
+ position?: number | null;
76
+ indent?: number | null;
77
+ published?: boolean | null;
78
+ /** ISO 8601 duration, e.g. `PT45M`. Canvas Career only. */
79
+ estimatedDuration?: string | null;
80
+ content?: GraphqlModuleItemContent | null;
81
+ }
82
+ export interface GraphqlModuleItemConnection {
83
+ nodes?: (GraphqlModuleItemNode | null)[] | null;
84
+ pageInfo: GraphqlPageInfo;
85
+ }
86
+ export interface GraphqlModuleNode {
87
+ _id: string | number;
88
+ name?: string | null;
89
+ position?: number | null;
90
+ published?: boolean | null;
91
+ unlockAt?: string | null;
92
+ /**
93
+ * `Int!` in the Canvas schema, so it is optional here only because a partial
94
+ * response can omit it. `normalizeModule` treats that as a hard error rather
95
+ * than a zero — see `PartialCanvasResponseError`.
96
+ */
97
+ moduleItemsTotalCount?: number | null;
98
+ requirementCount?: number | null;
99
+ requireSequentialProgress?: boolean | null;
100
+ prerequisites?: ({
101
+ id: string | number;
102
+ } | null)[] | null;
103
+ completionRequirements?: (GraphqlCompletionRequirement | null)[] | null;
104
+ /** Absent for a teacher/anonymous viewer, and `null` when Canvas has none. */
105
+ progression?: GraphqlModuleProgression | null;
106
+ submissionStatistics?: GraphqlModuleStatistics | null;
107
+ moduleItemsConnection?: GraphqlModuleItemConnection | null;
108
+ }
109
+ export interface GraphqlModuleConnection {
110
+ nodes?: (GraphqlModuleNode | null)[] | null;
111
+ pageInfo: GraphqlPageInfo;
112
+ }
113
+ export interface GetModulesResult {
114
+ legacyNode?: {
115
+ modulesConnection?: GraphqlModuleConnection | null;
116
+ } | null;
117
+ }
118
+ export interface GetModuleItemsResult {
119
+ legacyNode?: {
120
+ completionRequirements?: (GraphqlCompletionRequirement | null)[] | null;
121
+ progression?: GraphqlModuleProgression | null;
122
+ moduleItemsConnection?: GraphqlModuleItemConnection | null;
123
+ } | null;
124
+ }
125
+ /** What a `PartialCanvasResponseError` is about. */
126
+ export type PartialResponseSubject = 'module' | 'moduleItem';
127
+ /**
128
+ * A row arrived without a field this adapter cannot responsibly guess.
129
+ *
130
+ * Thrown rather than defaulted, because in both current cases the
131
+ * sensible-looking default is the dangerous one:
132
+ *
133
+ * - `moduleItemsTotalCount` is `Int!`. Coercing a missing value to `0` makes
134
+ * `resolveItemsMode` see an empty course and answer `inline`, so the page
135
+ * asks Canvas to inline every item it has — the most expensive transport
136
+ * setting, chosen when the response is least trustworthy.
137
+ * - `published` is null when the viewer cannot manage the object, which for
138
+ * such a viewer means "published". For a viewer who *can* manage, Canvas
139
+ * always sends a boolean, so a null contradicts what the host told us and
140
+ * the wrong guess renders an unpublished thing as published — to the one
141
+ * audience that acts on that badge.
142
+ *
143
+ * Failing is recoverable in a way silence is not: the hook surfaces the error,
144
+ * the host can retry, and `useItemsMode` already falls through to `onDemand` on
145
+ * a failed probe rather than deadlocking the page.
146
+ */
147
+ export declare class PartialCanvasResponseError extends Error {
148
+ readonly subject: PartialResponseSubject;
149
+ /** The module or item id, so a host can log which row came back incomplete. */
150
+ readonly id: string;
151
+ /** The field that was missing. */
152
+ readonly field: string;
153
+ constructor(subject: PartialResponseSubject, id: string, field: string);
154
+ }
155
+ /** Reported when a `__typename` has no place in the closed domain enum. */
156
+ export interface UnsupportedModuleItem {
157
+ /** The module item id, so a host can look the row up in Canvas. */
158
+ id: string;
159
+ /** The `content.__typename` we could not map, or `null` when content was absent. */
160
+ typename: string | null;
161
+ }
162
+ export interface NormalizeOptions {
163
+ /**
164
+ * Called for every item dropped because its content type is outside the
165
+ * closed enum (`AiExperience`, `PeerReviewSubAssignment`, or content that
166
+ * was deleted and came back `null`).
167
+ *
168
+ * The alternatives were worse: widening the enum makes the shared contract
169
+ * chase Canvas's release cadence, and throwing takes the whole page down for
170
+ * one unknown row.
171
+ *
172
+ * Dropping is silent data loss, so it must not *also* be unobservable. This
173
+ * defaults to a `console.warn` rather than to nothing — matching
174
+ * `platform-datetime` and ~30 other files in the repo — because "a host can log
175
+ * it" is only true if the host knows to opt in. Pass a no-op to silence it.
176
+ */
177
+ onUnsupportedItem?: (item: UnsupportedModuleItem) => void;
178
+ }
179
+ /**
180
+ * `__typename` (+ `isNewQuiz`) → domain type, or `null` when unmappable.
181
+ *
182
+ * A New Quiz is an `Assignment` with an LTI submission type in Canvas's data
183
+ * model; every UI that shows it calls it a quiz, so rule 3 collapses it here
184
+ * rather than leaving each caller to re-derive it.
185
+ */
186
+ export declare function normalizeItemType(content: GraphqlModuleItemContent | null | undefined): ModuleItemType | null;
187
+ /**
188
+ * ISO 8601 duration → whole minutes.
189
+ *
190
+ * Canvas exposes `estimatedDuration` as an `ISO8601Duration` scalar (`PT45M`)
191
+ * while the domain model carries minutes, because that is the only unit either
192
+ * host's UI renders. Year and month designators are rejected rather than
193
+ * guessed at — they are not calendar-independent, and Canvas never emits them
194
+ * for this field.
195
+ */
196
+ export declare function parseDurationMinutes(value: string | null | undefined): number | null;
197
+ /**
198
+ * Wire string → closed enum, with anything unrecognised becoming `'unknown'`.
199
+ *
200
+ * Unlike an unmappable item `__typename`, an unknown requirement type is not
201
+ * dropped: the requirement still exists, the student still has to satisfy it,
202
+ * and hiding the row would be a worse lie than labelling it vaguely.
203
+ */
204
+ export declare function normalizeCompletionRequirementType(type: string): CompletionRequirementType;
205
+ export declare function normalizePageInfo(pageInfo: GraphqlPageInfo | null | undefined): PageInfo;
206
+ /**
207
+ * The module-level student state every item in that module is read against.
208
+ *
209
+ * Built once per module and threaded into the item normaliser so the inline and
210
+ * on-demand paths derive the identical status for the identical item — which is
211
+ * why `GET_MODULE_ITEMS` re-reads `progression` alongside its items.
212
+ */
213
+ export interface ModuleStudentContext {
214
+ /** Canvas reports the module locked for this student. */
215
+ locked: boolean;
216
+ /** Raw `ModuleProgression.workflowState`: locked | unlocked | started | completed. */
217
+ workflowState: string | null;
218
+ completed: boolean;
219
+ started: boolean;
220
+ /**
221
+ * Module *item* ids whose completion requirement the student has satisfied.
222
+ *
223
+ * `ModuleProgression.requirementsMet` is a list of `Requirement`, and
224
+ * `Requirement.id` is the module item id — the same join `completionRequirements`
225
+ * uses. The domain model wants a count, so the list collapses here.
226
+ */
227
+ requirementsMetItemIds: ReadonlySet<string>;
228
+ }
229
+ /**
230
+ * The student's submission for an item, or `null`.
231
+ *
232
+ * Canvas returns a connection because the field is shared with the teacher view
233
+ * (where it is every student's submission); for a student it holds at most one
234
+ * row, so the first non-null node is the answer.
235
+ */
236
+ export declare function firstSubmission(content: GraphqlModuleItemContent | null | undefined): GraphqlSubmission | null;
237
+ /**
238
+ * `progression` → the per-module student context, or `undefined` when there is
239
+ * no student. This is the gate described at the top of this block.
240
+ *
241
+ * `requirementsMet` is filtered against the requirements the module currently
242
+ * defines, matching Canvas's own `filterRequirementsMet`: a progression can
243
+ * outlive the requirement that produced it, and counting a stale entry would
244
+ * show a student 4 of 3. Canvas matches on id, type, `minScore` *and*
245
+ * `minPercentage`; we match on id and type only, because the score bounds are
246
+ * the part a teacher edits in place and an edited bound should not silently
247
+ * un-meet a requirement the student genuinely satisfied.
248
+ */
249
+ export declare function buildStudentContext(progression: GraphqlModuleProgression | null | undefined, completionRequirements: readonly CompletionRequirement[]): ModuleStudentContext | undefined;
250
+ /**
251
+ * Per-item status. First rule that matches wins:
252
+ *
253
+ * 1. `locked` — the module is locked for this student. Canvas exposes no
254
+ * per-item lock in this field set (`masteryPaths.locked` is
255
+ * a host-specific affordance we deliberately do not fetch),
256
+ * and a locked module is locked for every item in it.
257
+ * 2. `missing` — `submission.missing`. Ahead of `graded` on purpose: this
258
+ * is the precedence Canvas's own student pill uses, and a
259
+ * teacher entering a zero on a missing submission should
260
+ * not make the "Missing" chip stop finding it.
261
+ * 3. `graded` — `state === 'graded'`, or `gradingStatus` is `graded`/
262
+ * `excused`. Excused counts as graded because there is
263
+ * nothing left for the student to do.
264
+ * 4. `submitted` — handed in and awaiting a grade: `submittedAt` is set, or
265
+ * the state is `submitted`/`pending_review`.
266
+ * 5. `in_progress` — the student has engaged but produced nothing gradable:
267
+ * either an attempt is open (`attempt > 0` with no
268
+ * `submittedAt`), or the item's completion requirement is
269
+ * already met with no submission at all — a viewed page, a
270
+ * followed link, a contribution to an ungraded discussion.
271
+ * 6. `not_started` — everything else.
272
+ *
273
+ * Rule 5 is the one to argue with. `ModuleItemStatus` has no "complete" member,
274
+ * so a satisfied `must_view` requirement has nowhere else to land; the domain
275
+ * comment already flags the same gap at module level ("All items complete" is
276
+ * not in the designer's list). If the designer adds one, this is the rule to
277
+ * change and the only one.
278
+ */
279
+ export declare function deriveModuleItemStatus(content: GraphqlModuleItemContent | null | undefined, itemId: string, student: ModuleStudentContext): ModuleItemStatus;
280
+ /**
281
+ * Per-module rollup. First rule that matches wins:
282
+ *
283
+ * 1. `locked` — `progression.locked` (or `workflowState === 'locked'`).
284
+ * 2. `missing` — `submissionStatistics.missingAssignmentCount > 0` and
285
+ * the module is not completed. That is the design's
286
+ * "1+ item past due", read from the module-level
287
+ * statistic rather than from items, so the filter row
288
+ * works with nothing expanded.
289
+ * 3. `all_graded` — completed, *and* items were fetched, *and* every
290
+ * gradable one is `graded`.
291
+ * 4. `all_submitted` — completed. The honest floor: `progression.completed`
292
+ * means every requirement is met, which is submission,
293
+ * not grading. See the caveat below.
294
+ * 5. `in_progress` — started, or at least one requirement met.
295
+ * 6. `not_started` — everything else.
296
+ *
297
+ * Caveat on rule 3: Canvas has no module-level "everything is graded" signal —
298
+ * `ModuleStatistics` is only `latestDueAt` + `missingAssignmentCount`. So
299
+ * `all_graded` is reachable **only in inline mode**, where the items ride along
300
+ * with the module in the same response. Upgrading on the evidence we have beats
301
+ * claiming a grade we cannot see.
302
+ *
303
+ * In `onDemand` mode a completed module reports `all_submitted` and stays there,
304
+ * even after its items are fetched and every one of them is `graded`. That is a
305
+ * real limitation, not a temporary state: `progress` is derived once, from the
306
+ * list response, and nothing merges items back into it —
307
+ * `normalizeModuleItemsPage` returns `{ items, pageInfo }` and `useModulesPageData`
308
+ * never revisits the module. No upgrade to `all_graded` ever happens.
309
+ *
310
+ * Closing it means either recomputing the module's status in the UI from the items
311
+ * it holds — which reintroduces the inline/onDemand asymmetry the module-level
312
+ * design exists to avoid — or Canvas exposing the signal. Left open deliberately
313
+ * rather than papered over.
314
+ */
315
+ export declare function deriveModuleStatus(student: ModuleStudentContext, missingCount: number, requirementsMet: number, items: ModuleItem[] | undefined): ModuleStatus;
316
+ /**
317
+ * The module's `ModuleProgress`, or `undefined` when there is no student.
318
+ *
319
+ * `requirementCount` is the module's own when Canvas set one and the number of
320
+ * completion requirements otherwise — Canvas uses `requirement_count` for
321
+ * "complete any N of these", so it is the denominator whenever it is present,
322
+ * and the full list is the denominator when it is not. `requirementsMet` is
323
+ * clamped to it so the pair can never read "3 of 1", and `percentComplete` is
324
+ * derived from that same clamped pair rather than computed independently, so a
325
+ * host can never render a bar that disagrees with the fraction beside it.
326
+ */
327
+ export declare function deriveModuleProgress(node: GraphqlModuleNode, completionRequirements: readonly CompletionRequirement[], student: ModuleStudentContext | undefined, items: ModuleItem[] | undefined): ModuleProgress | undefined;
328
+ /**
329
+ * A single item, or `null` when its content type is outside the closed enum.
330
+ *
331
+ * `dueAt` prefers `submission.cachedDueDate` — the per-student, override-applied
332
+ * date Canvas denormalises onto the submission row — and falls back to
333
+ * `content.dueAt`. That ordering is what closes the gap a live-Canvas probe
334
+ * found: `dueAt` exists on `Assignment` only, `Quiz` has none, and a graded
335
+ * `Discussion` keeps its date on the backing assignment, but all three carry a
336
+ * submission. The fallback still matters, because a teacher viewer gets an
337
+ * assignment's `dueAt` and no submission at all.
338
+ *
339
+ * `status` is present only when `student` is — see the student-state block.
340
+ */
341
+ export declare function normalizeModuleItem(node: GraphqlModuleItemNode, moduleId: string, completionRequirementsByItemId: ReadonlyMap<string, CompletionRequirement>, options?: NormalizeOptions, student?: ModuleStudentContext): ModuleItem | null;
342
+ export declare function normalizeModule(node: GraphqlModuleNode, options?: NormalizeOptions): Module;
343
+ export declare function normalizeModulesPage(result: GetModulesResult | null | undefined, options?: NormalizeOptions): ModulesPage;
344
+ export declare function normalizeModuleItemsPage(result: GetModuleItemsResult | null | undefined, moduleId: string, options?: NormalizeOptions): ModuleItemsPage;
345
+ //# sourceMappingURL=normalize.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"normalize.d.ts","sourceRoot":"","sources":["../../../src/adapters/canvasGraphql/normalize.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,69 @@
1
+ import { DocumentNode } from 'graphql';
2
+ /**
3
+ * Module list, optionally with each module's items inlined.
4
+ *
5
+ * `includeItems` is expressed as an `@include` directive on the nested
6
+ * `moduleItemsConnection` rather than as a second document. Reasoning:
7
+ *
8
+ * 1. One document means one operation name, which means one persisted-query
9
+ * hash to register with Canvas. Two documents would double that
10
+ * registration surface for what is a single conceptual read.
11
+ * 2. The directive is evaluated server-side, so `includeItems: false` skips
12
+ * resolving every per-item field — the title, content type, points and
13
+ * submissions for each row.
14
+ *
15
+ * What it does *not* skip is the
16
+ * `ModuleItemsVisibleLoader`. `moduleItemsTotalCount` and
17
+ * `completionRequirements` are selected unconditionally and
18
+ * `module_type.rb:137-141,166-169` resolves both through that same loader,
19
+ * so it runs whatever `includeItems` is. The saving is per-item field
20
+ * resolution, not the visibility query.
21
+ * 3. The response type is a single shape with an optional key, which is
22
+ * exactly how the domain model already treats it (`items?: ModuleItem[]`,
23
+ * where `undefined` means "not fetched"). A second document would force
24
+ * the normaliser to branch on which document produced the payload.
25
+ *
26
+ * `itemsFirst` bounds the inline item fetch. When a module has more items than
27
+ * that, the normaliser drops the partial list rather than reporting a
28
+ * truncated one — see `normalize.ts`.
29
+ */
30
+ export declare const GET_MODULES: DocumentNode;
31
+ /**
32
+ * A single module's items, paged on demand.
33
+ *
34
+ * `completionRequirements` is re-read at the module level here because a
35
+ * requirement is stored on the module, keyed by item id — without it, items
36
+ * fetched on demand would lose the requirement that items fetched inline keep,
37
+ * and the two paths would produce different domain objects for the same item.
38
+ *
39
+ * `progression` is re-read for the same reason: `ModuleItem.status` depends on
40
+ * module-level state (is the module locked for me, which of its requirements
41
+ * have I met), so an item fetched on demand would otherwise carry a different
42
+ * status from the identical item fetched inline.
43
+ */
44
+ export declare const GET_MODULE_ITEMS: DocumentNode;
45
+ export interface GetModulesVariables {
46
+ courseId: string;
47
+ first: number;
48
+ after?: string | null;
49
+ includeItems: boolean;
50
+ itemsFirst: number;
51
+ /**
52
+ * Whether to select per-item submissions at all.
53
+ *
54
+ * False unless a `studentId` is known. Canvas's `submissionsConnection` is
55
+ * *not* self-scoping for a privileged viewer — see the fragment docstring — so
56
+ * the safe default is to ask for nothing.
57
+ */
58
+ includeSubmissions: boolean;
59
+ /** The student whose submissions to select. Required whenever `includeSubmissions`. */
60
+ studentId?: string | null;
61
+ }
62
+ export interface GetModuleItemsVariables {
63
+ moduleId: string;
64
+ includeSubmissions: boolean;
65
+ studentId?: string | null;
66
+ first: number;
67
+ after?: string | null;
68
+ }
69
+ //# sourceMappingURL=queries.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"queries.d.ts","sourceRoot":"","sources":["../../../src/adapters/canvasGraphql/queries.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AAiL3C;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,eAAO,MAAM,WAAW,EAAE,YA0DzB,CAAA;AAED;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,gBAAgB,EAAE,YAiC9B,CAAA;AAED,MAAM,WAAW,mBAAmB;IAClC,QAAQ,EAAE,MAAM,CAAA;IAChB,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACrB,YAAY,EAAE,OAAO,CAAA;IACrB,UAAU,EAAE,MAAM,CAAA;IAClB;;;;;;OAMG;IACH,kBAAkB,EAAE,OAAO,CAAA;IAC3B,uFAAuF;IACvF,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAC1B;AAED,MAAM,WAAW,uBAAuB;IACtC,QAAQ,EAAE,MAAM,CAAA;IAChB,kBAAkB,EAAE,OAAO,CAAA;IAC3B,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACzB,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CACtB"}
@@ -0,0 +1,63 @@
1
+ import { Module, ModuleItem } from '../types/domain';
2
+ import { ModulesDataAdapter } from './types';
3
+ /**
4
+ * An adapter backed by a plain object instead of a network.
5
+ *
6
+ * It exists so the hooks can be built and tested without waiting on a real
7
+ * adapter, but it is not scaffolding — it stays as the test double for hook
8
+ * tests and as the fastest way to render a modules UI in Storybook with no
9
+ * server at all.
10
+ *
11
+ * It is a real implementation of the contract, deliberately: it paginates with
12
+ * opaque cursors, honours `AbortSignal`, and declines to inline items for modules
13
+ * over `INLINE_ITEM_LIMIT` — so hooks written against it meet the same edges
14
+ * Canvas will hand them, including the one Canvas Career gets wrong.
15
+ */
16
+ export { INLINE_ITEM_LIMIT } from './limits';
17
+ export interface InMemoryFixture {
18
+ /**
19
+ * The course these modules belong to.
20
+ *
21
+ * Present so the adapter can *check* it. Without it, `courseId` was
22
+ * destructured and dropped, which meant one adapter instance served identical
23
+ * modules for every course id in existence — and since `cache/keys.ts` keys the
24
+ * query cache by `courseId`, the entire class of bug where it is mis-threaded,
25
+ * defaulted or collided across courses was unfalsifiable against the package's
26
+ * primary test double. A hook that hardcoded `courseId: '1'` internally would
27
+ * have passed every test and every story.
28
+ */
29
+ courseId: string;
30
+ modules: Module[];
31
+ itemsByModuleId: Record<string, ModuleItem[]>;
32
+ }
33
+ /** What a served request was for. Handed to `onRequest`. */
34
+ export interface InMemoryRequest {
35
+ operation: 'listModules' | 'listModuleItems';
36
+ courseId: string;
37
+ /** Only set for `listModuleItems`. */
38
+ moduleId?: string;
39
+ }
40
+ export interface InMemoryAdapterOptions {
41
+ /**
42
+ * Invoked once per served request. Test hook for asserting exact request
43
+ * counts, which is how the scaling behaviour is locked in.
44
+ *
45
+ * Receives the operation name for the common case, and the full request for
46
+ * tests that care *which* course or module was asked for.
47
+ */
48
+ onRequest?: (operation: 'listModules' | 'listModuleItems', request: InMemoryRequest) => void;
49
+ }
50
+ /**
51
+ * Thrown when a caller asks this adapter for a course it was not built for.
52
+ *
53
+ * A real transport would return that other course's data, or 404. Silently
54
+ * serving the fixture regardless is the one behaviour that could not happen in
55
+ * production, so it is the one behaviour the test double must not have.
56
+ */
57
+ export declare class InMemoryCourseMismatchError extends Error {
58
+ readonly expected: string;
59
+ readonly received: string;
60
+ constructor(expected: string, received: string);
61
+ }
62
+ export declare function createInMemoryAdapter(fixture: InMemoryFixture, options?: InMemoryAdapterOptions): ModulesDataAdapter;
63
+ //# sourceMappingURL=inMemory.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"inMemory.d.ts","sourceRoot":"","sources":["../../src/adapters/inMemory.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,UAAU,EAAgC,MAAM,iBAAiB,CAAA;AAEvF,OAAO,KAAK,EAAwC,kBAAkB,EAAE,MAAM,SAAS,CAAA;AAEvF;;;;;;;;;;;;GAYG;AAIH,OAAO,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAA;AAE5C,MAAM,WAAW,eAAe;IAC9B;;;;;;;;;;OAUG;IACH,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE,MAAM,EAAE,CAAA;IACjB,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,EAAE,CAAC,CAAA;CAC9C;AAED,4DAA4D;AAC5D,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,aAAa,GAAG,iBAAiB,CAAA;IAC5C,QAAQ,EAAE,MAAM,CAAA;IAChB,sCAAsC;IACtC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB;AAED,MAAM,WAAW,sBAAsB;IACrC;;;;;;OAMG;IACH,SAAS,CAAC,EAAE,CAAC,SAAS,EAAE,aAAa,GAAG,iBAAiB,EAAE,OAAO,EAAE,eAAe,KAAK,IAAI,CAAA;CAC7F;AAED;;;;;;GAMG;AACH,qBAAa,2BAA4B,SAAQ,KAAK;IACpD,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;gBAEb,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM;CAS/C;AA4BD,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,eAAe,EACxB,OAAO,GAAE,sBAA2B,GACnC,kBAAkB,CAiEpB"}
@@ -0,0 +1,3 @@
1
+ export type { ListModuleItemsArgs, ListModulesArgs, ModulesDataAdapter } from './types';
2
+ export { createInMemoryAdapter, InMemoryCourseMismatchError, INLINE_ITEM_LIMIT, type InMemoryAdapterOptions, type InMemoryFixture, type InMemoryRequest, } from './inMemory';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/adapters/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,mBAAmB,EAAE,eAAe,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAA;AACvF,OAAO,EACL,qBAAqB,EACrB,2BAA2B,EAC3B,iBAAiB,EACjB,KAAK,sBAAsB,EAC3B,KAAK,eAAe,EACpB,KAAK,eAAe,GACrB,MAAM,YAAY,CAAA"}