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
27var RecordMode = {
28 View: 0,
29 Edit: 1,
30 AddNew: 2
31}
32
33// Define paged view model class
34function 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
506var 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