fix: Problemen met PUT op leerpaden en verschillende kleinere problemen

This commit is contained in:
Gerald Schmittinger 2025-05-13 16:21:06 +02:00
parent 2db5d77296
commit 96821c40ab
21 changed files with 205 additions and 103 deletions

View file

@ -68,13 +68,14 @@ export async function getLearningPaths(req: Request, res: Response): Promise<voi
function postOrPutLearningPath(isPut: boolean): (req: AuthenticatedRequest, res: Response) => Promise<void> { function postOrPutLearningPath(isPut: boolean): (req: AuthenticatedRequest, res: Response) => Promise<void> {
return async (req, res) => { return async (req, res) => {
const path: LearningPath = req.body; const path: LearningPath = req.body;
const teacher = await getTeacher(req.auth!.username);
if (isPut) { if (isPut) {
if (req.params.hruid !== path.hruid || req.params.language !== path.language) { if (req.params.hruid !== path.hruid || req.params.language !== path.language) {
throw new BadRequestException("id_not_matching_query_params"); throw new BadRequestException("id_not_matching_query_params");
} }
await learningPathService.deleteLearningPath({hruid: path.hruid, language: path.language as Language});
} }
const teacher = await getTeacher(req.auth!.username); res.json(await learningPathService.createNewLearningPath(path, [teacher]));
res.json(await learningPathService.createNewLearningPath(path, [teacher], isPut));
} }
} }

View file

@ -8,7 +8,10 @@ import { EntityAlreadyExistsException } from '../../exceptions/entity-already-ex
export class LearningPathRepository extends DwengoEntityRepository<LearningPath> { export class LearningPathRepository extends DwengoEntityRepository<LearningPath> {
public async findByHruidAndLanguage(hruid: string, language: Language): Promise<LearningPath | null> { public async findByHruidAndLanguage(hruid: string, language: Language): Promise<LearningPath | null> {
return this.findOne({ hruid: hruid, language: language }, { populate: ['nodes', 'nodes.transitions'] }); return this.findOne(
{ hruid: hruid, language: language },
{ populate: ['nodes', 'nodes.transitions', 'admins'] }
);
} }
/** /**
@ -24,7 +27,7 @@ export class LearningPathRepository extends DwengoEntityRepository<LearningPath>
language: language, language: language,
$or: [{ title: { $like: `%${query}%` } }, { description: { $like: `%${query}%` } }], $or: [{ title: { $like: `%${query}%` } }, { description: { $like: `%${query}%` } }],
}, },
populate: ['nodes', 'nodes.transitions'], populate: ['nodes', 'nodes.transitions', 'admins'],
}); });
} }
@ -37,7 +40,8 @@ export class LearningPathRepository extends DwengoEntityRepository<LearningPath>
admins: { admins: {
username: adminUsername username: adminUsername
} }
} },
populate: ['nodes', 'nodes.transitions', 'admins']
}); });
} }

View file

@ -1,4 +1,4 @@
import { Collection, Entity, Enum, ManyToOne, OneToMany, PrimaryKey, Property, Rel } from '@mikro-orm/core'; import { Cascade, Collection, Entity, Enum, ManyToOne, OneToMany, PrimaryKey, Property, Rel } from '@mikro-orm/core';
import { LearningPath } from './learning-path.entity.js'; import { LearningPath } from './learning-path.entity.js';
import { LearningPathTransition } from './learning-path-transition.entity.js'; import { LearningPathTransition } from './learning-path-transition.entity.js';
import { Language } from '@dwengo-1/common/util/language'; import { Language } from '@dwengo-1/common/util/language';
@ -26,7 +26,7 @@ export class LearningPathNode {
@Property({ type: 'bool' }) @Property({ type: 'bool' })
startNode!: boolean; startNode!: boolean;
@OneToMany({ entity: () => LearningPathTransition, mappedBy: 'node' }) @OneToMany({ entity: () => LearningPathTransition, mappedBy: 'node', cascade: [Cascade.ALL] })
transitions!: Collection<LearningPathTransition>; transitions!: Collection<LearningPathTransition>;
@Property({ length: 3 }) @Property({ length: 3 })

View file

@ -1,4 +1,4 @@
import { Collection, Entity, Enum, ManyToMany, OneToMany, PrimaryKey, Property } from '@mikro-orm/core'; import { Cascade, Collection, Entity, Enum, ManyToMany, OneToMany, PrimaryKey, Property } from '@mikro-orm/core';
import { Teacher } from '../users/teacher.entity.js'; import { Teacher } from '../users/teacher.entity.js';
import { LearningPathRepository } from '../../data/content/learning-path-repository.js'; import { LearningPathRepository } from '../../data/content/learning-path-repository.js';
import { LearningPathNode } from './learning-path-node.entity.js'; import { LearningPathNode } from './learning-path-node.entity.js';
@ -24,6 +24,6 @@ export class LearningPath {
@Property({ type: 'blob', nullable: true }) @Property({ type: 'blob', nullable: true })
image: Buffer | null = null; image: Buffer | null = null;
@OneToMany({ entity: () => LearningPathNode, mappedBy: 'learningPath' }) @OneToMany({ entity: () => LearningPathNode, mappedBy: 'learningPath', cascade: [Cascade.ALL] })
nodes: Collection<LearningPathNode> = new Collection<LearningPathNode>(this); nodes: Collection<LearningPathNode> = new Collection<LearningPathNode>(this);
} }

View file

@ -1,3 +1,4 @@
import { Language } from "@dwengo-1/common/util/language";
import learningPathService from "../../../services/learning-paths/learning-path-service"; import learningPathService from "../../../services/learning-paths/learning-path-service";
import { authorize } from "../auth"; import { authorize } from "../auth";
import { AuthenticatedRequest } from "../authenticated-request"; import { AuthenticatedRequest } from "../authenticated-request";
@ -5,8 +6,8 @@ import { AuthenticationInfo } from "../authentication-info";
export const onlyAdminsForLearningPath = authorize(async (auth: AuthenticationInfo, req: AuthenticatedRequest) => { export const onlyAdminsForLearningPath = authorize(async (auth: AuthenticationInfo, req: AuthenticatedRequest) => {
const adminsForLearningPath = await learningPathService.getAdmins({ const adminsForLearningPath = await learningPathService.getAdmins({
hruid: req.body.hruid, hruid: req.params.hruid,
language: req.body.language language: req.params.language as Language
}); });
return adminsForLearningPath && adminsForLearningPath.includes(auth.username); return adminsForLearningPath && adminsForLearningPath.includes(auth.username);
}); });

View file

@ -1,7 +1,7 @@
import express from 'express'; import express from 'express';
import { deleteLearningPath, getLearningPaths, postLearningPath, putLearningPath } from '../controllers/learning-paths.js'; import { deleteLearningPath, getLearningPaths, postLearningPath, putLearningPath } from '../controllers/learning-paths.js';
import { teachersOnly } from '../middleware/auth/auth.js'; import { teachersOnly } from '../middleware/auth/auth.js';
import { onlyAdminsForLearningObject } from '../middleware/auth/checks/learning-object-auth-checks.js'; import { onlyAdminsForLearningPath } from '../middleware/auth/checks/learning-path-auth-checks.js';
const router = express.Router(); const router = express.Router();
@ -27,7 +27,7 @@ const router = express.Router();
router.get('/', getLearningPaths); router.get('/', getLearningPaths);
router.post('/', teachersOnly, postLearningPath) router.post('/', teachersOnly, postLearningPath)
router.put('/:hruid/:language', onlyAdminsForLearningObject, putLearningPath); router.put('/:hruid/:language', onlyAdminsForLearningPath, putLearningPath);
router.delete('/:hruid/:language', onlyAdminsForLearningObject, deleteLearningPath); router.delete('/:hruid/:language', onlyAdminsForLearningPath, deleteLearningPath);
export default router; export default router;

View file

@ -72,6 +72,8 @@ const learningObjectService = {
learningObject.hruid = getEnvVar(envVars.UserContentPrefix) + learningObject.hruid; learningObject.hruid = getEnvVar(envVars.UserContentPrefix) + learningObject.hruid;
} }
await learningObjectRepository.getEntityManager().flush();
// Lookup the admin teachers based on their usernames and add them to the admins of the learning object. // Lookup the admin teachers based on their usernames and add them to the admins of the learning object.
const teacherRepo = getTeacherRepository(); const teacherRepo = getTeacherRepository();
const adminTeachers = await Promise.all( const adminTeachers = await Promise.all(

View file

@ -16,6 +16,9 @@ import { Language } from '@dwengo-1/common/util/language';
import { Group } from '../../entities/assignments/group.entity'; import { Group } from '../../entities/assignments/group.entity';
import { Collection } from '@mikro-orm/core'; import { Collection } from '@mikro-orm/core';
import { v4 } from 'uuid'; import { v4 } from 'uuid';
import { getLogger } from '../../logging/initalize.js';
const logger = getLogger();
/** /**
* Fetches the corresponding learning object for each of the nodes and creates a map that maps each node to its * Fetches the corresponding learning object for each of the nodes and creates a map that maps each node to its
@ -38,8 +41,13 @@ async function getLearningObjectsForNodes(nodes: Collection<LearningPathNode>):
) )
) )
); );
if (Array.from(nullableNodesToLearningObjects.values()).some((it) => it === null)) {
throw new Error('At least one of the learning objects on this path could not be found.'); // Ignore all learning objects that cannot be found such that the rest of the learning path keeps working.
for (const [key, value] of nullableNodesToLearningObjects) {
if (value === null) {
logger.warn(`Learning object ${key.learningObjectHruid}/${key.language}/${key.version} not found!`);
nullableNodesToLearningObjects.delete(key);
}
} }
return nullableNodesToLearningObjects as Map<LearningPathNode, FilteredLearningObject>; return nullableNodesToLearningObjects as Map<LearningPathNode, FilteredLearningObject>;
} }
@ -102,7 +110,14 @@ async function convertNode(
!personalizedFor || // If we do not want a personalized learning path, keep all transitions !personalizedFor || // If we do not want a personalized learning path, keep all transitions
isTransitionPossible(trans, optionalJsonStringToObject(lastSubmission?.content)) // Otherwise remove all transitions that aren't possible. isTransitionPossible(trans, optionalJsonStringToObject(lastSubmission?.content)) // Otherwise remove all transitions that aren't possible.
) )
.map((trans, i) => convertTransition(trans, i, nodesToLearningObjects)); .map((trans, i) => {
try {
return convertTransition(trans, i, nodesToLearningObjects)
} catch (_: unknown) {
logger.error(`Transition could not be resolved: ${JSON.stringify(trans)}`);
return undefined; // Do not crash on invalid transitions, just ignore them so the rest of the learning path keeps working.
}
}).filter(it => it);
return { return {
_id: learningObject.uuid, _id: learningObject.uuid,
language: learningObject.language, language: learningObject.language,

View file

@ -130,13 +130,12 @@ const learningPathService = {
* Add a new learning path to the database. * Add a new learning path to the database.
* @param dto Learning path DTO from which the learning path will be created. * @param dto Learning path DTO from which the learning path will be created.
* @param admins Teachers who should become an admin of the learning path. * @param admins Teachers who should become an admin of the learning path.
* @param allowReplace If this is set to true and there is already a learning path with the same identifier, it is replaced.
* @returns the created learning path. * @returns the created learning path.
*/ */
async createNewLearningPath(dto: LearningPath, admins: TeacherDTO[], allowReplace = false): Promise<LearningPathEntity> { async createNewLearningPath(dto: LearningPath, admins: TeacherDTO[]): Promise<LearningPathEntity> {
const repo = getLearningPathRepository(); const repo = getLearningPathRepository();
const path = mapToLearningPath(dto, admins); const path = mapToLearningPath(dto, admins);
await repo.save(path, { preventOverwrite: allowReplace }); await repo.save(path, { preventOverwrite: true });
return path; return path;
}, },

View file

@ -25,8 +25,8 @@ export interface LearningObjectNode {
language: Language; language: Language;
start_node?: boolean; start_node?: boolean;
transitions: Transition[]; transitions: Transition[];
created_at: string; created_at?: string;
updatedAt: string; updatedAt?: string;
done?: boolean; // True if a submission exists for this node by the user for whom the learning path is customized. done?: boolean; // True if a submission exists for this node by the user for whom the learning path is customized.
} }

View file

@ -1,8 +1,8 @@
import { BaseController } from "@/controllers/base-controller.ts"; import { BaseController } from "@/controllers/base-controller.ts";
import { LearningPath } from "@/data-objects/learning-paths/learning-path.ts";
import type { Language } from "@/data-objects/language.ts"; import type { Language } from "@/data-objects/language.ts";
import { LearningPath } from "@/data-objects/learning-paths/learning-path";
import { single } from "@/utils/response-assertions.ts"; import { single } from "@/utils/response-assertions.ts";
import type { LearningPathDTO } from "@/data-objects/learning-paths/learning-path-dto.ts"; import type { LearningPath as LearningPathDTO } from "@dwengo-1/common/interfaces/learning-content";
export class LearningPathController extends BaseController { export class LearningPathController extends BaseController {
constructor() { constructor() {

View file

@ -1,5 +1,5 @@
import type { Language } from "@/data-objects/language.ts"; import type { Language } from "@/data-objects/language.ts";
import type { LearningPathNodeDTO } from "@/data-objects/learning-paths/learning-path.ts"; import type { LearningObjectNode as LearningPathNodeDTO } from "@dwengo-1/common/interfaces/learning-content";
export class LearningPathNode { export class LearningPathNode {
public readonly learningobjectHruid: string; public readonly learningobjectHruid: string;
@ -14,7 +14,7 @@ export class LearningPathNode {
learningobjectHruid: string; learningobjectHruid: string;
version: number; version: number;
language: Language; language: Language;
transitions: { next: LearningPathNode; default: boolean }[]; transitions: { next: LearningPathNode; default?: boolean }[];
createdAt: Date; createdAt: Date;
updatedAt: Date; updatedAt: Date;
done?: boolean; done?: boolean;
@ -22,7 +22,7 @@ export class LearningPathNode {
this.learningobjectHruid = options.learningobjectHruid; this.learningobjectHruid = options.learningobjectHruid;
this.version = options.version; this.version = options.version;
this.language = options.language; this.language = options.language;
this.transitions = options.transitions; this.transitions = options.transitions.map(it => ({ next: it.next, default: it.default ?? false }));
this.createdAt = options.createdAt; this.createdAt = options.createdAt;
this.updatedAt = options.updatedAt; this.updatedAt = options.updatedAt;
this.done = options.done || false; this.done = options.done || false;
@ -50,8 +50,8 @@ export class LearningPathNode {
return undefined; return undefined;
}) })
.filter((it) => it !== undefined), .filter((it) => it !== undefined),
createdAt: new Date(dto.created_at), createdAt: dto.created_at ? new Date(dto.created_at) : new Date(),
updatedAt: new Date(dto.updatedAt), updatedAt: dto.updatedAt ? new Date(dto.updatedAt) : new Date(),
done: dto.done, done: dto.done,
}); });
} }

View file

@ -1,6 +1,6 @@
import type { Language } from "@/data-objects/language.ts"; import type { Language } from "@/data-objects/language.ts";
import { LearningPathNode } from "@/data-objects/learning-paths/learning-path-node.ts"; import { LearningPathNode } from "@/data-objects/learning-paths/learning-path-node.ts";
import type { LearningPathDTO } from "@/data-objects/learning-paths/learning-path-dto.ts"; import type { LearningObjectNode, LearningPath as LearningPathDTO } from "@dwengo-1/common/interfaces/learning-content";
export interface LearningPathNodeDTO { export interface LearningPathNodeDTO {
_id: string; _id: string;
@ -77,16 +77,19 @@ export class LearningPath {
hruid: dto.hruid, hruid: dto.hruid,
title: dto.title, title: dto.title,
description: dto.description, description: dto.description,
amountOfNodes: dto.num_nodes, amountOfNodes: dto.num_nodes ?? dto.nodes.length,
amountOfNodesLeft: dto.num_nodes_left, amountOfNodesLeft: dto.num_nodes_left ?? dto.nodes.length,
keywords: dto.keywords.split(" "), keywords: dto.keywords.split(" "),
targetAges: { min: dto.min_age, max: dto.max_age }, targetAges: {
min: dto.min_age ?? NaN,
max: dto.max_age ?? NaN
},
startNode: LearningPathNode.fromDTOAndOtherNodes(LearningPath.getStartNode(dto), dto.nodes), startNode: LearningPathNode.fromDTOAndOtherNodes(LearningPath.getStartNode(dto), dto.nodes),
image: dto.image, image: dto.image,
}); });
} }
static getStartNode(dto: LearningPathDTO): LearningPathNodeDTO { static getStartNode(dto: LearningPathDTO): LearningObjectNode {
const startNodeDtos = dto.nodes.filter((it) => it.start_node === true); const startNodeDtos = dto.nodes.filter((it) => it.start_node === true);
if (startNodeDtos.length < 1) { if (startNodeDtos.length < 1) {
// The learning path has no starting node -> use the first node. // The learning path has no starting node -> use the first node.

View file

@ -134,5 +134,9 @@
"invalidZip": "This is not a valid zip file.", "invalidZip": "This is not a valid zip file.",
"emptyZip": "This zip file is empty", "emptyZip": "This zip file is empty",
"missingMetadata": "This learning object is missing a metadata.json file.", "missingMetadata": "This learning object is missing a metadata.json file.",
"missingContent": "This learning object is missing a content.* file." "missingContent": "This learning object is missing a content.* file.",
"open": "open",
"editLearningPath": "Edit learning path",
"newLearningPath": "Create a new learning path",
"saveChanges": "Save changes"
} }

View file

@ -2,9 +2,9 @@ import { type MaybeRefOrGetter, toValue } from "vue";
import type { Language } from "@/data-objects/language.ts"; import type { Language } from "@/data-objects/language.ts";
import { useMutation, useQuery, useQueryClient, type UseMutationReturnType, type UseQueryReturnType } from "@tanstack/vue-query"; import { useMutation, useQuery, useQueryClient, type UseMutationReturnType, type UseQueryReturnType } from "@tanstack/vue-query";
import { getLearningPathController } from "@/controllers/controllers"; import { getLearningPathController } from "@/controllers/controllers";
import type { LearningPath } from "@/data-objects/learning-paths/learning-path.ts";
import type { AxiosError } from "axios"; import type { AxiosError } from "axios";
import type { LearningPathDTO } from "@/data-objects/learning-paths/learning-path-dto"; import type { LearningPath as LearningPathDTO } from "@dwengo-1/common/interfaces/learning-content";
import type { LearningPath } from "@/data-objects/learning-paths/learning-path";
export const LEARNING_PATH_KEY = "learningPath"; export const LEARNING_PATH_KEY = "learningPath";
const learningPathController = getLearningPathController(); const learningPathController = getLearningPathController();

View file

@ -106,6 +106,12 @@ const router = createRouter({
component: SingleDiscussion, component: SingleDiscussion,
meta: { requiresAuth: true }, meta: { requiresAuth: true },
}, },
{
path: "/my-content",
name: "OwnLearningContentPage",
component: OwnLearningContentPage,
meta: { requiresAuth: true }
},
{ {
path: "/learningPath", path: "/learningPath",
children: [ children: [
@ -115,18 +121,12 @@ const router = createRouter({
component: LearningPathSearchPage, component: LearningPathSearchPage,
meta: { requiresAuth: true }, meta: { requiresAuth: true },
}, },
{
path: "my",
name: "OwnLearningContentPage",
component: OwnLearningContentPage,
meta: { requiresAuth: true }
},
{ {
path: ":hruid/:language/:learningObjectHruid", path: ":hruid/:language/:learningObjectHruid",
name: "LearningPath", name: "LearningPath",
component: LearningPathPage, component: LearningPathPage,
props: true, props: true,
meta: { requiresAuth: true }, meta: { requiresAuth: true }
}, },
], ],
}, },

View file

@ -247,7 +247,7 @@
</template> </template>
</v-list-item> </v-list-item>
<v-divider></v-divider> <v-divider></v-divider>
<div v-if="props.learningObjectHruid"> <div>
<using-query-result <using-query-result
:query-result="learningObjectListQueryResult" :query-result="learningObjectListQueryResult"
v-slot="learningObjects: { data: LearningObject[] }" v-slot="learningObjects: { data: LearningObject[] }"

View file

@ -6,9 +6,9 @@
import UsingQueryResult from "@/components/UsingQueryResult.vue"; import UsingQueryResult from "@/components/UsingQueryResult.vue";
import type { LearningObject } from "@/data-objects/learning-objects/learning-object"; import type { LearningObject } from "@/data-objects/learning-objects/learning-object";
import { ref, type Ref } from "vue"; import { ref, type Ref } from "vue";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
import { useGetAllLearningPathsByAdminQuery } from "@/queries/learning-paths"; import { useGetAllLearningPathsByAdminQuery } from "@/queries/learning-paths";
import type { LearningPathDTO } from "@/data-objects/learning-paths/learning-path-dto"; import type { LearningPath as LearningPathDTO } from "@dwengo-1/common/interfaces/learning-content";
const { t } = useI18n(); const { t } = useI18n();
@ -43,7 +43,7 @@ import type { LearningPathDTO } from "@/data-objects/learning-paths/learning-pat
:query-result="learningPathsQuery" :query-result="learningPathsQuery"
v-slot="response: { data: LearningPathDTO[] }" v-slot="response: { data: LearningPathDTO[] }"
> >
<own-learning-paths-view :learning-paths="response.data"/> <own-learning-paths-view :learningPaths="response.data"/>
</using-query-result> </using-query-result>
</v-tabs-window-item> </v-tabs-window-item>
</v-tabs-window> </v-tabs-window>

View file

@ -2,7 +2,7 @@
import type { LearningObject } from '@/data-objects/learning-objects/learning-object'; import type { LearningObject } from '@/data-objects/learning-objects/learning-object';
import LearningObjectUploadButton from '@/views/own-learning-content/learning-objects/LearningObjectUploadButton.vue' import LearningObjectUploadButton from '@/views/own-learning-content/learning-objects/LearningObjectUploadButton.vue'
import LearningObjectPreviewCard from './LearningObjectPreviewCard.vue'; import LearningObjectPreviewCard from './LearningObjectPreviewCard.vue';
import { computed, ref, type Ref } from 'vue'; import { computed, ref, watch, type Ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
const { t } = useI18n(); const { t } = useI18n();
@ -19,6 +19,8 @@
const selectedLearningObjects: Ref<LearningObject[]> = ref([]); const selectedLearningObjects: Ref<LearningObject[]> = ref([]);
watch(() => props.learningObjects, () => selectedLearningObjects.value = []);
const selectedLearningObject = computed(() => const selectedLearningObject = computed(() =>
selectedLearningObjects.value ? selectedLearningObjects.value[0] : undefined selectedLearningObjects.value ? selectedLearningObjects.value[0] : undefined
); );
@ -27,16 +29,20 @@
<template> <template>
<div class="root"> <div class="root">
<v-data-table <div class="table-container">
class="table" <v-data-table
v-model="selectedLearningObjects" class="table"
:items="props.learningObjects" v-model="selectedLearningObjects"
:headers="tableHeaders" :items="props.learningObjects"
select-strategy="single" :headers="tableHeaders"
show-select select-strategy="single"
return-object show-select
/> return-object
<learning-object-preview-card class="preview" :selectedLearningObject="selectedLearningObject"/> />
</div>
<div class="preview-container" v-if="selectedLearningObject">
<learning-object-preview-card class="preview" :selectedLearningObject="selectedLearningObject"/>
</div>
</div> </div>
<div class="fab"> <div class="fab">
<learning-object-upload-button/> <learning-object-upload-button/>
@ -55,11 +61,17 @@
padding: 20px; padding: 20px;
flex-wrap: wrap; flex-wrap: wrap;
} }
.preview { .preview-container {
flex: 1; flex: 1;
min-width: 400px; min-width: 400px;
} }
.table { .table-container {
flex: 1; flex: 1;
} }
.preview {
width: 100%;
}
.table {
width: 100%;
}
</style> </style>

View file

@ -1,48 +1,80 @@
<script setup lang="ts"> <script setup lang="ts">
import UsingQueryResult from '@/components/UsingQueryResult.vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import type { LearningPathDTO } from '@/data-objects/learning-paths/learning-path-dto'; import { computed, ref, watch, type Ref } from 'vue';
import { computed, ref, watch } from 'vue';
import JsonEditorVue from 'json-editor-vue' import JsonEditorVue from 'json-editor-vue'
import { useMutation } from '@tanstack/vue-query'; import { useDeleteLearningPathMutation, usePostLearningPathMutation, usePutLearningPathMutation } from '@/queries/learning-paths';
import { usePostLearningPathMutation, usePutLearningPathMutation } from '@/queries/learning-paths'; import { Language } from '@/data-objects/language';
import type { LearningPath } from '@dwengo-1/common/interfaces/learning-content';
import type { AxiosError } from 'axios';
const { t } = useI18n(); const { t } = useI18n();
const props = defineProps<{ const props = defineProps<{
selectedLearningPath?: LearningPathDTO selectedLearningPath?: LearningPath
}>(); }>();
const INDENT = 4; const { isPending, mutate, error: deleteError, isSuccess: deleteSuccess } = useDeleteLearningPathMutation();
const DEFAULT_LEARNING_PATH: LearningPathDTO = {
language: '', const DEFAULT_LEARNING_PATH: LearningPath = {
hruid: '', language: 'en',
title: '', hruid: '...',
description: '', title: '...',
num_nodes: 0, description: '...',
num_nodes_left: 0, nodes: [
nodes: [], {
keywords: '', learningobject_hruid: '...',
target_ages: [], language: Language.English,
min_age: 0, version: 1,
max_age: 0, start_node: true,
__order: 0 transitions: [
{
default: true,
condition: "(remove if the transition should be unconditinal)",
next: {
hruid: '...',
version: 1,
language: '...'
}
}
]
}
],
keywords: 'Keywords separated by spaces',
target_ages: []
} }
const { isPending: isPostPending, mutate: doPost } = usePostLearningPathMutation(); const { isPending: isPostPending, error: postError, mutate: doPost } = usePostLearningPathMutation();
const { isPending: isPutPending, mutate: doPut } = usePutLearningPathMutation(); const { isPending: isPutPending, error: putError, mutate: doPut } = usePutLearningPathMutation();
const learningPath = ref(DEFAULT_LEARNING_PATH); const learningPath: Ref<LearningPath | string> = ref(DEFAULT_LEARNING_PATH);
const parsedLearningPath = computed(() =>
typeof learningPath.value === "string" ? JSON.parse(learningPath.value) as LearningPath
: learningPath.value
);
watch(() => props.selectedLearningPath, () => learningPath.value = props.selectedLearningPath ?? DEFAULT_LEARNING_PATH); watch(() => props.selectedLearningPath, () => learningPath.value = props.selectedLearningPath ?? DEFAULT_LEARNING_PATH);
function uploadLearningPath(): void { function uploadLearningPath(): void {
if (props.selectedLearningPath) { if (props.selectedLearningPath) {
doPut({ learningPath: learningPath.value }); doPut({ learningPath: parsedLearningPath.value });
} else { } else {
doPost({ learningPath: learningPath.value }); doPost({ learningPath: parsedLearningPath.value });
} }
} }
function deleteLearningObject(): void {
if (props.selectedLearningPath) {
mutate({
hruid: props.selectedLearningPath.hruid,
language: props.selectedLearningPath.language as Language
});
}
}
function extractErrorMessage(error: AxiosError | null): string | undefined {
return (error?.response?.data as {error: string}).error ?? error?.message;
}
</script> </script>
<template> <template>
@ -51,9 +83,27 @@ import { usePostLearningPathMutation, usePutLearningPathMutation } from '@/queri
> >
<template v-slot:text> <template v-slot:text>
<json-editor-vue v-model="learningPath"></json-editor-vue> <json-editor-vue v-model="learningPath"></json-editor-vue>
<v-alert
v-if="postError || putError || deleteError"
icon="mdi mdi-alert-circle"
type="error"
:title="t('error')"
:text="t(extractErrorMessage(postError) || extractErrorMessage(putError) || extractErrorMessage(deleteError)!)"
></v-alert>
</template> </template>
<template v-slot:actions> <template v-slot:actions>
<v-btn @click="uploadLearningPath" :loading="isPostPending || isPutPending">{{ props.selectedLearningPath ? t('saveChanges') : t('create') }}</v-btn> <v-btn @click="uploadLearningPath" :loading="isPostPending || isPutPending" :disabled="parsedLearningPath.hruid === DEFAULT_LEARNING_PATH.hruid">
{{ props.selectedLearningPath ? t('saveChanges') : t('create') }}
</v-btn>
<v-btn @click="deleteLearningObject" :disabled="!props.selectedLearningPath">
{{ t('delete') }}
</v-btn>
<v-btn
:href="`/learningPath/${props.selectedLearningPath?.hruid}/${props.selectedLearningPath?.language}/start`"
:disabled="!props.selectedLearningPath"
>
{{ t('open') }}
</v-btn>
</template> </template>
</v-card> </v-card>
</template> </template>

View file

@ -1,8 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
import LearningPathPreviewCard from './LearningPathPreviewCard.vue'; import LearningPathPreviewCard from './LearningPathPreviewCard.vue';
import { computed, ref, type Ref } from 'vue'; import { computed, ref, watch, type Ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import type { LearningPathDTO } from '@/data-objects/learning-paths/learning-path-dto'; import type { LearningPath as LearningPathDTO } from '@dwengo-1/common/interfaces/learning-content';
const { t } = useI18n(); const { t } = useI18n();
const props = defineProps<{ const props = defineProps<{
@ -10,7 +10,7 @@
}>(); }>();
const tableHeaders = [ const tableHeaders = [
{ title: t("hruid"), width: "250px", key: "key" }, { title: t("hruid"), width: "250px", key: "hruid" },
{ title: t("language"), width: "50px", key: "language" }, { title: t("language"), width: "50px", key: "language" },
{ title: t("title"), key: "title" } { title: t("title"), key: "title" }
]; ];
@ -21,20 +21,25 @@
selectedLearningPaths.value ? selectedLearningPaths.value[0] : undefined selectedLearningPaths.value ? selectedLearningPaths.value[0] : undefined
); );
watch(() => props.learningPaths, () => selectedLearningPaths.value = []);
</script> </script>
<template> <template>
<div class="root"> <div class="root">
<v-data-table <div class="table-container">
class="table" <v-data-table
v-model="selectedLearningPaths" class="table"
:items="props.learningPaths" v-model="selectedLearningPaths"
:headers="tableHeaders" :items="props.learningPaths"
select-strategy="single" :headers="tableHeaders"
show-select select-strategy="single"
return-object show-select
/> return-object
<learning-path-preview-card class="preview" :selectedLearningPath="selectedLearningPath"/> />
</div>
<div class="preview-container">
<learning-path-preview-card class="preview" :selectedLearningPath="selectedLearningPath"/>
</div>
</div> </div>
</template> </template>
@ -50,11 +55,17 @@
padding: 20px; padding: 20px;
flex-wrap: wrap; flex-wrap: wrap;
} }
.preview { .preview-container {
flex: 1; flex: 1;
min-width: 400px; min-width: 400px;
} }
.table { .table-container {
flex: 1; flex: 1;
} }
.preview {
width: 100%;
}
.table {
width: 100%;
}
</style> </style>