// Globally available components
import {PrintComponent} from '@eg/share/print/print.component';
import {DialogComponent} from '@eg/share/dialog/dialog.component';
+import {OrgSelectComponent} from '@eg/share/org-select/org-select.component';
import {ConfirmDialogComponent} from '@eg/share/dialog/confirm.component';
import {PromptDialogComponent} from '@eg/share/dialog/prompt.component';
import {ProgressInlineComponent} from '@eg/share/dialog/progress-inline.component';
@NgModule({
declarations: [
PrintComponent,
+ OrgSelectComponent,
DialogComponent,
ConfirmDialogComponent,
PromptDialogComponent,
NgbModule,
FormsModule,
PrintComponent,
+ OrgSelectComponent,
DialogComponent,
ConfirmDialogComponent,
PromptDialogComponent,
has_org_setting: boolean;
has_user_setting: boolean;
has_workstation_setting: boolean;
+ aous_view_perm: string;
+ aous_update_perm: string;
}
@Injectable({providedIn: 'root'})
.toPromise().then(appliedCount => {
if (Number(appliedCount) > 0) { // value applied
- return this.cache[key] = value;
+
+ if (this.cache[key]) {
+ // Update the cached value
+ return this.cache[key].value = value;
+ }
+
+ // Typically, a copy of the setting summary would have
+ // been fetched/cached before a new value is applied,
+ // but it's not guaranteed. If no local summary exists
+ // at apply-time, fetch the summary now so we'll have
+ // an accurate representation of the summary.
+ return this.getItem(key).then(() => value);
}
return Promise.reject(
const values: any = {};
keys.forEach(key => {
if (this.cache[key]) {
- values[key] = this.cache[key];
+ values[key] = this.cache[key].value;
}
});
serverKeys, this.auth.token()
).subscribe(
summary => {
- this.cache[summary.name] =
- values[summary.name] = summary.value;
+ this.cache[summary.name] = summary;
+ values[summary.name] = summary.value;
+ // boolean-ize the bools
+ summary.has_org_setting = summary.has_org_setting === 't';
+ summary.has_user_setting = summary.has_user_setting === 't';
+ summary.has_workstation_set = summary.has_workstation_set === 't';
},
err => reject,
() => resolve(values)
removeItem(key: string): Promise<any> {
return this.setItem(key, null);
}
+
+ getSettingSummary(key: string): Promise<ServerSettingSummary> {
+ if (this.cache[key]) {
+ return Promise.resolve(this.cache[key]);
+ }
+
+ // Setting summary not yet cached, fetch it
+ return this.getItem(key).then(() => this.cache[key]);
+ }
}
--- /dev/null
+<ng-template #dialogContent>
+ <div class="modal-header bg-info">
+ <h4 class="modal-title" i18n>Grid Configuration Library Setting</h4>
+ <button type="button" class="close"
+ i18n-aria-label aria-label="Close"
+ (click)="dismiss('cross_click')">
+ <span aria-hidden="true">×</span>
+ </button>
+ </div>
+ <div class="modal-body eg-grid-column-config-dialog">
+ <div class="row m-3">
+ <div class="col-lg-6">
+ <span>Apply settings to Org Unit:</span>
+ </div>
+ <div class="col-lg-6">
+ <!-- TODO -->
+ <eg-org-select></eg-org-select>
+ storeSettingsAsAousOrgs
+ </div>
+ </div>
+ <div class="row m-3">
+ <div class="col-lg-12">
+ <p i18n>
+ Applying values to setting: {{'eg.grid.' + context.persistKey}}.
+ </p>
+<pre>{{settingsAsString()}}</pre>
+ </div>
+ </div>
+ </div>
+ <div class="modal-footer">
+ <button type="button" class="btn btn-warning ml-2"
+ (click)="dismiss('canceled')" i18n>Cancel</button>
+ <button type="button" class="btn btn-success ml-2"
+ (click)="save()" i18n>Save</button>
+ </div>
+</ng-template>
+
+
+
--- /dev/null
+import {Component, Input, OnInit} from '@angular/core';
+import {DialogComponent} from '@eg/share/dialog/dialog.component';
+import {GridContext} from './grid';
+
+@Component({
+ selector: 'eg-grid-aous-org-select',
+ templateUrl: './grid-aous-org-select.component.html'
+})
+
+/**
+ */
+export class GridAousOrgSelectComponent extends DialogComponent implements OnInit {
+ @Input() context: GridContext;
+
+ settingsAsString(): string {
+ if (this.context) {
+ return JSON.stringify(
+ this.context.columnSet.compileSaveObject(), null, 2);
+ }
+ }
+
+ // TODO
+ /*
+ hideTheseOrgs(): number[] {
+ const orgs = [];
+ if (this.context) {
+
+ this.context.storeSettingsAsAousOrgs.forEach(ouId => {
+ if (
+
+ } else {
+ return [];
+ }
+ }
+ */
+}
+
+
<div class="eg-grid-toolbar mb-2">
+ <eg-grid-aous-org-select #aousOrgSelector [context]="gridContext">
+ </eg-grid-aous-org-select>
+
<div class="btn-toolbar">
<!-- buttons -->
<span class="material-icons">save</span>
<span class="ml-2" i18n>Save Grid Settings</span>
</a>
+ <a *ngIf="gridContext.storeSettingsAsAousOrgs.length"
+ class="dropdown-item label-with-material-icon"
+ (click)="aousOrgSelector.open()">
+ <span class="material-icons">save</span>
+ <span class="ml-2" i18n>Save Grid Settings As Library Setting</span>
+ </a>
+
<a class="dropdown-item label-with-material-icon"
(click)="gridContext.columnSet.reset()">
<span class="material-icons">restore</span>
import {Component, Input, OnInit, Host} from '@angular/core';
import {DomSanitizer, SafeUrl} from '@angular/platform-browser';
import {Pager} from '@eg/share/util/pager';
+import {ServerStoreService} from '@eg/core/server-store.service';
import {GridColumn, GridColumnSet, GridToolbarButton,
GridToolbarAction, GridContext, GridDataSource} from '@eg/share/grid/grid';
import {GridColumnWidthComponent} from './grid-column-width.component';
csvExportUrl: SafeUrl;
csvExportFileName: string;
- constructor(private sanitizer: DomSanitizer) {}
+ constructor(
+ private sanitizer: DomSanitizer,
+ private store: ServerStoreService
+ ) {}
ngOnInit() {}
saveGridConfig() {
- // TODO: when server-side settings are supported, this operation
- // may offer to save to user/workstation OR org unit settings
- // depending on perms.
-
this.gridContext.saveGridConfig().then(
// hide the with config after saving
ok => this.colWidthConfig.isVisible = false,
);
}
+ // Display a dialog where the user can select the org unit at
+ // which they want to apply the grid configuration.
+ // This action is only available if the permission to update the org
+ // unit setting in question has already been confirmed.
+ saveConfigAsAous() {
+ }
+
performAction(action: GridToolbarAction) {
action.action(this.gridContext.getSelectedRows());
}
import {Subscription} from 'rxjs/Subscription';
import {IdlService} from '@eg/core/idl.service';
import {OrgService} from '@eg/core/org.service';
+import {PermService} from '@eg/core/perm.service';
import {ServerStoreService} from '@eg/core/server-store.service';
import {FormatService} from '@eg/core/format.service';
import {GridContext, GridColumn, GridDataSource, GridRowFlairEntry} from './grid';
private idl: IdlService,
private org: OrgService,
private store: ServerStoreService,
+ private perms: PermService,
private format: FormatService
) {
- this.context =
- new GridContext(this.idl, this.org, this.store, this.format);
+ this.context = new GridContext(
+ this.idl, this.org, this.store, this.format, this.perms);
this.onRowActivate = new EventEmitter<any>();
this.onRowClick = new EventEmitter<any>();
}
import {GridColumnConfigComponent} from './grid-column-config.component';
import {GridColumnWidthComponent} from './grid-column-width.component';
import {GridPrintComponent} from './grid-print.component';
+import {GridAousOrgSelectComponent} from './grid-aous-org-select.component';
@NgModule({
GridToolbarActionComponent,
GridColumnConfigComponent,
GridColumnWidthComponent,
- GridPrintComponent
+ GridPrintComponent,
+ GridAousOrgSelectComponent
],
imports: [
EgCommonModule
import {Subscription} from 'rxjs/Subscription';
import {IdlService, IdlObject} from '@eg/core/idl.service';
import {OrgService} from '@eg/core/org.service';
+import {PermService} from '@eg/core/perm.service';
import {ServerStoreService} from '@eg/core/server-store.service';
import {FormatService} from '@eg/core/format.service';
import {Pager} from '@eg/share/util/pager';
defaultHiddenFields: string[];
overflowCells: boolean;
+ // List of org unit IDs where the logged in user is allowed to
+ // save a value to the org unit setting whose name matches this
+ // grid's persist key.
+ storeSettingsAsAousOrgs: number[];
+
// Services injected by our grid component
idl: IdlService;
org: OrgService;
store: ServerStoreService;
format: FormatService;
+ perms: PermService;
constructor(
idl: IdlService,
org: OrgService,
store: ServerStoreService,
- format: FormatService) {
+ format: FormatService,
+ perms: PermService) {
this.idl = idl;
this.org = org;
this.store = store;
+ this.perms = perms;
this.format = format;
this.pager = new Pager();
this.pager.limit = 10;
this.toolbarButtons = [];
this.toolbarCheckboxes = [];
this.toolbarActions = [];
+ this.storeSettingsAsAousOrgs = [];
}
init() {
// This is called regardless of the presence of saved
// settings so defaults can be applied.
this.columnSet.applyColumnSettings(columns);
+ this.checkStoreSettingAsAous();
});
}
+
+ // If the grid column configuration may be saved as an org unit
+ // setting value and the logged in user has permission to apply said
+ // value, display the save-as option in the grid settings menu.
+ checkStoreSettingAsAous() {
+
+ this.store.getSettingSummary('eg.grid.' + this.persistKey).then(
+ summary => {
+ if (summary && summary.has_org_setting) {
+ const perm = summary.aous_update_perm
+ || 'UPDATE_ORG_UNIT_SETTING_ALL';
+ this.perms.hasWorkPermAt([perm], true).then(
+ perms => this.storeSettingsAsAousOrgs = perms[perm]);
+ }
+ }
+ );
+ }
+
reload() {
// Give the UI time to settle before reloading grid data.
// This can help when data retrieval depends on a value
return this.store.setItem('eg.grid.' + this.persistKey, conf);
}
- // TODO: saveGridConfigAsOrgSetting(...)
-
getGridConfig(persistKey: string): Promise<GridPersistConf> {
if (!persistKey) { return Promise.resolve(null); }
return this.store.getItem('eg.grid.' + persistKey);
import {StaffBannerComponent} from './share/staff-banner.component';
import {ComboboxComponent} from '@eg/share/combobox/combobox.component';
import {ComboboxEntryComponent} from '@eg/share/combobox/combobox-entry.component';
-import {OrgSelectComponent} from '@eg/share/org-select/org-select.component';
import {AccessKeyDirective} from '@eg/share/accesskey/accesskey.directive';
import {AccessKeyService} from '@eg/share/accesskey/accesskey.service';
import {AccessKeyInfoComponent} from '@eg/share/accesskey/accesskey-info.component';
StaffBannerComponent,
ComboboxComponent,
ComboboxEntryComponent,
- OrgSelectComponent,
AccessKeyDirective,
AccessKeyInfoComponent,
ToastComponent,
StaffBannerComponent,
ComboboxComponent,
ComboboxEntryComponent,
- OrgSelectComponent,
AccessKeyDirective,
AccessKeyInfoComponent,
ToastComponent,
value JSON,
has_org_setting BOOLEAN,
has_user_setting BOOLEAN,
- has_workstation_setting BOOLEAN
+ has_workstation_setting BOOLEAN,
+ aous_view_perm TEXT,
+ aous_update_perm TEXT
);
CREATE OR REPLACE FUNCTION actor.get_cascade_setting(
FROM config.org_unit_setting_type WHERE name = setting_name;
IF FOUND THEN
summary.has_org_setting := TRUE;
+
+ -- Return view/update perm codes to the caller
+ IF org_setting_type.view_perm IS NOT NULL THEN
+ SELECT code INTO summary.aous_view_perm
+ FROM permission.perm_list WHERE id = org_setting_type.view_perm;
+ END IF;
+
+ IF org_setting_type.update_perm IS NOT NULL THEN
+ SELECT code INTO summary.aous_update_perm
+ FROM permission.perm_list WHERE id = org_setting_type.update_perm;
+ END IF;
ELSE
summary.has_org_setting := FALSE;
END IF;
--- /dev/null
+BEGIN;
+
+-- SELECT evergreen.upgrade_deps_block_check('TODO', :eg_version);
+
+ALTER TYPE actor.cascade_setting_summary
+ ADD ATTRIBUTE aous_view_perm TEXT,
+ ADD ATTRIBUTE aous_update_perm TEXT;
+
+CREATE OR REPLACE FUNCTION actor.get_cascade_setting(
+ setting_name TEXT, org_id INT, user_id INT, workstation_id INT)
+ RETURNS actor.cascade_setting_summary AS
+$FUNC$
+DECLARE
+ setting_value JSON;
+ summary actor.cascade_setting_summary;
+ org_setting_type config.org_unit_setting_type%ROWTYPE;
+BEGIN
+
+ summary.name := setting_name;
+
+ -- Collect the org setting type status first in case we exit early.
+ -- The existance of an org setting type is not considered
+ -- privileged information.
+ SELECT INTO org_setting_type *
+ FROM config.org_unit_setting_type WHERE name = setting_name;
+ IF FOUND THEN
+ summary.has_org_setting := TRUE;
+
+ -- Return view/update perm codes to the caller
+ IF org_setting_type.view_perm IS NOT NULL THEN
+ SELECT code INTO summary.aous_view_perm
+ FROM permission.perm_list WHERE id = org_setting_type.view_perm;
+ END IF;
+
+ IF org_setting_type.update_perm IS NOT NULL THEN
+ SELECT code INTO summary.aous_update_perm
+ FROM permission.perm_list WHERE id = org_setting_type.update_perm;
+ END IF;
+ ELSE
+ summary.has_org_setting := FALSE;
+ END IF;
+
+ -- User and workstation settings have the same priority.
+ -- Start with user settings since that's the simplest code path.
+ -- The workstation_id is ignored if no user_id is provided.
+ IF user_id IS NOT NULL THEN
+
+ SELECT INTO summary.value value FROM actor.usr_setting
+ WHERE usr = user_id AND name = setting_name;
+
+ IF FOUND THEN
+ -- if we have a value, we have a setting type
+ summary.has_user_setting := TRUE;
+
+ IF workstation_id IS NOT NULL THEN
+ -- Only inform the caller about the workstation
+ -- setting type disposition when a workstation id is
+ -- provided. Otherwise, it's NULL to indicate UNKNOWN.
+ summary.has_workstation_setting := FALSE;
+ END IF;
+
+ RETURN summary;
+ END IF;
+
+ -- no user setting value, but a setting type may exist
+ SELECT INTO summary.has_user_setting EXISTS (
+ SELECT TRUE FROM config.usr_setting_type
+ WHERE name = setting_name
+ );
+
+ IF workstation_id IS NOT NULL THEN
+
+ IF NOT summary.has_user_setting THEN
+ -- A workstation setting type may only exist when a user
+ -- setting type does not.
+
+ SELECT INTO summary.value value
+ FROM actor.workstation_setting
+ WHERE workstation = workstation_id AND name = setting_name;
+
+ IF FOUND THEN
+ -- if we have a value, we have a setting type
+ summary.has_workstation_setting := TRUE;
+ RETURN summary;
+ END IF;
+
+ -- no value, but a setting type may exist
+ SELECT INTO summary.has_workstation_setting EXISTS (
+ SELECT TRUE FROM config.workstation_setting_type
+ WHERE name = setting_name
+ );
+ END IF;
+
+ -- Finally make use of the workstation to determine the org
+ -- unit if none is provided.
+ IF org_id IS NULL AND summary.has_org_setting THEN
+ SELECT INTO org_id owning_lib
+ FROM actor.workstation WHERE id = workstation_id;
+ END IF;
+ END IF;
+ END IF;
+
+ -- Some org unit settings are protected by a view permission.
+ -- First see if we have any data that needs protecting, then
+ -- check the permission if needed.
+
+ IF NOT summary.has_org_setting THEN
+ RETURN summary;
+ END IF;
+
+ -- avoid putting the value into the summary until we confirm
+ -- the value should be visible to the caller.
+ SELECT INTO setting_value value
+ FROM actor.org_unit_ancestor_setting(setting_name, org_id);
+
+ IF NOT FOUND THEN
+ -- No value found -- perm check is irrelevant.
+ RETURN summary;
+ END IF;
+
+ IF org_setting_type.view_perm IS NOT NULL THEN
+
+ IF user_id IS NULL THEN
+ RAISE NOTICE 'Perm check required but no user_id provided';
+ RETURN summary;
+ END IF;
+
+ IF NOT permission.usr_has_perm(
+ user_id, (SELECT code FROM permission.perm_list
+ WHERE id = org_setting_type.view_perm), org_id)
+ THEN
+ RAISE NOTICE 'Perm check failed for user % on %',
+ user_id, org_setting_type.view_perm;
+ RETURN summary;
+ END IF;
+ END IF;
+
+ -- Perm check succeeded or was not necessary.
+ summary.value := setting_value;
+ RETURN summary;
+END;
+$FUNC$ LANGUAGE PLPGSQL;
+
+COMMIT;
+
+
+