Add course lists to subjects with add/edit option
Added count() method for hasManyThrough relations in a slightly hacky way
This commit is contained in:
parent
2a7aa2a65a
commit
9f78639690
28 changed files with 350 additions and 80 deletions
134
backend/src/controllers/course.controller.ts
Normal file
134
backend/src/controllers/course.controller.ts
Normal file
|
@ -0,0 +1,134 @@
|
||||||
|
import { Count, CountSchema, Filter, FilterExcludingWhere, repository, Where, } from '@loopback/repository';
|
||||||
|
import { del, get, getModelSchemaRef, param, patch, post, put, requestBody, response, } from '@loopback/rest';
|
||||||
|
import { Course } from '../models';
|
||||||
|
import { CourseRepository } from '../repositories';
|
||||||
|
|
||||||
|
export class CourseController {
|
||||||
|
constructor(
|
||||||
|
@repository(CourseRepository)
|
||||||
|
public courseRepository: CourseRepository,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@post('/courses')
|
||||||
|
@response(200, {
|
||||||
|
description: 'Course model instance',
|
||||||
|
content: {'application/json': {schema: getModelSchemaRef(Course)}},
|
||||||
|
})
|
||||||
|
async create(
|
||||||
|
@requestBody({
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: getModelSchemaRef(Course, {
|
||||||
|
title: 'NewCourse',
|
||||||
|
exclude: ['id'],
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
course: Omit<Course, 'id'>,
|
||||||
|
): Promise<Course> {
|
||||||
|
return this.courseRepository.create(course);
|
||||||
|
}
|
||||||
|
|
||||||
|
@get('/courses/count')
|
||||||
|
@response(200, {
|
||||||
|
description: 'Course model count',
|
||||||
|
content: {'application/json': {schema: CountSchema}},
|
||||||
|
})
|
||||||
|
async count(
|
||||||
|
@param.where(Course) where?: Where<Course>,
|
||||||
|
): Promise<Count> {
|
||||||
|
return this.courseRepository.count(where);
|
||||||
|
}
|
||||||
|
|
||||||
|
@get('/courses')
|
||||||
|
@response(200, {
|
||||||
|
description: 'Array of Course model instances',
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: {
|
||||||
|
type: 'array',
|
||||||
|
items: getModelSchemaRef(Course, {includeRelations: true}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
async find(
|
||||||
|
@param.filter(Course) filter?: Filter<Course>,
|
||||||
|
): Promise<Course[]> {
|
||||||
|
return this.courseRepository.find(filter);
|
||||||
|
}
|
||||||
|
|
||||||
|
@patch('/courses')
|
||||||
|
@response(200, {
|
||||||
|
description: 'Course PATCH success count',
|
||||||
|
content: {'application/json': {schema: CountSchema}},
|
||||||
|
})
|
||||||
|
async updateAll(
|
||||||
|
@requestBody({
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: getModelSchemaRef(Course, {partial: true}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
course: Course,
|
||||||
|
@param.where(Course) where?: Where<Course>,
|
||||||
|
): Promise<Count> {
|
||||||
|
return this.courseRepository.updateAll(course, where);
|
||||||
|
}
|
||||||
|
|
||||||
|
@get('/courses/{id}')
|
||||||
|
@response(200, {
|
||||||
|
description: 'Course model instance',
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: getModelSchemaRef(Course, {includeRelations: true}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
async findById(
|
||||||
|
@param.path.number('id') id: number,
|
||||||
|
@param.filter(Course, {exclude: 'where'}) filter?: FilterExcludingWhere<Course>
|
||||||
|
): Promise<Course> {
|
||||||
|
return this.courseRepository.findById(id, filter);
|
||||||
|
}
|
||||||
|
|
||||||
|
@patch('/courses/{id}')
|
||||||
|
@response(204, {
|
||||||
|
description: 'Course PATCH success',
|
||||||
|
})
|
||||||
|
async updateById(
|
||||||
|
@param.path.number('id') id: number,
|
||||||
|
@requestBody({
|
||||||
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
schema: getModelSchemaRef(Course, {partial: true}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
course: Course,
|
||||||
|
): Promise<void> {
|
||||||
|
await this.courseRepository.updateById(id, course);
|
||||||
|
}
|
||||||
|
|
||||||
|
@put('/courses/{id}')
|
||||||
|
@response(204, {
|
||||||
|
description: 'Course PUT success',
|
||||||
|
})
|
||||||
|
async replaceById(
|
||||||
|
@param.path.number('id') id: number,
|
||||||
|
@requestBody() course: Course,
|
||||||
|
): Promise<void> {
|
||||||
|
await this.courseRepository.replaceById(id, course);
|
||||||
|
}
|
||||||
|
|
||||||
|
@del('/courses/{id}')
|
||||||
|
@response(204, {
|
||||||
|
description: 'Course DELETE success',
|
||||||
|
})
|
||||||
|
async deleteById(@param.path.number('id') id: number): Promise<void> {
|
||||||
|
await this.courseRepository.deleteById(id);
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,3 +7,4 @@ export * from './user-course.controller';
|
||||||
export * from './course-requirement.controller';
|
export * from './course-requirement.controller';
|
||||||
export * from './course-requirement.controller';
|
export * from './course-requirement.controller';
|
||||||
export * from './subject.controller';
|
export * from './subject.controller';
|
||||||
|
export * from './course.controller';
|
||||||
|
|
|
@ -1,24 +1,6 @@
|
||||||
import {
|
import { Count, CountSchema, Filter, repository, Where, } from '@loopback/repository';
|
||||||
Count,
|
import { del, get, getModelSchemaRef, getWhereSchemaFor, param, patch, post, requestBody, response, } from '@loopback/rest';
|
||||||
CountSchema,
|
import { Course, Subject, } from '../models';
|
||||||
Filter,
|
|
||||||
repository,
|
|
||||||
Where,
|
|
||||||
} from '@loopback/repository';
|
|
||||||
import {
|
|
||||||
del,
|
|
||||||
get,
|
|
||||||
getModelSchemaRef,
|
|
||||||
getWhereSchemaFor,
|
|
||||||
param,
|
|
||||||
patch,
|
|
||||||
post,
|
|
||||||
requestBody,
|
|
||||||
} from '@loopback/rest';
|
|
||||||
import {
|
|
||||||
Subject,
|
|
||||||
Course,
|
|
||||||
} from '../models';
|
|
||||||
import { SubjectRepository } from '../repositories';
|
import { SubjectRepository } from '../repositories';
|
||||||
|
|
||||||
export class SubjectCourseController {
|
export class SubjectCourseController {
|
||||||
|
@ -46,6 +28,18 @@ export class SubjectCourseController {
|
||||||
return this.subjectRepository.courses(id).find(filter);
|
return this.subjectRepository.courses(id).find(filter);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@get('/subjects/{id}/courses/count')
|
||||||
|
@response(200, {
|
||||||
|
description: 'Course model count',
|
||||||
|
content: {'application/json': {schema: CountSchema}},
|
||||||
|
})
|
||||||
|
async count(
|
||||||
|
@param.path.number('id') id: number,
|
||||||
|
@param.where(Course) where?: Where<Course>,
|
||||||
|
): Promise<Count> {
|
||||||
|
return this.subjectRepository.courses(id).count(where);
|
||||||
|
}
|
||||||
|
|
||||||
@post('/subjects/{id}/courses', {
|
@post('/subjects/{id}/courses', {
|
||||||
responses: {
|
responses: {
|
||||||
'200': {
|
'200': {
|
||||||
|
|
29
backend/src/repositories/CustomHasManyRepository.ts
Normal file
29
backend/src/repositories/CustomHasManyRepository.ts
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,21 +1,27 @@
|
||||||
import { inject, Getter } from '@loopback/core';
|
import { Getter, inject } from '@loopback/core';
|
||||||
import { DefaultCrudRepository, repository, HasManyRepositoryFactory } from '@loopback/repository';
|
import { DataObject, DefaultCrudRepository, repository } from '@loopback/repository';
|
||||||
import { DatabaseDataSource } from '../datasources';
|
import { DatabaseDataSource } from '../datasources';
|
||||||
import { Subject, SubjectRelations, Course } 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';
|
||||||
|
|
||||||
export class SubjectRepository extends DefaultCrudRepository<Subject,
|
export class SubjectRepository extends DefaultCrudRepository<Subject,
|
||||||
typeof Subject.prototype.id,
|
typeof Subject.prototype.id,
|
||||||
SubjectRelations> {
|
SubjectRelations> {
|
||||||
|
|
||||||
public readonly courses: HasManyRepositoryFactory<Course, typeof Subject.prototype.id>;
|
public readonly courses: CustomHasManyRepositoryFactory<Course, typeof Subject.prototype.id>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@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);
|
||||||
this.courses = this.createHasManyRepositoryFactoryFor('courses', courseRepositoryGetter,);
|
const origCourses = this.createHasManyRepositoryFactoryFor('courses', courseRepositoryGetter,);
|
||||||
this.registerInclusionResolver('courses', this.courses.inclusionResolver);
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -59,11 +59,10 @@ export class SzakdolgozatUserService implements UserService<User, Credentials> {
|
||||||
|
|
||||||
//function to find user by id
|
//function to find user by id
|
||||||
async findUserById(id: number): Promise<User & UserWithRelations> {
|
async findUserById(id: number): Promise<User & UserWithRelations> {
|
||||||
const userNotfound = 'invalid user';
|
|
||||||
const foundUser = await this.userRepository.findById(id);
|
const foundUser = await this.userRepository.findById(id);
|
||||||
|
|
||||||
if (!foundUser) {
|
if (!foundUser) {
|
||||||
throw new HttpErrors.Unauthorized(userNotfound);
|
throw new HttpErrors.Unauthorized('invalid user');
|
||||||
}
|
}
|
||||||
return foundUser;
|
return foundUser;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,24 @@
|
||||||
# Szakdolgozat
|
# Szakdolgozat
|
||||||
|
|
||||||
Egy webalkalmazás, amely nyomonköveti egy-egy kurzus követelményeinek teljesitését oktatók és hallgatók számára.
|
Egy webalkalmazás, amely nyomonköveti egy-egy kurzus követelményeinek teljesitését oktatók és hallgatók számára.
|
||||||
|
|
||||||
|
## Adatok
|
||||||
|
|
||||||
|
### Kurzus
|
||||||
|
|
||||||
|
Egy kurzus egy adott tárgy egy adott félévben egy adott csoporttal.
|
||||||
|
|
||||||
## Szerepkörök
|
## Szerepkörök
|
||||||
|
|
||||||
Csak bejelentkezett felhasználók férhetnek hozzá bármilyen adathoz. A saját adataikat mindig tudják módositani.
|
Csak bejelentkezett felhasználók férhetnek hozzá bármilyen adathoz. A saját adataikat mindig tudják módositani.
|
||||||
|
|
||||||
### Admin
|
### Admin
|
||||||
|
|
||||||
* Teljes jogosultsága van az adatokhoz, kivéve a felhasználók adatait
|
* Teljes jogosultsága van az adatokhoz, kivéve a felhasználók adatait
|
||||||
* Hozzá tud rendelni más felhasználókat szerepkörökhöz egy-egy kurzus kapcsán
|
* Hozzá tud rendelni más felhasználókat szerepkörökhöz egy-egy kurzus kapcsán
|
||||||
|
|
||||||
### Hallgató
|
### Hallgató
|
||||||
|
|
||||||
* Az adott kurzushoz tartozó adatokat csak megtekinteni tudja
|
* Az adott kurzushoz tartozó adatokat csak megtekinteni tudja
|
||||||
|
|
||||||
### Oktató
|
### Oktató
|
||||||
|
|
|
@ -1,8 +1,4 @@
|
||||||
{
|
{
|
||||||
"firestore": {
|
|
||||||
"rules": "firestore.rules",
|
|
||||||
"indexes": "firestore.indexes.json"
|
|
||||||
},
|
|
||||||
"hosting": {
|
"hosting": {
|
||||||
"public": "dist/Szakdolgozat",
|
"public": "dist/Szakdolgozat",
|
||||||
"ignore": [
|
"ignore": [
|
||||||
|
|
|
@ -1,4 +0,0 @@
|
||||||
{
|
|
||||||
"indexes": [],
|
|
||||||
"fieldOverrides": []
|
|
||||||
}
|
|
|
@ -1,29 +0,0 @@
|
||||||
rules_version = '2';
|
|
||||||
service cloud.firestore {
|
|
||||||
match /databases/{database}/documents {
|
|
||||||
function sameUser(user) {
|
|
||||||
return request.auth != null && request.auth.uid == user;
|
|
||||||
}
|
|
||||||
function getUserData() {
|
|
||||||
return get(/databases/$(database)/documents/users/$(request.auth.uid)).data;
|
|
||||||
}
|
|
||||||
|
|
||||||
//Felhasználói adatok kezelése
|
|
||||||
match /users/{user} {
|
|
||||||
allow read, write: if sameUser(user) && request.auth.uid == request.resource.data.author_uid;
|
|
||||||
}
|
|
||||||
|
|
||||||
//Adminisztrátoroknak mindent lehet
|
|
||||||
match /data/{document=**} {
|
|
||||||
allow get, list, create, update, delete: if auth.token.admin;
|
|
||||||
}
|
|
||||||
//Diákok megnézhetik a tárgy adatait
|
|
||||||
match /data/subjects/{subject=**} {
|
|
||||||
allow get, list: if request.auth.uid in resource.data.students;
|
|
||||||
}
|
|
||||||
//Az oktatók módosithatják a követelményeket
|
|
||||||
match /data/subjects/{subject}/requirements/{requirement=**} {
|
|
||||||
allow read, write: if request.auth.uid in resource.data.teachers;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
6
frontend/src/app/model/course.model.ts
Normal file
6
frontend/src/app/model/course.model.ts
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
import { Model } from './model';
|
||||||
|
|
||||||
|
export class Course extends Model {
|
||||||
|
semester: string;
|
||||||
|
subjectId: number;
|
||||||
|
}
|
|
@ -18,6 +18,10 @@ export class EditComponent<T extends Model> implements OnInit {
|
||||||
@Input() apiPath: string;
|
@Input() apiPath: string;
|
||||||
@Input() fields: { title: string, name: keyof T, readonly?: (item: T) => boolean }[];
|
@Input() fields: { title: string, name: keyof T, readonly?: (item: T) => boolean }[];
|
||||||
@Input() itemType: Type<T>;
|
@Input() itemType: Type<T>;
|
||||||
|
/**
|
||||||
|
* Beküldés előtt extra adat hozzáadása
|
||||||
|
*/
|
||||||
|
@Input() beforeSubmit: (item: T) => Partial<T>;
|
||||||
formGroup: FormGroup;
|
formGroup: FormGroup;
|
||||||
|
|
||||||
constructor(private api: ApiService, private router: Router, private fb: FormBuilder, private route: ActivatedRoute) {
|
constructor(private api: ApiService, private router: Router, private fb: FormBuilder, private route: ActivatedRoute) {
|
||||||
|
@ -48,13 +52,14 @@ export class EditComponent<T extends Model> implements OnInit {
|
||||||
|
|
||||||
async submit(): Promise<void> {
|
async submit(): Promise<void> {
|
||||||
this.isLoading = true;
|
this.isLoading = true;
|
||||||
|
const value = Object.assign({}, this.formGroup.value, this.beforeSubmit(this.item) ?? {});
|
||||||
try {
|
try {
|
||||||
if (this.item && !this.creating) {
|
if (this.item && !this.creating) {
|
||||||
await this.api.request('patch', this.apiPath + '/' + this.item.id, this.formGroup.value);
|
await this.api.request('patch', this.apiPath + '/' + this.item.id, value);
|
||||||
} else {
|
} else {
|
||||||
await this.api.request('post', this.apiPath, this.formGroup.value);
|
await this.api.request('post', this.apiPath, value);
|
||||||
}
|
}
|
||||||
await this.router.navigate(this.route.parent.snapshot.url.map(segment => segment.path));
|
await this.router.navigate(['..'], {relativeTo: this.route});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
alert(e.message);
|
alert(e.message);
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,5 +2,6 @@
|
||||||
<button mat-raised-button color="primary" (click)="newItem()">Hozzáadás</button>
|
<button mat-raised-button color="primary" (click)="newItem()">Hozzáadás</button>
|
||||||
</div>
|
</div>
|
||||||
<app-table [items]="items" (pageChange)="handlePageChange($event)" [paginationData]="paginationData" [columns]="columns"
|
<app-table [items]="items" (pageChange)="handlePageChange($event)" [paginationData]="paginationData" [columns]="columns"
|
||||||
[loading]="loading" (editItem)="editItem($event)">
|
[loading]="loading" (editItem)="editItem($event)" [customActions]="customActions"
|
||||||
|
[allowEditing]="allowEditing">
|
||||||
</app-table>
|
</app-table>
|
||||||
|
|
|
@ -16,6 +16,8 @@ export class ListComponent<T extends Model> implements OnInit {
|
||||||
@Input() itemType: Type<T>;
|
@Input() itemType: Type<T>;
|
||||||
@Input() columns: { title: string, prop: keyof T }[];
|
@Input() columns: { title: string, prop: keyof T }[];
|
||||||
@Input() allowNew = false;
|
@Input() allowNew = false;
|
||||||
|
@Input() customActions: { icon: string, label: string, action: (model: T) => void }[] = [];
|
||||||
|
@Input() allowEditing = true;
|
||||||
|
|
||||||
paginationData: PaginationData = {};
|
paginationData: PaginationData = {};
|
||||||
items: T[] = [];
|
items: T[] = [];
|
||||||
|
|
|
@ -12,6 +12,7 @@ import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||||
import { ReactiveFormsModule } from '@angular/forms';
|
import { ReactiveFormsModule } from '@angular/forms';
|
||||||
import { MatInputModule } from '@angular/material/input';
|
import { MatInputModule } from '@angular/material/input';
|
||||||
import { MatCheckboxModule } from '@angular/material/checkbox';
|
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||||
|
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||||
|
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
|
@ -31,7 +32,8 @@ import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||||
MatProgressSpinnerModule,
|
MatProgressSpinnerModule,
|
||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
MatInputModule,
|
MatInputModule,
|
||||||
MatCheckboxModule
|
MatCheckboxModule,
|
||||||
|
MatTooltipModule
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class SharedComponentsModule {
|
export class SharedComponentsModule {
|
||||||
|
|
|
@ -18,15 +18,18 @@
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ng-container matColumnDef="actions" stickyEnd>
|
<ng-container matColumnDef="actions" stickyEnd>
|
||||||
<th mat-header-cell *matHeaderCellDef style="width: 2rem"></th>
|
<th mat-header-cell *matHeaderCellDef [style.width]="2.5 * (1 + customActions.length) + 'rem'"></th>
|
||||||
<td mat-cell *matCellDef="let item">
|
<td mat-cell *matCellDef="let item">
|
||||||
<button mat-icon-button color="primary" (click)="editItem.emit(item)">
|
<button *ngIf="allowEditing" mat-icon-button color="primary" (click)="editItem.emit(item)">
|
||||||
<mat-icon>edit</mat-icon>
|
<mat-icon>edit</mat-icon>
|
||||||
</button>
|
</button>
|
||||||
|
<button *ngFor="let action of customActions" mat-icon-button color="primary" (click)="action.action(item)">
|
||||||
|
<mat-icon [matTooltip]="action.label">{{ action.icon }}</mat-icon>
|
||||||
|
</button>
|
||||||
</td>
|
</td>
|
||||||
<tr mat-header-row *matHeaderRowDef="getPropNames()"></tr>
|
|
||||||
<tr mat-row *matRowDef="let row; columns: getPropNames()"></tr>
|
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
<tr mat-header-row *matHeaderRowDef="getPropNames()"></tr>
|
||||||
|
<tr mat-row *matRowDef="let row; columns: getPropNames()"></tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<mat-paginator
|
<mat-paginator
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Component, EventEmitter, Input, OnInit, Output} from '@angular/core';
|
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
|
||||||
import { PaginationData } from '../../utility/pagination-data';
|
import { PaginationData } from '../../utility/pagination-data';
|
||||||
import { PageEvent } from '@angular/material/paginator';
|
import { PageEvent } from '@angular/material/paginator';
|
||||||
|
|
||||||
|
@ -14,6 +14,8 @@ export class TableComponent<T> implements OnInit {
|
||||||
@Input() loading = false;
|
@Input() loading = false;
|
||||||
@Input() columns: { title: string, prop: string }[] = [];
|
@Input() columns: { title: string, prop: string }[] = [];
|
||||||
@Input() paginationData: PaginationData = {page: 1, limit: 10};
|
@Input() paginationData: PaginationData = {page: 1, limit: 10};
|
||||||
|
@Input() customActions: { icon: string, label: string, action: (model: T) => void }[] = [];
|
||||||
|
@Input() allowEditing = true;
|
||||||
|
|
||||||
@Output() pageChange = new EventEmitter<PageEvent>();
|
@Output() pageChange = new EventEmitter<PageEvent>();
|
||||||
@Output() editItem = new EventEmitter<T>();
|
@Output() editItem = new EventEmitter<T>();
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
<app-edit #edit [itemType]="itemType" apiPath="/courses" [beforeSubmit]="beforeSubmit" [fields]="[
|
||||||
|
{title: 'Félév', name: 'semester'}
|
||||||
|
]"></app-edit>
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { CourseEditComponent } from './course-edit.component';
|
||||||
|
|
||||||
|
describe('CourseEditComponent', () => {
|
||||||
|
let component: CourseEditComponent;
|
||||||
|
let fixture: ComponentFixture<CourseEditComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
declarations: [CourseEditComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(CourseEditComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,20 @@
|
||||||
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
import { Course } from '../../../../model/course.model';
|
||||||
|
import { ActivatedRoute } from '@angular/router';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-course-edit',
|
||||||
|
templateUrl: './course-edit.component.html',
|
||||||
|
styleUrls: ['./course-edit.component.css']
|
||||||
|
})
|
||||||
|
export class CourseEditComponent implements OnInit {
|
||||||
|
itemType = Course;
|
||||||
|
beforeSubmit = () => ({subjectId: +this.route.snapshot.params.subjectId});
|
||||||
|
|
||||||
|
constructor(private route: ActivatedRoute) {
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
<app-list [apiPath]="'/subjects/'+subjectId+'/courses'" [itemType]="itemType" allowNew="true" [columns]="[
|
||||||
|
{prop: 'semester', title: 'Félév'}
|
||||||
|
]"></app-list>
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { CourseListComponent } from './course-list.component';
|
||||||
|
|
||||||
|
describe('CoursesComponent', () => {
|
||||||
|
let component: CourseListComponent;
|
||||||
|
let fixture: ComponentFixture<CourseListComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
declarations: [CourseListComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(CourseListComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,21 @@
|
||||||
|
import { Component, OnInit } from '@angular/core';
|
||||||
|
import { ActivatedRoute } from '@angular/router';
|
||||||
|
import { Course } from '../../../../model/course.model';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-courses',
|
||||||
|
templateUrl: './course-list.component.html',
|
||||||
|
styleUrls: ['./course-list.component.css']
|
||||||
|
})
|
||||||
|
export class CourseListComponent implements OnInit {
|
||||||
|
subjectId: string;
|
||||||
|
itemType = Course;
|
||||||
|
|
||||||
|
constructor(route: ActivatedRoute) {
|
||||||
|
this.subjectId = route.snapshot.params.subjectId;
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,4 +1,6 @@
|
||||||
<app-list apiPath="/subjects" [itemType]="itemType" allowNew="true" [columns]="[
|
<app-list apiPath="/subjects" [itemType]="itemType" allowNew="true" [columns]="[
|
||||||
{title: 'Név', prop: 'name'},
|
{title: 'Név', prop: 'name'},
|
||||||
{title: 'Leirás', prop: 'description'}
|
{title: 'Leirás', prop: 'description'}
|
||||||
]"></app-list>
|
]" [customActions]="[
|
||||||
|
{icon: 'chevron_right', label: 'Kurzusok', action: listCourses}
|
||||||
|
]"></app-list>
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { Component, OnInit } from '@angular/core';
|
import { Component, OnInit } from '@angular/core';
|
||||||
import { Subject } from '../../model/subject.model';
|
import { Subject } from '../../model/subject.model';
|
||||||
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-subject-list',
|
selector: 'app-subject-list',
|
||||||
|
@ -9,9 +10,14 @@ import { Subject } from '../../model/subject.model';
|
||||||
export class SubjectListComponent implements OnInit {
|
export class SubjectListComponent implements OnInit {
|
||||||
itemType = Subject;
|
itemType = Subject;
|
||||||
|
|
||||||
constructor() { }
|
constructor(private route: ActivatedRoute, private router: Router) {
|
||||||
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
listCourses = (subject: Subject): void => {
|
||||||
|
this.router.navigate([subject.id, 'courses'], {relativeTo: this.route});
|
||||||
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,14 +5,22 @@ import { SubjectEditComponent } from './subject-edit/subject-edit.component';
|
||||||
import { SharedComponentsModule } from '../shared-components/shared-components.module';
|
import { SharedComponentsModule } from '../shared-components/shared-components.module';
|
||||||
import { RouterModule, Routes } from '@angular/router';
|
import { RouterModule, Routes } from '@angular/router';
|
||||||
import { RouteData } from '../app-routing.module';
|
import { RouteData } from '../app-routing.module';
|
||||||
|
import { CourseListComponent } from './subject-edit/courses/course-list/course-list.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: 'Tárgyak'} as RouteData},
|
||||||
{path: ':id', component: SubjectEditComponent, data: {title: 'Szerkesztés'}}
|
{path: ':id', component: SubjectEditComponent, data: {title: 'Szerkesztés'}},
|
||||||
|
{
|
||||||
|
path: ':subjectId/courses', children: [
|
||||||
|
{path: ':id', component: CourseEditComponent, data: {title: 'Szerkesztés'} as RouteData},
|
||||||
|
{path: '', component: CourseListComponent, data: {title: 'Kurzusok'}}
|
||||||
|
]
|
||||||
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [SubjectListComponent, SubjectEditComponent],
|
declarations: [SubjectListComponent, SubjectEditComponent, CourseListComponent, CourseEditComponent],
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
SharedComponentsModule,
|
SharedComponentsModule,
|
||||||
|
|
Loading…
Reference in a new issue