From b978a02fbb8bb413e85bf171e632991fce62b224 Mon Sep 17 00:00:00 2001 From: tab22 <30366222+tab22@users.noreply.github.com> Date: Thu, 30 Oct 2025 20:03:47 +0000 Subject: [PATCH 1/4] Create change_soc.js --- .../Custom Change Schedule/change_soc.js | 1018 +++++++++++++++++ 1 file changed, 1018 insertions(+) create mode 100644 Client-Side Components/UI Scripts/Custom Change Schedule/change_soc.js diff --git a/Client-Side Components/UI Scripts/Custom Change Schedule/change_soc.js b/Client-Side Components/UI Scripts/Custom Change Schedule/change_soc.js new file mode 100644 index 0000000000..25c02cf500 --- /dev/null +++ b/Client-Side Components/UI Scripts/Custom Change Schedule/change_soc.js @@ -0,0 +1,1018 @@ +angular.module("sn.chg_soc.change_soc", [ + "ngAria", + "sn.common", + "sn.common.glide", + "sn.angularstrap", + "sn.chg_soc.accessibility", + "sn.chg_soc.tooltip_overflow", + "sn.chg_soc.notification", + "sn.chg_soc.mousedown", + "sn.chg_soc.gantt", + "sn.chg_soc.data", + "sn.chg_soc.style", + "sn.chg_soc.config", + "sn.chg_soc.share", + "sn.chg_soc.landing_wizard", + "sn.chg_soc.context_menu", + "sn.chg_soc.snCreateNewInvite", + "sn.chg_soc.keyboard", + "sn.chg_soc.popover", + "sn.chg_soc.duration", + "sn.app_itsm.now.filter", + "sn.chg_soc.filter_control", + "sn.chg_soc.loading", + "sn.itsm.change.overflow" + ]) + .constant("SOC", { + BLACKOUT: "blackout", + BLACKOUT_SPAN_COLOR: "#BDC0C4", + CHANGE_REQUEST: "change_request", + DATE_FORMAT: "%Y-%m-%d %H:%i:%s", + GET_CHANGE_SCHEDULE: "/api/sn_chg_soc/soc/changeschedule/", + GET_PARSE_QUERY: "/api/now/ui/query_parse/change_request?sysparm_query=", + ISO_WEEK: "isoWeek", + MAINT: "maint", + MAINT_SPAN_COLOR: "#BDDCFC", + STYLE_PREFIX: "soc_", + SYSPARM_ID: "sysparm_id", + ZOOM_LEVEL_PREF: "sn_chg_soc.change_soc_zoom_level", + COLUMN: { + SHORT_DESCRIPTION: "short_description", + CONFIG_ITEM: "config_item", + // DURATION: "duration", + NUMBER: "number" + }, + STYLE_CLASS_MAP: { + soc_event_bar: "soc-event-bar", + soc_row_child: "soc-row-child", + soc_row_child_end: "soc-row-child-end", + soc_row_child_start: "soc-row-child-start", + soc_row_child_single: "soc-row-child-single" + }, + KEYS: { + TABKEY: 9, + ENTER: 13, + ESCAPE: 27, + SPACE: 32, + LEFT_ARROW: 37, + UP_ARROW: 38, + RIGHT_ARROW: 39, + DOWN_ARROW: 40, + D: 68, + E: 69, + F: 70, + SLASH: 191 + } + }) + .config(["$httpProvider", "$locationProvider", function($httpProvider, $locationProvider) { + $locationProvider.html5Mode({ + enabled: true, + requireBase: false + }); + $httpProvider.interceptors.push("xhrInterceptor"); + }]) + .service("urlService", ["$location", "SOC", function($location, SOC) { + var urlService = this; + + urlService.socId = $location.search()[SOC.SYSPARM_ID]; + + urlService.setChangeScheduleId = function() { + var params = $location.search(); + urlService.socId = params[SOC.SYSPARM_ID]; + }; + }]) + .service("clientService", ["dataService", function(dataService) { + var clientService = this; + + clientService.filter = dataService.definition; + }]) + .directive("changeSoc", ["urlService", "ganttChart", "ganttScale", "dataService", "i18n", "getTemplateUrl", "$templateRequest", "$templateCache", "$filter", "$compile", "$window", "SOC", "TextSearchService", "socNotification", + function(urlService, ganttChart, ganttScale, dataService, i18n, getTemplateUrl, $templateRequest, $templateCache, $filter, $compile, $window, SOC, TextSearchService, socNotification) { + return { + restrict: "A", + scope: false, + transclude: true, + template: "
", + link: function($scope, $element, $attrs, changeSoCCtrl) { + var position = { + delta: { + top: 0, + left: 0 + }, + original: { + top: 0, + left: 0 + } + }; + $scope.ganttInstance = ganttChart.getInstance(urlService.socId); + $scope.gantt = $scope.ganttInstance.gantt; + + // destroy all popovers when resizing the window + angular.element(window).on("resize", function() { + angular.element(".popover.soc-task-popover").popover("destroy"); + _handleDestroyPopover(); + }); + + angular.element(window).on("keydown", function($event) { + if ($event.keyCode === SOC.KEYS.ESCAPE) + _handleDestroyPopover(); + }); + + angular.element(window).on("click", function($event) { + var target = getTargetElement($event); + if (target === null) + _handleDestroyPopover(); // Clicking outside a gantt task + }); + + //size of gantt + $scope.$watch(function() { + return $element[0].offsetWidth + "." + $element[0].offsetHeight; + }, function() { + $scope.gantt.setSizes(); + }); + + $scope.$watch("dataService.definition.condition.dryRun", function(newValue, oldValue) { + if (newValue) + angular.element(".control-left .filter-btn").addClass("dry-run"); + else + angular.element(".control-left .filter-btn").removeClass("dry-run"); + }); + /** + * Marker config + */ + $scope.gantt.config.show_markers = true; + + /** + * Column config + */ + var msgSelectRecord = i18n.getMessage("Show span start"); + $scope.gantt.config.columns = [{ + name: SOC.COLUMN.NUMBER, + label: i18n.getMessage("Number"), + align: "left", + tree: true, + width: 160, + min_width: 160, + resize: true, + template: function(content) { + return "" + content.number + "" + + ""; + } + }, + { + name: SOC.COLUMN.CONFIG_ITEM, + label: i18n.getMessage("Configuration Item"), + align: "left", + width: 220, + min_width: 220, + resize: true, + template: function (content) { + return "" + + content[SOC.COLUMN.CONFIG_ITEM] + + ""; + } + }, + { + + name: SOC.COLUMN.SHORT_DESCRIPTION, + label: "Short Description", + align: "left", + tree: true, + width: 160, + min_width: 160, + resize: true, + template: function(content) { + return "" + content.short_description + "" + + ""; + } + + }, + // { + // name: SOC.COLUMN.DURATION, + // label: i18n.getMessage("Duration"), + // align: "left", + // width: 130, + // min_width: 130, + // template: function(content) { + // return content.dur_display; + // }, + // resize: true + // } + ]; + + /** + * Core Config + */ + // internal date time format + $scope.gantt.config.xml_date = SOC.DATE_FORMAT; + // ARIA attributes + $scope.gantt.config.wai_aria_attributes = true; + // Keyboard navigation + $scope.gantt.config.keyboard_navigation = true; + + /** + * Scrolling + */ + // Prevents scrolling gantt on load of data + $scope.gantt.config.initial_scroll = false; + + $scope.gantt.showTask = function(id) { + var task = this.getTask(id); + var taskSize = this.getTaskPosition(task, task.start, task.end); + var left = Math.max(taskSize.left - this.config.task_scroll_offset, 0); + var ganttVerScrollWidth = angular.element(".gantt_ver_scroll").width(); + var ganttTaskWidth = angular.element(".gantt_task").width() - ganttVerScrollWidth; + + if (Math.abs(this.getScrollState().x - taskSize.left) < ganttTaskWidth && (taskSize.left + taskSize.width) > this.getScrollState().x) + left = null; + + var scrollStateTop = this.getScrollState().y; + var scrollStateBottom = scrollStateTop + this._scroll_sizes().y; + var visibleTaskTop = taskSize.top; + var visibleTaskBottom = taskSize.top + this.config.row_height; + var top = null; + + if (visibleTaskTop < scrollStateTop) + top = visibleTaskTop; + else if (visibleTaskTop > scrollStateTop && (visibleTaskTop < scrollStateBottom && visibleTaskBottom < scrollStateBottom)) + top = null; + else if (visibleTaskTop > scrollStateTop && visibleTaskBottom > scrollStateBottom) + top = visibleTaskBottom - scrollStateBottom + scrollStateTop; + + this.scrollTo(left, top); + }; + + function isPopoverInViewport(el) { + var visibleArea = { + minWidth: angular.element(".gantt_grid_data").width() - 15, // 15px considering the arrow can be shifted to the right (still visible) + maxWidth: angular.element("body").width(), + minTop: angular.element(".gantt_data_area").offset().top, + maxTop: angular.element("body").height() + }; + var currentArea = { + minWidth: el.offset().left, + maxWidth: el.offset().left + el.width(), + minTop: el.offset().top - 15, // 15px considering the arrow + maxTop: el.offset().top + el.height() + 15, // 15px considering the arrow + }; + if (currentArea.minWidth > visibleArea.minWidth && currentArea.maxWidth < visibleArea.maxWidth && + currentArea.minTop > visibleArea.minTop && currentArea.maxTop < visibleArea.maxTop) + return true; + return false; + } + + function adjustPopover() { + popoverElement = angular.element(".popover.soc-task-popover"); + if (position.delta.top - angular.element(".gantt_ver_scroll").scrollTop() === 0 && position.delta.left - angular.element(".gantt_hor_scroll").scrollLeft() === 0) + return; + if (popoverElement.hasClass("in")) { + var newPopoverPosition = { + top: position.original.top + position.delta.top - angular.element(".gantt_ver_scroll").scrollTop(), + left: position.original.left + position.delta.left - angular.element(".gantt_hor_scroll").scrollLeft() + }; + popoverElement.offset(newPopoverPosition); + if (!isPopoverInViewport(popoverElement)) + _handleDestroyPopover(); + } + } + + $scope.lastScrollTop = 0; + $scope.loadScrollTop = 0; + $scope.lazyLoading = false; + $scope.gantt.attachEvent("onGanttScroll", function(left, top) { + if ($scope.lazyLoading) + $scope.lastScrollTop = top; + + if (dataService.count >= $window.NOW.sn_chg_soc.limit) + return; + + var gridHeight = angular.element("div.gantt_ver_scroll").find("div").height(); + var shouldLoad = top > ($scope.loadScrollTop + ((gridHeight - $scope.loadScrollTop) / 4)); + adjustPopover(); + if (!shouldLoad || $scope.isLoading() || !dataService.more || $scope.lazyLoading || top <= $scope.loadScrollTop || top <= $scope.lastScrollTop) + return; + + $scope.loadScrollTop = top; + $scope.lazyLoading = true; + dataService.getChanges(urlService.socId).then(function(model) { + if (dataService.count >= $window.NOW.sn_chg_soc.limit) + socNotification.show("warning", i18n.format(i18n.getMessage("This schedule has exceeded the event limit. The first {0} events based on your order criteria will be displayed."), $window.NOW.sn_chg_soc.limit), 0); + + // Need to provide the tasks so it can calc min/max + ganttScale.setDateRange(dataService.tasks.data); + ganttScale.configureScale(); + $scope.gantt.clearAll(); + ganttChart.addNowMarker(urlService.socId); + // these are the created tasks that will be added to the gantt + $scope.gantt.parse(dataService.tasks, "json"); + $scope.lazyLoading = false; + }); + }); + + /** + * Scales + */ + // Only visible scale is rendered + $scope.gantt.config.smart_scales = true; + // Removes vertical borders on cells + $scope.gantt.config.show_task_cells = false; + $scope.gantt.config.scale_height = 60; + $scope.gantt.config.row_height = 40; + $scope.gantt.config.duration_unit = "hour"; + $scope.gantt.config.duration_step = 1; + $scope.gantt.config.scale_unit = "day"; + $scope.gantt.config.date_scale = "%j %M %Y"; + $scope.gantt.config.subscales = [{ + unit: "hour", + step: 1, + date: "%H:%i" + }]; + + /** + * UI Components + */ + $scope.gantt.config.show_progress = false; + $scope.gantt.config.drag_links = false; + $scope.gantt.config.drag_move = false; + $scope.gantt.config.drag_resize = false; + + /** + * Templates + */ + // Configure use of icons in the gantt rows + $scope.gantt.templates.grid_open = function(item) { + return "
"; + }; + $scope.gantt.templates.grid_folder = function(item) { + return ""; + }; + $scope.gantt.templates.grid_file = function(item) { + return ""; + }; + $scope.gantt.templates.grid_indent = function(item) { + return ""; + }; + $scope.gantt.templates.grid_row_class = function(start, end, task) { + return ""; + }; + $scope.gantt.templates.task_row_class = function(start, end, task) { + return ""; + }; + $scope.gantt.templates.task_class = function(start, end, task) { + return SOC.STYLE_CLASS_MAP.soc_event_bar; + }; + $scope.gantt.templates.task_text = function(start, end, task) { + return ""; + }; + + function getNode(node) { + if (node.hasClass("gantt_row")) + return angular.element(node.children(".gantt_cell")[0]); + if (!node.hasClass("gantt_cell") || node.hasClass("gantt_task_content") || node.hasClass("gantt_task_drag")) + node = node.parent(); + return node; + } + + function getTargetElement($event) { + var node = angular.element($event.target || $event.srcElement); + if ($event.type === "keydown") + return angular.element($event.target); + node = getNode(node); + if (node.hasClass("gantt_task_line")) + return node; + return null; + } + + function handleOpenRecord() { + var task = $filter("filter")(dataService.tasks.data, { + id: this.targetId + })[0]; + $window.location.href = task.table + ".do?&sys_id=" + task.sys_id + + "&sysparm_redirect=" + encodeURIComponent("sn_chg_soc_change_soc.do?sysparm_id=" + urlService.socId); + } + + function _handleDestroyPopover() { + if (angular.element("[soc-popover]").length === 0) + return ""; + angular.element("[soc-popover]").focus(); + angular.element("[soc-popover]").attr("aria-expanded", "false"); + angular.element("[soc-popover]").removeAttr("soc-popover"); + angular.element(".popover.soc-task-popover").popover("destroy"); + } + + function _handleDestroyFlyout() { + $scope.$broadcast("sn.aside.change_soc_side.close"); + } + + function getTargetSelector($event, taskObj) { + if ($event.type !== "keydown") { + var selector = ".gantt_grid_data ." + $event.target.className; + var result = angular.element(selector); + var targetClass = (result.length > 0) ? ".gantt_grid" : ".gantt_task"; + return (result.length > 0) ? targetClass + " [task_id='" + taskObj.id + "'] .gantt_cell:first" : targetClass + " .gantt_task_line[task_id='" + taskObj.id + "']"; + } else + return ".gantt_grid [task_id='" + taskObj.id + "'] .gantt_cell:first"; + } + + function getX(target) { + var result = { + "start": 0, + "end": 0 + }; + var targetElement = { + "start": angular.element(target).offset().left, + "end": angular.element(target).offset().left + angular.element(target).width() + }; + var visibleArea = angular.element(".gantt_task"); + var visibleAreaLimits = { + "start": visibleArea.offset().left, + "end": visibleArea.offset().left + visibleArea.width() + }; + result.start = (targetElement.start > visibleAreaLimits.start) ? targetElement.start : visibleAreaLimits.start; + result.end = (targetElement.end < visibleAreaLimits.end) ? targetElement.end : visibleAreaLimits.end; + return result.start + (result.end - result.start) / 2; + } + + // Callback function used for building the popover template + function buildPopoverTemplate(taskObj, $event, popoverContent, popoverTemplate) { + var $popoverScope = $scope.$new(true); + $popoverScope.openRecord = i18n.getMessage("Open Record"); + $popoverScope.handleOpenRecord = handleOpenRecord; + var targetSelector = getTargetSelector($event, taskObj); + var target = angular.element(targetSelector); + $popoverScope.targetId = taskObj.id; + popoverTemplate = $compile(popoverTemplate)($popoverScope); + target.attr("tabindex", "0"); + target.attr("aria-expanded", "true"); + target.attr("soc-popover", "opened"); + var options = { + "container": "body", + "viewport": { + "selector": "body", + "padding": 20 + }, + "html": true, + "trigger": "manual", + "placement": "auto", + "title": taskObj.number + " - " + (taskObj.record.short_description ? taskObj.record.short_description.display_value : ""), + "content": popoverContent, + "template": popoverTemplate + }; + target.popover(options); + if (targetSelector.indexOf("gantt_task") !== -1) { + target.data("bs.popover").options.atMouse = $event.pageX !== 0; + target.data("bs.popover").options.mousePos = { + "x": getX(target), + "y": $event.pageY + }; + } + var action = angular.element(".popover.soc-task-popover").hasClass("in") ? "hidden" : "shown"; + target.on(action + ".bs.popover", function($ev) { + if ($ev.type === "shown") { + _handleDestroyFlyout(); + angular.element(".soc-btn-open-record").focus(); + position.delta = { + top: angular.element(".gantt_ver_scroll").scrollTop(), + left: angular.element(".gantt_hor_scroll").scrollLeft() + }; + position.original = { + top: angular.element(".popover.soc-task-popover").offset().top, + left: angular.element(".popover.soc-task-popover").offset().left + }; + // Amend popover height if it is taller than remaining part of the window + var popoverElement = angular.element(".soc-task-popover"); + var windowHeight = angular.element(window).height(); + var maxHeight = windowHeight - popoverElement.offset().top; + if (popoverElement.height() > maxHeight) + popoverElement.height(maxHeight + "px"); + } else + _handleDestroyPopover(); + }); + target.popover("toggle"); + } + + function getTooltipTextToDisplay() { + + } + + // Callback function used for building the popover content + function buildPopoverContent(taskObj, $event, popoverContent) { + $templateCache.remove(getTemplateUrl("sn_chg_soc_change_soc_popover_template.xml")); + var $popoverContentScope = $scope.$new(true); + $popoverContentScope.leftFields = taskObj.left_fields; + $popoverContentScope.rightFields = taskObj.right_fields; + $popoverContentScope.emptyValue = "[" + i18n.getMessage("Empty") + "]"; + popoverContent = $compile(popoverContent)($popoverContentScope); + $templateRequest(getTemplateUrl("sn_chg_soc_change_soc_popover_template.xml")).then(buildPopoverTemplate.bind(this, taskObj, $event, popoverContent)); + } + + function openPopover(id, $event) { + var targetElement = getTargetElement($event); + var openedPopover = angular.element("[soc-popover]"); + if (targetElement === null || openedPopover.length > 0) { + _handleDestroyPopover(); + if (targetElement === null || openedPopover.attr("task_id") === id) + return; + } + _handleDestroyFlyout(); + $event.stopPropagation(); + var taskObj = $filter("filter")(dataService.tasks.data, { + "id": id + }, true)[0]; + $templateRequest(getTemplateUrl("sn_chg_soc_change_soc_task_popover.xml")).then(buildPopoverContent.bind(this, taskObj, $event)); + } + + /** + * Events + **/ + $scope.gantt.attachEvent("onTaskClick", function(id, $event) { + openPopover(id, $event); + return true; + }); + + $scope.gantt.attachEvent("onTaskDblClick", function(id, e) { + return false; + }); + + $scope.gantt.addShortcut("enter", function($event) { + openPopover(this.taskId, $event); + }, "taskRow"); + + $scope.gantt.addShortcut("tab", function($event) {}, "taskRow"); + + $scope.gantt.attachEvent("onTaskSelected", function(id, item) { + return true; + }); + + $scope.gantt.attachEvent("onBeforeTaskSelected", function(id, item) { + return true; + }); + + function getScheduleEvent(task, startDate, endDate, styleClass) { + startDate = $scope.gantt.date.parseDate(startDate, "xml_date"); + endDate = $scope.gantt.date.parseDate(endDate, "xml_date"); + var sizes = $scope.gantt.getTaskPosition(task, startDate, endDate); + var el = document.createElement("div"); + el.className = "schedule-bar " + styleClass; + el.style.left = sizes.left + "px"; + el.style.width = sizes.width + "px"; + el.style.top = sizes.top + "px"; + return el; + } + + // Add task layer for blackout windows + $scope.ganttInstance.addTaskLayer(function(task) { + if (task.blackout_spans.length === 0 && task.maint_spans.length === 0) + return; + var wrapper = document.createElement("div"); + if (dataService.definition.show_maintenance.value) + task.maint_spans.forEach(function(maintSpan) { + wrapper.appendChild(getScheduleEvent(task, maintSpan.start, maintSpan.end, "maint")); + }); + if (dataService.definition.show_blackout.value) + task.blackout_spans.forEach(function(blackoutSpan) { + wrapper.appendChild(getScheduleEvent(task, blackoutSpan.start, blackoutSpan.end, "blackout")); + }); + return wrapper; + }); + + $scope.gantt.attachEvent("onGanttRender", function() { + $element.find(".gantt_container").attr("role", "grid"); + angular.element('[data-toggle="tooltip"]').tooltip('destroy'); + angular.element(".tooltip[id^='tooltip']").remove(); + $element.find('[data-toggle="tooltip"]').tooltip(); + }); + + // Locale information must be associated with gantt object attached to window + $window.gantt.locale = { + date: { + month_full: [i18n.getMessage("January"), + i18n.getMessage("February"), + i18n.getMessage("March"), + i18n.getMessage("April"), + i18n.getMessage("May"), + i18n.getMessage("June"), + i18n.getMessage("July"), + i18n.getMessage("August"), + i18n.getMessage("September"), + i18n.getMessage("October"), + i18n.getMessage("November"), + i18n.getMessage("December") + ], + month_short: [i18n.getMessage("Jan"), + i18n.getMessage("Feb"), + i18n.getMessage("Mar"), + i18n.getMessage("Apr"), + i18n.getMessage("May"), + i18n.getMessage("Jun"), + i18n.getMessage("Jul"), + i18n.getMessage("Aug"), + i18n.getMessage("Sep"), + i18n.getMessage("Oct"), + i18n.getMessage("Nov"), + i18n.getMessage("Dec") + ], + day_full: [i18n.getMessage("Sunday"), + i18n.getMessage("Monday"), + i18n.getMessage("Tuesday"), + i18n.getMessage("Wednesday"), + i18n.getMessage("Thursday"), + i18n.getMessage("Friday"), + i18n.getMessage("Saturday") + ], + day_short: [i18n.getMessage("Sun"), + i18n.getMessage("Mon"), + i18n.getMessage("Tue"), + i18n.getMessage("Wed"), + i18n.getMessage("Thu"), + i18n.getMessage("Fri"), + i18n.getMessage("Sat") + ] + }, + labels: {} + }; + + $scope.zoomIn = function() { + _handleDestroyPopover(); + ganttScale.zoom(++$scope.ganttScale.level, urlService.socId); + }; + + $scope.zoomOut = function() { + _handleDestroyPopover(); + ganttScale.zoom(--$scope.ganttScale.level, urlService.socId); + }; + + $scope.gantt.init($element[0]); + } + }; + } + ]) + .controller("ChangeSoCCtrl", ["$scope", "$document", "$timeout", "$window", "$location", "ganttChart", "styleService", "configService", "shareService", "dataService", "urlService", "ganttScale", "getTemplateUrl", "i18n", "SOC", "TextSearchService", "socNotification", + function($scope, $document, $timeout, $window, $location, ganttChart, styleService, configService, shareService, dataService, urlService, ganttScale, getTemplateUrl, i18n, SOC, TextSearchService, socNotification) { + var changeSoCCtrl = this; + + changeSoCCtrl.share = { + canWrite: false + }; + + changeSoCCtrl.closeFlyout = function() { + $scope.$apply(function() { + $scope.$broadcast("sn.aside.change_soc_side.close"); + }); + }; + + $scope.loadingElements = {}; + $scope.dataService = dataService; + $scope.ganttScale = ganttScale; + $scope.urlService = urlService; + + $scope.pageLeft = function($event) { + if ($event && $event.keyCode !== SOC.KEYS.ENTER && $event.keyCode !== SOC.KEYS.SPACE) + return; + var gantt = ganttChart.getGantt(urlService.socId); + var left = gantt.getScrollState().x - angular.element("div.gantt_scale_cell").width(); + gantt.scrollTo(left < 0 ? 0 : left, null); + }; + + $scope.pageRight = function($event) { + if ($event && $event.keyCode !== SOC.KEYS.ENTER && $event.keyCode !== SOC.KEYS.SPACE) + return; + var gantt = ganttChart.getGantt(urlService.socId); + var left = gantt.getScrollState().x + angular.element("div.gantt_scale_cell").width(); + var scrollLength = angular.element("div.gantt_hor_scroll > div").width(); + gantt.scrollTo(left > scrollLength ? scrollLength : left, null); + }; + + $scope.scrollToday = function() { + var gantt = ganttChart.getGantt(urlService.socId); + gantt.showDate(new Date()); + }; + + $scope.openView = function(viewId, event) { + // We already have something open, need to deal with that first + if ($scope.activeAside === viewId) { + $scope.$broadcast("sn.aside.change_soc_side.close"); + if (event) + event.target.blur(); + } else { + var view; + switch (viewId) { + case "share": + view = getView(viewId, "sn_chg_soc_aside_share.xml"); + break; + case "style": + view = getView(viewId, "sn_chg_soc_aside_style.xml"); + break; + case "style_def": + view = getView(viewId, "sn_chg_soc_aside_style_page.xml", true); + break; + case "config": + view = getView(viewId, "sn_chg_soc_aside_config.xml"); + break; + case "keyboard": + view = getView(viewId, "sn_chg_soc_aside_keyboard.xml"); + break; + } + if (view !== undefined) { + angular.element(".sn-aside_right").show(); + $scope.activeAside = viewId; + $scope.$broadcast("sn.aside.change_soc_side.open", view, "320px"); + } + } + }; + + $scope.$on("sn.aside.change_soc_side.close", function() { + switch ($scope.activeAside) { + case "share": + angular.element("#share_side").focus(); + break; + case "style": + angular.element("#style_side").focus(); + break; + case "style_def": + angular.element("#style_side").focus(); + break; + case "config": + angular.element("#config_side").focus(); + break; + case "keyboard": + angular.element("#keyboard_side").focus(); + break; + } + $scope.activeAside = ""; + angular.element(".sn-aside_right").hide(); + }); + + $scope.$on("sn.aside.change_soc_side.open_style", function(event, style) { + styleService.selectedStyle = style; + $scope.openView("style_def"); + }); + + $scope.$on("sn.aside.change_soc_side.style_updated", function(event, result) { + if (result.style_sheet) { + var socStyleSheet = $document[0].getElementById("soc_span_style"); + socStyleSheet.innerHTML = result.style_sheet; + } + + if (result.records) { + var gantt = ganttChart.getGantt(urlService.socId); + for (var i = 0; i < dataService.tasks.data.length; i++) + if (result.records[dataService.tasks.data[i].id].style_rule) + dataService.tasks.data[i].style_class = SOC.STYLE_PREFIX + result.records[dataService.tasks.data[i].id].style_rule.sys_id; + gantt.parse(dataService.tasks, "json"); + } + }); + + $scope.$on("sn.aside.change_soc_side.open_share", function(event, model) { + shareService.model = model; + $scope.openView("share"); + }); + + $scope.$on("sn.aside.change_soc_side.open_share:closed", function(event, model) { + $scope.openView("share"); + }); + + $scope.$on("sn.aside.change_soc_side.historyBack.completed", function(e, view) { + $scope.activeAside = view.title; + }); + + function getView(name, template, isChild) { + return { + scope: $scope, + title: name, + templateUrl: getTemplateUrl(template), + isChild: isChild + }; + } + + // Global keyboard shortcuts + $document.on("keydown", function(event) { + // Open keyboard help side + if (event.shiftKey && event.which == SOC.KEYS.SLASH && event.originalEvent.target.tagName !== "INPUT") { + $scope.$apply(function() { + if ($scope.activeAside === "keyboard") { + $scope.$broadcast("sn.aside.change_soc_side.close"); + if (event) + event.target.blur(); + } else { + $scope.activeAside = "keyboard"; + $scope.$broadcast("sn.aside.change_soc_side.open", getView("keyboard", "sn_chg_soc_aside_keyboard.xml"), "320px"); + } + }); + } + }); + + var getChildTaskDividerClass = function(start, end, task) { + if (!task.parent) + return ""; + + var classStyle = " " + SOC.STYLE_CLASS_MAP.soc_row_child; + + var nextTask = this.ganttChart.getNext(task.id); + nextTask = nextTask ? this.ganttChart.getTask(nextTask) : null; + var previousTask = this.ganttChart.getPrev(task.id); + previousTask = previousTask ? this.ganttChart.getTask(previousTask) : null; + + // Only child task for a parent + if (previousTask && !previousTask.parent) + if (!nextTask || (nextTask && !nextTask.parent)) + return classStyle += " " + SOC.STYLE_CLASS_MAP.soc_row_child_single; + + // First child task for their parent + if (previousTask && !previousTask.parent && (nextTask && nextTask.parent)) + return classStyle += " " + SOC.STYLE_CLASS_MAP.soc_row_child_start; + + // Last child task for their parent + if (previousTask && previousTask.parent && ((nextTask && !nextTask.parent) || !nextTask)) + return classStyle += " " + SOC.STYLE_CLASS_MAP.soc_row_child_end; + + return classStyle; + }; + + var defineClassTemplate = function(start, end, task) { + var classStyle = ""; + + if (this.type) + classStyle += SOC.STYLE_CLASS_MAP[this.type]; + + classStyle += getChildTaskDividerClass.call({ + ganttChart: this.ganttChart + }, null, null, task); + + if (task.style_class) + classStyle += " " + task.style_class; + + return classStyle; + }; + + var dateToStr = gantt.date.date_to_str(gantt.config.task_date); + + function updateMarkerInterval(gantt, markerId) { + var today = gantt.getMarker(markerId); + today.start_date = new Date(); + today.title = dateToStr(today.start_date); + gantt.updateMarker(markerId); + } + + function addNowMarker(gantt) { + var markerId = gantt.addMarker({ + start_date: new Date(), + css: "today-marker", + title: dateToStr(new Date()), + text: " " + }); + setInterval(updateMarkerInterval(gantt, markerId), 1000 * 60); + } + + function addScheduleSpanStyle(definition) { + var socStyleSheet = $document[0].createElement("style"); + socStyleSheet.id = "soc_schedule_style"; + $document[0].head.appendChild(socStyleSheet); + + var maintColor = definition.maintenance_span_color.value ? definition.maintenance_span_color.value : SOC.MAINT_SPAN_COLOR; + var blackoutColor = definition.blackout_span_color.value ? definition.blackout_span_color.value : SOC.BLACKOUT_SPAN_COLOR; + + var spanStyleSheet; + for (var i = 0; i < $document[0].styleSheets.length; i++) + if ($document[0].styleSheets[i].ownerNode.id === socStyleSheet.id) { + spanStyleSheet = $document[0].styleSheets[i]; + break; + } + + if (!spanStyleSheet) + return; + + spanStyleSheet.insertRule("div.schedule-bar.maint {background-color: " + maintColor + ";}", 0); + spanStyleSheet.insertRule("div.schedule-bar.blackout {background-color: " + blackoutColor + ";}", 0); + } + + changeSoCCtrl.initPage = function() { + dataService.initPage(urlService.socId).then(function() { + styleService.initStyle(); + + // Setup for share panel + changeSoCCtrl.share.canWrite = dataService.canWrite(); + + // Setup configuration panel + configService.showBlackoutOption = configService.showBlackoutSchedules = dataService.definition.show_blackout.value; + configService.showMaintOption = configService.showMaintSchedules = dataService.definition.show_maintenance.value; + configService.setChildRecords(dataService.child_table); + + // Need to apply changes due to style info + var socStyleSheet = document.createElement("style"); + socStyleSheet.id = "soc_span_style"; + document.head.appendChild(socStyleSheet); + socStyleSheet.innerHTML = dataService.style.style_sheet; + + addScheduleSpanStyle(dataService.definition); + + var gantt = ganttChart.getGantt(urlService.socId); + gantt.templates.grid_row_class = defineClassTemplate.bind({ + ganttChart: gantt, + type: "", + }); + gantt.templates.task_row_class = getChildTaskDividerClass.bind({ + ganttChart: gantt + }); + gantt.templates.task_class = defineClassTemplate.bind({ + ganttChart: gantt, + type: "soc_event_bar", + }); + gantt.render(); + + // Need to provide the tasks so it can calc min/max + ganttScale.setDateRange(dataService.tasks.data); + ganttScale.configureScale(); + gantt.clearAll(); + addNowMarker(gantt); + // these are the created tasks that will be added to the gantt + gantt.parse(dataService.tasks, "json"); + if (dataService.tasks.data.length > 0) { + gantt.showTask(dataService.tasks.data[0].id); + $scope.noResults = false; + } else + $scope.noResults = true; + }).catch(function(response) { + socNotification.show("error", response.data.error.message); + }); + }; + + $scope.filter = { + filterConditions: ["number", "config_item", "Short Description", "children.number", "children.config_item"], + placeholderText: i18n.getMessage("Search Change Schedule") + }; + + function buildFilterData() { + var augmentedData = dataService.tasks.data; + dataService.tasks.data.forEach(function(obj, index) { + augmentedData[index].children = dataService.getChildren(obj.id); + }); + return augmentedData; + } + + $scope.$on("textSearch", function(event, textSearch) { + var filteredRecords = TextSearchService.getFilteredArray(buildFilterData(), textSearch); + ganttChart.updateGanttData(urlService.socId, filteredRecords); + $scope.noResults = filteredRecords.length === 0; + }); + + $scope.isLoading = function() { + return $scope.$parent.loading; + }; + + changeSoCCtrl.messages = { + "No records to display": i18n.getMessage("No records to display"), + "No records match the filter": i18n.getMessage("No records match the filter"), + "Change Schedule": i18n.getMessage("Change Schedule"), + "Close panel": i18n.getMessage("Close panel"), + "Configuration": i18n.getMessage("Configuration"), + "Share": i18n.getMessage("Share"), + "Open context menu": i18n.getMessage("Open context menu"), + "Filter": i18n.getMessage("Filter"), + "Keyboard Shortcuts": i18n.getMessage("Keyboard Shortcuts"), + "Search Change Schedule": i18n.getMessage("Search Change Schedule"), + "Span Styles": i18n.getMessage("Span Styles"), + "Today": i18n.getMessage("Today"), + "Zoom in": i18n.getMessage("Zoom in"), + "Zoom out": i18n.getMessage("Zoom out"), + "Page left": i18n.getMessage("Page left"), + "Page right": i18n.getMessage("Page right"), + "Show today": i18n.getMessage("Show today") + }; + + $scope.noResults = false; + var noResultsElem = "
" + changeSoCCtrl.messages["No records to display"] + "
"; + + function noResults(newValue, oldValue) { + if (newValue === oldValue) + return; + if (newValue) { + angular.element("div.gantt_marker.today-marker").hide(); + angular.element("div.gantt_task_scale").after(noResultsElem); + } else { + angular.element("div.gantt_marker.today-marker").show(); + angular.element("div.no-results").remove(); + } + } + $scope.$watch("noResults", noResults); + } + ]) + .filter("objectKeys", [function() { + return function(anObject) { + if (!anObject) + return null; + return Object.keys(anObject); + }; + }]) + .filter("objectKeysLength", [function() { + return function(anObject) { + if (!anObject) + return null; + return Object.keys(anObject).length; + }; + }]); From 9964335c043aaa7c5a952bb19f287f2924dfa8a1 Mon Sep 17 00:00:00 2001 From: tab22 <30366222+tab22@users.noreply.github.com> Date: Thu, 30 Oct 2025 20:04:57 +0000 Subject: [PATCH 2/4] Create config.js --- .../Custom Change Schedule/config.js | 146 ++++++++++++++++++ 1 file changed, 146 insertions(+) create mode 100644 Client-Side Components/UI Scripts/Custom Change Schedule/config.js diff --git a/Client-Side Components/UI Scripts/Custom Change Schedule/config.js b/Client-Side Components/UI Scripts/Custom Change Schedule/config.js new file mode 100644 index 0000000000..0a388521e6 --- /dev/null +++ b/Client-Side Components/UI Scripts/Custom Change Schedule/config.js @@ -0,0 +1,146 @@ +angular.module("sn.chg_soc.config", ["sn.common"]) + .service("configService", ["dataService", "ganttChart", "urlService", "SOC", function(dataService, ganttChart, urlService, SOC) { + var configService = this; + + configService.showConfigItem = true; + configService.showDuration = true; + configService.showShortDesc=true; + configService.showBlackoutOption = true; + configService.showBlackoutSchedules = true; + configService.showMaintOption = true; + configService.showMaintSchedules = true; + + configService.childRecords = {}; + + configService.setChildRecords = function(childTables) { + for (var tableName in childTables) + configService.childRecords[tableName] = { + inputId: tableName + "Option", + label: childTables[tableName].__label, + name: tableName + "Show", + show: true, + change: updateChildRecords + }; + }; + + function updateChildRecords(tableName) { + var gantt = ganttChart.getGantt(urlService.socId); + var ganttTasks = gantt.getTaskByTime(); + for (var i = 0; i < ganttTasks.length; i++) + if (ganttTasks[i].parent && ganttTasks[i].table === tableName) + ganttTasks[i].__visible = configService.childRecords[tableName].show; + gantt.attachEvent("onBeforeTaskDisplay", function(id, task) { + if (task.parent) + return task.__visible; + return true; + }); + gantt.templates.grid_open = gridOpen; + gantt.render(); + } + + function gridOpen(task) { + var gantt = ganttChart.getGantt(urlService.socId); + var children = gantt.getChildren(task.id); + + for (var i = 0; i < children.length; i++) { + var childTask = gantt.getTask(children[i]); + if (childTask.__visible) + return "
"; + } + + return "
"; + } + }]) + .directive("socAsideConfig", ["getTemplateUrl", "configService", "ganttChart", "dataService", "objectKeysLengthFilter", "SOC", "i18n", function(getTemplateUrl, configService, ganttChart, dataService, objectKeysLengthFilter, SOC, i18n) { + "use strict"; + return { + restrict: "A", + templateUrl: getTemplateUrl("sn_chg_soc_aside_config_body.xml"), + scope: { + socDefId: "=" + }, + controller: function($scope, objectKeysLengthFilter) { + // $scope.showConfigItem = configService.showConfigItem; + // $scope.showDuration = configService.showDuration; + $scope.showBlackoutOption = configService.showBlackoutOption; + $scope.showShortDesc = configService.showShortDesc; + $scope.showBlackoutSchedules = configService.showBlackoutSchedules; + $scope.showMaintOption = configService.showMaintOption; + $scope.showMaintSchedules = configService.showMaintSchedules; + $scope.childRecords = configService.childRecords; + $scope.objectKeysLengthFilter = objectKeysLengthFilter; + $scope.messages = { + "Child Records": i18n.getMessage("Child Records"), + "Columns": i18n.getMessage("Columns"), + "Configuration Item": i18n.getMessage("Configuration Item"), + "Short Description": i18n.getMessage("Short Description"), + "Duration": i18n.getMessage("Duration"), + "Related Records": i18n.getMessage("Related Records"), + "Schedules": i18n.getMessage("Schedules"), + "Blackout": i18n.getMessage("Blackout"), + "Maintenance": i18n.getMessage("Maintenance") + }; + + $scope.updateColumnLayout = function(columnId) { + var gantt = ganttChart.getGantt($scope.socDefId); + var column = gantt.getGridColumn(columnId); + if (SOC.COLUMN.CONFIG_ITEM === columnId) { + configService.showConfigItem = !configService.showConfigItem; + column.hide = !configService.showConfigItem; + } else if (SOC.COLUMN.DURATION === columnId) { + configService.showDuration = !configService.showDuration; + column.hide = !configService.showDuration; + } else if (SOC.COLUMN.SHORT_DESCRIPTION === columnId) { + configService.showShortDesc = !configService.showShortDesc; + column.hide = !configService.showShortDesc; + } else + return; + gantt.render(); + }; + + function getScheduleEvent(task, startDate, endDate, styleClass) { + var gantt = ganttChart.getGantt($scope.socDefId); + startDate = gantt.date.parseDate(startDate, "xml_date"); + endDate = gantt.date.parseDate(endDate, "xml_date"); + var sizes = gantt.getTaskPosition(task, startDate, endDate); + var el = document.createElement("div"); + el.className = "schedule-bar " + styleClass; + el.style.left = sizes.left + "px"; + el.style.width = sizes.width + "px"; + el.style.top = sizes.top + "px"; + return el; + } + + var scheduleTaskLayer = function(task) { + if ((!this.show_blackout && !this.show_maint) || (task.blackout_spans.length === 0 && task.maint_spans.length === 0)) + return; + var wrapper = document.createElement("div"); + if (this.show_blackout && dataService.definition.show_blackout.value) + task.blackout_spans.forEach(function(blackoutSpan) { + wrapper.appendChild(getScheduleEvent(task, blackoutSpan.start, blackoutSpan.end, "blackout")); + }); + if (this.show_maint && dataService.definition.show_maintenance.value) + task.maint_spans.forEach(function(maintSpan) { + wrapper.appendChild(getScheduleEvent(task, maintSpan.start, maintSpan.end, "maint")); + }); + return wrapper; + }; + + $scope.updateScheduleLayer = function() { + configService.showBlackoutSchedules = $scope.showBlackoutSchedules; + configService.showMaintSchedules = $scope.showMaintSchedules; + var ganttInstance = ganttChart.getInstance($scope.socDefId); + ganttInstance.removeTaskLayer(); + + if ($scope.showBlackoutSchedules || $scope.showMaintSchedules) { + ganttInstance.addTaskLayer(scheduleTaskLayer.bind({ + show_blackout: $scope.showBlackoutSchedules, + show_maint: $scope.showMaintSchedules + })); + ganttInstance.gantt.render(); + } + }; + } + }; + }]); From ac42dbac403e692229264653c42a894e1bb4744e Mon Sep 17 00:00:00 2001 From: tab22 <30366222+tab22@users.noreply.github.com> Date: Thu, 30 Oct 2025 20:06:07 +0000 Subject: [PATCH 3/4] Create data.js --- .../UI Scripts/Custom Change Schedule/data.js | 272 ++++++++++++++++++ 1 file changed, 272 insertions(+) create mode 100644 Client-Side Components/UI Scripts/Custom Change Schedule/data.js diff --git a/Client-Side Components/UI Scripts/Custom Change Schedule/data.js b/Client-Side Components/UI Scripts/Custom Change Schedule/data.js new file mode 100644 index 0000000000..e521f41ba3 --- /dev/null +++ b/Client-Side Components/UI Scripts/Custom Change Schedule/data.js @@ -0,0 +1,272 @@ +angular.module("sn.chg_soc.data", []) + .service("dataService", ["$http", "$q", "$window", "i18n", "urlService", "ganttChart", "durationFormatter", "SOC", "$filter", function($http, $q, $window, i18n, urlService, ganttChart, durationFormatter, SOC, $filter) { + var dataService = this; + + dataService.more = false; + dataService.count = 0; + dataService.child_table = {}; + dataService.definition = {}; + dataService.style = { + chg_soc_style_rule: {}, + chg_soc_definition_style_rule: {}, + chg_soc_def_child_style_rule: {}, + style_sheet: "" + }; + dataService.tasks = { + data: [], + links: [] + }; + dataService.allRecords = {}; + + function isValidDate(date) { + if (Object.prototype.toString.call(date) === "[object Date]" && !isNaN(date.getTime())) + return true; + return false; + } + + function buildFields(record, selectedFieldsList, tableMeta) { + var result = []; + if (!selectedFieldsList) + return result; + var selectedFields = selectedFieldsList.split(","); + selectedFields.forEach(function(fieldName) { + if (fieldName && tableMeta[fieldName]) + result.push({ + column_name: fieldName, + label: tableMeta[fieldName].label, + display_value: record[fieldName].display_value, + value: record[fieldName].value, + }); + }); + return result; + } + + function buildRecord(record, chgSocDef, tableMeta, styleRule, scheduleWindow) { + var ganttUtil = ganttChart.getGantt(urlService.socId); + var startDate = ganttUtil.date.parseDate(record[chgSocDef.start_date_field.value].display_value_internal, "xml_date"); + var endDate = ganttUtil.date.parseDate(record[chgSocDef.end_date_field.value].display_value_internal, "xml_date"); + // Check start/end dates are valid before adding the task to gantt chart + if (!isValidDate(startDate) || !isValidDate(endDate)) + return; + + var recordEvent = { + id: record.sys_id ? record.sys_id.value : "", + text: record.number ? record.number.display_value : "", + number: record.number ? record.number.display_value : "", + chg_soc_def: chgSocDef.sys_id.value, + config_item: record.cmdb_ci ? record.cmdb_ci.display_value : "", + start_date: startDate, + end_date: endDate, + dur_display: durationFormatter.buildDurationDisplay(startDate, endDate), + order: 0, + progress: 0, + table: record.sys_class_name ? record.sys_class_name.value : chgSocDef.table_name.value, + left_fields: buildFields(record, chgSocDef.popover_left_col_fields.value, tableMeta), + right_fields: buildFields(record, chgSocDef.popover_right_col_fields.value, tableMeta), + record: record, + blackout_spans: [], + maint_spans: [], + sys_id: record.sys_id ? record.sys_id.value : "", + short_description: record.short_description ? record.short_description.display_value : "", + __visible: true + }; + + if (styleRule && styleRule.sys_id) + recordEvent.style_class = SOC.STYLE_PREFIX + styleRule.sys_id; + + if (scheduleWindow) { + if (chgSocDef.show_maintenance.value) + angular.forEach(scheduleWindow.maintenance, function (schedule) { + Array.prototype.push.apply(recordEvent.maint_spans, schedule.spans); + }); + if (chgSocDef.show_blackout.value) + angular.forEach(scheduleWindow.blackout, function (schedule) { + Array.prototype.push.apply(recordEvent.blackout_spans, schedule.spans); + }); + } else { + recordEvent.id = chgSocDef.sys_id.value + "_" + recordEvent.id; + recordEvent.parent = record[chgSocDef.reference_field.value].value; + } + + dataService.allRecords[recordEvent.id] = { + style_rule: styleRule, + sys_id: record.sys_id ? record.sys_id.value : "", + table_name: record.sys_class_name ? record.sys_class_name.value : chgSocDef.table_name.value, + chg_soc_def: chgSocDef.sys_id.value + }; + + return recordEvent; + } + + function buildItem(result, item) { + // Build change_request record + var record = buildRecord(result[item.table_name][item.sys_id], result.chg_soc_definition, result[item.table_name].__table_meta, item.style, item.schedule_window); + if (!record) + return; + + dataService.tasks.data.push(record); + + // Build related tasks + if (item.related) + for (var childSocDefId in item.related) { + var childRecords = item.related[childSocDefId]; + for (var i = 0; i < childRecords.length; i++) { + var childRecord = buildRecord(result[childRecords[i].table_name][childRecords[i].sys_id], result.chg_soc_definition.__child[childSocDefId], result[childRecords[i].table_name].__table_meta, childRecords[i].style); + if (childRecord) + dataService.tasks.data.push(childRecord); + } + } + } + + dataService.buildData = function(result) { + if (!result) + return; + + dataService.more = result.__more; + dataService.count = result.__change_count; + + // Start with the definition object + if (result.chg_soc_definition) + dataService.definition = result.chg_soc_definition; + + // Ordered change requests with style and related records + if (result.__struct) + for (var i = 0; i < result.__struct.length; i++) + buildItem(result, result.__struct[i]); + + // Find all child tables + for (var table in result) + if (result[table].__has_children) + dataService.child_table[table] = result[table].__table_meta; + + // Set style rules and style sheet to the model + dataService.style.chg_soc_style_rule = result.chg_soc_style_rule; + dataService.style.chg_soc_definition_style_rule = result.chg_soc_definition_style_rule; + dataService.style.chg_soc_def_child_style_rule = result.chg_soc_def_child_style_rule; + dataService.style.style_sheet = result._css; + }; + + dataService.addData = function(result) { + dataService.more = result.__more; + dataService.count = result.__change_count; + + if (result.__struct) + for (var i = 0; i < result.__struct.length; i++) + buildItem(result, result.__struct[i]); + + for (var table in result) + if (result[table].__has_children) + dataService.child_table[table] = result[table].__table_meta; + }; + + dataService.initPage = function(chgSocDefId, condition) { + var deferred = $q.defer(); + var url = SOC.GET_CHANGE_SCHEDULE + chgSocDefId; + var config = {}; + config.params = { + sysparm_ck: $window.g_ck + }; + if (condition) + config.params.condition = condition; + $http.get(url, config).then(function(response){ + deferred.resolve(dataService.buildData(response.data.result)); + }, function(response) { + deferred.reject(response); + }); + return deferred.promise; + }; + + dataService.getChanges = function() { + var deferred = $q.defer(); + var url = SOC.GET_CHANGE_SCHEDULE + dataService.definition.sys_id.value; + var config = {}; + config.params = { + sysparm_ck: $window.g_ck, + count: dataService.count + }; + if (dataService.definition.condition.dryRun) + config.params.condition = dataService.definition.condition.value; + + $http.get(url, config).then(function(response){ + deferred.resolve(dataService.addData(response.data.result)); + }, function(response) { + deferred.reject(response); + }); + return deferred.promise; + }; + + dataService.getChildren = function(parentId) { + var res = $filter("filter")(dataService.tasks.data, function(task) { + return task.parent === parentId; + }); + return res; + }; + + dataService.destroyData = function() { + dataService.more = false; + dataService.count = 0; + dataService.child_table = {}; + dataService.definition = {}; + dataService.style = { + chg_soc_style_rule: {}, + chg_soc_definition_style_rule: {}, + chg_soc_def_child_style_rule: {}, + style_sheet: "" + }; + dataService.tasks = { + data: [], + links: [] + }; + dataService.allRecords = {}; + }; + + dataService.parseQuery = function(condition) { + condition = condition + ""; + var deferred = $q.defer(); + var url = SOC.GET_PARSE_QUERY + condition; + var config = {}; + config.params = {}; + config.params.sysparm_ck = $window.g_ck; + + $http.get(url, config).then(function(response) { + deferred.resolve(response.data.result); + }, function(response) { + deferred.reject(response); + }); + + return deferred.promise; + }; + + function checkSecurityObject() { + return dataService.definition && dataService.definition.__security; + } + + dataService.canCreate = function() { + if (checkSecurityObject() && dataService.definition.__security.canCreate) + return dataService.definition.__security.canCreate; + return false; + }; + + dataService.canRead = function() { + if (checkSecurityObject() && dataService.definition.__security.canRead) + return dataService.definition.__security.canRead; + return false; + }; + + dataService.canWrite = function() { + if (checkSecurityObject() && dataService.definition.__security.canWrite) + return dataService.definition.__security.canWrite; + return false; + }; + + dataService.canDelete = function() { + if (checkSecurityObject() && dataService.definition.__security.canDelete) + return dataService.definition.__security.canDelete; + return false; + }; + + dataService.trackEvent = function(source) { + if ($window.GlideWebAnalytics && $window.GlideWebAnalytics.trackEvent) + $window.GlideWebAnalytics.trackEvent('com.snc.change_management.soc', 'Change Schedules', source, 0, 0); + }; + }]); From 897cc09fc612abea46975c65cf7400aabc95b1b2 Mon Sep 17 00:00:00 2001 From: tab22 <30366222+tab22@users.noreply.github.com> Date: Thu, 30 Oct 2025 20:27:11 +0000 Subject: [PATCH 4/4] Create README.md --- .../Custom Change Schedule/README.md | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 Client-Side Components/UI Scripts/Custom Change Schedule/README.md diff --git a/Client-Side Components/UI Scripts/Custom Change Schedule/README.md b/Client-Side Components/UI Scripts/Custom Change Schedule/README.md new file mode 100644 index 0000000000..dfecace0d5 --- /dev/null +++ b/Client-Side Components/UI Scripts/Custom Change Schedule/README.md @@ -0,0 +1,36 @@ +# ๐Ÿงพ ServiceNow Change Schedule Enhancement +### _(UI Scripts: `sn_chg_soc.change_soc`, `sn.chg_soc.config`, `sn.chg_soc.data`)_ + +--- + +## ๐Ÿ“˜ Overview + +This customization extends the **ServiceNow Change Schedule (Change Calendar)** functionality. +The enhancement adds visibility and interactivity to the Change Calendar by including: + +- A **Short Description** column in the Change Schedule view. +- A **configurable UI** allowing users to toggle visibility of columns such as _Configuration Item_, _Short Description_, and _Duration_. +- Integration of additional data services for fetching and rendering change records with enhanced details. +- A **Change Schedule button** that refreshes and displays these changes dynamically. + +The result is a more informative and user-friendly Change Schedule interface for Change Managers, CAB members, and ITSM users. + +--- + +## ๐Ÿงฉ Architecture + +| Module | Description | +|--------|-------------| +| **`sn_chg_soc.change_soc`** | Main controller and directive for the Change Schedule Gantt Chart UI. Handles initialisation, rendering, zoom, and popovers. | +| **`sn.chg_soc.config`** | Manages configuration settings for displayed columns and schedules (blackout, maintenance). Allows toggling visibility. | +| **`sn.chg_soc.data`** | Provide the data on the gantt chat from the change records + + +Requirement: +As an ITIL user, you can click the Change Schedule button to navigate directly to the Change Schedule view. +This allows you to see all planned changes and plan your own changes accordingly, especially useful for customers who do not have a well-established CMDB integrated with Discovery. + +image + +image +