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';
15 import {OrgFamily} from '@eg/share/org-family-select/org-family-select.component';
18 * General purpose CRUD interface for IDL objects
20 * Object types using this component must be editable via PCRUD.
24 selector: 'eg-admin-page',
25 templateUrl: './admin-page.component.html'
28 export class AdminPageComponent implements OnInit {
30 @Input() idlClass: string;
32 // Default sort field, used when no grid sorting is applied.
33 @Input() sortField: string;
35 // Data source may be provided by the caller. This gives the caller
36 // complete control over the contents of the grid. If no data source
37 // is provided, a generic one is create which is sufficient for data
38 // that requires no special handling, filtering, etc.
39 @Input() dataSource: GridDataSource;
41 // Size of create/edito dialog. Uses large by default.
42 @Input() dialogSize: 'sm' | 'lg' = 'lg';
44 // If an org unit field is specified, an org unit filter
45 // is added to the top of the page.
46 @Input() orgField: string;
48 // Disable the auto-matic org unit field filter
49 @Input() disableOrgFilter: boolean;
51 // Include objects linking to org units which are ancestors
52 // of the selected org unit.
53 @Input() includeOrgAncestors: boolean;
55 // Ditto includeOrgAncestors, but descendants.
56 @Input() includeOrgDescendants: boolean;
58 // Optional grid persist key. This is the part of the key
60 @Input() persistKey: string;
62 // Optional path component to add to the generated grid persist key,
63 // formatted as (for example):
64 // 'eg.grid.admin.${persistKeyPfx}.config.billing_type'
65 @Input() persistKeyPfx: string;
67 // Optional comma-separated list of read-only fields
68 @Input() readonlyFields: string;
70 // Optional template containing help/about text which will
71 // be added to the page, above the grid.
72 @Input() helpTemplate: TemplateRef<any>;
74 @ViewChild('grid') grid: GridComponent;
75 @ViewChild('editDialog') editDialog: FmRecordEditorComponent;
76 @ViewChild('successString') successString: StringComponent;
77 @ViewChild('createString') createString: StringComponent;
78 @ViewChild('createErrString') createErrString: StringComponent;
79 @ViewChild('updateFailedString') updateFailedString: StringComponent;
80 @ViewChild('translator') translator: TranslateComponent;
85 // True if any columns on the object support translations
86 translateRowIdx: number;
87 translateFieldIdx: number;
88 translatableFields: string[];
90 contextOrg: IdlObject;
91 searchOrgs: OrgFamily;
92 orgFieldLabel: string;
96 // Filters may be passed via URL query param.
97 // They are used to augment the grid data search query.
98 gridFilters: {[key: string]: string | number};
101 private route: ActivatedRoute,
102 private idl: IdlService,
103 private org: OrgService,
104 private auth: AuthService,
105 private pcrud: PcrudService,
106 private perm: PermService,
107 private toast: ToastService
109 this.translatableFields = [];
112 applyOrgValues(orgId?: number) {
114 if (this.disableOrgFilter) {
115 this.orgField = null;
119 if (!this.orgField) {
120 // If no org unit field is specified, try to find one.
121 // If an object type has multiple org unit fields, the
122 // caller should specify one or disable org unit filter.
123 this.idlClassDef.fields.forEach(field => {
124 if (field['class'] === 'aou') {
125 this.orgField = field.name;
131 this.orgFieldLabel = this.idlClassDef.field_map[this.orgField].label;
132 this.contextOrg = this.org.get(orgId) || this.org.root();
133 this.searchOrgs = {primaryOrgId: this.contextOrg.id()};
138 this.idlClassDef = this.idl.classes[this.idlClass];
139 this.pkeyField = this.idlClassDef.pkey || 'id';
141 this.translatableFields =
142 this.idlClassDef.fields.filter(f => f.i18n).map(f => f.name);
144 if (!this.persistKey) {
147 (this.persistKeyPfx ? this.persistKeyPfx + '.' : '') +
148 this.idlClassDef.table;
151 // gridFilters are a JSON encoded string
152 const filters = this.route.snapshot.queryParamMap.get('gridFilters');
155 this.gridFilters = JSON.parse(filters);
157 console.error('Invalid grid filters provided: ', filters);
161 // Limit the view org selector to orgs where the user has
162 // permacrud-encoded view permissions.
163 const pc = this.idlClassDef.permacrud;
164 if (pc && pc.retrieve) {
165 this.viewPerms = pc.retrieve.perms;
168 const contextOrg = this.route.snapshot.queryParamMap.get('contextOrg');
169 this.checkCreatePerms();
170 this.applyOrgValues(Number(contextOrg));
172 // If the caller provides not data source, create a generic one.
173 if (!this.dataSource) {
174 this.initDataSource();
177 // TODO: pass the row activate handler via the grid markup
178 this.grid.onRowActivate.subscribe(
179 (idlThing: IdlObject) => this.showEditDialog(idlThing)
184 this.canCreate = false;
185 const pc = this.idlClassDef.permacrud || {};
186 const perms = pc.create ? pc.create.perms : [];
187 if (perms.length === 0) { return; }
189 this.perm.hasWorkPermAt(perms, true).then(permMap => {
190 Object.keys(permMap).forEach(key => {
191 if (permMap[key].length > 0) {
192 this.canCreate = true;
199 this.dataSource = new GridDataSource();
201 this.dataSource.getRows = (pager: Pager, sort: any[]) => {
202 const orderBy: any = {};
205 // Sort specified from grid
206 orderBy[this.idlClass] = sort[0].name + ' ' + sort[0].dir;
208 } else if (this.sortField) {
209 // Default sort field
210 orderBy[this.idlClass] = this.sortField;
214 offset: pager.offset,
219 if (!this.contextOrg && !this.gridFilters) {
220 // No org filter -- fetch all rows
221 return this.pcrud.retrieveAll(
222 this.idlClass, searchOps, {fleshSelectors: true});
225 const search: any = {};
227 search[this.orgField] = this.searchOrgs.orgIds || [this.contextOrg.id()];
229 if (this.gridFilters) {
230 // Lay the URL grid filters over our search object.
231 Object.keys(this.gridFilters).forEach(key => {
232 search[key] = this.gridFilters[key];
236 return this.pcrud.search(
237 this.idlClass, search, searchOps, {fleshSelectors: true});
241 showEditDialog(idlThing: IdlObject): Promise<any> {
242 this.editDialog.mode = 'update';
243 this.editDialog.recId = idlThing[this.pkeyField]();
244 return new Promise((resolve, reject) => {
245 this.editDialog.open({size: this.dialogSize}).subscribe(
247 this.successString.current()
248 .then(str => this.toast.success(str));
253 this.updateFailedString.current()
254 .then(str => this.toast.danger(str));
261 editSelected(idlThings: IdlObject[]) {
263 // Edit each IDL thing one at a time
264 const editOneThing = (thing: IdlObject) => {
265 if (!thing) { return; }
267 this.showEditDialog(thing).then(
268 () => editOneThing(idlThings.shift()));
271 editOneThing(idlThings.shift());
274 deleteSelected(idlThings: IdlObject[]) {
275 idlThings.forEach(idlThing => idlThing.isdeleted(true));
276 this.pcrud.autoApply(idlThings).subscribe(
277 val => console.debug('deleted: ' + val),
279 () => this.grid.reload()
284 this.editDialog.mode = 'create';
285 // We reuse the same editor for all actions. Be sure
286 // create action does not try to modify an existing record.
287 this.editDialog.recId = null;
288 this.editDialog.record = null;
289 this.editDialog.open({size: this.dialogSize}).subscribe(
291 this.createString.current()
292 .then(str => this.toast.success(str));
296 if (!rejection.dismissed) {
297 this.createErrString.current()
298 .then(str => this.toast.danger(str));
303 // Open the field translation dialog.
304 // Link the next/previous actions to cycle through each translatable
305 // field on each row.
307 this.translateRowIdx = 0;
308 this.translateFieldIdx = 0;
309 this.translator.fieldName = this.translatableFields[this.translateFieldIdx];
310 this.translator.idlObject = this.dataSource.data[this.translateRowIdx];
312 this.translator.nextString = () => {
314 if (this.translateFieldIdx < this.translatableFields.length - 1) {
315 this.translateFieldIdx++;
317 } else if (this.translateRowIdx < this.dataSource.data.length - 1) {
318 this.translateRowIdx++;
319 this.translateFieldIdx = 0;
322 this.translator.idlObject =
323 this.dataSource.data[this.translateRowIdx];
324 this.translator.fieldName =
325 this.translatableFields[this.translateFieldIdx];
328 this.translator.prevString = () => {
330 if (this.translateFieldIdx > 0) {
331 this.translateFieldIdx--;
333 } else if (this.translateRowIdx > 0) {
334 this.translateRowIdx--;
335 this.translateFieldIdx = 0;
338 this.translator.idlObject =
339 this.dataSource.data[this.translateRowIdx];
340 this.translator.fieldName =
341 this.translatableFields[this.translateFieldIdx];
344 this.translator.open({size: 'lg'});