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/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, FmFieldOptions
14 } from '@eg/share/fm-editor/fm-editor.component';
15 import {StringComponent} from '@eg/share/string/string.component';
16 import {OrgFamily} from '@eg/share/org-family-select/org-family-select.component';
19 * General purpose CRUD interface for IDL objects
21 * Object types using this component must be editable via PCRUD.
25 selector: 'eg-admin-page',
26 templateUrl: './admin-page.component.html'
29 export class AdminPageComponent implements OnInit {
31 @Input() idlClass: string;
33 // Default sort field, used when no grid sorting is applied.
34 @Input() sortField: string;
36 // Data source may be provided by the caller. This gives the caller
37 // complete control over the contents of the grid. If no data source
38 // is provided, a generic one is create which is sufficient for data
39 // that requires no special handling, filtering, etc.
40 @Input() dataSource: GridDataSource;
42 // Size of create/edito dialog. Uses large by default.
43 @Input() dialogSize: 'sm' | 'lg' = 'lg';
45 // comma-separated list of fields to hide.
46 // This does not imply all other fields should be visible, only that
47 // the selected fields will be hidden.
48 @Input() hideGridFields: string;
50 // If an org unit field is specified, an org unit filter
51 // is added to the top of the page.
52 @Input() orgField: string;
54 // Disable the auto-matic org unit field filter
55 @Input() disableOrgFilter: boolean;
57 // Include objects linking to org units which are ancestors
58 // of the selected org unit.
59 @Input() includeOrgAncestors: boolean;
61 // Ditto includeOrgAncestors, but descendants.
62 @Input() includeOrgDescendants: boolean;
64 // Optional grid persist key. This is the part of the key
66 @Input() persistKey: string;
68 // Optional path component to add to the generated grid persist key,
69 // formatted as (for example):
70 // 'eg.grid.admin.${persistKeyPfx}.config.billing_type'
71 @Input() persistKeyPfx: string;
73 // Optional comma-separated list of read-only fields
74 @Input() readonlyFields: string;
76 // Optional template containing help/about text which will
77 // be added to the page, above the grid.
78 @Input() helpTemplate: TemplateRef<any>;
80 // Override field options for create/edit dialog
81 @Input() fieldOptions: {[field: string]: FmFieldOptions};
83 // Override default values for fm-editor
84 @Input() defaultNewRecord: IdlObject;
87 @ViewChild('grid', { static: true }) grid: GridComponent;
88 @ViewChild('editDialog', { static: true }) editDialog: FmRecordEditorComponent;
89 @ViewChild('successString', { static: true }) successString: StringComponent;
90 @ViewChild('createString', { static: true }) createString: StringComponent;
91 @ViewChild('createErrString', { static: true }) createErrString: StringComponent;
92 @ViewChild('updateFailedString', { static: true }) updateFailedString: StringComponent;
93 @ViewChild('deleteFailedString', { static: true }) deleteFailedString: StringComponent;
94 @ViewChild('deleteSuccessString', { static: true }) deleteSuccessString: StringComponent;
95 @ViewChild('translator', { static: true }) translator: TranslateComponent;
99 configFields: string[];
101 // True if any columns on the object support translations
102 translateRowIdx: number;
103 translateFieldIdx: number;
104 translatableFields: string[];
106 contextOrg: IdlObject;
107 searchOrgs: OrgFamily;
108 orgFieldLabel: string;
112 // Filters may be passed via URL query param.
113 // They are used to augment the grid data search query.
114 gridFilters: {[key: string]: string | number};
117 private route: ActivatedRoute,
118 public idl: IdlService,
119 private org: OrgService,
120 public auth: AuthService,
121 public pcrud: PcrudService,
122 private perm: PermService,
123 public toast: ToastService
125 this.translatableFields = [];
126 this.configFields = [];
129 applyOrgValues(orgId?: number) {
131 if (this.disableOrgFilter) {
132 this.orgField = null;
136 if (!this.orgField) {
137 // If no org unit field is specified, try to find one.
138 // If an object type has multiple org unit fields, the
139 // caller should specify one or disable org unit filter.
140 this.idlClassDef.fields.forEach(field => {
141 if (field['class'] === 'aou') {
142 this.orgField = field.name;
148 this.orgFieldLabel = this.idlClassDef.field_map[this.orgField].label;
149 this.contextOrg = this.org.get(orgId) || this.org.root();
150 this.searchOrgs = {primaryOrgId: this.contextOrg.id()};
155 this.idlClassDef = this.idl.classes[this.idlClass];
156 this.pkeyField = this.idlClassDef.pkey || 'id';
158 this.translatableFields =
159 this.idlClassDef.fields.filter(f => f.i18n).map(f => f.name);
161 if (!this.persistKey) {
164 (this.persistKeyPfx ? this.persistKeyPfx + '.' : '') +
165 this.idlClassDef.table;
169 this.idlClassDef.fields.filter(f => f.config_field);
171 // gridFilters are a JSON encoded string
172 const filters = this.route.snapshot.queryParamMap.get('gridFilters');
175 this.gridFilters = JSON.parse(filters);
177 console.error('Invalid grid filters provided: ', filters);
181 // Limit the view org selector to orgs where the user has
182 // permacrud-encoded view permissions.
183 const pc = this.idlClassDef.permacrud;
184 if (pc && pc.retrieve) {
185 this.viewPerms = pc.retrieve.perms;
188 const contextOrg = this.route.snapshot.queryParamMap.get('contextOrg');
189 this.checkCreatePerms();
190 this.applyOrgValues(Number(contextOrg));
192 // If the caller provides not data source, create a generic one.
193 if (!this.dataSource) {
194 this.initDataSource();
197 // TODO: pass the row activate handler via the grid markup
198 this.grid.onRowActivate.subscribe(
199 (idlThing: IdlObject) => this.showEditDialog(idlThing)
204 this.canCreate = false;
205 const pc = this.idlClassDef.permacrud || {};
206 const perms = pc.create ? pc.create.perms : [];
207 if (perms.length === 0) { return; }
209 this.perm.hasWorkPermAt(perms, true).then(permMap => {
210 Object.keys(permMap).forEach(key => {
211 if (permMap[key].length > 0) {
212 this.canCreate = true;
219 this.dataSource = new GridDataSource();
221 this.dataSource.getRows = (pager: Pager, sort: any[]) => {
222 const orderBy: any = {};
225 // Sort specified from grid
226 orderBy[this.idlClass] = sort[0].name + ' ' + sort[0].dir;
228 } else if (this.sortField) {
229 // Default sort field
230 orderBy[this.idlClass] = this.sortField;
234 offset: pager.offset,
239 if (!this.contextOrg && !this.gridFilters) {
240 // No org filter -- fetch all rows
241 return this.pcrud.retrieveAll(
242 this.idlClass, searchOps, {fleshSelectors: true});
245 const search: any = {};
247 search[this.orgField] = this.searchOrgs.orgIds || [this.contextOrg.id()];
249 if (this.gridFilters) {
250 // Lay the URL grid filters over our search object.
251 Object.keys(this.gridFilters).forEach(key => {
252 search[key] = this.gridFilters[key];
256 return this.pcrud.search(
257 this.idlClass, search, searchOps, {fleshSelectors: true});
261 showEditDialog(idlThing: IdlObject): Promise<any> {
262 this.editDialog.mode = 'update';
263 this.editDialog.recordId = idlThing[this.pkeyField]();
264 return new Promise((resolve, reject) => {
265 this.editDialog.open({size: this.dialogSize}).subscribe(
267 this.successString.current()
268 .then(str => this.toast.success(str));
273 this.updateFailedString.current()
274 .then(str => this.toast.danger(str));
281 editSelected(idlThings: IdlObject[]) {
283 // Edit each IDL thing one at a time
284 const editOneThing = (thing: IdlObject) => {
285 if (!thing) { return; }
287 this.showEditDialog(thing).then(
288 () => editOneThing(idlThings.shift()));
291 editOneThing(idlThings.shift());
294 deleteSelected(idlThings: IdlObject[]) {
295 idlThings.forEach(idlThing => idlThing.isdeleted(true));
296 this.pcrud.autoApply(idlThings).subscribe(
298 console.debug('deleted: ' + val);
299 this.deleteSuccessString.current()
300 .then(str => this.toast.success(str));
303 this.deleteFailedString.current()
304 .then(str => this.toast.danger(str));
306 () => this.grid.reload()
311 this.editDialog.mode = 'create';
312 // We reuse the same editor for all actions. Be sure
313 // create action does not try to modify an existing record.
314 this.editDialog.recordId = null;
315 this.editDialog.record = null;
316 this.editDialog.open({size: this.dialogSize}).subscribe(
318 this.createString.current()
319 .then(str => this.toast.success(str));
323 if (!rejection.dismissed) {
324 this.createErrString.current()
325 .then(str => this.toast.danger(str));
330 // Open the field translation dialog.
331 // Link the next/previous actions to cycle through each translatable
332 // field on each row.
334 this.translateRowIdx = 0;
335 this.translateFieldIdx = 0;
336 this.translator.fieldName = this.translatableFields[this.translateFieldIdx];
337 this.translator.idlObject = this.dataSource.data[this.translateRowIdx];
339 this.translator.nextString = () => {
341 if (this.translateFieldIdx < this.translatableFields.length - 1) {
342 this.translateFieldIdx++;
344 } else if (this.translateRowIdx < this.dataSource.data.length - 1) {
345 this.translateRowIdx++;
346 this.translateFieldIdx = 0;
349 this.translator.idlObject =
350 this.dataSource.data[this.translateRowIdx];
351 this.translator.fieldName =
352 this.translatableFields[this.translateFieldIdx];
355 this.translator.prevString = () => {
357 if (this.translateFieldIdx > 0) {
358 this.translateFieldIdx--;
360 } else if (this.translateRowIdx > 0) {
361 this.translateRowIdx--;
362 this.translateFieldIdx = 0;
365 this.translator.idlObject =
366 this.dataSource.data[this.translateRowIdx];
367 this.translator.fieldName =
368 this.translatableFields[this.translateFieldIdx];
371 this.translator.open({size: 'lg'});