fix: Problemen met PUT op leerpaden en verschillende kleinere problemen
This commit is contained in:
parent
2db5d77296
commit
96821c40ab
21 changed files with 205 additions and 103 deletions
|
@ -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));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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']
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 })
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
});
|
});
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -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.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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 }
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
|
@ -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[] }"
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue