Skip to content

Commit

Permalink
#585 Add button for adding/removing all children profiles from user's…
Browse files Browse the repository at this point in the history
… managed profiles (#586)
  • Loading branch information
oroztocil committed Sep 7, 2023
1 parent 752e0db commit 9544026
Show file tree
Hide file tree
Showing 10 changed files with 240 additions and 74 deletions.
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
<table class="table table-hover">
<tbody>
<tr *ngFor="let profile of profiles">
<th>{{profile.name}}</th>
<tr *ngFor="let model of selectionModels">
<th>{{model.profile.name}}</th>
<td class="text-right">
<button *ngIf="!hasProfile(profile.id)" class="btn btn-primary btn-sm" (click)="addProfile(profile.id)">Přidat</button>
<button *ngIf="hasProfile(profile.id)" class="btn btn-default btn-sm" (click)="removeProfile(profile.id)">Odebrat</button>
<button *ngIf="model.children.length > 0 && !model.isAnyChildrenManaged" class="btn btn-success btn-sm mr-3"
(click)="addChildren(model)">Přidat podřazené profily</button>
<button *ngIf="model.children.length > 0 && model.isAnyChildrenManaged" class="btn btn-danger btn-sm mr-3"
(click)="removeChildren(model)">Odebrat podřazené profily</button>
<button *ngIf="!model.isManaged" class="btn btn-primary btn-sm"
(click)="addProfile(model.profile.id)">Přidat</button>
<button *ngIf="model.isManaged" class="btn btn-default btn-sm"
(click)="removeProfile(model.profile.id)">Odebrat</button>
</td>
</tr>
</tbody>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,13 @@ import { NG_VALUE_ACCESSOR, ControlValueAccessor } from '@angular/forms';
import { AdminService } from 'app/services/admin.service';
import { Profile } from 'app/schema';

interface ProfileSelectionModel {
profile: Profile;
children: Profile[];
isManaged: boolean;
isAnyChildrenManaged: boolean;
}

@Component({
selector: 'managed-profiles-selector',
templateUrl: './managed-profiles-selector.component.html',
Expand All @@ -20,6 +27,7 @@ export class ManagedProfilesSelectorComponent implements OnInit, ControlValueAcc
managedProfiles: number[]

profiles: Profile[];
selectionModels: ProfileSelectionModel[];

constructor(
private adminService: AdminService,
Expand All @@ -41,25 +49,65 @@ export class ManagedProfilesSelectorComponent implements OnInit, ControlValueAcc
}

addProfile(profileId: number) {
if (!this.hasProfile(profileId)) this.managedProfiles.push(profileId);
if (!this.userManagesProfile(profileId)) this.managedProfiles.push(profileId);
this.onChange(this.managedProfiles);
this.cdRef.markForCheck();
this.updateSelectionModels();
}

removeProfile(profileId: number) {
const i = this.managedProfiles.indexOf(profileId);
if (i !== -1) this.managedProfiles.splice(i, 1);
this.onChange(this.managedProfiles);
this.cdRef.markForCheck();
this.updateSelectionModels();
}

addChildren(model: ProfileSelectionModel) {
model.children.forEach(child => {
if (!this.userManagesProfile(child.id)) {
this.managedProfiles.push(child.id);
}
});

this.onChange(this.managedProfiles);
this.cdRef.markForCheck();
this.updateSelectionModels();
}

removeChildren(model: ProfileSelectionModel) {
model.children.forEach(child => {
const i = this.managedProfiles.indexOf(child.id);

if (i !== -1) {
this.managedProfiles.splice(i, 1);
}
});

this.onChange(this.managedProfiles);
this.cdRef.markForCheck();
this.updateSelectionModels();
}

async loadProfiles() {
this.profiles = await this.adminService.getProfiles();
this.profiles.sort((a,b) => (Number(this.hasProfile(b.id)) - Number(this.hasProfile(a.id))) || (a.name.localeCompare(b.name)));
this.profiles.sort((a, b) => (Number(this.userManagesProfile(b.id)) - Number(this.userManagesProfile(a.id))) || (a.name.localeCompare(b.name)));
this.updateSelectionModels();
}

hasProfile(profileId: number): boolean {
userManagesProfile(profileId: number): boolean {
return this.managedProfiles.indexOf(profileId) !== -1;
}

private updateSelectionModels() {
this.selectionModels = this.profiles.map(profile => {
const isManaged = this.userManagesProfile(profile.id);
const children = this.profiles.filter(otherProfile => otherProfile.parent === profile.id);
const isAnyChildrenManaged = children.some(child => this.userManagesProfile(child.id))

return { profile, children, isManaged, isAnyChildrenManaged };
});

console.log(this.selectionModels);
}
}
102 changes: 101 additions & 1 deletion client/src/styles/styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -357,4 +357,104 @@ button.list-group-item {

body .p-component {
font-family: 'Roboto', sans-serif;
}
}

/* Margin Utility Classes */
.m-0 { margin: 0; }
.m-1 { margin: 0.25rem; }
.m-2 { margin: 0.5rem; }
.m-3 { margin: 1rem; }
.m-4 { margin: 1.5rem; }
.m-5 { margin: 3rem; }

.mt-0 { margin-top: 0; }
.mt-1 { margin-top: 0.25rem; }
.mt-2 { margin-top: 0.5rem; }
.mt-3 { margin-top: 1rem; }
.mt-4 { margin-top: 1.5rem; }
.mt-5 { margin-top: 3rem; }

.mb-0 { margin-bottom: 0; }
.mb-1 { margin-bottom: 0.25rem; }
.mb-2 { margin-bottom: 0.5rem; }
.mb-3 { margin-bottom: 1rem; }
.mb-4 { margin-bottom: 1.5rem; }
.mb-5 { margin-bottom: 3rem; }

.ml-0 { margin-left: 0; }
.ml-1 { margin-left: 0.25rem; }
.ml-2 { margin-left: 0.5rem; }
.ml-3 { margin-left: 1rem; }
.ml-4 { margin-left: 1.5rem; }
.ml-5 { margin-left: 3rem; }

.mr-0 { margin-right: 0; }
.mr-1 { margin-right: 0.25rem; }
.mr-2 { margin-right: 0.5rem; }
.mr-3 { margin-right: 1rem; }
.mr-4 { margin-right: 1.5rem; }
.mr-5 { margin-right: 3rem; }

.mx-0 { margin-left: 0; margin-right: 0; }
.mx-1 { margin-left: 0.25rem; margin-right: 0.25rem; }
.mx-2 { margin-left: 0.5rem; margin-right: 0.5rem; }
.mx-3 { margin-left: 1rem; margin-right: 1rem; }
.mx-4 { margin-left: 1.5rem; margin-right: 1.5rem; }
.mx-5 { margin-left: 3rem; margin-right: 3rem; }

.my-0 { margin-top: 0; margin-bottom: 0; }
.my-1 { margin-top: 0.25rem; margin-bottom: 0.25rem; }
.my-2 { margin-top: 0.5rem; margin-bottom: 0.5rem; }
.my-3 { margin-top: 1rem; margin-bottom: 1rem; }
.my-4 { margin-top: 1.5rem; margin-bottom: 1.5rem; }
.my-5 { margin-top: 3rem; margin-bottom: 3rem; }

/* Padding Utility Classes */
.p-0 { padding: 0; }
.p-1 { padding: 0.25rem; }
.p-2 { padding: 0.5rem; }
.p-3 { padding: 1rem; }
.p-4 { padding: 1.5rem; }
.p-5 { padding: 3rem; }

.pt-0 { padding-top: 0; }
.pt-1 { padding-top: 0.25rem; }
.pt-2 { padding-top: 0.5rem; }
.pt-3 { padding-top: 1rem; }
.pt-4 { padding-top: 1.5rem; }
.pt-5 { padding-top: 3rem; }

.pb-0 { padding-bottom: 0; }
.pb-1 { padding-bottom: 0.25rem; }
.pb-2 { padding-bottom: 0.5rem; }
.pb-3 { padding-bottom: 1rem; }
.pb-4 { padding-bottom: 1.5rem; }
.pb-5 { padding-bottom: 3rem; }

.pl-0 { padding-left: 0; }
.pl-1 { padding-left: 0.25rem; }
.pl-2 { padding-left: 0.5rem; }
.pl-3 { padding-left: 1rem; }
.pl-4 { padding-left: 1.5rem; }
.pl-5 { padding-left: 3rem; }

.pr-0 { padding-right: 0; }
.pr-1 { padding-right: 0.25rem; }
.pr-2 { padding-right: 0.5rem; }
.pr-3 { padding-right: 1rem; }
.pr-4 { padding-right: 1.5rem; }
.pr-5 { padding-right: 3rem; }

.px-0 { padding-left: 0; padding-right: 0; }
.px-1 { padding-left: 0.25rem; padding-right: 0.25rem; }
.px-2 { padding-left: 0.5rem; padding-right: 0.5rem; }
.px-3 { padding-left: 1rem; padding-right: 1rem; }
.px-4 { padding-left: 1.5rem; padding-right: 1.5rem; }
.px-5 { padding-left: 3rem; padding-right: 3rem; }

.py-0 { padding-top: 0; padding-bottom: 0; }
.py-1 { padding-top: 0.25rem; padding-bottom: 0.25rem; }
.py-2 { padding-top: 0.5rem; padding-bottom: 0.5rem; }
.py-3 { padding-top: 1rem; padding-bottom: 1rem; }
.py-4 { padding-top: 1.5rem; padding-bottom: 1.5rem; }
.py-5 { padding-top: 3rem; padding-bottom: 3rem; }
6 changes: 5 additions & 1 deletion server/migrations/20230731174842_add_pbo_categories.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@ exports.up = function (knex) {
})
.then(() => {
return knex('app.pbo_categories').insert([
{ pbo_category_id: 'unclassified', pbo_category_cs_name: 'Nezařazeno', pbo_category_en_name: 'Unclassified'},
{
pbo_category_id: 'unclassified',
pbo_category_cs_name: 'Nezařazeno',
pbo_category_en_name: 'Unclassified',
},
]);
})
.then(() => {
Expand Down
4 changes: 2 additions & 2 deletions server/src/config/roles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export const aclRoles = {
'users:read': true,
'users:write': true,
'options:read': true,
"options:write": true,
'options:write': true,
},
},

Expand All @@ -76,7 +76,7 @@ export const aclRoles = {
'profile-accounting:list': true,
'profile-accounting:write': req => isManagedProfile(req),
'options:read': true,
"options:write": false,
'options:write': false,
},
},
};
2 changes: 1 addition & 1 deletion server/src/routers/admin/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {AdminProfilesRouter} from './profiles';
import {AdminUsersRouter} from './users';
import {AdminProfileImportTokenRouter} from './profile-import-token';
import {AdminProfileImportsRouter} from './profile-imports';
import { PboCategoriesRouter } from './pbo-categories';
import {PboCategoriesRouter} from './pbo-categories';

const router = express.Router();

Expand Down
90 changes: 52 additions & 38 deletions server/src/routers/admin/pbo-categories.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import express, { Request, Response } from "express";
import express, {Request, Response} from 'express';
import acl from 'express-dynacl';
import { db } from "../../db";
import { PboCategoryRecord } from "../../schema/database/pbo-category";
import {db} from '../../db';
import {PboCategoryRecord} from '../../schema/database/pbo-category';

const router = express.Router();

Expand All @@ -14,26 +14,30 @@ router.get('/', acl('options:read'), async (req: Request, res: Response) => {

res.json(data ?? []);
} catch (err) {
res.status(500).json({ error: err instanceof Error ? err.message : err });
res.status(500).json({error: err instanceof Error ? err.message : err});
}
});

// READ
router.get('/:id', acl('options:read'), async (req: Request<{ id: string }>, res: Response) => {
try {
const data = await db<PboCategoryRecord>('app.pbo_categories')
.where('pboCategoryId', req.params.id)
.first();
router.get(
'/:id',
acl('options:read'),
async (req: Request<{id: string}>, res: Response) => {
try {
const data = await db<PboCategoryRecord>('app.pbo_categories')
.where('pboCategoryId', req.params.id)
.first();

if (data) {
res.json(data);
} else {
res.sendStatus(404);
if (data) {
res.json(data);
} else {
res.sendStatus(404);
}
} catch (err) {
res.status(500).json({error: err instanceof Error ? err.message : err});
}
} catch (err) {
res.status(500).json({ error: err instanceof Error ? err.message : err });
}
});
);

// CREATE
router.post('/', acl('options:write'), async (req: Request, res: Response) => {
Expand All @@ -42,40 +46,50 @@ router.post('/', acl('options:write'), async (req: Request, res: Response) => {
const idIsValid: boolean = /^\w[\w-]{1,14}\w$/.test(body.pboCategoryId);

if (!idIsValid) {
res.status(400).json({ error: 'Invalid \'pboCategoryId\' value' });
res.status(400).json({error: "Invalid 'pboCategoryId' value"});
} else {
const [id] = await db('app.pbo_categories').insert(body, ['pboCategoryId']);
const [id] = await db('app.pbo_categories').insert(body, [
'pboCategoryId',
]);
res.status(201).json(id);
}
} catch (err) {
res.status(500).json({ error: err instanceof Error ? err.message : err });
res.status(500).json({error: err instanceof Error ? err.message : err});
}
});

// UPDATE
router.put('/:id', acl('options:write'), async (req: Request<{ id: string }>, res: Response) => {
try {
const body: Partial<PboCategoryRecord> = req.body;
router.put(
'/:id',
acl('options:write'),
async (req: Request<{id: string}>, res: Response) => {
try {
const body: Partial<PboCategoryRecord> = req.body;

await db('app.pbo_categories')
.where('pboCategoryId', req.params.id)
.update(body);
await db('app.pbo_categories')
.where('pboCategoryId', req.params.id)
.update(body);

res.sendStatus(200);
} catch (err) {
res.status(500).json({ error: err instanceof Error ? err.message : err });
res.sendStatus(200);
} catch (err) {
res.status(500).json({error: err instanceof Error ? err.message : err});
}
}
});
);

// DELETE
router.delete('/:id', acl('options:write'), async (req: Request<{ id: string }>, res: Response) => {
try {
await db('app.pbo_categories')
.where({ pboCategoryId: req.params.id })
.delete();
router.delete(
'/:id',
acl('options:write'),
async (req: Request<{id: string}>, res: Response) => {
try {
await db('app.pbo_categories')
.where({pboCategoryId: req.params.id})
.delete();

res.sendStatus(204);
} catch (err) {
res.status(500).json({ error: err instanceof Error ? err.message : err });
res.sendStatus(204);
} catch (err) {
res.status(500).json({error: err instanceof Error ? err.message : err});
}
}
});
);
Loading

0 comments on commit 9544026

Please sign in to comment.