diff --git a/src/api/OfflineApi/index.ts b/src/api/OfflineApi/index.ts index 1c743fe54..4d4aad345 100644 --- a/src/api/OfflineApi/index.ts +++ b/src/api/OfflineApi/index.ts @@ -159,14 +159,56 @@ export default class OfflineApi { // Method to handle POST requests async post(url: string, data: T, ...config: any): AxiosPromise { - const { model, id, query } = urlParts(url); + const { model, id, query, parts } = urlParts(url); log('post', url, data); switch (model) { case 'admin': return resolveResponse(data as unknown as T); case 'cms': return resolveResponse({} as unknown as T); + case 'users': + if (parts.length === 1 && parts[0] === 'documentRoots') { + const { documentRootIds, type } = data as { + documentRootIds: string[]; + type?: DocumentType; + }; + if (documentRootIds.length === 0) { + resolveResponse([] as unknown as T); + } + const documentRootDocs = await Promise.all( + documentRootIds.map((id) => this.documentsBy(id)) + ); + const filteredDocs = type + ? documentRootDocs.map((docs) => docs.filter((doc) => doc.type === type)) + : documentRootDocs; + + const documenRoots = documentRootIds.map((rid) => { + return { + id: rid, + access: Access.RW_DocumentRoot, + sharedAccess: Access.RW_DocumentRoot, + userPermissions: [], + groupPermissions: [], + documents: + filteredDocs.find( + (docs) => docs.length > 0 && docs[0].documentRootId === rid + ) || [] + }; + }) as unknown as T; + log('-> post', url, documenRoots); + return resolveResponse(documenRoots); + } + return resolveResponse([] as unknown as T); case 'documents': + if (url === 'documents/multiple') { + const documentRootIds = new Set((data as { documentRootIds: string[] }).documentRootIds); + const allDocuments = await this.dbAdapter.getAll>(DOCUMENTS_STORE); + + const filteredDocuments = allDocuments.filter((doc) => + documentRootIds.has(doc.documentRootId) + ); + return resolveResponse(filteredDocuments as unknown as T); + } const document = await this.upsertDocumentRecord( data as Partial>, query.has('uniqueMain') @@ -214,35 +256,6 @@ export default class OfflineApi { switch (model) { case 'user': return resolveResponse(OfflineUser as unknown as T); - case 'users': - if (parts.length === 1 && parts[0] === 'documentRoots') { - const ids = query.getAll('ids'); - const docType = query.get('type') as DocumentType | null; - if (ids.length === 0) { - resolveResponse([] as unknown as T); - } - const documentRootDocs = await Promise.all(ids.map((id) => this.documentsBy(id))); - const filteredDocs = docType - ? documentRootDocs.map((docs) => docs.filter((doc) => doc.type === docType)) - : documentRootDocs; - - const documenRoots = ids.map((rid) => { - return { - id: rid, - access: Access.RW_DocumentRoot, - sharedAccess: Access.RW_DocumentRoot, - userPermissions: [], - groupPermissions: [], - documents: - filteredDocs.find( - (docs) => docs.length > 0 && docs[0].documentRootId === rid - ) || [] - }; - }) as unknown as T; - log('-> get', url, documenRoots); - return resolveResponse(documenRoots); - } - return resolveResponse([OfflineUser] as unknown as T); case 'admin': return resolveResponse([] as unknown as T); case 'allowedActions': @@ -255,6 +268,7 @@ export default class OfflineApi { } return resolveResponse(null); } + // TODO: is this needed/used at all? if (query.has('ids')) { const ids = query.getAll('ids'); const filteredDocuments: Document[] = []; @@ -266,19 +280,12 @@ export default class OfflineApi { } return resolveResponse(filteredDocuments as unknown as T); } - if (query.has('rids')) { - const rids = query.getAll('rids'); - - const allDocuments = await this.dbAdapter.getAll>(DOCUMENTS_STORE); - - const filteredDocuments = allDocuments.filter((doc) => rids.includes(doc.documentRootId)); - - return resolveResponse(filteredDocuments as unknown as T); - } return resolveResponse( (await this.dbAdapter.getAll>(DOCUMENTS_STORE)) as unknown as T ); + case 'users': + return resolveResponse([OfflineUser] as unknown as T); case 'documentRoots': if (parts[0] === 'permissions') { return resolveResponse({ diff --git a/src/api/document.ts b/src/api/document.ts index 5172792f6..51f6c5abc 100644 --- a/src/api/document.ts +++ b/src/api/document.ts @@ -249,9 +249,7 @@ export function update( * TODO: would it be better to only grab documents from a specific student group? */ export function allDocuments(documentRootIds: string[], signal: AbortSignal): AxiosPromise[]> { - return api.get(`/documents?${documentRootIds.map((id) => `rids=${id}`).join('&')}`, { - signal - }); + return api.post('/documents/multiple', { documentRootIds }, { signal }); } export function linkTo( diff --git a/src/api/documentRoot.ts b/src/api/documentRoot.ts index b8710ab64..887adb2c7 100644 --- a/src/api/documentRoot.ts +++ b/src/api/documentRoot.ts @@ -58,7 +58,20 @@ export function findManyFor( if (documentType) { params.append('type', documentType); } - return api.get(`/users/${userId}/documentRoots?${params.toString()}`, { signal }); + const data: { + documentRootIds: string[]; + ignoreMissingRoots?: boolean; + type?: string; + } = { + documentRootIds: ids + }; + if (ignoreMissingRoots) { + data.ignoreMissingRoots = true; + } + if (documentType) { + data.type = documentType; + } + return api.post(`/users/${userId}/documentRoots`, data, { signal }); } export function create( diff --git a/src/models/Page.ts b/src/models/Page.ts index c72527a3d..f25f22164 100644 --- a/src/models/Page.ts +++ b/src/models/Page.ts @@ -113,11 +113,20 @@ export default class Page { .sort((a, b) => a!.root!.meta!.pagePosition - b!.root!.meta.pagePosition); } + @computed + get primaryStudentGroupName() { + return this._primaryStudentGroupName ?? this.store.currentStudentGroupName; + } + @action setPrimaryStudentGroupName(name?: string) { this._primaryStudentGroupName = name; } + get hasCustomPrimaryStudentGroup() { + return !!this._primaryStudentGroupName; + } + @computed get primaryStudentGroup() { return this._primaryStudentGroupName diff --git a/src/stores/DocumentRootStore.ts b/src/stores/DocumentRootStore.ts index 1c5a38daa..db56726b8 100644 --- a/src/stores/DocumentRootStore.ts +++ b/src/stores/DocumentRootStore.ts @@ -161,18 +161,12 @@ export class DocumentRootStore extends iStore { access: accessConfig || {} }); this.loadQueued(); - if (this.queued.size > 42) { - // max 2048 characters in URL - flush if too many - this.loadQueued.flush(); - } } /** * load the documentRoots only * - after 20 ms of "silence" (=no further load-requests during this period) * - or after 25ms have elapsed - * - or when more then 42 records are queued (@see loadInNextBatch) - * (otherwise the URL maxlength would be reached) */ loadQueued = _.debounce(action(this._loadQueued), 25, { leading: false, @@ -192,12 +186,6 @@ export class DocumentRootStore extends iStore { } const batch = [...this.queued]; this.queued.clear(); - if (batch.length > 42) { - const postponed = batch.splice(42); - postponed.forEach((item) => this.queued.set(item[0], item[1])); - this.loadQueued(); - console.log('Postponing', postponed.length, 'document roots for next batch'); - } const currentBatch = new Map(batch); /** * if the user is not logged in, we can't load the documents diff --git a/src/stores/PageStore.ts b/src/stores/PageStore.ts index 18e50e2ab..32f706d59 100644 --- a/src/stores/PageStore.ts +++ b/src/stores/PageStore.ts @@ -12,6 +12,10 @@ import globalData from '@generated/globalData'; const ensureTrailingSlash = (str: string) => { return str.endsWith('/') ? str : `${str}/`; }; +const ensureLeadingSlash = (str: string) => { + return str.startsWith('/') ? str : `/${str}`; +}; +const BasePathRegex = new RegExp(`^${siteConfig.baseUrl}`, 'i'); export const AUTO_GENERATED_PAGE_PREFIX = '__auto_generated__'; export const SidebarVersions = ( globalData['docusaurus-plugin-content-docs'].default as GlobalPluginData @@ -43,12 +47,14 @@ interface PagesIndex { export class PageStore extends iStore { readonly root: RootStore; - pages = observable([]); + pages = observable([], { deep: false }); @observable accessor currentPageId: string | undefined = undefined; @observable accessor runningTurtleScriptId: string | undefined = undefined; @observable.ref accessor _pageIndex: PageIndex[] = []; + @observable accessor currentPath: string | undefined = undefined; + loadedPageIndices = new Set(); constructor(store: RootStore) { super(); @@ -59,14 +65,24 @@ export class PageStore extends iStore { return SidebarVersions; } + @computed + get sidebarVersionPaths() { + return SidebarVersions.map((version) => version.versionPath); + } + @computed get landingPages() { return this.pages.filter((page) => page.isLandingpage); } + @computed + get isPageIndexLoaded() { + return this._pageIndex.length > 0; + } + @action loadPageIndex(force: boolean = false) { - if (!force && this._pageIndex.length > 0) { + if (!force && this.isPageIndexLoaded) { return Promise.resolve(); } return fetch(`${siteConfig.baseUrl}tdev-artifacts/page-progress-state/pageIndex.json`) @@ -96,7 +112,9 @@ export class PageStore extends iStore { }) ) .then(() => { - this.loadTaskableDocuments(); + if (this.currentPath) { + this.loadTaskableDocuments(this.currentStudentGroupName); + } }) .catch((err) => { console.error('Failed to load page index', err); @@ -104,15 +122,62 @@ export class PageStore extends iStore { } @action - loadTaskableDocuments() { - this.pages.forEach((page) => { - page.taskableDocumentRootIds.forEach((id) => { - this.root.documentRootStore.loadInNextBatch(id, undefined, { - skipCreate: true, - documentRoot: 'addIfMissing' + setCurrentPath(path: string | undefined) { + if (path === this.currentPath) { + return; + } + if (!path) { + this.currentPath = undefined; + return; + } + this.currentPath = path.replace(BasePathRegex, '/'); + if (this.isPageIndexLoaded) { + this.loadTaskableDocuments(this.currentStudentGroupName); + } + this.resetPagesStudentGroups(); + } + + @action + resetPagesStudentGroups() { + this.pages + .filter((p) => p.hasCustomPrimaryStudentGroup) + .forEach((p) => p.setPrimaryStudentGroupName(undefined)); + } + + @computed + get currentPathParts() { + if (!this.currentPath) { + return []; + } + return this.currentPath.split('/').filter((p) => p.length > 0); + } + + @computed + get currentStudentGroupName() { + return this.currentPathParts[0]; + } + + @action + loadTaskableDocuments(pathPrefix: string | undefined, force?: boolean) { + const prefix = ensureLeadingSlash(ensureTrailingSlash(pathPrefix ?? '')); + const isEntryPoint = this.sidebarVersionPaths.includes(prefix); + if (!isEntryPoint) { + return; + } + if (!force && this.loadedPageIndices.has(prefix)) { + return; + } + this.loadedPageIndices.add(prefix); + this.pages + .filter((p) => p.path.startsWith(prefix) && !p.isAutoGenerated) + .forEach((page) => { + page.taskableDocumentRootIds.forEach((id) => { + this.root.documentRootStore.loadInNextBatch(id, undefined, { + skipCreate: true, + documentRoot: 'addIfMissing' + }); }); }); - }); } find = computedFn( diff --git a/src/stores/UserStore.ts b/src/stores/UserStore.ts index 6d41f3378..34b03788d 100644 --- a/src/stores/UserStore.ts +++ b/src/stores/UserStore.ts @@ -160,7 +160,7 @@ export class UserStore extends iStore<`update-${string}`> { return this.withAbortController(`load-all`, async (ct) => { return apiAll(ct.signal).then( action((res) => { - const models = res.data.map((d) => this.createModel(d)); + const models = res.data?.map((d) => this.createModel(d)); this.users.replace(models); }) ); diff --git a/src/theme/DocItem/Content/index.tsx b/src/theme/DocItem/Content/index.tsx deleted file mode 100644 index b8cb753cb..000000000 --- a/src/theme/DocItem/Content/index.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import React from 'react'; -import Content from '@theme-original/DocItem/Content'; -import type ContentType from '@theme/DocItem/Content'; -import type { WrapperProps } from '@docusaurus/types'; -import { observer } from 'mobx-react-lite'; -import { useStore } from '@tdev-hooks/useStore'; -import { useLocation } from '@docusaurus/router'; -type Props = WrapperProps; - -const ContentWrapper = observer((props: Props): React.ReactNode => { - const pageStore = useStore('pageStore'); - const location = useLocation(); - - React.useEffect(() => { - if (pageStore.current) { - const primaryClass = location.pathname.split('/')[1]; - pageStore.current.setPrimaryStudentGroupName(primaryClass); - } - return () => { - pageStore.current?.setPrimaryStudentGroupName(undefined); - }; - }, [pageStore.current, location.pathname]); - - return ; -}); - -export default ContentWrapper; diff --git a/src/theme/Root.tsx b/src/theme/Root.tsx index 3fba443d5..dd1b10f47 100644 --- a/src/theme/Root.tsx +++ b/src/theme/Root.tsx @@ -6,7 +6,7 @@ import siteConfig from '@generated/docusaurus.config'; import { useStore } from '@tdev-hooks/useStore'; import { reaction } from 'mobx'; import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; -import { useHistory } from '@docusaurus/router'; +import { useHistory, useLocation } from '@docusaurus/router'; import LoggedOutOverlay from '@tdev-components/LoggedOutOverlay'; import { authClient } from '@tdev/auth-client'; import { getOfflineUser } from '@tdev-api/OfflineApi'; @@ -219,6 +219,15 @@ const DevGlobalDataTracker = observer(() => { return null; }); +const VisitedPagesTracker = observer(() => { + const location = useLocation(); + const pageStore = useStore('pageStore'); + React.useEffect(() => { + pageStore.setCurrentPath(location.pathname); + }, [location]); + return null; +}); + function Root({ children }: { children: React.ReactNode }) { const { siteConfig } = useDocusaurusContext(); @@ -246,6 +255,7 @@ function Root({ children }: { children: React.ReactNode }) { {SENTRY_DSN && } + {children}