1 import {Component, Input, OnInit, TemplateRef, ViewChild} from '@angular/core';
2 import {IdlService, IdlObject} from '@eg/core/idl.service';
3 import {GridDataSource} from '@eg/share/grid/grid';
4 import {GridComponent} from '@eg/share/grid/grid.component';
5 import {TranslateComponent} from '@eg/staff/share/translate/translate.component';
6 import {ToastService} from '@eg/share/toast/toast.service';
7 import {Pager} from '@eg/share/util/pager';
8 import {PcrudService} from '@eg/core/pcrud.service';
9 import {OrgService} from '@eg/core/org.service';
10 import {PermService} from '@eg/core/perm.service';
11 import {AuthService} from '@eg/core/auth.service';
12 import {FmRecordEditorComponent} from '@eg/share/fm-editor/fm-editor.component';
13 import {StringComponent} from '@eg/share/string/string.component';
16 * General purpose CRUD interface for IDL objects
18 * Object types using this component must be editable via PCRUD.
22 selector: 'eg-admin-page',
23 templateUrl: './admin-page.component.html'
26 export class AdminPageComponent implements OnInit {
28 @Input() idlClass: string;
30 // Default sort field, used when no grid sorting is applied.
31 @Input() sortField: string;
33 // Data source may be provided by the caller. This gives the caller
34 // complete control over the contents of the grid. If no data source
35 // is provided, a generic one is create which is sufficient for data
36 // that requires no special handling, filtering, etc.
37 @Input() dataSource: GridDataSource;
39 // Size of create/edito dialog. Uses large by default.
40 @Input() dialogSize: 'sm' | 'lg' = 'lg';
42 // If an org unit field is specified, an org unit filter
43 // is added to the top of the page.
44 @Input() orgField: string;
46 // Disable the auto-matic org unit field filter
47 @Input() disableOrgFilter: boolean;
49 // Include objects linking to org units which are ancestors
50 // of the selected org unit.
51 @Input() includeOrgAncestors: boolean;
53 // Ditto includeOrgAncestors, but descendants.
54 @Input() includeOrgDescendants: boolean;
56 // Optional grid persist key. This is the part of the key
58 @Input() persistKey: string;
60 // Optional path component to add to the generated grid persist key,
61 // formatted as (for example):
62 // 'eg.grid.admin.${persistKeyPfx}.config.billing_type'
63 @Input() persistKeyPfx: string;
65 // Optional comma-separated list of read-only fields
66 @Input() readonlyFields: string;
68 @ViewChild('grid') grid: GridComponent;
69 @ViewChild('editDialog') editDialog: FmRecordEditorComponent;
70 @ViewChild('successString') successString: StringComponent;
71 @ViewChild('createString') createString: StringComponent;
72 @ViewChild('createErrString') createErrString: StringComponent;
73 @ViewChild('updateFailedString') updateFailedString: StringComponent;
74 @ViewChild('translator') translator: TranslateComponent;
78 createNew: () => void;
79 deleteSelected: (rows: IdlObject[]) => void;
80 editSelected: (rows: IdlObject[]) => void;
82 // True if any columns on the object support translations
83 translateRowIdx: number;
84 translateFieldIdx: number;
85 translatableFields: string[];
86 translate: () => void;
88 contextOrg: IdlObject;
89 orgFieldLabel: string;
94 private idl: IdlService,
95 private org: OrgService,
96 private auth: AuthService,
97 private pcrud: PcrudService,
98 private perm: PermService,
99 private toast: ToastService
101 this.translatableFields = [];
106 if (this.disableOrgFilter) {
107 this.orgField = null;
111 if (!this.orgField) {
112 // If no org unit field is specified, try to find one.
113 // If an object type has multiple org unit fields, the
114 // caller should specify one or disable org unit filter.
115 this.idlClassDef.fields.forEach(field => {
116 if (field['class'] === 'aou') {
117 this.orgField = field.name;
123 this.orgFieldLabel = this.idlClassDef.field_map[this.orgField].label;
124 this.contextOrg = this.org.root();
129 this.idlClassDef = this.idl.classes[this.idlClass];
130 this.pkeyField = this.idlClassDef.pkey || 'id';
132 this.translatableFields =
133 this.idlClassDef.fields.filter(f => f.i18n).map(f => f.name);
135 if (!this.persistKey) {
138 (this.persistKeyPfx ? this.persistKeyPfx + '.' : '') +
139 this.idlClassDef.table;
142 // Limit the view org selector to orgs where the user has
143 // permacrud-encoded view permissions.
144 const pc = this.idlClassDef.permacrud;
145 if (pc && pc.retrieve) {
146 this.viewPerms = pc.retrieve.perms;
149 this.checkCreatePerms();
150 this.applyOrgValues();
152 // If the caller provides not data source, create a generic one.
153 if (!this.dataSource) {
154 this.initDataSource();
157 // TODO: pass the row activate handler via the grid markup
158 this.grid.onRowActivate.subscribe(
159 (idlThing: IdlObject) => this.showEditDialog(idlThing)
162 this.editSelected = (idlThings: IdlObject[]) => {
164 // Edit each IDL thing one at a time
165 const editOneThing = (thing: IdlObject) => {
168 this.showEditDialog(thing).then(
169 () => editOneThing(idlThings.shift()));
172 editOneThing(idlThings.shift()); };
174 this.createNew = () => {
175 this.editDialog.mode = 'create';
176 this.editDialog.open({size: this.dialogSize}).then(
178 this.createString.current()
179 .then(str => this.toast.success(str));
183 this.createErrString.current()
184 .then(str => this.toast.danger(str));
189 this.deleteSelected = (idlThings: IdlObject[]) => {
190 idlThings.forEach(idlThing => idlThing.isdeleted(true));
191 this.pcrud.autoApply(idlThings).subscribe(
192 val => console.debug('deleted: ' + val),
194 () => this.grid.reload()
198 // Open the field translation dialog.
199 // Link the next/previous actions to cycle through each translatable
200 // field on each row.
201 this.translate = () => {
202 this.translateRowIdx = 0;
203 this.translateFieldIdx = 0;
204 this.translator.fieldName = this.translatableFields[this.translateFieldIdx];
205 this.translator.idlObject = this.dataSource.data[this.translateRowIdx];
207 this.translator.nextString = () => {
209 if (this.translateFieldIdx < this.translatableFields.length - 1) {
210 this.translateFieldIdx++;
212 } else if (this.translateRowIdx < this.dataSource.data.length - 1) {
213 this.translateRowIdx++;
214 this.translateFieldIdx = 0;
217 this.translator.idlObject =
218 this.dataSource.data[this.translateRowIdx];
219 this.translator.fieldName =
220 this.translatableFields[this.translateFieldIdx];
223 this.translator.prevString = () => {
225 if (this.translateFieldIdx > 0) {
226 this.translateFieldIdx--;
228 } else if (this.translateRowIdx > 0) {
229 this.translateRowIdx--;
230 this.translateFieldIdx = 0;
233 this.translator.idlObject =
234 this.dataSource.data[this.translateRowIdx];
235 this.translator.fieldName =
236 this.translatableFields[this.translateFieldIdx];
239 this.translator.open({size: 'lg'});
244 this.canCreate = false;
245 const pc = this.idlClassDef.permacrud || {};
246 const perms = pc.create ? pc.create.perms : [];
247 if (perms.length === 0) { return; }
249 this.perm.hasWorkPermAt(perms, true).then(permMap => {
250 Object.keys(permMap).forEach(key => {
251 if (permMap[key].length > 0) {
252 this.canCreate = true;
258 orgOnChange(org: IdlObject) {
259 this.contextOrg = org;
264 this.dataSource = new GridDataSource();
266 this.dataSource.getRows = (pager: Pager, sort: any[]) => {
267 const orderBy: any = {};
270 // Sort specified from grid
271 orderBy[this.idlClass] = sort[0].name + ' ' + sort[0].dir;
273 } else if (this.sortField) {
274 // Default sort field
275 orderBy[this.idlClass] = this.sortField;
279 offset: pager.offset,
284 if (this.contextOrg) {
285 // Filter rows by those linking to the context org and
286 // optionally ancestor and descendant org units.
288 let orgs = [this.contextOrg.id()];
290 if (this.includeOrgAncestors) {
291 orgs = this.org.ancestors(this.contextOrg, true);
294 if (this.includeOrgDescendants) {
295 // can result in duplicate workstation org IDs... meh
297 this.org.descendants(this.contextOrg, true));
301 search[this.orgField] = orgs;
302 return this.pcrud.search(this.idlClass, search, searchOps);
305 // No org filter -- fetch all rows
306 return this.pcrud.retrieveAll(this.idlClass, searchOps);
310 disableAncestorSelector(): boolean {
311 return this.contextOrg &&
312 this.contextOrg.id() === this.org.root().id();
315 disableDescendantSelector(): boolean {
316 return this.contextOrg && this.contextOrg.children().length === 0;
319 showEditDialog(idlThing: IdlObject) {
320 this.editDialog.mode = 'update';
321 this.editDialog.recId = idlThing[this.pkeyField]();
322 return this.editDialog.open({size: this.dialogSize}).then(
324 this.successString.current()
325 .then(str => this.toast.success(str));
329 this.updateFailedString.current()
330 .then(str => this.toast.danger(str));