Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions src/components/documents/Assessable/ChoiceAnswer/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import UnknownDocumentType from '@tdev-components/shared/Alert/UnknownDocumentTy
import Loader from '@tdev-components/Loader';
import useIsBrowser from '@docusaurus/useIsBrowser';
import { useDocumentRootId } from '@tdev-hooks/useContextDocumentRootId';
import { useFirstDocumentBy } from '@tdev-hooks/useFirstDocumentBy';
import { useNestedAssessableDocumentBy } from '@tdev-hooks/useNestedAssessableDocumentBy';
import { DocContext } from '@tdev-components/documents/DocumentContext';
import { AssessableComponentProps } from '@tdev-models/documents/Assessable/AssessableMeta';
import { type default as ChoiceAnswerModel, ModelMeta } from '@tdev-models/documents/Assessable/ChoiceAnswer';
Expand All @@ -18,6 +18,7 @@ interface SharedProps extends AssessableComponentProps<'choice_answer'> {
multiple?: boolean;
randomizeOptions?: boolean;
optionsCount: number;
allowSelection?: boolean;
}

export interface StandaloneProps extends SharedProps {
Expand All @@ -44,7 +45,7 @@ const ChoiceAnswer = observer((props: ChoiceAnswerProps) => {
const [meta] = React.useState(new ModelMeta(props));
const docRootId = useDocumentRootId(props.id);

const doc = useFirstDocumentBy(docRootId, meta, props.qid);
const doc = useNestedAssessableDocumentBy(docRootId, meta, props.qid);
const isBrowser = useIsBrowser();

if (!doc) {
Expand All @@ -56,7 +57,7 @@ const ChoiceAnswer = observer((props: ChoiceAnswerProps) => {
}

return (
<QuestionCard doc={doc}>
<QuestionCard doc={doc} allowSelection={props.allowSelection}>
<DocContext.Provider value={doc}>{props.children}</DocContext.Provider>
</QuestionCard>
);
Expand Down
15 changes: 13 additions & 2 deletions src/components/documents/Assessable/Inputs/Option/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { observer } from 'mobx-react-lite';
import { useDocument } from '@tdev-hooks/useContextDocument';
import Button from '@tdev-components/shared/Button';
import { mdiTrashCanOutline } from '@mdi/js';
import { AssessableType, AssessableTypeModelMapping } from '@tdev-api/document';
import type { AssessableType, AssessableTypeModelMapping } from '@tdev-api/document';

export interface Props<T extends AssessableType> {
type?: T;
Expand All @@ -22,7 +22,18 @@ const Option = observer(<T extends AssessableType>(props: Props<T>) => {
const doc = useDocument<T>();
const optionId = React.useId();
const { children, optionIndex, optionOrder, onChange, isChecked } = props;

if (doc.keepExpanded === 'none') {
return null;
}
if (doc.keepExpanded === 'selected' && !isChecked) {
return null;
}
const correct = doc.linkedMeta?.correct ?? [];
if (doc.keepExpanded === 'correct' && correct.length > 0) {
if (!correct.includes(optionIndex)) {
return null;
}
}
return (
<div
key={optionId}
Expand Down
21 changes: 19 additions & 2 deletions src/components/documents/Assessable/Inputs/Options/index.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,30 @@
import React from 'react';
import clsx from 'clsx';
import styles from '../styles.module.scss';
import { observer } from 'mobx-react-lite';
import { useDocument } from '@tdev-hooks/useContextDocument';
import { AssessableType } from '@tdev-api/document';
import Button from '@tdev-components/shared/Button';
import { mdiCollapseAll, mdiExpandAll } from '@mdi/js';

const Options = ({ children }: { children: React.ReactNode }) => {
const Options = observer(({ children }: { children: React.ReactNode }) => {
const doc = useDocument<AssessableType>();
return (
<div className={clsx(styles.optionsBlock)}>
<div className={styles.optionsContainer}>{children}</div>
{doc.canCollapseOptions && (
<>
<Button
icon={doc.showAllOptions ? mdiCollapseAll : mdiExpandAll}
onClick={() => doc.setShowAllOptions(!doc.showAllOptions)}
className={styles.btnExpandCollapseOptions}
color={doc.showAllOptions ? 'red' : 'primary'}
title={doc.showAllOptions ? 'Alle Optionen einklappen' : 'Alle Optionen ausklappen'}
/>
</>
)}
</div>
);
};
});

export default Options;
12 changes: 12 additions & 0 deletions src/components/documents/Assessable/Inputs/styles.module.scss
Original file line number Diff line number Diff line change
@@ -1,6 +1,18 @@
.optionsBlock {
--tdev-assessable-btn-remove-answer-transition-duration: 0.15s;
position: relative;

.btnExpandCollapseOptions {
position: absolute;
right: 0em;
bottom: 0em;
}
&:not(:hover) {
.btnExpandCollapseOptions {
opacity: 0.25;
transition: opacity var(--tdev-assessable-btn-remove-answer-transition-duration);
}
}
&:not(:last-child) {
margin-bottom: 1em;
}
Expand Down
7 changes: 6 additions & 1 deletion src/components/documents/Assessable/QuestionCard/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import QuestionControls from './Controls';

interface Props<T extends AssessableType> {
doc: TypeModelMapping[T];
allowSelection?: boolean;
children: React.ReactNode;
}

Expand All @@ -19,7 +20,11 @@ const QuestionCard = observer(<T extends AssessableType>(props: Props<T>) => {
return (
<Card
classNames={{
card: clsx(styles.questionCard, styles[doc.correctness]),
card: clsx(
styles.questionCard,
styles[doc.correctness],
props.allowSelection && styles.allowSelection
),
header: clsx(styles.header, styles[doc.correctness])
}}
style={{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
:root {
--tdev-choice-answers-user-selection: none;
}
.questionCard {
position: relative;
margin-bottom: 1em;
--tdev-box-shadow-defaults: 0 0 8px;
// prevent selection of text in the card when clicking on the card to select it
user-select: var(--tdev-choice-answers-user-selection);
&.allowSelection {
--tdev-choice-answers-user-selection: text;
}

p {
margin: 0 0 0.5em 0;
Expand Down
5 changes: 2 additions & 3 deletions src/components/documents/Assessable/Quiz/index.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
import { useFirstMainDocument } from '@tdev-hooks/useFirstMainDocument';
import { observer } from 'mobx-react-lite';
import React from 'react';
import UnknownDocumentType from '@tdev-components/shared/Alert/UnknownDocumentType';
import Loader from '@tdev-components/Loader';
import styles from './styles.module.scss';
import useIsBrowser from '@docusaurus/useIsBrowser';
import { DocumentRootIdContext } from '@tdev-hooks/useContextDocumentRootId';
import { AssessableComponentProps } from '@tdev-models/documents/Assessable/AssessableMeta';
import { ModelMeta } from '@tdev-models/documents/Assessable/Quiz';
Expand All @@ -24,6 +21,7 @@ export interface Props extends AssessableComponentProps<AssessableType> {
randomizeOptions?: boolean;
randomizeQuestions?: boolean;
minPoints?: number;
allowSelection?: boolean;
}

const Quiz = observer((props: Props) => {
Expand All @@ -41,6 +39,7 @@ const Quiz = observer((props: Props) => {
className={clsx(
styles.quiz,
animate && styles.animate,
props.allowSelection && styles.allowSelection,
doc.isAssessed && doc.assessment && styles[doc.assessment?.correctness]
)}
ref={ref}
Expand Down
3 changes: 3 additions & 0 deletions src/components/documents/Assessable/Quiz/styles.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@

.quiz {
--tdev-quiz-flash-color: var(--ifm-color-warning);
&.allowSelection {
--tdev-choice-answers-user-selection: text;
}
&.correct {
--tdev-quiz-flash-color: var(--ifm-color-success);
}
Expand Down
4 changes: 2 additions & 2 deletions src/components/documents/Assessable/TrueFalseAnswer/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
ModelMeta
} from '@tdev-models/documents/Assessable/TrueFalseAnswer';
import { useDocumentRootId } from '@tdev-hooks/useContextDocumentRootId';
import { useFirstDocumentBy } from '@tdev-hooks/useFirstDocumentBy';
import { useNestedAssessableDocumentBy } from '@tdev-hooks/useNestedAssessableDocumentBy';
import UnknownDocumentType from '@tdev-components/shared/Alert/UnknownDocumentType';
import QuestionCard from '../QuestionCard';
import { DocContext } from '@tdev-components/documents/DocumentContext';
Expand Down Expand Up @@ -37,7 +37,7 @@ const onUpdateSelection = action((doc: TrueFalseAnswerModel, optionIndex: number
const TrueFalseAnswer = observer((props: Props) => {
const [meta] = React.useState(new ModelMeta({ ...props }));
const docRootId = useDocumentRootId(props.id);
const doc = useFirstDocumentBy(docRootId, meta, props.qid);
const doc = useNestedAssessableDocumentBy(docRootId, meta, props.qid);

if (!doc) {
return <UnknownDocumentType type={meta.type} />;
Expand Down
2 changes: 1 addition & 1 deletion src/components/documents/CmsText/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,5 @@ export function useFirstCmsTextDocumentIfExists(id?: string): CmsText | undefine

// Not using useFirstMainDocument() here because that would always supply a (dummy) document.
const docRoot = useDocumentRoot(id, meta, false);
return docRoot?.firstMainDocument;
return docRoot?.documentsByType?.get(meta.type)?.[0] as CmsText | undefined;
}
6 changes: 5 additions & 1 deletion src/hooks/useDocumentRoot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,11 @@ export const useDocumentRoot = <Type extends DocumentType>(
return reaction(
() => documentRootStore.find(dummyDocumentRoot.id)?._triggerDocumentReload,
() => {
const firstMainDoc = documentRootStore.find(dummyDocumentRoot.id)?.firstMainDocument;
const docRoot = documentRootStore.find(dummyDocumentRoot.id);
if (!docRoot) {
return;
}
const firstMainDoc = docRoot.documentsByType.get(meta.type)?.[0];
if (firstMainDoc) {
return;
}
Expand Down
5 changes: 2 additions & 3 deletions src/hooks/useFirstMainDocument.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react';
import { DocumentType } from '@tdev-api/document';
import { DocumentType, TypeModelMapping } from '@tdev-api/document';
import { TypeMeta } from '@tdev-models/DocumentRoot';
import { useDocumentRoot } from '@tdev-hooks/useDocumentRoot';
import { useStore } from '@tdev-hooks/useStore';
Expand Down Expand Up @@ -65,6 +65,5 @@ export const useFirstMainDocument = <Type extends DocumentType>(
{ fireImmediately: true }
);
}, [userStore, createDocument, documentRoot]);

return documentRoot?.firstMainDocument || dummyDocument;
return (documentRoot?.documentsByType?.get(meta.type)?.[0] as TypeModelMapping[Type]) || dummyDocument;
};
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,12 @@ import { reaction } from 'mobx';
import { DUMMY_DOCUMENT_ID } from './useFirstMainDocument';
import { AssessableMeta } from '@tdev-models/documents/Assessable/AssessableMeta';
import useLinkedMetaModel from './useLinkedMetaModel';
import _ from 'es-toolkit/compat';

const access = {} as Config;

const requested = new Set<string>();

/**
* This hook provides access to the first main document of the rootDocument.
* This is especially useful, when the DocumentType is expected to have only
Expand All @@ -19,7 +22,7 @@ const access = {} as Config;
* For bridging the time until the first main document is loaded,
* a dummy document is provided in the meantime.
*/
export const useFirstDocumentBy = <Type extends AssessableType>(
export const useNestedAssessableDocumentBy = <Type extends AssessableType>(
documentRootId: string | undefined,
/** ensure to put meta in a React.useState */
meta: AssessableMeta<Type>,
Expand Down Expand Up @@ -54,26 +57,48 @@ export const useFirstDocumentBy = <Type extends AssessableType>(
updatedAt: new Date().toISOString()
}) as AssessableTypeModelMapping[Type]
);
const [canRequest, setCanRequest] = React.useState(false);
React.useEffect(() => {
if (!documentRoot) {
return;
}
const timeoutId = setTimeout(() => {
setCanRequest(true);
}, 5);
return () => {
clearTimeout(timeoutId);
};
}, [documentRoot]);

React.useEffect(() => {
if (!documentRoot || !canRequest) {
return;
}
return reaction(
() => documentRoot?._canInitializeDocuments && !documentRoot.documents.some(selector),
(needsCreation) => {
if (!needsCreation) {
return;
}
documentStore.create({
documentRootId: documentRoot.id,
authorId: userStore.current!.id,
type: meta.type,
data: meta.defaultData
});
const key = `${documentRoot.id}::${userStore.current!.id}::${meta.type}::${qid}`;
if (requested.has(key)) {
return;
}
requested.add(key);
documentStore
.create({
documentRootId: documentRoot.id,
authorId: userStore.current!.id,
type: meta.type,
data: meta.defaultData
})
.then(() => {
requested.delete(key);
});
},
{ fireImmediately: true }
);
}, [userStore, documentRoot]);
}, [userStore, documentRoot, canRequest]);

const firstDoc = documentRoot?.documents.find(selector) as AssessableTypeModelMapping[Type] | undefined;
const doc = firstDoc || dummyDocument;
Expand Down
48 changes: 12 additions & 36 deletions src/models/DocumentRoot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,41 +227,16 @@ class DocumentRoot<T extends DocumentType> {
return this.store.root.userStore.viewedUserId;
}

/**
* All documents which
* - **don't have a parent**
* - having the **same type** as this document root
*
* @returns All main documents, **ordered by creation date**, oldest first.
*/
@computed
get mainDocuments(): TypeModelMapping[T][] {
const docs = orderBy(
this.documents.filter((d) => d.isMain),
['createdAt', 'id'],
['asc', 'asc']
) as TypeModelMapping[T][];
if (this.isDummy) {
return docs;
}
const byUser = docs.filter((d) => d.authorId === this.viewedUserId);

if (
this.store.root.userStore.current?.hasElevatedAccess &&
this.store.root.userStore.isUserSwitched
) {
return byUser;
}

if (NoneAccess.has(this.sharedAccess)) {
return byUser;
}
return [...byUser, ...docs.filter((d) => d.authorId !== this.viewedUserId)];
}

@computed
get firstMainDocument(): TypeModelMapping[T] | undefined {
return this.mainDocuments[0];
get documentsByType(): Map<DocumentType, TypeModelMapping[DocumentType][]> {
return orderBy(this.documents, ['createdAt', 'id'], ['asc', 'asc']).reduce((map, doc) => {
const docs = map.get(doc.type) || [];
if (docs.length === 0) {
map.set(doc.type, docs);
}
docs.push(doc);
return map;
}, new Map<DocumentType, TypeModelMapping[DocumentType][]>());
}

@action
Expand Down Expand Up @@ -300,12 +275,13 @@ class DocumentRoot<T extends DocumentType> {

@computed
get _needsInitialDocumentCreation() {
return this._canInitializeDocuments && !this.firstMainDocument;
return this._canInitializeDocuments && !this.documentsByType.has(this.meta.type);
}

@computed
get _triggerDocumentReload() {
return `${this.firstMainDocument?.id}-${this.store.root.userStore.viewedUserId}`;
const firstMainDoc = this.documentsByType.get(this.meta.type)?.[0];
return `${firstMainDoc?.id}-${this.store.root.userStore.viewedUserId}`;
}
}

Expand Down
Loading