1 import {Component, Input, OnInit, TemplateRef, ViewChild} from '@angular/core';
2 import {ActivatedRoute} from '@angular/router';
3 import {IdlService, IdlObject} from '@eg/core/idl.service';
4 import {GridDataSource} from '@eg/share/grid/grid';
5 import {GridComponent} from '@eg/share/grid/grid.component';
6 import {TranslateComponent} from '@eg/staff/share/translate/translate.component';
7 import {ToastService} from '@eg/share/toast/toast.service';
8 import {Pager} from '@eg/share/util/pager';
9 import {PcrudService} from '@eg/core/pcrud.service';
10 import {OrgService} from '@eg/core/org.service';
11 import {PermService} from '@eg/core/perm.service';
12 import {AuthService} from '@eg/core/auth.service';
13 import {FmRecordEditorComponent} from '@eg/share/fm-editor/fm-editor.component';
14 import {StringComponent} from '@eg/share/string/string.component';
17 * General purpose CRUD interface for IDL objects
19 * Object types using this component must be editable via PCRUD.
23 selector: 'eg-admin-page',
24 templateUrl: './admin-page.component.html'
27 export class AdminPageComponent implements OnInit {
29 @Input() idlClass: string;
31 // Default sort field, used when no grid sorting is applied.
32 @Input() sortField: string;
34 // Data source may be provided by the caller. This gives the caller
35 // complete control over the contents of the grid. If no data source
36 // is provided, a generic one is create which is sufficient for data
37 // that requires no special handling, filtering, etc.
38 @Input() dataSource: GridDataSource;
40 // Size of create/edito dialog. Uses large by default.
41 @Input() dialogSize: 'sm' | 'lg' = 'lg';
43 // If an org unit field is specified, an org unit filter
44 // is added to the top of the page.
45 @Input() orgField: string;
47 // Disable the auto-matic org unit field filter
48 @Input() disableOrgFilter: boolean;
50 // Include objects linking to org units which are ancestors
51 // of the selected org unit.
52 @Input() includeOrgAncestors: boolean;
54 // Ditto includeOrgAncestors, but descendants.
55 @Input() includeOrgDescendants: boolean;
57 // Optional grid persist key. This is the part of the key
59 @Input() persistKey: string;
61 // Optional path component to add to the generated grid persist key,
62 // formatted as (for example):
63 // 'eg.grid.admin.${persistKeyPfx}.config.billing_type'
64 @Input() persistKeyPfx: string;
66 // Optional comma-separated list of read-only fields
67 @Input() readonlyFields: string;
69 @ViewChild('grid') grid: GridComponent;
70 @ViewChild('editDialog') editDialog: FmRecordEditorComponent;
71 @ViewChild('successString') successString: StringComponent;
72 @ViewChild('createString') createString: StringComponent;
73 @ViewChild('createErrString') createErrString: StringComponent;
74 @ViewChild('updateFailedString') updateFailedString: StringComponent;
75 @ViewChild('translator') translator: TranslateComponent;
80 // True if any columns on the object support translations
81 translateRowIdx: number;
82 translateFieldIdx: number;
83 translatableFields: string[];
85 contextOrg: IdlObject;
86 orgFieldLabel: string;
90 // Filters may be passed via URL query param.
91 // They are used to augment the grid data search query.
92 gridFilters: {[key: string]: string | number};
95 private route: ActivatedRoute,
96 private idl: IdlService,
97 private org: OrgService,
98 private auth: AuthService,
99 private pcrud: PcrudService,
100 private perm: PermService,
101 private toast: ToastService
103 this.translatableFields = [];
106 applyOrgValues(orgId?: number) {
108 if (this.disableOrgFilter) {
109 this.orgField = null;
113 if (!this.orgField) {
114 // If no org unit field is specified, try to find one.
115 // If an object type has multiple org unit fields, the
116 // caller should specify one or disable org unit filter.
117 this.idlClassDef.fields.forEach(field => {
118 if (field['class'] === 'aou') {
119 this.orgField = field.name;
125 this.orgFieldLabel = this.idlClassDef.field_map[this.orgField].label;
126 this.contextOrg = this.org.get(orgId) || this.org.root();
131 this.idlClassDef = this.idl.classes[this.idlClass];
132 this.pkeyField = this.idlClassDef.pkey || 'id';
134 this.translatableFields =
135 this.idlClassDef.fields.filter(f => f.i18n).map(f => f.name);
137 if (!this.persistKey) {
140 (this.persistKeyPfx ? this.persistKeyPfx + '.' : '') +
141 this.idlClassDef.table;
144 // gridFilters are a JSON encoded string
145 const filters = this.route.snapshot.queryParamMap.get('gridFilters');
148 this.gridFilters = JSON.parse(filters);
150 console.error('Invalid grid filters provided: ', filters);
154 // Limit the view org selector to orgs where the user has
155 // permacrud-encoded view permissions.
156 const pc = this.idlClassDef.permacrud;
157 if (pc && pc.retrieve) {
158 this.viewPerms = pc.retrieve.perms;
161 const contextOrg = this.route.snapshot.queryParamMap.get('contextOrg');
162 this.checkCreatePerms();
163 this.applyOrgValues(Number(contextOrg));
165 // If the caller provides not data source, create a generic one.
166 if (!this.dataSource) {
167 this.initDataSource();
170 // TODO: pass the row activate handler via the grid markup
171 this.grid.onRowActivate.subscribe(
172 (idlThing: IdlObject) => this.showEditDialog(idlThing)
175 this.editSelected = (idlThings: IdlObject[]) => {
177 // Edit each IDL thing one at a time
178 const editOneThing = (thing: IdlObject) => {
179 if (!thing) { return; }
181 this.showEditDialog(thing).then(
182 () => editOneThing(idlThings.shift()));
185 editOneThing(idlThings.shift());
188 this.createNew = () => {
189 this.editDialog.mode = 'create';
190 // We reuse the same editor for all actions. Be sure
191 // create action does not try to modify an existing record.
192 this.editDialog.recId = null;
193 this.editDialog.record = null;
194 this.editDialog.open({size: this.dialogSize}).subscribe(
196 this.createString.current()
197 .then(str => this.toast.success(str));
201 this.createErrString.current()
202 .then(str => this.toast.danger(str));
207 this.deleteSelected = (idlThings: IdlObject[]) => {
208 idlThings.forEach(idlThing => idlThing.isdeleted(true));
209 this.pcrud.autoApply(idlThings).subscribe(
210 val => console.debug('deleted: ' + val),
212 () => this.grid.reload()
216 // Open the field translation dialog.
217 // Link the next/previous actions to cycle through each translatable
218 // field on each row.
219 this.translate = () => {
220 this.translateRowIdx = 0;
221 this.translateFieldIdx = 0;
222 this.translator.fieldName = this.translatableFields[this.translateFieldIdx];
223 this.translator.idlObject = this.dataSource.data[this.translateRowIdx];
225 this.translator.nextString = () => {
227 if (this.translateFieldIdx < this.translatableFields.length - 1) {
228 this.translateFieldIdx++;
230 } else if (this.translateRowIdx < this.dataSource.data.length - 1) {
231 this.translateRowIdx++;
232 this.translateFieldIdx = 0;
235 this.translator.idlObject =
236 this.dataSource.data[this.translateRowIdx];
237 this.translator.fieldName =
238 this.translatableFields[this.translateFieldIdx];
241 this.translator.prevString = () => {
243 if (this.translateFieldIdx > 0) {
244 this.translateFieldIdx--;
246 } else if (this.translateRowIdx > 0) {
247 this.translateRowIdx--;
248 this.translateFieldIdx = 0;
251 this.translator.idlObject =
252 this.dataSource.data[this.translateRowIdx];
253 this.translator.fieldName =
254 this.translatableFields[this.translateFieldIdx];
257 this.translator.open({size: 'lg'});
262 this.canCreate = false;
263 const pc = this.idlClassDef.permacrud || {};
264 const perms = pc.create ? pc.create.perms : [];
265 if (perms.length === 0) { return; }
267 this.perm.hasWorkPermAt(perms, true).then(permMap => {
268 Object.keys(permMap).forEach(key => {
269 if (permMap[key].length > 0) {
270 this.canCreate = true;
276 orgOnChange(org: IdlObject) {
277 this.contextOrg = org;
282 this.dataSource = new GridDataSource();
284 this.dataSource.getRows = (pager: Pager, sort: any[]) => {
285 const orderBy: any = {};
288 // Sort specified from grid
289 orderBy[this.idlClass] = sort[0].name + ' ' + sort[0].dir;
291 } else if (this.sortField) {
292 // Default sort field
293 orderBy[this.idlClass] = this.sortField;
297 offset: pager.offset,
302 if (!this.contextOrg && !this.gridFilters) {
303 // No org filter -- fetch all rows
304 return this.pcrud.retrieveAll(
305 this.idlClass, searchOps, {fleshSelectors: true});
308 const search: any = {};
310 if (this.contextOrg) {
311 // Filter rows by those linking to the context org and
312 // optionally ancestor and descendant org units.
314 let orgs = [this.contextOrg.id()];
316 if (this.includeOrgAncestors) {
317 orgs = this.org.ancestors(this.contextOrg, true);
320 if (this.includeOrgDescendants) {
321 // can result in duplicate workstation org IDs... meh
323 this.org.descendants(this.contextOrg, true));
326 search[this.orgField] = orgs;
329 if (this.gridFilters) {
330 // Lay the URL grid filters over our search object.
331 Object.keys(this.gridFilters).forEach(key => {
332 search[key] = this.gridFilters[key];
336 return this.pcrud.search(
337 this.idlClass, search, searchOps, {fleshSelectors: true});
341 disableAncestorSelector(): boolean {
342 return this.contextOrg &&
343 this.contextOrg.id() === this.org.root().id();
346 disableDescendantSelector(): boolean {
347 return this.contextOrg && this.contextOrg.children().length === 0;
350 showEditDialog(idlThing: IdlObject): Promise<any> {
351 this.editDialog.mode = 'update';
352 this.editDialog.recId = idlThing[this.pkeyField]();
353 return new Promise((resolve, reject) => {
354 this.editDialog.open({size: this.dialogSize}).subscribe(
356 this.successString.current()
357 .then(str => this.toast.success(str));
362 this.updateFailedString.current()
363 .then(str => this.toast.danger(str));
370 editSelected(idlThings: IdlObject[]) {
372 // Edit each IDL thing one at a time
373 const editOneThing = (thing: IdlObject) => {
374 if (!thing) { return; }
376 this.showEditDialog(thing).then(
377 () => editOneThing(idlThings.shift()));
380 editOneThing(idlThings.shift());
383 deleteSelected(idlThings: IdlObject[]) {
384 idlThings.forEach(idlThing => idlThing.isdeleted(true));
385 this.pcrud.autoApply(idlThings).subscribe(
386 val => console.debug('deleted: ' + val),
388 () => this.grid.reload()
393 this.editDialog.mode = 'create';
394 // We reuse the same editor for all actions. Be sure
395 // create action does not try to modify an existing record.
396 this.editDialog.recId = null;
397 this.editDialog.record = null;
398 this.editDialog.open({size: this.dialogSize}).subscribe(
400 this.createString.current()
401 .then(str => this.toast.success(str));
405 if (!rejection.dismissed) {
406 this.createErrString.current()
407 .then(str => this.toast.danger(str));
412 // Open the field translation dialog.
413 // Link the next/previous actions to cycle through each translatable
414 // field on each row.
416 this.translateRowIdx = 0;
417 this.translateFieldIdx = 0;
418 this.translator.fieldName = this.translatableFields[this.translateFieldIdx];
419 this.translator.idlObject = this.dataSource.data[this.translateRowIdx];
421 this.translator.nextString = () => {
423 if (this.translateFieldIdx < this.translatableFields.length - 1) {
424 this.translateFieldIdx++;
426 } else if (this.translateRowIdx < this.dataSource.data.length - 1) {
427 this.translateRowIdx++;
428 this.translateFieldIdx = 0;
431 this.translator.idlObject =
432 this.dataSource.data[this.translateRowIdx];
433 this.translator.fieldName =
434 this.translatableFields[this.translateFieldIdx];
437 this.translator.prevString = () => {
439 if (this.translateFieldIdx > 0) {
440 this.translateFieldIdx--;
442 } else if (this.translateRowIdx > 0) {
443 this.translateRowIdx--;
444 this.translateFieldIdx = 0;
447 this.translator.idlObject =
448 this.dataSource.data[this.translateRowIdx];
449 this.translator.fieldName =
450 this.translatableFields[this.translateFieldIdx];
453 this.translator.open({size: 'lg'});