History

(1) GSF.Security: Added modeled security tables. (2) GSF.Web: Added security, data context, view template and scripts related to modeled table operations

Ritchie Carroll committed 10 years ago

Commit a02edca1 parent 46782aa32 files · +5897 −0Browse files

Showing the changes to Source/Libraries/GSF.Web/Model/Scripts/pagedViewModel.js (1 of 32 files). Show all

Unified · Split

Source/Libraries/GSF.Web/Model/Scripts/pagedViewModel.js+568−0
@@ -0,0 +1,568 @@
1+//******************************************************************************************************
2+// pagedViewModel.js - Gbtc
3+//
4+// Copyright © 2016, Grid Protection Alliance. All Rights Reserved.
5+//
6+// Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See
7+// the NOTICE file distributed with this work for additional information regarding copyright ownership.
8+// The GPA licenses this file to you under the MIT License (MIT), the "License"; you may
9+// not use this file except in compliance with the License. You may obtain a copy of the License at:
10+//
11+// http://opensource.org/licenses/MIT
12+//
13+// Unless agreed to in writing, the subject software distributed under the License is distributed on an
14+// "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the
15+// License for the specific language governing permissions and limitations.
16+//
17+// Code Modification History:
18+// ----------------------------------------------------------------------------------------------------
19+// 01/23/2016 - J. Ritchie Carroll
20+// Generated original version of source code.
21+//
22+//******************************************************************************************************
23+
24+// Paged view model base class scripts
25+"use strict";
26+
27+var RecordMode = {
28+ View: 0,
29+ Edit: 1,
30+ AddNew: 2
31+}
32+
33+// Define paged view model class
34+function PagedViewModel() {
35+ const self = this;
36+
37+ // Configuration fields
38+ self.labelField = "{name}"; // Field name that best represents row for delete confirmation
39+ self.primaryKeyFields = ["{id}"]; // Primary key field names array
40+ self.defaultSortField = "{id}"; // Default sort field
41+ self.defaultSortAscending = true; // Default sort ascending flag
42+ self.initialFocusField = ""; // Initial add/edit field with focus
43+ self.modelName = "{name}"; // Name of model used for cookie names, defaults to page title
44+
45+ // Observable fields
46+ self.pageRecords = ko.observableArray(); // Records queried for current page
47+ self.recordCount = ko.observable(0); // Queried record count
48+ self.sortField = ko.observable(self.defaultSortField); // Current sort field, persisted per model
49+ self.sortAscending = ko.observable(self.defaultSortAscending); // Current sort ascending flag, persisted per model
50+ self.unassignedFieldCount = ko.observable(0); // Number of bound fields with missing data
51+ self.dataHubIsConnected = ko.observable(false); // Data hub connected observable flag - externally managed
52+ self.errors = ko.validation.group([ko.observable(0)]); // Validation errors array
53+ self.canEdit = ko.observable(true); // Can edit flag - normally controlled by external roles
54+ self.canAddNew = ko.observable(true); // Can add new flag - normally controlled by external roles
55+ self.canDelete = ko.observable(true); // Can delete flag - normally controlled by external roles
56+
57+ // Internal fields
58+ self._currentPageSize = ko.observable(1);
59+ self._currentPage = ko.observable(0);
60+ self._currentRecord = ko.observable();
61+ self._recordMode = ko.observable(RecordMode.View);
62+ self._isDirty = ko.observable(false);
63+ self._columnWidths = [];
64+
65+ // Properties
66+
67+ // Gets or sets the number records to display on the page
68+ self.currentPageSize = ko.pureComputed({
69+ read: self._currentPageSize,
70+ write: function (value) {
71+ if (value < 1)
72+ value = 1;
73+
74+ if (value !== self._currentPageSize()) {
75+ self._currentPageSize(value);
76+
77+ // Validate current page after page size change
78+ if (self.currentPage())
79+ self.currentPage(self.currentPage());
80+ }
81+ },
82+ owner: self
83+ });
84+
85+ // Gets or sets the current page number
86+ self.currentPage = ko.pureComputed({
87+ read: self._currentPage,
88+ write: function (value) {
89+ if (value < 1)
90+ value = 1;
91+ else if (value > self.totalPages())
92+ value = self.totalPages();
93+
94+ // Requery records for page when current page changes
95+ if (value !== self._currentPage()) {
96+ self._currentPage(value);
97+ self.queryPageRecords();
98+ }
99+ },
100+ owner: self
101+ });
102+
103+ // Gets or sets current record on page
104+ self.currentRecord = ko.pureComputed({
105+ read: self._currentRecord,
106+ write: function(value) {
107+ self._currentRecord(value);
108+ self.isDirty(false);
109+ $(self).trigger("currentRecordChanged");
110+ self.applyValidationParameters();
111+
112+ // Watch for changes to fields in current record
113+ ko.watch(self._currentRecord(), function (parents, child, item) {
114+ self.isDirty(true);
115+ $(self).trigger("currentRecordUpdated", [child]);
116+
117+ // Recalculate unassigned field count after bindings may have been updated by field change
118+ setTimeout(self.calculateUnassignedFieldCount, 10);
119+ });
120+ },
121+ owner: self
122+ });
123+
124+ // Gets or sets the record mode, i.e., view, edit or add new
125+ self.recordMode = ko.pureComputed({
126+ read: self._recordMode,
127+ write: function (newMode) {
128+ const oldMode = self._recordMode();
129+
130+ if (newMode !== oldMode) {
131+ self._recordMode(newMode);
132+
133+ if (oldMode === RecordMode.View && newMode === RecordMode.Edit)
134+ self.setFocusOnInitialField();
135+
136+ $(self).trigger("recordModeChanged", [oldMode, newMode]);
137+ }
138+ },
139+ owner: self
140+ });
141+
142+ // Gets or sets current record changed flag
143+ self.isDirty = ko.pureComputed({
144+ read: self._isDirty,
145+ write: function (value) {
146+ if (value === undefined)
147+ value = true;
148+
149+ self._isDirty(value);
150+ self.calculateUnassignedFieldCount();
151+ },
152+ owner: self
153+ });
154+
155+ // Gets total number pages based on total record count and current page size
156+ self.totalPages = ko.pureComputed(function () {
157+ return Math.max(Math.ceil(self.recordCount() / self.currentPageSize()), 1);
158+ });
159+
160+ // Gets flag that determines if current page is first page
161+ self.onFirstPage = ko.pureComputed(function () {
162+ return self.currentPage() <= 1;
163+ });
164+
165+ // Gets flag that determines if current page is last page
166+ self.onLastPage = ko.pureComputed(function () {
167+ return self.currentPage() >= self.totalPages();
168+ });
169+
170+ // Return validation errors, i.e., non-required field errors
171+ self.validationErrors = ko.pureComputed(function() {
172+ return self.errors().length - self.unassignedFieldCount();
173+ }).extend({ notify: "always" });
174+
175+ // Delegates
176+ self.queryRecordCount = function () { };
177+ self.queryRecords = function (/* sortField, ascending, page, pageSize */) { };
178+ self.deleteRecord = function (/* keyValues[] */) { };
179+ self.newRecord = function () { };
180+ self.addNewRecord = function (/* record */) { };
181+ self.updateRecord = function (/* record */) { };
182+ self.applyValidationParameters = function () { };
183+
184+ // Setters needed to assign delegate properties, 'cause Javascript
185+ self.setQueryRecordCount = function (queryRecordCountFunction) {
186+ self.queryRecordCount = queryRecordCountFunction;
187+ }
188+
189+ self.setQueryRecords = function (queryRecordsFunction) {
190+ self.queryRecords = queryRecordsFunction;
191+ }
192+
193+ self.setDeleteRecord = function (deleteRecordFunction) {
194+ self.deleteRecord = deleteRecordFunction;
195+ }
196+
197+ self.setNewRecord = function (newRecordFunction) {
198+ self.newRecord = newRecordFunction;
199+ }
200+
201+ self.setAddNewRecord = function (addNewRecordFunction) {
202+ self.addNewRecord = addNewRecordFunction;
203+ }
204+
205+ self.setUpdateRecord = function (updateRecordFunction) {
206+ self.updateRecord = updateRecordFunction;
207+ }
208+
209+ self.setApplyValidationParameters = function (applyValidationParametersFunction) {
210+ self.applyValidationParameters = applyValidationParametersFunction;
211+ }
212+
213+ // Methods
214+ self.initialize = function () {
215+ // Restore any previous sort order
216+ const lastSortField = Cookies.get(self.modelName + "!LastSortField");
217+ const lastSortAscending = Cookies.get(self.modelName + "!LastSortAscending");
218+
219+ if (lastSortField === undefined)
220+ self.sortField(self.defaultSortField);
221+ else
222+ self.sortField(lastSortField);
223+
224+ if (lastSortAscending === undefined)
225+ self.sortAscending(self.defaultSortAscending);
226+ else
227+ self.sortAscending(lastSortAscending === "true");
228+
229+ if (self.dataHubIsConnected()) {
230+ // Query total record count
231+ self.queryRecordCount().done(function (count) {
232+ // Update record count observable
233+ self.recordCount(count);
234+
235+ // Force page refresh when record count has been updated
236+ const currentPage = self.currentPage();
237+ self._currentPage(0);
238+ self.currentPage(currentPage);
239+ });
240+
241+ // Initialize current record with an empty row
242+ self.newRecord().done(function (emptyRecord) {
243+ self.deriveObservableRecord(emptyRecord).done(function (observableRecord) {
244+ self.currentRecord(observableRecord);
245+ });
246+ });
247+ }
248+
249+ // Initialize column widths array
250+ self._columnWidths = [];
251+ const columns = $("#recordRow").find("td");
252+
253+ for (let i = 0; i < columns.length; i++) {
254+ self._columnWidths.push($(columns[i]).width());
255+ }
256+ }
257+
258+ self.calculatePageSize = function (forceRefresh) {
259+ // Calculate total number of table rows that will fit within current page height
260+ const remainingHeight = calculateRemainingBodyHeight() -
261+ $("#contentWell").paddingHeight() -
262+ $("#responsiveTableDiv").paddingHeight() -
263+ $("#recordsTable").paddingHeight() -
264+ $("#pageControlsRow").outerHeight(true);
265+
266+ // Estimate page size based on height of first record row
267+ const firstRow = $("#recordRow");
268+ var pageSize = (remainingHeight / firstRow.outerHeight(true)).truncate();
269+
270+ if (!pageSize || isNaN(pageSize) || !isFinite(pageSize) || pageSize < 1)
271+ pageSize = 1;
272+
273+ if (forceRefresh === undefined)
274+ forceRefresh = false;
275+
276+ // Check for dynamic Bootstrap column resizing, in which case we need to refresh data
277+ // for cases where binding may be truncating data lengths, see $.fn.truncateToWidth
278+ const columns = firstRow.find("td");
279+ var columnWidth;
280+
281+ for (let i = 0; i < columns.length; i++) {
282+ columnWidth = $(columns[i]).width();
283+
284+ if (self._columnWidths[i] !== columnWidth) {
285+ self._columnWidths[i] = columnWidth;
286+ forceRefresh = true;
287+ }
288+ }
289+
290+ if (pageSize !== self.currentPageSize() || forceRefresh) {
291+ const currentPage = self.currentPage();
292+
293+ // Updating page size will validate current page number
294+ self.currentPageSize(pageSize);
295+
296+ // Requery data for page unless current page was dynamically changed
297+ // by page size update and has reloaded data already
298+ if (currentPage === self.currentPage())
299+ self.queryPageRecords();
300+ }
301+ }
302+
303+ self.calculateUnassignedFieldCount = function () {
304+ // Derive unassigned field count based on existence of Bootstrap "has-error" class
305+ self.unassignedFieldCount($("#addNewEditDialog div.form-group.has-error").length);
306+ }
307+
308+ self.refreshValidationErrors = function () {
309+ // Force re-evaluation of validation errors property
310+ self.unassignedFieldCount.valueHasMutated();
311+
312+ // Make sure any initial validation error messages are visible
313+ self.errors.showAllMessages();
314+ }
315+
316+ // Convert observable object to a simple Javascript record
317+ self.deriveJSRecord = function () {
318+ return $.Deferred(function (deferred) {
319+ const observableRecord = self.currentRecord();
320+ const promises = [];
321+
322+ // Allow customization of observable record before conversion
323+ $(self).trigger("derivingJSRecord", [observableRecord, promises]);
324+
325+ $(promises).whenAll().done(function () {
326+ deferred.resolve(ko.mapping.toJS(observableRecord));
327+ });
328+ }).promise();
329+ }
330+
331+ // Convert simple Javascript record to an observable object
332+ self.deriveObservableRecord = function (record) {
333+ return $.Deferred(function (deferred) {
334+ const observableRecord = ko.mapping.fromJS(record);
335+ const promises = [];
336+
337+ // Allow customization of new observable record
338+ $(self).trigger("derivingObservableRecord", [observableRecord, promises]);
339+
340+ $(promises).whenAll().done(function () {
341+ // Apply validation binding to current observable record
342+ self.errors = ko.validation.group(observableRecord);
343+ self.refreshValidationErrors();
344+ deferred.resolve(observableRecord);
345+ });
346+ }).promise();
347+ }
348+
349+ self.setFocusOnInitialField = function () {
350+ if (!isEmpty(self.initialFocusField))
351+ $("#" + self.initialFocusField).focus();
352+ }
353+
354+ self.nextPage = function () {
355+ if (self.currentPage() < self.totalPages())
356+ self.currentPage(self.currentPage() + 1);
357+ }
358+
359+ self.previousPage = function () {
360+ if (self.currentPage() > 1)
361+ self.currentPage(self.currentPage() - 1);
362+ }
363+
364+ self.updateSortOrder = function (fieldName, ascending) {
365+ self.sortField(fieldName);
366+ self.sortAscending(ascending);
367+ self.queryPageRecords();
368+
369+ // Save last sort order
370+ Cookies.set(self.modelName + "!LastSortField", self.sortField(), { expires: 365 });
371+ Cookies.set(self.modelName + "!LastSortAscending", self.sortAscending().toString(), { expires: 365 });
372+ }
373+
374+ self.isSortOrder = function (fieldName, ascending) {
375+ return self.sortField().toUpperCase() === fieldName.toUpperCase() && self.sortAscending() === ascending;
376+ }
377+
378+ self.queryPageRecords = function () {
379+ if (self.dataHubIsConnected())
380+ self.queryRecords(self.sortField(), self.sortAscending(), self.currentPage(), self.currentPageSize()).done(function (records) {
381+ $(self).trigger("pageRecordsQueried", [records]);
382+ self.pageRecords.removeAll();
383+ self.pageRecords(records);
384+ refreshHubDependentControlState();
385+ $("[id='recordRow']").css("visibility", "visible");
386+ $("#loadingDataLabel").hide();
387+
388+ // Validate proper page size after any record refresh
389+ setTimeout(self.calculatePageSize, 150);
390+ }).fail(function (error) {
391+ showErrorMessage(error);
392+ });
393+ }
394+
395+ self.removePageRecord = function (record) {
396+ if (!self.canDelete())
397+ return;
398+
399+ if (self.dataHubIsConnected() && confirm("Are you sure you want to delete \"" + record[self.labelField] + "\"?")) {
400+ const keyValues = [];
401+
402+ for (var i = 0; i < self.primaryKeyFields.length; i++) {
403+ keyValues.push(record[self.primaryKeyFields[i]]);
404+ }
405+
406+ self.deleteRecord(keyValues).done(function () {
407+ self.pageRecords.remove(record);
408+ self.initialize();
409+ $(self).trigger("recordDeleted", [record]);
410+ showInfoMessage("Deleted record...");
411+ }).fail(function (error) {
412+ showErrorMessage(error);
413+ });
414+ }
415+ }
416+
417+ self.savePageRecord = function () {
418+ switch (self.recordMode()) {
419+ case RecordMode.Edit:
420+ self.saveEditedRecord();
421+ break;
422+ case RecordMode.AddNew:
423+ self.saveNewRecord();
424+ break;
425+ }
426+ }
427+
428+ self.saveEditedRecord = function () {
429+ if (!self.canEdit())
430+ return;
431+
432+ if (self.dataHubIsConnected()) {
433+ self.deriveJSRecord().done(function (record) {
434+ self.updateRecord(record).done(function () {
435+ self.initialize();
436+ $(self).trigger("recordSaved", [record, false]);
437+ showInfoMessage("Saved updated record...");
438+ }).fail(function (error) {
439+ showErrorMessage(error);
440+ });
441+ });
442+ }
443+ }
444+
445+ self.saveNewRecord = function () {
446+ if (!self.canAddNew())
447+ return;
448+
449+ if (self.dataHubIsConnected()) {
450+ self.deriveJSRecord().done(function (record) {
451+ self.addNewRecord(record).done(function () {
452+ self.initialize();
453+ $(self).trigger("recordSaved", [record, true]);
454+ showInfoMessage("Saved new record...");
455+ }).fail(function (error) {
456+ showErrorMessage(error);
457+ });
458+ });
459+ }
460+ }
461+
462+ self.viewPageRecord = function (record) {
463+ self.deriveObservableRecord(record).done(function (observableRecord) {
464+ self.recordMode(RecordMode.View);
465+ self.currentRecord(observableRecord);
466+ $("#addNewEditDialog").modal("show");
467+ });
468+ }
469+
470+ self.editPageRecord = function (record) {
471+ if (!self.canEdit())
472+ return;
473+
474+ self.deriveObservableRecord(record).done(function (observableRecord) {
475+ self.recordMode(RecordMode.Edit);
476+ self.currentRecord(observableRecord);
477+ $("#addNewEditDialog").modal("show");
478+ });
479+ }
480+
481+ self.addPageRecord = function () {
482+ if (!self.canAddNew())
483+ return;
484+
485+ if (self.dataHubIsConnected()) {
486+ self.newRecord().done(function (emptyRecord) {
487+ // Raise event to allow any new record initialization
488+ $(self).trigger("newRecord", [emptyRecord]);
489+
490+ self.deriveObservableRecord(emptyRecord).done(function (observableRecord) {
491+ self.recordMode(RecordMode.AddNew);
492+ self.currentRecord(observableRecord);
493+ $("#addNewEditDialog").modal("show");
494+ });
495+ });
496+ }
497+ }
498+
499+ self.cancelPageRecord = function () {
500+ if (!self.isDirty() || confirm("Are you sure you want to discard unsaved changes?"))
501+ $("#addNewEditDialog").modal("hide");
502+ }
503+};
504+
505+// Define page scoped view model instance
506+var viewModel = new PagedViewModel();
507+
508+(function ($, viewPort) {
509+ $("#bodyContainer").addClass("fill-height");
510+
511+ $("#titleText").html("Records: <span data-bind='text: recordCount'>calculating...</span>");
512+
513+ $("#firstPageButton").click(function () {
514+ viewModel.currentPage(1);
515+ });
516+
517+ $("#previousPageButton").click(function () {
518+ viewModel.previousPage();
519+ });
520+
521+ $("#nextPageButton").click(function () {
522+ viewModel.nextPage();
523+ });
524+
525+ $("#lastPageButton").click(function () {
526+ viewModel.currentPage(viewModel.totalPages());
527+ });
528+
529+ $(window).on("hubConnected", function (event) {
530+ viewModel.dataHubIsConnected(true);
531+ viewModel.initialize();
532+ });
533+
534+ $(window).on("hubDisconnected", function (event) {
535+ viewModel.dataHubIsConnected(false);
536+ });
537+
538+ $(window).on("messageVisibiltyChanged", function (event) {
539+ viewModel.calculatePageSize();
540+ });
541+
542+ $("#addNewEditDialog").on("shown.bs.modal", function () {
543+ viewModel.setFocusOnInitialField();
544+ $("[data-toggle='tooltip']").tooltip();
545+ });
546+
547+ $(window).resize(
548+ viewPort.changed(function () {
549+ viewModel.calculatePageSize();
550+ }));
551+
552+ viewModel.calculatePageSize();
553+
554+ ko.validation.rules.pattern.message = "Invalid";
555+
556+ ko.validation.init({
557+ registerExtenders: true,
558+ messagesOnModified: true,
559+ insertMessages: true,
560+ parseInputAttributes: true,
561+ allowHtmlMessages: true,
562+ messageTemplate: null,
563+ grouping: { deep: true, observable: true, live: true }
564+ }, true);
565+
566+ ko.applyBindings(viewModel);
567+
568+})(jQuery, ResponsiveBootstrapToolkit);

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t