Fix and improve custom titles, add course support, fix things
Implemented hasManyThrough repository with count() method
This commit is contained in:
parent
38b06f54e6
commit
d076e0a013
20 changed files with 293 additions and 100 deletions
|
@ -1,5 +1,5 @@
|
||||||
import { repository } from '@loopback/repository';
|
import { repository } from '@loopback/repository';
|
||||||
import { CourseRepository } from '../repositories';
|
import { CourseRepository, SubjectRepository, UserRepository } from '../repositories';
|
||||||
import { arg, ID, Int, mutation, query, resolver } from '@loopback/graphql';
|
import { arg, ID, Int, mutation, query, resolver } from '@loopback/graphql';
|
||||||
import { Course } from '../models';
|
import { Course } from '../models';
|
||||||
import { CourseUpdateInput } from '../graphql-types/input/course-update.input';
|
import { CourseUpdateInput } from '../graphql-types/input/course-update.input';
|
||||||
|
@ -9,13 +9,20 @@ import { CourseList } from '../graphql-types/course';
|
||||||
@resolver(of => Course)
|
@resolver(of => Course)
|
||||||
export class CourseResolver {
|
export class CourseResolver {
|
||||||
constructor(
|
constructor(
|
||||||
@repository('CourseRepository') private courseRepo: CourseRepository
|
@repository('CourseRepository') private courseRepo: CourseRepository,
|
||||||
|
@repository('SubjectRepository') private subjectRepo: SubjectRepository,
|
||||||
|
@repository('UserRepository') private userRepo: UserRepository
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@query(returns => CourseList)
|
@query(returns => CourseList)
|
||||||
async courses(@arg('offset', returns => Int) offset: number, @arg('limit', returns => Int) limit: number): Promise<ListResponse<Course>> {
|
async coursesBySubject(@arg('subject', returns => ID) subject: number, @arg('offset', returns => Int) offset: number, @arg('limit', returns => Int) limit: number): Promise<ListResponse<Course>> {
|
||||||
return listResponse(this.courseRepo, offset, limit, CourseList);
|
return listResponse(this.subjectRepo.courses(subject), offset, limit, CourseList);
|
||||||
|
}
|
||||||
|
|
||||||
|
@query(returns => CourseList)
|
||||||
|
async coursesByUser(@arg('user', returns => ID) user: number, @arg('offset', returns => Int) offset: number, @arg('limit', returns => Int) limit: number): Promise<ListResponse<Course>> {
|
||||||
|
return listResponse(this.userRepo.courses(user), offset, limit, CourseList);
|
||||||
}
|
}
|
||||||
|
|
||||||
@query(returns => Course)
|
@query(returns => Course)
|
||||||
|
|
|
@ -13,7 +13,7 @@ export class SubjectResolver {
|
||||||
|
|
||||||
@query(returns => SubjectList)
|
@query(returns => SubjectList)
|
||||||
async subjects(@arg('offset', returns => Int) offset: number, @arg('limit', returns => Int) limit: number): Promise<ListResponse<Subject>> {
|
async subjects(@arg('offset', returns => Int) offset: number, @arg('limit', returns => Int) limit: number): Promise<ListResponse<Subject>> {
|
||||||
return listResponse(this.subjectRepo, offset, limit, SubjectList);
|
return await listResponse(this.subjectRepo, offset, limit, SubjectList);
|
||||||
}
|
}
|
||||||
|
|
||||||
@query(returns => Subject)
|
@query(returns => Subject)
|
||||||
|
|
|
@ -75,7 +75,7 @@ export class UserResolver {
|
||||||
}
|
}
|
||||||
|
|
||||||
@authorized()
|
@authorized()
|
||||||
@query(returns => [User])
|
@query(returns => UserList)
|
||||||
async users(@arg('limit', returns => Int) limit: number, @arg('offset', returns => Int) offset: number) {
|
async users(@arg('limit', returns => Int) limit: number, @arg('offset', returns => Int) offset: number) {
|
||||||
return await listResponse(this.userRepository, offset, limit, UserList);
|
return await listResponse(this.userRepository, offset, limit, UserList);
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,9 @@ export interface ListResponse<T> {
|
||||||
list: T[];
|
list: T[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function listResponse<T extends Entity, U extends ListResponse<Partial<T>>>(repo: DefaultCrudRepository<T, number>, offset: number, limit: number, listType: ClassType<U>) {
|
export type ListRepository<T extends Entity> = Pick<DefaultCrudRepository<T, number>, 'find' | 'count'>;
|
||||||
|
|
||||||
|
export async function listResponse<T extends Entity, U extends ListResponse<Partial<T>>>(repo: ListRepository<T>, offset: number, limit: number, listType: ClassType<U>) {
|
||||||
const list = new listType();
|
const list = new listType();
|
||||||
list.list = await repo.find({offset, limit});
|
list.list = await repo.find({offset, limit});
|
||||||
list.count = (await repo.count()).count;
|
list.count = (await repo.count()).count;
|
||||||
|
|
|
@ -1,29 +0,0 @@
|
||||||
import { constrainWhere, Count, DefaultHasManyRepository, Entity, EntityCrudRepository, Where } from '@loopback/repository';
|
|
||||||
import { Options } from '@loopback/repository/src/common-types';
|
|
||||||
import { HasManyRepository } from '@loopback/repository/src/relations/has-many/has-many.repository';
|
|
||||||
import { InclusionResolver } from '@loopback/repository/src/relations/relation.types';
|
|
||||||
|
|
||||||
export interface CustomHasManyRepositoryFactory<Target extends Entity,
|
|
||||||
ForeignKeyType,
|
|
||||||
> {
|
|
||||||
/**
|
|
||||||
* Invoke the function to obtain HasManyRepository.
|
|
||||||
*/
|
|
||||||
(fkValue: ForeignKeyType): CustomHasManyRepository<Target>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Use `resolver` property to obtain an InclusionResolver for this relation.
|
|
||||||
*/
|
|
||||||
inclusionResolver: InclusionResolver<Entity, Target>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CustomHasManyRepository<Target extends Entity> extends HasManyRepository<Target> {
|
|
||||||
count(where?: Where<Target>, options?: Options): Promise<Count>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class DefaultCustomHasManyRepository<TEntity extends Entity, TID, TRepository extends EntityCrudRepository<TEntity, TID>> extends DefaultHasManyRepository<TEntity, TID, TRepository> implements CustomHasManyRepository<TEntity> {
|
|
||||||
async count(where?: Where<TEntity>, options?: Options) {
|
|
||||||
const targetRepository = await this.getTargetRepository();
|
|
||||||
return targetRepository.count(constrainWhere(where, this.constraint), options);
|
|
||||||
}
|
|
||||||
}
|
|
149
backend/src/repositories/custom-has-many-repository.ts
Normal file
149
backend/src/repositories/custom-has-many-repository.ts
Normal file
|
@ -0,0 +1,149 @@
|
||||||
|
import {
|
||||||
|
constrainFilter,
|
||||||
|
constrainWhere,
|
||||||
|
Count,
|
||||||
|
DataObject,
|
||||||
|
DefaultHasManyRepository,
|
||||||
|
DefaultHasManyThroughRepository,
|
||||||
|
DefaultTransactionalRepository,
|
||||||
|
Entity,
|
||||||
|
EntityCrudRepository,
|
||||||
|
HasManyRepository,
|
||||||
|
HasManyThroughRepository,
|
||||||
|
InclusionResolver,
|
||||||
|
Options,
|
||||||
|
Where
|
||||||
|
} from '@loopback/repository';
|
||||||
|
import { Getter } from '@loopback/core';
|
||||||
|
|
||||||
|
export interface CustomHasManyRepositoryFactory<Target extends Entity,
|
||||||
|
ForeignKeyType,
|
||||||
|
> {
|
||||||
|
/**
|
||||||
|
* Invoke the function to obtain HasManyRepository.
|
||||||
|
*/
|
||||||
|
(fkValue: ForeignKeyType): CustomHasManyRepository<Target>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use `resolver` property to obtain an InclusionResolver for this relation.
|
||||||
|
*/
|
||||||
|
inclusionResolver: InclusionResolver<Entity, Target>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CustomHasManyRepository<Target extends Entity> extends HasManyRepository<Target> {
|
||||||
|
count(where?: Where<Target>, options?: Options): Promise<Count>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DefaultCustomHasManyRepository<TEntity extends Entity, TID, TRepository extends EntityCrudRepository<TEntity, TID>> extends DefaultHasManyRepository<TEntity, TID, TRepository> implements CustomHasManyRepository<TEntity> {
|
||||||
|
async count(where?: Where<TEntity>, options?: Options) {
|
||||||
|
const targetRepository = await this.getTargetRepository();
|
||||||
|
return targetRepository.count(constrainWhere(where, this.constraint), options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CustomHasManyThroughRepositoryFactory<TargetEntity extends Entity, TargetID, ThroughEntity extends Entity, SourceID> {
|
||||||
|
(fkValue: SourceID): CustomHasManyThroughRepository<TargetEntity, TargetID, ThroughEntity>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use `resolver` property to obtain an InclusionResolver for this relation.
|
||||||
|
*/
|
||||||
|
inclusionResolver: InclusionResolver<Entity, TargetEntity>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CustomHasManyThroughRepository<Target extends Entity, TID, Through extends Entity> extends HasManyThroughRepository<Target, TID, Through> {
|
||||||
|
count(where?: Where<Target>, options?: Options): Promise<Count>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DefaultCustomHasManyThroughRepository<TargetEntity extends Entity,
|
||||||
|
TargetID,
|
||||||
|
TargetRepository extends EntityCrudRepository<TargetEntity, TargetID>,
|
||||||
|
ThroughEntity extends Entity,
|
||||||
|
ThroughID,
|
||||||
|
ThroughRepository extends EntityCrudRepository<ThroughEntity, ThroughID>,
|
||||||
|
> extends DefaultHasManyThroughRepository<TargetEntity, TargetID, TargetRepository, ThroughEntity, ThroughID, ThroughRepository>
|
||||||
|
implements CustomHasManyThroughRepository<TargetEntity, TargetID, ThroughEntity> {
|
||||||
|
async count(where?: Where<TargetEntity>, options?: Options): Promise<Count> {
|
||||||
|
const targetRepository = await this.getTargetRepository();
|
||||||
|
const throughRepository = await this.getThroughRepository();
|
||||||
|
const sourceConstraint = this.getThroughConstraintFromSource();
|
||||||
|
const throughInstances = await throughRepository.find(
|
||||||
|
constrainFilter(undefined, sourceConstraint),
|
||||||
|
options?.throughOptions,
|
||||||
|
);
|
||||||
|
const targetConstraint =
|
||||||
|
this.getTargetConstraintFromThroughModels(throughInstances);
|
||||||
|
return targetRepository.count(
|
||||||
|
constrainWhere(where, targetConstraint),
|
||||||
|
options,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CustomCrudRepository<T extends Entity, ID, Relations extends object = {}> extends DefaultTransactionalRepository<T, ID, Relations> {
|
||||||
|
createCustomHasManyRepositoryFactoryFor<Target extends Entity, TargetID, ForeignKeyType extends Target[ForeignKeyName], ForeignKeyName extends keyof Target>(
|
||||||
|
relationName: string,
|
||||||
|
targetRepositoryGetter: Getter<EntityCrudRepository<Target, TargetID>>,
|
||||||
|
fkName: ForeignKeyName
|
||||||
|
): CustomHasManyRepositoryFactory<Target, ForeignKeyType | undefined> {
|
||||||
|
const origEntities = this.createHasManyRepositoryFactoryFor(relationName, targetRepositoryGetter);
|
||||||
|
this.registerInclusionResolver(relationName, origEntities.inclusionResolver);
|
||||||
|
const entities = function(fkValue: ForeignKeyType | undefined) {
|
||||||
|
return new DefaultCustomHasManyRepository(targetRepositoryGetter, {[fkName]: fkValue} as unknown as DataObject<Target>);
|
||||||
|
};
|
||||||
|
entities.inclusionResolver = origEntities.inclusionResolver;
|
||||||
|
return entities;
|
||||||
|
}
|
||||||
|
|
||||||
|
createCustomHasManyThroughFactoryFor<Target extends Entity, Through extends Entity, ThroughID, SourceKeyName extends keyof Through, TargetKeyName extends keyof Through>(
|
||||||
|
relationName: string,
|
||||||
|
targetRepoGetter: Getter<EntityCrudRepository<Target, Through[TargetKeyName]>>,
|
||||||
|
throughRepoGetter: Getter<EntityCrudRepository<Through, ThroughID>>,
|
||||||
|
sourceKeyName: SourceKeyName,
|
||||||
|
targetKeyName: TargetKeyName
|
||||||
|
): CustomHasManyThroughRepositoryFactory<Target, Through[TargetKeyName] | undefined, Through, Through[SourceKeyName] | undefined> {
|
||||||
|
const origEntities = this.createHasManyThroughRepositoryFactoryFor(relationName, targetRepoGetter, throughRepoGetter);
|
||||||
|
this.registerInclusionResolver(relationName, origEntities.inclusionResolver);
|
||||||
|
const entities = function(fkValue: Through[SourceKeyName] | undefined) {
|
||||||
|
function getTargetConstraintFromThroughModels(
|
||||||
|
throughInstances: Through[],
|
||||||
|
): DataObject<Target> {
|
||||||
|
const keys = throughInstances.map(instance => instance.getId());
|
||||||
|
return {id: keys.length > 1 ? {inq: keys} : keys[0]} as unknown as DataObject<Target>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTargetKeys(throughInstances: Through[]): Through[TargetKeyName][] {
|
||||||
|
return throughInstances.map(instance => instance[targetKeyName]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getThroughConstraintFromSource(): DataObject<Through> {
|
||||||
|
return {[sourceKeyName]: fkValue} as unknown as DataObject<Through>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTargetIds(targetInstances: Target[]): Through[TargetKeyName][] {
|
||||||
|
return targetInstances.map(target => target.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
function getThroughConstraintFromTarget(fkValues: Through[TargetKeyName][]): DataObject<Through> {
|
||||||
|
return {[targetKeyName]: fkValues.length > 1 ? {inq: fkValues} : fkValues[0]} as unknown as DataObject<Through>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new DefaultCustomHasManyThroughRepository<Target,
|
||||||
|
Through[TargetKeyName],
|
||||||
|
EntityCrudRepository<Target, Through[TargetKeyName]>,
|
||||||
|
Through,
|
||||||
|
ThroughID,
|
||||||
|
EntityCrudRepository<Through, ThroughID>>(
|
||||||
|
targetRepoGetter,
|
||||||
|
throughRepoGetter,
|
||||||
|
getTargetConstraintFromThroughModels,
|
||||||
|
getTargetKeys,
|
||||||
|
getThroughConstraintFromSource,
|
||||||
|
getTargetIds,
|
||||||
|
getThroughConstraintFromTarget,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
entities.inclusionResolver = origEntities.inclusionResolver;
|
||||||
|
return entities;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,14 +1,12 @@
|
||||||
import { Getter, inject } from '@loopback/core';
|
import { Getter, inject } from '@loopback/core';
|
||||||
import { DataObject, DefaultCrudRepository, repository } from '@loopback/repository';
|
import { repository } from '@loopback/repository';
|
||||||
import { DatabaseDataSource } from '../datasources';
|
import { DatabaseDataSource } from '../datasources';
|
||||||
import { Course, Subject, SubjectRelations } from '../models';
|
import { Course, Subject, SubjectRelations } from '../models';
|
||||||
import { UserRepository } from './user.repository';
|
import { UserRepository } from './user.repository';
|
||||||
import { CourseRepository } from './course.repository';
|
import { CourseRepository } from './course.repository';
|
||||||
import { CustomHasManyRepositoryFactory, DefaultCustomHasManyRepository } from './CustomHasManyRepository';
|
import { CustomCrudRepository, CustomHasManyRepositoryFactory } from './custom-has-many-repository';
|
||||||
|
|
||||||
export class SubjectRepository extends DefaultCrudRepository<Subject,
|
export class SubjectRepository extends CustomCrudRepository<Subject, typeof Subject.prototype.id, SubjectRelations> {
|
||||||
typeof Subject.prototype.id,
|
|
||||||
SubjectRelations> {
|
|
||||||
|
|
||||||
public readonly courses: CustomHasManyRepositoryFactory<Course, typeof Subject.prototype.id>;
|
public readonly courses: CustomHasManyRepositoryFactory<Course, typeof Subject.prototype.id>;
|
||||||
|
|
||||||
|
@ -16,12 +14,6 @@ export class SubjectRepository extends DefaultCrudRepository<Subject,
|
||||||
@inject('datasources.database') dataSource: DatabaseDataSource, @repository.getter('UserRepository') protected userRepositoryGetter: Getter<UserRepository>, @repository.getter('CourseRepository') protected courseRepositoryGetter: Getter<CourseRepository>,
|
@inject('datasources.database') dataSource: DatabaseDataSource, @repository.getter('UserRepository') protected userRepositoryGetter: Getter<UserRepository>, @repository.getter('CourseRepository') protected courseRepositoryGetter: Getter<CourseRepository>,
|
||||||
) {
|
) {
|
||||||
super(Subject, dataSource);
|
super(Subject, dataSource);
|
||||||
const origCourses = this.createHasManyRepositoryFactoryFor('courses', courseRepositoryGetter,);
|
this.courses = this.createCustomHasManyRepositoryFactoryFor('courses', courseRepositoryGetter, 'subjectId');
|
||||||
this.registerInclusionResolver('courses', origCourses.inclusionResolver);
|
|
||||||
const courses = function(fkValue: number | undefined) {
|
|
||||||
return new DefaultCustomHasManyRepository(courseRepositoryGetter, {subject_id: fkValue} as DataObject<Course>);
|
|
||||||
};
|
|
||||||
courses.inclusionResolver = origCourses.inclusionResolver;
|
|
||||||
this.courses = courses;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,16 +1,17 @@
|
||||||
import { inject, Getter } from '@loopback/core';
|
import { Getter, inject } from '@loopback/core';
|
||||||
import { repository, HasManyThroughRepositoryFactory, DefaultTransactionalRepository } from '@loopback/repository';
|
import { repository } from '@loopback/repository';
|
||||||
import { DatabaseDataSource } from '../datasources';
|
import { DatabaseDataSource } from '../datasources';
|
||||||
import { User, UserRelations, Course, CourseUser } from '../models';
|
import { Course, CourseUser, User, UserRelations } from '../models';
|
||||||
import { SubjectRepository } from './subject.repository';
|
import { SubjectRepository } from './subject.repository';
|
||||||
import { CourseUserRepository } from './course-user.repository';
|
import { CourseUserRepository } from './course-user.repository';
|
||||||
import { CourseRepository } from './course.repository';
|
import { CourseRepository } from './course.repository';
|
||||||
|
import { CustomCrudRepository, CustomHasManyThroughRepositoryFactory } from './custom-has-many-repository';
|
||||||
|
|
||||||
export class UserRepository extends DefaultTransactionalRepository<User,
|
export class UserRepository extends CustomCrudRepository<User,
|
||||||
typeof User.prototype.id,
|
typeof User.prototype.id,
|
||||||
UserRelations> {
|
UserRelations> {
|
||||||
|
|
||||||
public readonly courses: HasManyThroughRepositoryFactory<Course, typeof Course.prototype.id,
|
public readonly courses: CustomHasManyThroughRepositoryFactory<Course, typeof Course.prototype.id,
|
||||||
CourseUser,
|
CourseUser,
|
||||||
typeof User.prototype.id>;
|
typeof User.prototype.id>;
|
||||||
|
|
||||||
|
@ -18,7 +19,6 @@ export class UserRepository extends DefaultTransactionalRepository<User,
|
||||||
@inject('datasources.database') dataSource: DatabaseDataSource, @repository.getter('SubjectRepository') protected subjectRepositoryGetter: Getter<SubjectRepository>, @repository.getter('CourseUserRepository') protected courseUserRepositoryGetter: Getter<CourseUserRepository>, @repository.getter('CourseRepository') protected courseRepositoryGetter: Getter<CourseRepository>,
|
@inject('datasources.database') dataSource: DatabaseDataSource, @repository.getter('SubjectRepository') protected subjectRepositoryGetter: Getter<SubjectRepository>, @repository.getter('CourseUserRepository') protected courseUserRepositoryGetter: Getter<CourseUserRepository>, @repository.getter('CourseRepository') protected courseRepositoryGetter: Getter<CourseRepository>,
|
||||||
) {
|
) {
|
||||||
super(User, dataSource);
|
super(User, dataSource);
|
||||||
this.courses = this.createHasManyThroughRepositoryFactoryFor('courses', courseRepositoryGetter, courseUserRepositoryGetter,);
|
this.courses = this.createCustomHasManyThroughFactoryFor('courses', courseRepositoryGetter, courseUserRepositoryGetter, 'userId', 'courseId');
|
||||||
this.registerInclusionResolver('courses', this.courses.inclusionResolver);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { BreakpointObserver, Breakpoints } from '@angular/cdk/layout';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { filter, map, shareReplay } from 'rxjs/operators';
|
import { filter, map, shareReplay } from 'rxjs/operators';
|
||||||
import { LoginService } from './auth/login.service';
|
import { LoginService } from './auth/login.service';
|
||||||
import { ActivatedRoute, ActivatedRouteSnapshot, NavigationEnd, Route, Router, Routes } from '@angular/router';
|
import { ActivatedRoute, ActivatedRouteSnapshot, NavigationEnd, Params, Route, Router, Routes } from '@angular/router';
|
||||||
import { RouteData } from './app-routing.module';
|
import { RouteData } from './app-routing.module';
|
||||||
import { Title } from '@angular/platform-browser';
|
import { Title } from '@angular/platform-browser';
|
||||||
|
|
||||||
|
@ -27,7 +27,7 @@ export class AppComponent implements OnInit {
|
||||||
|
|
||||||
pageTitle: string;
|
pageTitle: string;
|
||||||
|
|
||||||
private activeRouteTitle: string;
|
private activeComponentVars: object;
|
||||||
|
|
||||||
constructor(private breakpointObserver: BreakpointObserver, public loginService: LoginService,
|
constructor(private breakpointObserver: BreakpointObserver, public loginService: LoginService,
|
||||||
private router: Router, private activeRoute: ActivatedRoute, private title: Title) {
|
private router: Router, private activeRoute: ActivatedRoute, private title: Title) {
|
||||||
|
@ -43,7 +43,9 @@ export class AppComponent implements OnInit {
|
||||||
item.title = 'NOTFOUND';
|
item.title = 'NOTFOUND';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.setPageTitle();
|
this.router.events.pipe(filter(event => event instanceof NavigationEnd)).subscribe(() => {
|
||||||
|
this.setPageTitle();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getRoutes(data: { path: string, routes: Routes }[]): { routes: Routes, path: string, currentRoute: Route }[] {
|
getRoutes(data: { path: string, routes: Routes }[]): { routes: Routes, path: string, currentRoute: Route }[] {
|
||||||
|
@ -82,32 +84,44 @@ export class AppComponent implements OnInit {
|
||||||
return routeParts;
|
return routeParts;
|
||||||
}
|
}
|
||||||
const data = snapshot.data as RouteData;
|
const data = snapshot.data as RouteData;
|
||||||
if (data.title && snapshot.url.length) {
|
if (data.title) {
|
||||||
routeParts.push({
|
routeParts.push({
|
||||||
title: data.title,
|
title: data.title,
|
||||||
url: snapshot.url[0].path
|
url: snapshot.url.length ? snapshot.url[0].path : undefined,
|
||||||
|
params: snapshot.params
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return routeParts;
|
return routeParts;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getTitleParts(): string[] {
|
||||||
|
const routeParts = this.getRouteSegments(this.activeRoute.snapshot);
|
||||||
|
if (!routeParts.length) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function replaceVars(replacements: object, title: string): string {
|
||||||
|
return Object.keys(replacements).reduce((pv, cv) => pv.replace(':' + cv, replacements[cv]), title);
|
||||||
|
}
|
||||||
|
|
||||||
|
let titleParts = routeParts.reverse().map(part => replaceVars(part.params, part.title));
|
||||||
|
if (this.activeComponentVars) {
|
||||||
|
titleParts = titleParts.map(part => replaceVars(this.activeComponentVars, part));
|
||||||
|
}
|
||||||
|
return titleParts.map(part => part.startsWith(':') ? '~' : part);
|
||||||
|
}
|
||||||
|
|
||||||
setPageTitle(): void {
|
setPageTitle(): void {
|
||||||
this.router.events.pipe(filter(event => event instanceof NavigationEnd)).subscribe(() => {
|
const titleParts = this.getTitleParts();
|
||||||
const routeParts = this.getRouteSegments(this.activeRoute.snapshot);
|
if (!titleParts.length) {
|
||||||
if (!routeParts.length) {
|
this.pageTitle = 'Szakdolgozat';
|
||||||
this.pageTitle = 'Szakdolgozat';
|
return this.title.setTitle('Szakdolgozat');
|
||||||
return this.title.setTitle('Szakdolgozat');
|
}
|
||||||
}
|
let pageTitle = titleParts.reduce((partA, partI) => `${partA} > ${partI}`);
|
||||||
const titleParts = routeParts.reverse().map(part => part.title);
|
this.pageTitle = pageTitle;
|
||||||
if (this.activeRouteTitle) { // TODO: Get title parts from parent components even if parent wasn't opened
|
pageTitle += ` | Szakdolgozat`;
|
||||||
titleParts[titleParts.length - 1] = this.activeRouteTitle;
|
this.title.setTitle(pageTitle);
|
||||||
}
|
|
||||||
let pageTitle = titleParts.reduce((partA, partI) => `${partA} > ${partI}`);
|
|
||||||
this.pageTitle = pageTitle;
|
|
||||||
pageTitle += ` | Szakdolgozat`;
|
|
||||||
this.title.setTitle(pageTitle);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async logout(): Promise<void> {
|
async logout(): Promise<void> {
|
||||||
|
@ -119,23 +133,25 @@ export class AppComponent implements OnInit {
|
||||||
return this.menu.filter(item => item.requiredRole === 'admin' ? this.loginService.user?.isAdmin : true); // TODO: Roles
|
return this.menu.filter(item => item.requiredRole === 'admin' ? this.loginService.user?.isAdmin : true); // TODO: Roles
|
||||||
}
|
}
|
||||||
|
|
||||||
routeActivated($event: any): void {
|
async routeActivated($event: any): Promise<void> {
|
||||||
if (this.isCustomTitleComponent($event)) {
|
if (this.isCustomTitleComponent($event)) {
|
||||||
this.activeRouteTitle = $event.getPageTitle();
|
const title = $event.getPageTitleVars();
|
||||||
|
this.activeComponentVars = title instanceof Promise ? await title : title;
|
||||||
} else {
|
} else {
|
||||||
this.activeRouteTitle = null;
|
this.activeComponentVars = null;
|
||||||
}
|
}
|
||||||
|
this.setPageTitle();
|
||||||
}
|
}
|
||||||
|
|
||||||
isCustomTitleComponent(obj: any): obj is CustomTitleComponent {
|
isCustomTitleComponent(obj: any): obj is CustomTitleComponent {
|
||||||
return obj?.getPageTitle instanceof Function;
|
return obj?.getPageTitleVars instanceof Function;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type MenuItem = { path: string, requiredRole: 'admin', title?: string }; // TODO: Role
|
type MenuItem = { path: string, requiredRole: 'admin', title?: string }; // TODO: Role
|
||||||
type RouteSegment = { title: string, url: string };
|
type RouteSegment = { title: string, url: string, params: Params };
|
||||||
|
|
||||||
export interface CustomTitleComponent {
|
export interface CustomTitleComponent {
|
||||||
getPageTitle(): string;
|
getPageTitleVars(): object | Promise<object>;
|
||||||
}
|
}
|
||||||
|
|
33
frontend/src/app/graphql/course.graphql
Normal file
33
frontend/src/app/graphql/course.graphql
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
query CourseListBySubject($subject: ID!, $limit: Int!, $offset: Int!) {
|
||||||
|
coursesBySubject(subject: $subject, limit: $limit, offset: $offset) {
|
||||||
|
count
|
||||||
|
list {
|
||||||
|
id
|
||||||
|
alias
|
||||||
|
semester
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
query CourseListByUser($user: ID!, $limit: Int!, $offset: Int!) {
|
||||||
|
coursesByUser(user: $user, limit: $limit, offset: $offset) {
|
||||||
|
list {
|
||||||
|
id
|
||||||
|
alias
|
||||||
|
semester
|
||||||
|
}
|
||||||
|
count
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
query Course($id: ID!) {
|
||||||
|
course(id: $id) {
|
||||||
|
id
|
||||||
|
semester
|
||||||
|
alias
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mutation EditCourse($input: CourseUpdateInput!) {
|
||||||
|
courseUpdate(course: $input)
|
||||||
|
}
|
|
@ -16,10 +16,13 @@ mutation Logout {
|
||||||
|
|
||||||
query UserList($limit: Int!, $offset: Int!) {
|
query UserList($limit: Int!, $offset: Int!) {
|
||||||
users(limit: $limit, offset: $offset) {
|
users(limit: $limit, offset: $offset) {
|
||||||
id
|
list {
|
||||||
name
|
id
|
||||||
email
|
name
|
||||||
isAdmin
|
email
|
||||||
|
isAdmin
|
||||||
|
}
|
||||||
|
count
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -55,6 +55,11 @@ export class EditComponent<T extends HasID, QT extends QueryResult<T>, UT extend
|
||||||
} else {
|
} else {
|
||||||
this.item = {} as T;
|
this.item = {} as T;
|
||||||
this.creating = true;
|
this.creating = true;
|
||||||
|
if (!this.createMutation) {
|
||||||
|
alert('Nem hozható létre új elem ezzel a tipussal');
|
||||||
|
await this.router.navigate(['..'], {relativeTo: this.route});
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,15 +3,18 @@ import { PageEvent } from '@angular/material/paginator';
|
||||||
import { PaginationData } from '../../utility/pagination-data';
|
import { PaginationData } from '../../utility/pagination-data';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { Query } from 'apollo-angular';
|
import { Query } from 'apollo-angular';
|
||||||
|
import { ListVariableType } from '../../utility/types';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-list',
|
selector: 'app-list',
|
||||||
templateUrl: './list.component.html',
|
templateUrl: './list.component.html',
|
||||||
styleUrls: ['./list.component.css']
|
styleUrls: ['./list.component.css']
|
||||||
})
|
})
|
||||||
export class ListComponent<T extends { id: number }, U extends { [entityName: string]: { count: number; list: T[] } }> implements OnInit {
|
export class ListComponent<T extends { id: number }, U extends { [entityName: string]: { count: number; list: T[] } },
|
||||||
|
V extends object | undefined> implements OnInit {
|
||||||
|
|
||||||
@Input() gql: Query<U, { limit: number; offset: number }>;
|
@Input() gql: Query<U, ListVariableType<V>>;
|
||||||
|
@Input() queryVariables: V;
|
||||||
@Input() itemType: T; // TODO: Remove
|
@Input() itemType: T; // TODO: Remove
|
||||||
@Input() columns: { title: string, prop: keyof T }[];
|
@Input() columns: { title: string, prop: keyof T }[];
|
||||||
@Input() allowNew = false;
|
@Input() allowNew = false;
|
||||||
|
@ -36,7 +39,8 @@ export class ListComponent<T extends { id: number }, U extends { [entityName: st
|
||||||
async getItems(limit: number, page: number): Promise<void> {
|
async getItems(limit: number, page: number): Promise<void> {
|
||||||
try {
|
try {
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
const {data} = await this.gql.fetch({limit, offset: (page - 1) * limit}).toPromise(); // TODO: Watch
|
const vars = (this.queryVariables === undefined ? {} : this.queryVariables) as V extends object ? V : {};
|
||||||
|
const {data} = await this.gql.fetch({...vars, limit, offset: (page - 1) * limit}).toPromise(); // TODO: Watch
|
||||||
const key = Object.keys(data).filter(k => k !== '__typename')[0];
|
const key = Object.keys(data).filter(k => k !== '__typename')[0];
|
||||||
this.paginationData.total = data[key].count;
|
this.paginationData.total = data[key].count;
|
||||||
this.paginationData.page = page;
|
this.paginationData.page = page;
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
<!-- <app-edit [beforeSubmit]="beforeSubmit" [fields]="[
|
<app-edit [beforeSubmit]="beforeSubmit" [gql]="itemGQL" [itemType]="itemType" [updateMutation]="editGQL" [fields]="[
|
||||||
{title: 'Félév', name: 'semester'},
|
{title: 'Félév', name: 'semester'},
|
||||||
{title: 'Név', name: 'alias'}
|
{title: 'Név', name: 'alias'}
|
||||||
]"></app-edit> -->
|
]"></app-edit>
|
||||||
|
|
|
@ -1,18 +1,27 @@
|
||||||
import { Component, OnInit } from '@angular/core';
|
import { Component, OnInit } from '@angular/core';
|
||||||
import { ActivatedRoute } from '@angular/router';
|
import { ActivatedRoute } from '@angular/router';
|
||||||
|
import { CustomTitleComponent } from '../../../../app.component';
|
||||||
|
import { Course, CourseGQL, EditCourseGQL, SubjectGQL } from '../../../../services/graphql';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-course-edit',
|
selector: 'app-course-edit',
|
||||||
templateUrl: './course-edit.component.html',
|
templateUrl: './course-edit.component.html',
|
||||||
styleUrls: ['./course-edit.component.css']
|
styleUrls: ['./course-edit.component.css']
|
||||||
})
|
})
|
||||||
export class CourseEditComponent implements OnInit {
|
export class CourseEditComponent implements OnInit, CustomTitleComponent {
|
||||||
|
subjectId: string;
|
||||||
|
itemType: Course;
|
||||||
beforeSubmit = () => ({subjectId: +this.route.snapshot.params.subjectId});
|
beforeSubmit = () => ({subjectId: +this.route.snapshot.params.subjectId});
|
||||||
|
|
||||||
constructor(private route: ActivatedRoute) {
|
constructor(private route: ActivatedRoute, public subjectGQL: SubjectGQL, public itemGQL: CourseGQL, public editGQL: EditCourseGQL) {
|
||||||
|
this.subjectId = route.snapshot.params.subjectId;
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getPageTitleVars(): object | Promise<object> {
|
||||||
|
return this.subjectGQL.fetch({id: this.subjectId}).toPromise().then(subject => ({subjectName: subject.data.subject.name}));
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
<app-list [itemType]="itemType" allowNew="true" [columns]="[
|
<app-list [itemType]="itemType" allowNew="true" [gql]="listGQL" [queryVariables]="{subject: subjectId}" [columns]="[
|
||||||
{prop: 'semester', title: 'Félév'},
|
{prop: 'semester', title: 'Félév'},
|
||||||
{prop: 'alias', title: 'Név'}
|
{prop: 'alias', title: 'Név'}
|
||||||
]"></app-list>
|
]"></app-list>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { Component, OnInit } from '@angular/core';
|
import { Component, OnInit } from '@angular/core';
|
||||||
import { ActivatedRoute } from '@angular/router';
|
import { ActivatedRoute } from '@angular/router';
|
||||||
import { CustomTitleComponent } from '../../../../app.component';
|
import { CustomTitleComponent } from '../../../../app.component';
|
||||||
import { Course, SubjectGQL } from '../../../../services/graphql';
|
import { Course, CourseListBySubjectGQL, SubjectGQL } from '../../../../services/graphql';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-courses',
|
selector: 'app-courses',
|
||||||
|
@ -12,14 +12,14 @@ export class CourseListComponent implements OnInit, CustomTitleComponent {
|
||||||
subjectId: string;
|
subjectId: string;
|
||||||
itemType: Course;
|
itemType: Course;
|
||||||
|
|
||||||
constructor(route: ActivatedRoute, public listGQL: SubjectGQL) {
|
constructor(route: ActivatedRoute, public subjectGQL: SubjectGQL, public listGQL: CourseListBySubjectGQL) {
|
||||||
this.subjectId = route.snapshot.params.subjectId;
|
this.subjectId = route.snapshot.params.subjectId;
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
}
|
}
|
||||||
|
|
||||||
getPageTitle(): string {
|
getPageTitleVars(): object | Promise<object> {
|
||||||
return 'Custom title'; //TODO: SubjectGQL
|
return this.subjectGQL.fetch({id: this.subjectId}).toPromise().then(subject => ({subjectName: subject.data.subject.name}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,10 +9,10 @@ import { CourseListComponent } from './subject-edit/courses/course-list/course-l
|
||||||
import { CourseEditComponent } from './subject-edit/courses/course-edit/course-edit.component';
|
import { CourseEditComponent } from './subject-edit/courses/course-edit/course-edit.component';
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
{path: '', component: SubjectListComponent, data: {title: 'Tárgyak'} as RouteData},
|
{path: '', component: SubjectListComponent, data: {title: ''} as RouteData},
|
||||||
{path: ':id', component: SubjectEditComponent, data: {title: 'Szerkesztés'}},
|
{path: ':id', component: SubjectEditComponent, data: {title: 'Szerkesztés'}},
|
||||||
{
|
{
|
||||||
path: ':subjectId/courses', data: {title: 'Kurzusok'}, children: [
|
path: ':subjectId/courses', data: {title: ':subjectName'}, children: [
|
||||||
{path: ':id', component: CourseEditComponent, data: {title: 'Szerkesztés'} as RouteData},
|
{path: ':id', component: CourseEditComponent, data: {title: 'Szerkesztés'} as RouteData},
|
||||||
{path: '', component: CourseListComponent, data: {title: 'Kurzusok'}}
|
{path: '', component: CourseListComponent, data: {title: 'Kurzusok'}}
|
||||||
]
|
]
|
||||||
|
|
|
@ -7,7 +7,7 @@ import { SharedComponentsModule } from '../shared-components/shared-components.m
|
||||||
import { UserEditComponent } from './user-edit/user-edit.component';
|
import { UserEditComponent } from './user-edit/user-edit.component';
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
{path: '', component: UserListComponent, data: {title: 'Felhasználók'} as RouteData},
|
{path: '', component: UserListComponent, data: {title: ''} as RouteData},
|
||||||
{path: ':id', component: UserEditComponent, data: {title: 'Szerkesztés'}}
|
{path: ':id', component: UserEditComponent, data: {title: 'Szerkesztés'}}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
@ -3,3 +3,5 @@ import { Scalars } from '../services/graphql';
|
||||||
export type HasID = { id: Scalars['ID'] };
|
export type HasID = { id: Scalars['ID'] };
|
||||||
export type QueryResult<T extends HasID> = { [entityName: string]: T };
|
export type QueryResult<T extends HasID> = { [entityName: string]: T };
|
||||||
export type MutationInput<T extends Partial<U>, U extends HasID> = { input: T };
|
export type MutationInput<T extends Partial<U>, U extends HasID> = { input: T };
|
||||||
|
export type ListInput = { limit: number; offset: number };
|
||||||
|
export type ListVariableType<V extends object | undefined> = (V extends object ? V : {}) & ListInput;
|
||||||
|
|
Loading…
Reference in a new issue