diff --git a/resources/js/app-core.js b/resources/js/app-core.js new file mode 100644 index 0000000000..dd8532461e --- /dev/null +++ b/resources/js/app-core.js @@ -0,0 +1 @@ +require("./bootstrap-core"); diff --git a/resources/js/app-layout-core.js b/resources/js/app-layout-core.js new file mode 100644 index 0000000000..9cda952db0 --- /dev/null +++ b/resources/js/app-layout-core.js @@ -0,0 +1,399 @@ +import { BNavbar } from "bootstrap-vue"; +import { Multiselect } from "@processmaker/vue-multiselect"; +import moment from "moment-timezone"; +import { sanitizeUrl } from "@braintree/sanitize-url"; +import VueHtml2Canvas from "vue-html2canvas"; +import newRequestModal from "./components/requests/requestModal"; +import requestModal from "./components/requests/modal"; +import requestModalMobile from "./components/requests/modalMobile"; +import notifications from "./notifications/components/notifications"; +import sessionModal from "./components/Session"; +import Sidebaricon from "./components/Sidebaricon"; +import ConfirmationModal from "./components/Confirm"; +import MessageModal from "./components/Message"; +import NavbarProfile from "./components/NavbarProfile"; +import SelectStatus from "./components/SelectStatus"; +import SelectUser from "./components/SelectUser"; +import SelectUserGroup from "./components/SelectUserGroup"; +import CategorySelect from "./processes/categories/components/CategorySelect"; +import ProjectSelect from "./components/shared/ProjectSelect"; +import SelectFromApi from "./components/SelectFromApi"; +import Breadcrumbs from "./components/Breadcrumbs"; +import TimelineItem from "./components/TimelineItem"; +import Required from "./components/shared/Required"; +import WelcomeModal from "./Mobile/WelcomeModal"; +import Menu from "./components/Menu.vue"; +/** **** + * Global adjustment parameters for moment.js. + */ +import __ from "./modules/lang"; + +require("bootstrap"); + +const { Vue } = window; + +Vue.use(VueHtml2Canvas); + +if (window.ProcessMaker && window.ProcessMaker.user) { + moment.tz.setDefault(window.ProcessMaker.user.timezone); + moment.defaultFormat = window.ProcessMaker.user.datetime_format; + moment.defaultFormatUtc = window.ProcessMaker.user.datetime_format; +} +if (document.documentElement.lang) { + moment.locale(document.documentElement.lang); + window.ProcessMaker.user.lang = document.documentElement.lang; +} +Vue.prototype.moment = moment; +// initializing global instance of a moment object +window.moment = moment; +/** ***** */ + +Vue.prototype.$sanitize = sanitizeUrl; + +Vue.component("Multiselect", Multiselect); +Vue.component("Sidebaricon", Sidebaricon); +Vue.component("SelectStatus", SelectStatus); +Vue.component("SelectUser", SelectUser); +Vue.component("SelectUserGroup", SelectUserGroup); +Vue.component("CategorySelect", CategorySelect); +Vue.component("ProjectSelect", ProjectSelect); +Vue.component("SelectFromApi", SelectFromApi); +Vue.component("Breadcrumbs", Breadcrumbs); +Vue.component("TimelineItem", TimelineItem); +Vue.component("Required", Required); +Vue.component("Welcome", WelcomeModal); +Vue.component("LanguageSelectorButton", (resolve) => { + if (window.ProcessMaker.languageSelectorButtonComponent) { + resolve(window.ProcessMaker.languageSelectorButtonComponent); + } else { + window.ProcessMaker.languageSelectorButtonComponentResolve = resolve; + } +}); + +// Event bus ProcessMaker +window.ProcessMaker.events = new Vue(); + +// Verify if is mobile +const browser = navigator.userAgent; +const isMobileDevice = /Android|webOS|iPhone|iPad|iPod|BlackBerry|Windows Phone/i.test(browser); +window.ProcessMaker.mobileApp = false; +if (isMobileDevice) { + window.ProcessMaker.mobileApp = true; +} + +// Verify is in mobile mode +const isMobileNavbar = window.ProcessMaker.events.$cookies.get("isMobile"); + +window.ProcessMaker.nodeTypes = []; +window.ProcessMaker.nodeTypes.get = function (id) { + return this.find((node) => node.id === id); +}; + +// Assign our navbar component to our global ProcessMaker object +window.ProcessMaker.navbar = new Vue({ + el: "#navbar", + components: { + TopMenu: Menu, + "b-navbar": BNavbar, + requestModal, + notifications, + sessionModal, + ConfirmationModal, + MessageModal, + NavbarProfile, + newRequestModal, + GlobalSearch: (resolve) => { + if (window.ProcessMaker.globalSearchComponent) { + resolve(window.ProcessMaker.globalSearchComponent); + } else { + window.ProcessMaker.globalSearchComponentResolve = resolve; + } + }, + }, + data() { + return { + screenBuilder: null, + messages: ProcessMaker.notifications, + alerts: this.loadLocalAlerts(), + confirmTitle: "", + confirmMessage: "", + confirmVariant: "", + confirmCallback: "", + confirmSize: "md", + confirmDataTestClose: "confirm-btn-close", + confirmDataTestOk: "confirm-btn-ok", + messageTitle: "", + messageMessage: "", + messageVariant: "", + messageCallback: "", + confirmShow: false, + sessionShow: false, + messageShow: false, + sessionTitle: "", + sessionMessage: "", + sessionTime: "", + sessionWarnSeconds: "", + sessionIsRenewing: false, + taskTitle: "", + isMobile: false, + isMobileDevice: window.ProcessMaker.mobileApp, + isNavbarExpanded: false, + }; + }, + watch: { + alerts(array) { + this.saveLocalAlerts(array); + }, + }, + mounted() { + Vue.nextTick() // This is needed to override the default alert method. + .then(() => { + this.onResize(); + window.addEventListener("resize", this.onResize, { passive: true }); + + if (document.querySelector("meta[name='alert']")) { + ProcessMaker.alert( + document.querySelector("meta[name='alertMessage']").getAttribute("content"), + document.querySelector("meta[name='alertVariant']").getAttribute("content"), + ); + } + const findSB = setInterval(() => { + this.screenBuilder = window.ProcessMaker.ScreenBuilder; + if (this.screenBuilder) { + clearInterval(findSB); + } + }, 80); + }); + }, + methods: { + alertDownChanged(dismissCountDown, item) { + item.alertShow = dismissCountDown; + this.saveLocalAlerts(this.alerts); + }, + alertDismissed(alert) { + alert.alertShow = 0; + const index = this.alerts.indexOf(alert); + let copy = _.cloneDeep(this.alerts); + index > -1 ? copy.splice(index, 1) : null; + // remove old alerts + copy = copy.filter((item) => ((Date.now() - item.timestamp) / 1000) < item.alertShow); + this.saveLocalAlerts(copy); + }, + loadLocalAlerts() { + try { + return window.localStorage.processmakerAlerts + && window.localStorage.processmakerAlerts.substr(0, 1) === "[" + ? JSON.parse(window.localStorage.processmakerAlerts) : []; + } catch (e) { + return []; + } + }, + saveLocalAlerts(array) { + const nextScreenAlerts = array.filter((alert) => alert.stayNextScreen); + window.localStorage.processmakerAlerts = JSON.stringify(nextScreenAlerts); + }, + switchToMobile() { + this.$cookies.set("isMobile", true); + window.open("/requests", "_self"); + }, + getRoutes() { + if (this.$refs.breadcrumbs) { + return this.$refs.breadcrumbs.list; + } + return []; + }, + setRoutes(routes) { + if (this.$refs.breadcrumbs) { + return this.$refs.breadcrumbs.updateRoutes(routes); + } + return false; + }, + onResize() { + this.isMobile = window.innerWidth < 992; + }, + toggleNavbar() { + this.isNavbarExpanded = !this.isNavbarExpanded; + }, + }, +}); + +// Assign our navbar component to our global ProcessMaker object +if (isMobileNavbar === "true") { + window.ProcessMaker.navbarMobile = new Vue({ + el: "#navbarMobile", + components: { + requestModalMobile, + WelcomeModal, + }, + data() { + return { + display: true, + }; + }, + mounted() { + if (this.$cookies.get("firstMounted") === "true") { + $("#welcomeModal").modal("show"); + } + }, + methods: { + switchToDesktop() { + this.$cookies.set("isMobile", false); + window.location.reload(); + }, + onResize() { + this.isMobile = window.innerWidth < 992; + }, + }, + }); +} + +// Breadcrumbs are now part of the navbar component. Alias it here. +window.ProcessMaker.breadcrumbs = window.ProcessMaker.navbar; + +// Set our own specific alert function at the ProcessMaker global object that could +// potentially be overwritten by some custom theme support +window.ProcessMaker.alert = function (msg, variant, showValue = 5, stayNextScreen = false, showLoader = false, msgLink = "", msgTitle = "") { + if (showValue === 0) { + // Just show it indefinitely, no countdown + showValue = true; + } + // amount of items allowed in array + if (ProcessMaker.navbar.alerts.length > 5) { + ProcessMaker.navbar.alerts.shift(); + } + ProcessMaker.navbar.alerts.push({ + alertText: msg, + alertLink: msgLink, + alertTitle: msgTitle, + alertShow: showValue, + alertVariant: String(variant), + showLoader, + stayNextScreen, + timestamp: Date.now(), + }); +}; + +// Setup our login modal +window.ProcessMaker.sessionModal = function (title, message, time, warnSeconds) { + ProcessMaker.navbar.sessionTitle = title || __("Session Warning"); + ProcessMaker.navbar.sessionMessage = message || __("Your session is about to expire."); + ProcessMaker.navbar.sessionTime = time; + ProcessMaker.navbar.sessionWarnSeconds = warnSeconds; + ProcessMaker.navbar.sessionShow = true; +}; + +window.ProcessMaker.closeSessionModal = function () { + ProcessMaker.navbar.sessionShow = false; + ProcessMaker.navbar.sessionIsRenewing = false; +}; + +// Set out own specific confirm modal. +window.ProcessMaker.confirmModal = function (title, message, variant, callback, size = "md", dataTestClose = "confirm-btn-close", dataTestOk = "confirm-btn-ok") { + ProcessMaker.navbar.confirmTitle = title || __("Confirm"); + ProcessMaker.navbar.confirmMessage = message || __("Are you sure you want to delete?"); + ProcessMaker.navbar.confirmVariant = variant; + ProcessMaker.navbar.confirmCallback = callback; + ProcessMaker.navbar.confirmShow = true; + ProcessMaker.navbar.confirmSize = size; + ProcessMaker.navbar.confirmDataTestClose = dataTestClose; + ProcessMaker.navbar.confirmDataTestOk = dataTestOk; +}; + +// Set out our specific message modal. +window.ProcessMaker.messageModal = function (title, message, variant, callback) { + ProcessMaker.navbar.messageTitle = title || __("Message"); + ProcessMaker.navbar.messageMessage = message || __(""); + ProcessMaker.navbar.messageVariant = variant; + ProcessMaker.navbar.messageCallback = callback; + ProcessMaker.navbar.messageShow = true; +}; + +// flags print forms +window.ProcessMaker.apiClient.requestCount = 0; +window.ProcessMaker.apiClient.requestCountFlag = false; + +window.ProcessMaker.apiClient.interceptors.request.use((request) => { + // flags print forms + if (window.ProcessMaker.apiClient.requestCountFlag) { + window.ProcessMaker.apiClient.requestCount++; + } + + window.ProcessMaker.EventBus.$emit("api-client-loading", request); + return request; +}); + +window.ProcessMaker.apiClient.interceptors.response.use((response) => { + // TODO: this could be used to show a default "created/upated/deleted resource" alert + // response.config.method (PUT, POST, DELETE) + // response.config.url (extract resource name) + window.ProcessMaker.EventBus.$emit("api-client-done", response); + // flags print forms + if (window.ProcessMaker.apiClient.requestCountFlag && window.ProcessMaker.apiClient.requestCount > 0) { + window.ProcessMaker.apiClient.requestCount--; + } + return response; +}, (error) => { + // Set in your .catch to false to not show the alert inside window.ProcessMaker.apiClient + if (!error?.response?.showAlert) { + return Promise.reject(error); + } + + if (error.code && error.code === "ERR_CANCELED") { + return Promise.reject(error); + } + window.ProcessMaker.EventBus.$emit("api-client-error", error); + if (error.response && error.response.status && error.response.status === 401) { + // stop 401 error consuming endpoints with data-sources + const { url } = error.config; + if (url.includes("/data_sources/")) { + if (url.includes("requests/") || url.includes("/test")) { + throw error; + } + } + window.location = "/login"; + } else { + if (_.has(error, "config.url") && !error.config.url.match("/debug")) { + window.ProcessMaker.apiClient.post("/debug", { + name: "Javascript ProcessMaker.apiClient Error", + message: JSON.stringify({ + message: error.message, + code: error.code, + config: error.config, + }), + }); + } + return Promise.reject(error); + } +}); + +// Display any uncaught promise rejections from axios in the Process Maker alert box +window.addEventListener("unhandledrejection", (event) => { + const error = event.reason; + if (error.config && error.config._defaultErrorShown) { + // Already handeled + event.preventDefault(); // stops the unhandled rejection error + } else if (error.response && error.response.data && error.response.data.message) { + if (!(error.code && error.code === "ECONNABORTED")) { + window.ProcessMaker.alert(error.response.data.message, "danger"); + } + } else if (error.message) { + if (!(error.code && error.code === "ECONNABORTED")) { + window.ProcessMaker.alert(error.message, "danger"); + } + } +}); + +new Vue({ + el: "#sidebar", + components: { + Sidebaricon, + }, + data() { + return { + expanded: false, + }; + }, + created() { + this.expanded === false; + }, +}); diff --git a/resources/js/bootstrap-base.js b/resources/js/bootstrap-base.js new file mode 100644 index 0000000000..092e1999cc --- /dev/null +++ b/resources/js/bootstrap-base.js @@ -0,0 +1,414 @@ +import "bootstrap-vue/dist/bootstrap-vue.css"; +import { BootstrapVue, BootstrapVueIcons } from "bootstrap-vue"; +import * as bootstrap from "bootstrap"; +import TenantAwareEcho from "./common/TenantAwareEcho"; +import { initSessionSync } from "./common/sessionSync"; +import Router from "vue-router"; +import ScreenBuilder, { initializeScreenCache } from "@processmaker/screen-builder"; +import * as VueDeepSet from "vue-deepset"; +/** + * Setup Translations + */ +import i18next from "i18next"; +import Backend from "i18next-chained-backend"; +import LocalStorageBackend from "i18next-localstorage-backend"; +import XHR from "i18next-xhr-backend"; +import VueI18Next from "@panter/vue-i18next"; +import { install as VuetableInstall } from "vuetable-2"; +import Vue from "vue"; +import * as vue from "vue"; +import VueCookies from "vue-cookies"; +import GlobalStore from "./globalStore"; +import Pagination from "./components/common/Pagination"; +import translator from "./modules/lang.js"; +import datetime_format from "./data/datetime_formats.json"; +import RequestChannel from "./tasks/components/ProcessRequestChannel"; +import Modal from "./components/shared/Modal"; +import AccessibilityMixin from "./components/common/mixins/accessibility"; +import PmqlInput from "./components/shared/PmqlInput.vue"; +import DataTreeToggle from "./components/common/data-tree-toggle.vue"; +import TreeView from "./components/TreeView.vue"; +import FilterTable from "./components/shared/FilterTable.vue"; +import PaginationTable from "./components/shared/PaginationTable.vue"; +import PMDropdownSuggest from "./components/PMDropdownSuggest"; +import "@processmaker/screen-builder/dist/vue-form-builder.css"; + +window.__ = translator; +window._ = require("lodash"); +window.Popper = require("popper.js").default; + +/** + * Give node plugins access to our custom screen builder components + */ +window.ProcessmakerComponents = require("./processes/screen-builder/components"); + +/** + * Give node plugins access to additional components + */ +window.SharedComponents = require("./components/shared"); + +window.ProcessesComponents = require("./processes/components"); +window.ScreensComponents = require("./processes/screens/components"); +window.ScriptsComponents = require("./processes/scripts/components"); +window.ProcessesCatalogueComponents = require("./processes-catalogue/components/utils"); +window.Utils = require("./utils"); + +window.PMDropdownSuggest = PMDropdownSuggest; + +window.Vue = Vue; +window.vue = vue; +window.bootstrap = bootstrap; +window.Vue.use(BootstrapVue); +window.Vue.use(BootstrapVueIcons); +window.Vue.use(ScreenBuilder); +window.Vue.use(GlobalStore); +window.Vue.use(VueDeepSet); +window.Vue.use(VueCookies); +if (!document.head.querySelector("meta[name=\"is-horizon\"]")) { + window.Vue.use(Router); +} + +window.ScreenBuilder = require("@processmaker/screen-builder"); +window.VueFormElements = require("@processmaker/vue-form-elements"); + +window.VueRouter = Router; + +/** + * We'll load jQuery and the Bootstrap jQuery plugin which provides support + * for JavaScript based Bootstrap features such as modals and tabs. This + * code may be modified to fit the specific needs of your application. + */ +window.$ = window.jQuery = require("jquery"); + +window.Vue.use(VueI18Next); +VuetableInstall(window.Vue); +window.Vue.component("pagination", Pagination); +window.Vue.component("pm-modal", Modal); +window.Vue.component("pmql-input", PmqlInput); +window.Vue.component("data-tree-toggle", DataTreeToggle); +window.Vue.component("tree-view", TreeView); +window.Vue.component("filter-table", FilterTable); +window.Vue.component("pagination-table", PaginationTable); + +let translationsLoaded = false; +const mdates = JSON.parse( + document.head.querySelector("meta[name=\"i18n-mdate\"]")?.content, +); + +// Make $t available to all vue instances +Vue.mixin({ i18n: new VueI18Next(i18next) }); +Vue.mixin(AccessibilityMixin); + +window.ProcessMaker = { + i18n: i18next, + + /** + * A general use global event bus that can be used + */ + EventBus: new Vue(), + /** + * A general use global router that can be used + */ + Router: new Router({ + mode: "history", + }), + /** + * ProcessMaker Notifications + */ + notifications: [], + /** + * Push a notification. + * + * @param {object} notification + * + * @returns {void} + */ + pushNotification(notification) { + if (!notification || window.ProcessMaker.notifications.some((x) => x.id === notification.id)) { + return; + } + window.ProcessMaker.notifications.push(notification); + }, + + /** + * Removes notifications by message ids or urls + * + * @returns {void} + * @param messageIds + * + * @param urls + */ + removeNotifications(messageIds = [], urls = []) { + return window.ProcessMaker.apiClient.put("/read_notifications", { message_ids: messageIds, routes: urls }).then(() => { + messageIds.forEach((messageId) => { + ProcessMaker.notifications.splice(ProcessMaker.notifications.findIndex((x) => x.id === messageId), 1); + }); + + urls.forEach((url) => { + const messageIndex = ProcessMaker.notifications.findIndex((x) => x.url === url); + if (messageIndex >= 0) { + ProcessMaker.removeNotification(ProcessMaker.notifications[messageIndex].id); + } + }); + }); + }, + /** + * Mark as unread a list of notifications + * + * @returns {void} + * @param messageIds + * + * @param urls + */ + unreadNotifications(messageIds = [], urls = []) { + return window.ProcessMaker.apiClient.put("/unread_notifications", { message_ids: messageIds, routes: urls }); + }, + + missingTranslations: new Set(), + missingTranslation(value) { + if (this.missingTranslations.has(value)) { return; } + this.missingTranslations.add(value); + if (!isProd) { + console.warn("Missing Translation:", value); + } + }, + + RequestChannel, + + $notifications: { + icons: {}, + }, +}; + +window.ProcessMaker.setValidatorLanguage = (validator, lang) => { + const availableLanguages = ["ar", "az", "be", "bg", "bs", "ca", "cs", "cy", "da", "de", "el", "en", "es", "et", "eu", "fa", "fi", + "fr", "hr", "hu", "id", "it", "ja", "ka", "km", "ko", "lt", "lv", "mk", "mn", "ms", "nb_NO", "nl", "pl", "pt", "pt_BR", "ro", "ru", + "se", "sl", "sq", "sr", "sv", "tr", "ua", "uk", "uz", "vi", "zh", "zh_TW"]; + const selectedLang = availableLanguages.includes(lang) ? lang : "en"; + if (validator) { + validator.useLang(selectedLang); + } +}; + +window.ProcessMaker.i18nPromise = i18next.use(Backend).init({ + lng: document.documentElement.lang, + fallbackLng: "en", // default language when no translations + returnEmptyString: false, // When a translation is an empty string, return the default language, not empty + nsSeparator: false, + keySeparator: false, + parseMissingKeyHandler(value) { + if (!translationsLoaded) { return value; } + // Report that a translation is missing + window.ProcessMaker.missingTranslation(value); + // Fallback to showing the english version + return value; + }, + backend: { + backends: [ + LocalStorageBackend, // Try cache first + XHR, + ], + backendOptions: [ + { versions: mdates }, + { loadPath: "/i18next/fetch/{{lng}}/_default" }, + ], + }, +}); + +window.ProcessMaker.i18nPromise.then(() => { translationsLoaded = true; }); + +/** + * Create a axios instance which any vue component can bring in to call + * REST api endpoints through oauth authentication + * + */ +window.ProcessMaker.apiClient = require("axios"); + +window.ProcessMaker.apiClient.defaults.headers.common["X-Requested-With"] = "XMLHttpRequest"; + +/** + * Next we will register the CSRF Token as a common header with Axios so that + * all outgoing HTTP requests automatically have it attached. This is just a + * simple convenience so we don't have to attach every token manually. + */ + +const token = document.head.querySelector("meta[name=\"csrf-token\"]"); +const isProd = document.head.querySelector("meta[name=\"is-prod\"]")?.content === "true"; + +if (token) { + window.ProcessMaker.apiClient.defaults.headers.common["X-CSRF-TOKEN"] = token.content; +} else { + console.error("CSRF token not found: https://laravel.com/docs/csrf#csrf-x-csrf-token"); +} + +// Setup api versions +const apiVersionConfig = [ + { version: "1.0", baseURL: "/api/1.0/" }, + { version: "1.1", baseURL: "/api/1.1/" }, +]; + +window.ProcessMaker.apiClient.defaults.baseURL = apiVersionConfig[0].baseURL; +window.ProcessMaker.apiClient.interceptors.request.use((config) => { + if (typeof config.url !== "string" || !config.url) { + throw new Error("Invalid URL in the request configuration"); + } + + apiVersionConfig.forEach(({ version, baseURL }) => { + const versionPrefix = `/api/${version}/`; + if (config.url.startsWith(versionPrefix)) { + // eslint-disable-next-line no-param-reassign + config.baseURL = baseURL; + // eslint-disable-next-line no-param-reassign + config.url = config.url.replace(versionPrefix, ""); + } + }); + + return config; +}); + +// Set the default API timeout +let apiTimeout = 5000; +if (window.Processmaker && window.Processmaker.apiTimeout !== undefined) { + apiTimeout = window.Processmaker.apiTimeout; +} +window.ProcessMaker.apiClient.defaults.timeout = apiTimeout; + +// Default alert functionality +window.ProcessMaker.alert = function (text, variant) { + if (typeof text === "string") { + window.alert(text); + } +}; + +const openAiEnabled = document.head.querySelector("meta[name=\"open-ai-nlq-to-pmql\"]"); + +if (openAiEnabled) { + window.ProcessMaker.openAi = { + enabled: openAiEnabled.content, + }; +} else { + window.ProcessMaker.openAi = { + enabled: false, + }; +} + +const userID = document.head.querySelector("meta[name=\"user-id\"]"); +const userFullName = document.head.querySelector("meta[name=\"user-full-name\"]"); +const userAvatar = document.head.querySelector("meta[name=\"user-avatar\"]"); +const formatDate = document.head.querySelector("meta[name=\"datetime-format\"]"); +const timezone = document.head.querySelector("meta[name=\"timezone\"]"); +const appUrl = document.head.querySelector("meta[name=\"app-url\"]"); + +if (appUrl) { + window.ProcessMaker.app = { + url: appUrl.content, + }; +} + +if (userID) { + window.ProcessMaker.user = { + id: userID.content, + datetime_format: formatDate?.content, + calendar_format: formatDate?.content, + timezone: timezone?.content, + fullName: userFullName?.content, + avatar: userAvatar?.content, + }; + datetime_format.forEach((value) => { + if (formatDate.content === value.format) { + window.ProcessMaker.user.datetime_format = value.momentFormat; + window.ProcessMaker.user.calendar_format = value.calendarFormat; + } + }); +} + +if (window.Processmaker && window.Processmaker.broadcasting) { + const config = window.Processmaker.broadcasting; + + if (config.broadcaster == "pusher") { + window.Pusher = require("pusher-js"); + window.Pusher.logToConsole = config.debug; + } + + window.Echo = new TenantAwareEcho(config); +} + +if (userID) { + const timeoutScript = document.head.querySelector("meta[name=\"timeout-worker\"]")?.content; + const accountTimeoutLength = parseInt(eval(document.head.querySelector("meta[name=\"timeout-length\"]")?.content)); + const warnSeconds = parseInt(document.head.querySelector("meta[name=\"timeout-warn-seconds\"]")?.content); + const accountTimeoutWarnSeconds = Number.isNaN(warnSeconds) ? 0 : warnSeconds; + const accountTimeoutEnabled = document.head.querySelector("meta[name=\"timeout-enabled\"]") ? parseInt(document.head.querySelector("meta[name=\"timeout-enabled\"]")?.content) : 1; + + const sessionSyncState = initSessionSync({ + userId: userID.content, + isProd, + timeoutScript, + accountTimeoutLength, + accountTimeoutWarnSeconds, + accountTimeoutEnabled, + Vue, + Echo: window.Echo, + pushNotification: window.ProcessMaker.pushNotification, + alert: window.ProcessMaker.alert, + getSessionModal: () => window.ProcessMaker.sessionModal, + getCloseSessionModal: () => window.ProcessMaker.closeSessionModal, + getNavbar: () => window.ProcessMaker.navbar, + }); + + if (sessionSyncState) { + window.ProcessMaker.AccountTimeoutLength = sessionSyncState.AccountTimeoutLength; + window.ProcessMaker.AccountTimeoutWarnSeconds = sessionSyncState.AccountTimeoutWarnSeconds; + window.ProcessMaker.AccountTimeoutWarnMinutes = sessionSyncState.AccountTimeoutWarnMinutes; + window.ProcessMaker.AccountTimeoutEnabled = sessionSyncState.AccountTimeoutEnabled; + window.ProcessMaker.AccountTimeoutWorker = sessionSyncState.AccountTimeoutWorker; + window.ProcessMaker.sessionSync = sessionSyncState.sessionSync; + } +} + +// Configuration Global object used by ScreenBuilder +// @link https://processmaker.atlassian.net/browse/FOUR-6833 Cache configuration +const screenCacheEnabled = document.head.querySelector("meta[name=\"screen-cache-enabled\"]")?.content ?? "false"; +const screenCacheTimeout = document.head.querySelector("meta[name=\"screen-cache-timeout\"]")?.content ?? "5000"; +const screenSecureHandlerToggleVisible = document.head.querySelector("meta[name='screen-secure-handler-toggle-visible']"); +window.ProcessMaker.screen = { + cacheEnabled: screenCacheEnabled === "true", + cacheTimeout: Number(screenCacheTimeout), + secureHandlerToggleVisible: !!Number(screenSecureHandlerToggleVisible?.content), +}; +// Initialize screen-builder cache +initializeScreenCache(window.ProcessMaker.apiClient, window.ProcessMaker.screen); + +const clickTab = () => { + const { hash } = window.location; + if (!hash) { + return; + } + const tab = $(`[role="tab"][href="${hash}"]`); + if (tab.length) { + tab.tab("show"); + } +}; +window.addEventListener("hashchange", clickTab); + +// click an active tab after all components have mounted +Vue.use({ + install(vue) { + vue.mixin({ + mounted() { + if (this.$parent) { + // only run on root + return; + } + + // Run after component mounted + this.$nextTick(() => { + clickTab(); + }); + }, + }); + }, +}); + +// Send an event when the global Vue and ProcessMaker instance is available +window.dispatchEvent(new Event("app-bootstrapped")); diff --git a/resources/js/bootstrap-core.js b/resources/js/bootstrap-core.js new file mode 100644 index 0000000000..95874f5350 --- /dev/null +++ b/resources/js/bootstrap-core.js @@ -0,0 +1 @@ +import "./bootstrap-base"; diff --git a/resources/js/bootstrap.js b/resources/js/bootstrap.js index 6f10bfad4a..1f0ed36cea 100644 --- a/resources/js/bootstrap.js +++ b/resources/js/bootstrap.js @@ -1,448 +1,10 @@ -import "bootstrap-vue/dist/bootstrap-vue.css"; -import { BootstrapVue, BootstrapVueIcons } from "bootstrap-vue"; -import * as bootstrap from "bootstrap"; -import Router from "vue-router"; -import ScreenBuilder, { initializeScreenCache } from "@processmaker/screen-builder"; -import * as VueDeepSet from "vue-deepset"; - -/** - * Setup Translations - */ -import i18next from "i18next"; -import Backend from "i18next-chained-backend"; -import LocalStorageBackend from "i18next-localstorage-backend"; -import XHR from "i18next-xhr-backend"; -import VueI18Next from "@panter/vue-i18next"; -import { install as VuetableInstall } from "vuetable-2"; +import "./bootstrap-core"; import MonacoEditor from "vue-monaco"; -import Vue from "vue"; -import * as vue from "vue"; -import VueCookies from "vue-cookies"; -import { initSessionSync } from "./common/sessionSync"; -import { - applyCsrfToken, - attachCsrfRequestInterceptor, - attachSessionRenewalInterceptor, - getCsrfToken, -} from "./common/csrfToken"; -import TenantAwareEcho from "./common/TenantAwareEcho"; -import GlobalStore from "./globalStore"; -import Pagination from "./components/common/Pagination"; import ScreenSelect from "./processes/modeler/components/inspector/ScreenSelect.vue"; -import translator from "./modules/lang.js"; -import datetime_format from "./data/datetime_formats.json"; -import RequestChannel from "./tasks/components/ProcessRequestChannel"; -import Modal from "./components/shared/Modal"; -import AccessibilityMixin from "./components/common/mixins/accessibility"; -import PmqlInput from "./components/shared/PmqlInput.vue"; -import DataTreeToggle from "./components/common/data-tree-toggle.vue"; -import TreeView from "./components/TreeView.vue"; -import FilterTable from "./components/shared/FilterTable.vue"; -import PaginationTable from "./components/shared/PaginationTable.vue"; -import PMDropdownSuggest from "./components/PMDropdownSuggest"; -import "@processmaker/screen-builder/dist/vue-form-builder.css"; - -window.__ = translator; -window._ = require("lodash"); -window.Popper = require("popper.js").default; - -/** - * Give node plugins access to our custom screen builder components - */ -window.ProcessmakerComponents = require("./processes/screen-builder/components"); - -/** - * Give node plugins access to additional components - */ -window.SharedComponents = require("./components/shared"); - -window.ProcessesComponents = require("./processes/components"); -window.ScreensComponents = require("./processes/screens/components"); -window.ScriptsComponents = require("./processes/scripts/components"); -window.ProcessesCatalogueComponents = require("./processes-catalogue/components/utils"); -window.Utils = require("./utils"); - -window.PMDropdownSuggest = PMDropdownSuggest; - -/** - * Exporting Modeler inspector components - */ -window.ModelerInspector = require("./processes/modeler/components/inspector"); -/** - * We'll load jQuery and the Bootstrap jQuery plugin which provides support - * for JavaScript based Bootstrap features such as modals and tabs. This - * code may be modified to fit the specific needs of your application. - */ -window.$ = window.jQuery = require("jquery"); - -/** - * Vue is a modern JavaScript library for building interactive web interfaces - * using reactive data binding and reusable components. Vue's API is clean - * and simple, leaving you to focus on building your next great project. - */ - -window.Vue = Vue; -window.vue = vue; -window.bootstrap = bootstrap; -window.Vue.use(BootstrapVue); -window.Vue.use(BootstrapVueIcons); -window.Vue.use(ScreenBuilder); -window.Vue.use(GlobalStore); -window.Vue.use(VueDeepSet); -window.Vue.use(VueCookies); -if (!document.head.querySelector("meta[name=\"is-horizon\"]")) { - window.Vue.use(Router); -} window.VueMonaco = require("vue-monaco"); - -window.ScreenBuilder = require("@processmaker/screen-builder"); -window.VueFormElements = require("@processmaker/vue-form-elements"); window.Modeler = require("@processmaker/modeler"); +window.ModelerInspector = require("./processes/modeler/components/inspector"); -window.VueRouter = Router; - -window.Vue.use(VueI18Next); -VuetableInstall(window.Vue); -window.Vue.component("pagination", Pagination); -window.Vue.component("monaco-editor", MonacoEditor); window.Vue.component("screen-select", ScreenSelect); -window.Vue.component("pm-modal", Modal); -window.Vue.component("pmql-input", PmqlInput); -window.Vue.component("data-tree-toggle", DataTreeToggle); -window.Vue.component("tree-view", TreeView); -window.Vue.component("filter-table", FilterTable); -window.Vue.component("pagination-table", PaginationTable); - -let translationsLoaded = false; -const mdates = JSON.parse( - document.head.querySelector("meta[name=\"i18n-mdate\"]")?.content, -); - -// Make $t available to all vue instances -Vue.mixin({ i18n: new VueI18Next(i18next) }); -Vue.mixin(AccessibilityMixin); - -window.ProcessMaker = { - i18n: i18next, - - /** - * A general use global event bus that can be used - */ - EventBus: new Vue(), - /** - * A general use global router that can be used - */ - Router: new Router({ - mode: "history", - }), - /** - * ProcessMaker Notifications - */ - notifications: [], - /** - * Push a notification. - * - * @param {object} notification - * - * @returns {void} - */ - pushNotification(notification) { - if (!notification || window.ProcessMaker.notifications.some((x) => x.id === notification.id)) { - return; - } - window.ProcessMaker.notifications.push(notification); - }, - - /** - * Removes notifications by message ids or urls - * - * @returns {void} - * @param messageIds - * - * @param urls - */ - removeNotifications(messageIds = [], urls = []) { - return window.ProcessMaker.apiClient.put("/read_notifications", { message_ids: messageIds, routes: urls }).then(() => { - messageIds.forEach((messageId) => { - ProcessMaker.notifications.splice(ProcessMaker.notifications.findIndex((x) => x.id === messageId), 1); - }); - - urls.forEach((url) => { - const messageIndex = ProcessMaker.notifications.findIndex((x) => x.url === url); - if (messageIndex >= 0) { - ProcessMaker.removeNotification(ProcessMaker.notifications[messageIndex].id); - } - }); - }); - }, - /** - * Mark as unread a list of notifications - * - * @returns {void} - * @param messageIds - * - * @param urls - */ - unreadNotifications(messageIds = [], urls = []) { - return window.ProcessMaker.apiClient.put("/unread_notifications", { message_ids: messageIds, routes: urls }); - }, - - missingTranslations: new Set(), - missingTranslation(value) { - if (this.missingTranslations.has(value)) { return; } - this.missingTranslations.add(value); - if (!isProd) { - console.warn("Missing Translation:", value); - } - }, - - RequestChannel, - - $notifications: { - icons: {}, - }, -}; - -window.ProcessMaker.setValidatorLanguage = (validator, lang) => { - const availableLanguages = ["ar", "az", "be", "bg", "bs", "ca", "cs", "cy", "da", "de", "el", "en", "es", "et", "eu", "fa", "fi", - "fr", "hr", "hu", "id", "it", "ja", "ka", "km", "ko", "lt", "lv", "mk", "mn", "ms", "nb_NO", "nl", "pl", "pt", "pt_BR", "ro", "ru", - "se", "sl", "sq", "sr", "sv", "tr", "ua", "uk", "uz", "vi", "zh", "zh_TW"]; - const selectedLang = availableLanguages.includes(lang) ? lang : "en"; - if (validator) { - validator.useLang(selectedLang); - } -}; - -window.ProcessMaker.i18nPromise = i18next.use(Backend).init({ - lng: document.documentElement.lang, - fallbackLng: "en", // default language when no translations - returnEmptyString: false, // When a translation is an empty string, return the default language, not empty - nsSeparator: false, - keySeparator: false, - parseMissingKeyHandler(value) { - if (!translationsLoaded) { return value; } - // Report that a translation is missing - window.ProcessMaker.missingTranslation(value); - // Fallback to showing the english version - return value; - }, - backend: { - backends: [ - LocalStorageBackend, // Try cache first - XHR, - ], - backendOptions: [ - { versions: mdates }, - { loadPath: "/i18next/fetch/{{lng}}/_default" }, - ], - }, -}); - -window.ProcessMaker.i18nPromise.then(() => { translationsLoaded = true; }); - -/** - * Create a axios instance which any vue component can bring in to call - * REST api endpoints through oauth authentication - * - */ -window.ProcessMaker.apiClient = require("axios"); - -window.ProcessMaker.apiClient.defaults.withCredentials = true; - -window.ProcessMaker.apiClient.defaults.headers.common["X-Requested-With"] = "XMLHttpRequest"; - -const token = document.head.querySelector("meta[name=\"csrf-token\"]"); -const isProd = document.head.querySelector("meta[name=\"is-prod\"]")?.content === "true"; - -window.ProcessMaker.applyCsrfToken = applyCsrfToken; -window.ProcessMaker.getCsrfToken = getCsrfToken; -// Attach CSRF interceptor before other interceptors so it runs last in the axios chain. -attachCsrfRequestInterceptor(window.ProcessMaker.apiClient); - -/** - * Next we will register the CSRF Token as a common header with Axios so that - * all outgoing HTTP requests automatically have it attached. This is just - * a simple convenience so we don't have to attach every token manually. - */ - -if (token) { - applyCsrfToken(token.content, "page-load"); -} else { - console.error("CSRF token not found: https://laravel.com/docs/csrf#csrf-x-csrf-token"); -} - -// Setup api versions -const apiVersionConfig = [ - { version: "1.0", baseURL: "/api/1.0/" }, - { version: "1.1", baseURL: "/api/1.1/" }, -]; - -window.ProcessMaker.apiClient.defaults.baseURL = apiVersionConfig[0].baseURL; -window.ProcessMaker.apiClient.interceptors.request.use((config) => { - if (typeof config.url !== "string" || !config.url) { - throw new Error("Invalid URL in the request configuration"); - } - - apiVersionConfig.forEach(({ version, baseURL }) => { - const versionPrefix = `/api/${version}/`; - if (config.url.startsWith(versionPrefix)) { - // eslint-disable-next-line no-param-reassign - config.baseURL = baseURL; - // eslint-disable-next-line no-param-reassign - config.url = config.url.replace(versionPrefix, ""); - } - }); - - return config; -}); - -attachSessionRenewalInterceptor(window.ProcessMaker.apiClient); - -// Set the default API timeout -let apiTimeout = 5000; -if (window.Processmaker && window.Processmaker.apiTimeout !== undefined) { - apiTimeout = window.Processmaker.apiTimeout; -} -window.ProcessMaker.apiClient.defaults.timeout = apiTimeout; - -// Default alert functionality -window.ProcessMaker.alert = function (text, variant) { - if (typeof text === "string") { - window.alert(text); - } -}; - -const openAiEnabled = document.head.querySelector("meta[name=\"open-ai-nlq-to-pmql\"]"); - -if (openAiEnabled) { - window.ProcessMaker.openAi = { - enabled: openAiEnabled.content, - }; -} else { - window.ProcessMaker.openAi = { - enabled: false, - }; -} - -const userID = document.head.querySelector("meta[name=\"user-id\"]"); -const userFullName = document.head.querySelector("meta[name=\"user-full-name\"]"); -const userAvatar = document.head.querySelector("meta[name=\"user-avatar\"]"); -const formatDate = document.head.querySelector("meta[name=\"datetime-format\"]"); -const timezone = document.head.querySelector("meta[name=\"timezone\"]"); -const appUrl = document.head.querySelector("meta[name=\"app-url\"]"); - -if (appUrl) { - window.ProcessMaker.app = { - url: appUrl.content, - }; -} - -if (userID) { - window.ProcessMaker.user = { - id: userID.content, - datetime_format: formatDate?.content, - calendar_format: formatDate?.content, - timezone: timezone?.content, - fullName: userFullName?.content, - avatar: userAvatar?.content, - }; - datetime_format.forEach((value) => { - if (formatDate.content === value.format) { - window.ProcessMaker.user.datetime_format = value.momentFormat; - window.ProcessMaker.user.calendar_format = value.calendarFormat; - } - }); -} - -if (window.Processmaker && window.Processmaker.broadcasting) { - const config = window.Processmaker.broadcasting; - - if (config.broadcaster == "pusher") { - window.Pusher = require("pusher-js"); - window.Pusher.logToConsole = config.debug; - } - - window.Echo = new TenantAwareEcho(config); -} - -if (userID) { - const timeoutScript = document.head.querySelector("meta[name=\"timeout-worker\"]")?.content; - const timeoutEnabledMeta = document.head.querySelector("meta[name=\"timeout-enabled\"]")?.content; - const accountTimeoutLength = Number(document.head.querySelector("meta[name=\"timeout-length\"]")?.content); - const warnSeconds = Number(document.head.querySelector("meta[name=\"timeout-warn-seconds\"]")?.content); - const accountTimeoutWarnSeconds = Number.isNaN(warnSeconds) ? 0 : warnSeconds; - const accountTimeoutEnabled = timeoutEnabledMeta ? Number(timeoutEnabledMeta) : 1; - - const sessionSyncState = initSessionSync({ - userId: userID.content, - isProd, - timeoutScript, - accountTimeoutLength, - accountTimeoutWarnSeconds, - accountTimeoutEnabled, - Vue, - Echo: window.Echo, - pushNotification: window.ProcessMaker.pushNotification, - alert: window.ProcessMaker.alert, - getSessionModal: () => window.ProcessMaker.sessionModal, - getCloseSessionModal: () => window.ProcessMaker.closeSessionModal, - getNavbar: () => window.ProcessMaker.navbar, - }); - - if (sessionSyncState) { - window.ProcessMaker.AccountTimeoutLength = sessionSyncState.AccountTimeoutLength; - window.ProcessMaker.AccountTimeoutWarnSeconds = sessionSyncState.AccountTimeoutWarnSeconds; - window.ProcessMaker.AccountTimeoutWarnMinutes = sessionSyncState.AccountTimeoutWarnMinutes; - window.ProcessMaker.AccountTimeoutEnabled = sessionSyncState.AccountTimeoutEnabled; - window.ProcessMaker.AccountTimeoutWorker = sessionSyncState.AccountTimeoutWorker; - window.ProcessMaker.sessionSync = sessionSyncState.sessionSync; - } -} - -// Configuration Global object used by ScreenBuilder -// @link https://processmaker.atlassian.net/browse/FOUR-6833 Cache configuration -const screenCacheEnabled = document.head.querySelector("meta[name=\"screen-cache-enabled\"]")?.content ?? "false"; -const screenCacheTimeout = document.head.querySelector("meta[name=\"screen-cache-timeout\"]")?.content ?? "5000"; -const screenSecureHandlerToggleVisible = document.head.querySelector("meta[name='screen-secure-handler-toggle-visible']"); -window.ProcessMaker.screen = { - cacheEnabled: screenCacheEnabled === "true", - cacheTimeout: Number(screenCacheTimeout), - secureHandlerToggleVisible: !!Number(screenSecureHandlerToggleVisible?.content), -}; -// Initialize screen-builder cache -initializeScreenCache(window.ProcessMaker.apiClient, window.ProcessMaker.screen); - -const clickTab = () => { - const { hash } = window.location; - if (!hash) { - return; - } - const tab = $(`[role="tab"][href="${hash}"]`); - if (tab.length) { - tab.tab("show"); - } -}; -window.addEventListener("hashchange", clickTab); - -// click an active tab after all components have mounted -Vue.use({ - install(vue) { - vue.mixin({ - mounted() { - if (this.$parent) { - // only run on root - return; - } - - // Run after component mounted - this.$nextTick(() => { - clickTab(); - }); - }, - }); - }, -}); - -// Send an event when the global Vue and ProcessMaker instance is available -window.dispatchEvent(new Event("app-bootstrapped")); +window.Vue.component("monaco-editor", MonacoEditor); diff --git a/resources/views/layouts/layout.blade.php b/resources/views/layouts/layout.blade.php index 4f5c11a98a..162783f226 100644 --- a/resources/views/layouts/layout.blade.php +++ b/resources/views/layouts/layout.blade.php @@ -127,6 +127,9 @@ class="main flex-grow-1 h-100
{{__('Something went wrong. Try refreshing the application')}}
+@php +$isInboxPage = request()->is('inbox', 'inbox/*'); +@endphp @if(config('broadcasting.default') == 'redis') @@ -134,9 +137,14 @@ class="main flex-grow-1 h-100 +@if($isInboxPage) + + +@else +@endif +@if($isInboxPage) + +@else +@endif +@if(!$isInboxPage) @include('shared.monaco') +@endif @foreach(GlobalScripts::getScripts() as $script) @endforeach diff --git a/webpack.mix.js b/webpack.mix.js index 39dfe16484..2b57fac561 100644 --- a/webpack.mix.js +++ b/webpack.mix.js @@ -159,6 +159,8 @@ mix .js("resources/jscomposition/cases/casesMain/loader.js", "public/js/composition/cases/casesMain") .js("resources/jscomposition/cases/casesDetail/loader.js", "public/js/composition/cases/casesDetail") .js("resources/js/initialLoad.js", "public/js") + .js("resources/js/app-core.js", "public/js") + .js("resources/js/app-layout-core.js", "public/js") .js("resources/js/tasks/loaderMain.js", "public/js/tasks") .js("resources/js/tasks/loaderPreview.js", "public/js/tasks")