diff --git a/ProcessMaker/Http/Controllers/CasesController.php b/ProcessMaker/Http/Controllers/CasesController.php index 942c87ac95..f96d6cdc1a 100644 --- a/ProcessMaker/Http/Controllers/CasesController.php +++ b/ProcessMaker/Http/Controllers/CasesController.php @@ -20,10 +20,12 @@ class CasesController extends Controller */ public function index() { + $manager = app(ScreenBuilderManager::class); + event(new ScreenBuilderStarting($manager, 'FORM')); $currentUser = Auth::user()->only(['id', 'username', 'fullname', 'firstname', 'lastname', 'avatar']); // This is a temporary API the engine team will provide the new - return view('cases.casesMain', compact('currentUser')); + return view('cases.casesMain', compact('currentUser', 'manager')); } /** diff --git a/resources/js/components/TreeView.vue b/resources/js/components/TreeView.vue index e60a779c4e..459fdcdcb6 100644 --- a/resources/js/components/TreeView.vue +++ b/resources/js/components/TreeView.vue @@ -37,7 +37,7 @@ export default { }, }, mounted() { - const jsonCrackEmbed = this.$refs.jsonCrackEmbed; + const { jsonCrackEmbed } = this.$refs; const json = this.jsonData === "" ? "{}" : this.jsonData; const options = { theme: "light", diff --git a/resources/js/next/badPractices.js b/resources/js/next/badPractices.js new file mode 100644 index 0000000000..36307e2b89 --- /dev/null +++ b/resources/js/next/badPractices.js @@ -0,0 +1,53 @@ +/** + * BAD PRACTICES + */ + +// Use multiple forms of an event bus. +window.ProcessMaker = { + EventBus: new Vue(), + events: new Vue() +}; + +// Force to overload the vue object +window.Vue.prototype.moment = moment; + +// Load all statements in a single file bootstrap.js + +// Overload the window.ProcessMaker variable without using the setGlobalVariable function + +// Use local variables instead of window.ProcessMaker +window.ProcessMaker.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); + } + }); + }); +}; + +// Remove the saved search unneeded +class InjectJavascript +{ + public function handle($request, Closure $next) + { + $response = $next($request); + if (get_class($response) !== Response::class) { + return $response; + } + $content = $response->getContent(); + $tag = ''; + $tag .= ''; + $tag .= ''; + $content = str_replace('', $tag . "\n", $content); + $response->setContent($content); + + return $response; + } +} + diff --git a/resources/js/next/components/index.js b/resources/js/next/components/index.js new file mode 100644 index 0000000000..2b938c2b73 --- /dev/null +++ b/resources/js/next/components/index.js @@ -0,0 +1,112 @@ +import { getGlobalVariable } from "../globalVariables"; + +// Multiselect + +const Vue = getGlobalVariable("Vue"); + +const pmComponents = { + // Components folder + AvatarImage: () => import("../../components/AvatarImage.vue"), + Breadcrumbs: () => import("../../components/Breadcrumbs.vue"), + Confirm: () => import("../../components/Confirm.vue"), + CustomActions: () => import("../../components/CustomActions.vue"), + DetailRow: () => import("../../components/DetailRow.vue"), + FilterBar: () => import("../../components/FilterBar.vue"), + Menu: () => import("../../components/Menu.vue"), + Message: () => import("../../components/Message.vue"), + NavbarProfile: () => import("../../components/NavbarProfile.vue"), + PMBadgesFilters: () => import("../../components/PMBadgesFilters.vue"), + PMDatetimePicker: () => import("../../components/PMDatetimePicker.vue"), + PMDropdownSuggest: () => import("../../components/PMDropdownSuggest.vue"), + PMFloatingButtons: () => import("../../components/PMFloatingButtons.vue"), + PMFormSelectSuggest: () => import("../../components/PMFormSelectSuggest.vue"), + PMMessageResults: () => import("../../components/PMMessageResults.vue"), + PMMessageScreen: () => import("../../components/PMMessageScreen.vue"), + PMPanelWithCustomHeader: () => import("../../components/PMPanelWithCustomHeader.vue"), + PMPopoverConfirmation: () => import("../../components/PMPopoverConfirmation.vue"), + PMSearchBar: () => import("../../components/PMSearchBar.vue"), + PMTable: () => import("../../components/PMTable.vue"), + PMTabs: () => import("../../components/PMTabs.vue"), + Recommendations: () => import("../../components/Recommendations.vue"), + SelectFromApi: () => import("../../components/SelectFromApi.vue"), + SelectLanguage: () => import("../../components/SelectLanguage.vue"), + SelectScreen: () => import("../../components/SelectScreen.vue"), + SelectStatus: () => import("../../components/SelectStatus.vue"), + SelectUser: () => import("../../components/SelectUser.vue"), + SelectUserGroup: () => import("../../components/SelectUserGroup.vue"), + Session: () => import("../../components/Session.vue"), + Sidebaricon: () => import("../../components/Sidebaricon.vue"), + Timeline: () => import("../../components/Timeline.vue"), + TimelineItem: () => import("../../components/TimelineItem.vue"), + TreeView: () => import("../../components/TreeView.vue"), + // Shared components folder + AddToBundle: () => import("../../components/shared/AddToBundle"), + AddToProjectModal: () => import("../../components/shared/AddToProjectModal"), + AssetDependentTreeModal: () => import("../../components/shared/AssetDependentTreeModal.vue"), + AssetTreeModal: () => import("../../components/shared/AssetTreeModal.vue"), + BackendSelect: () => import("../../components/shared/BackendSelect.vue"), + BasicSearch: () => import("../../components/shared/BasicSearch.vue"), + CategorySelect: () => import("../../components/shared/CategorySelect.vue"), + ChangeLog: () => import("../../components/shared/ChangeLog.vue"), + ColorSchemeSelector: () => import("../../components/shared/ColorSchemeSelector.vue"), + Column: () => import("../../components/shared/Column.vue"), + ColumnChooser: () => import("../../components/shared/ColumnChooser.vue"), + ColumnConfig: () => import("../../components/shared/ColumnConfig.vue"), + DataCard: () => import("../../components/shared/DataCard.vue"), + DataFormatSelector: () => import("../../components/shared/DataFormatSelector.vue"), + DataMaskSelector: () => import("../../components/shared/DataMaskSelector.vue"), + DataNode: () => import("../../components/shared/DataNode.vue"), + DataTree: () => import("../../components/shared/DataTree.vue"), + DownloadSvgButton: () => import("../../components/shared/DownloadSvgButton.vue"), + DraggableFileUpload: () => import("../../components/shared/DraggableFileUpload.vue"), + EllipsisMenu: () => import("../../components/shared/EllipsisMenu.vue"), + FileUploadButton: () => import("../../components/shared/FileUploadButton.vue"), + FilterTable: () => import("../../components/shared/FilterTable.vue"), + IconDropdown: () => import("../../components/shared/IconDropdown.vue"), + IconSelector: () => import("../../components/shared/IconSelector.vue"), + InputImageCarousel: () => import("../../components/shared/InputImageCarousel.vue"), + LaunchpadSettingsModal: () => import("../../components/shared/LaunchpadSettingsModal.vue"), + Modal: () => import("../../components/shared/Modal.vue"), + ModalSaveVersion: () => import("../../components/shared/ModalSaveVersion.vue"), + MultiThumbnailFileUploader: () => import("../../components/shared/MultiThumbnailFileUploader.vue"), + PaginationTable: () => import("../../components/shared/PaginationTable.vue"), + PmqlInput: () => import("../../components/shared/PmqlInput.vue"), + PmqlInputFilters: () => import("../../components/shared/PmqlInputFilters.vue"), + ProjectSelect: () => import("../../components/shared/ProjectSelect.vue"), + PTab: () => import("../../components/shared/PTab.vue"), + PTabs: () => import("../../components/shared/PTabs.vue"), + Required: () => import("../../components/shared/Required.vue"), + SidebarButton: () => import("../../components/shared/SidebarButton.vue"), + SidebarNav: () => import("../../components/shared/SidebarNav.vue"), + SliderWithInput: () => import("../../components/shared/SliderWithInput.vue"), + // Common components folder + DataTreeToggle: () => import("../../components/common/data-tree-toggle.vue"), + // Tasks components folder + MobileTasks: () => import("../../tasks/components/MobileTasks.vue"), + NavbarTaskMobile: () => import("../../tasks/components/NavbarTaskMobile.vue"), + QuickFillPreview: () => import("../../tasks/components/QuickFillPreview.vue"), + ReassignMobileModal: () => import("../../tasks/components/ReassignMobileModal.vue"), + SplitpaneContainer: () => import("../../tasks/components/SplitpaneContainer.vue"), + TaskDetailsMobile: () => import("../../tasks/components/TaskDetailsMobile.vue"), + TaskListRowButtons: () => import("../../tasks/components/TaskListRowButtons.vue"), + TaskLoading: () => import("../../tasks/components/TaskLoading.vue"), + TaskSaveNotification: () => import("../../tasks/components/TaskSaveNotification.vue"), + TaskSavePanel: () => import("../../tasks/components/TaskSavePanel.vue"), + TasksHome: () => import("../../tasks/components/TasksHome.vue"), + TasksList: () => import("../../tasks/components/TasksList.vue"), + TasksListCounter: () => import("../../tasks/components/TasksListCounter.vue"), + TasksPreview: () => import("../../tasks/components/TasksPreview.vue"), + TaskTooltip: () => import("../../tasks/components/TaskTooltip.vue"), + TaskView: () => import("../../tasks/components/TaskView.vue"), +}; + +Object.entries(pmComponents).forEach(([key, component]) => { + Vue.component(key, component); +}); + +// Multiselect +Vue.component("Multiselect", (resolve, reject) => { + import("@processmaker/vue-multiselect").then((Multiselect) => { + resolve(Multiselect.Multiselect); + }).catch(reject); +}); diff --git a/resources/js/next/config/accesibility.js b/resources/js/next/config/accesibility.js new file mode 100644 index 0000000000..1b502a4815 --- /dev/null +++ b/resources/js/next/config/accesibility.js @@ -0,0 +1,6 @@ +import AccessibilityMixin from "./components/common/mixins/accessibility"; +import { getGlobalVariable } from "../globalVariables"; + +const Vue = getGlobalVariable("Vue"); + +Vue.mixin(AccessibilityMixin); diff --git a/resources/js/next/config/i18n.js b/resources/js/next/config/i18n.js new file mode 100644 index 0000000000..06c0cc7880 --- /dev/null +++ b/resources/js/next/config/i18n.js @@ -0,0 +1,59 @@ +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 { setGlobalPMVariable, getGlobalVariable } from "../globalVariables"; + +const Vue = getGlobalVariable("Vue"); + +const isProd = document.head.querySelector("meta[name=\"is-prod\"]")?.content === "true"; +let translationsLoaded = false; + +const mdates = JSON.parse( + document.head.querySelector("meta[name=\"i18n-mdate\"]")?.content, +); + +const missingTranslations = new Set(); +const missingTranslation = function (value) { + if (missingTranslations.has(value)) { return; } + missingTranslations.add(value); + if (!isProd) { + console.warn("Missing Translation:", value); + } +}; + +const 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 + 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" }, + ], + }, +}); + +i18nPromise.then(() => { translationsLoaded = true; }); + +Vue.use(VueI18Next); +Vue.mixin({ i18n: new VueI18Next(i18next) }); + +setGlobalPMVariable("i18n", i18next); +setGlobalPMVariable("i18nPromise", i18nPromise); +setGlobalPMVariable("missingTranslations", missingTranslations); +setGlobalPMVariable("missingTranslation", missingTranslation); diff --git a/resources/js/next/config/notifications.js b/resources/js/next/config/notifications.js new file mode 100644 index 0000000000..7887d1ce00 --- /dev/null +++ b/resources/js/next/config/notifications.js @@ -0,0 +1,36 @@ +import { getGlobalPMVariable, setGlobalPMVariable } from "../globalVariables"; + +const apiClient = getGlobalPMVariable("apiClient"); + +const notifications = []; + +const pushNotification = (notification) => { + if (notifications.filter((x) => x.id === notification).length === 0) { + notifications.push(notification); + } +}; + +const removeNotifications = (messageIds = [], urls = []) => apiClient.put("/read_notifications", { message_ids: messageIds, routes: urls }).then(() => { + messageIds.forEach((messageId) => { + notifications.splice(notifications.findIndex((x) => x.id === messageId), 1); + }); + + urls.forEach((url) => { + const messageIndex = notifications.findIndex((x) => x.url === url); + if (messageIndex >= 0) { + removeNotifications(notifications[messageIndex].id); + } + }); +}); + +const unreadNotifications = (messageIds = [], urls = []) => apiClient.put("/unread_notifications", { message_ids: messageIds, routes: urls }); + +const $notifications = { + icons: {}, +}; + +setGlobalPMVariable("notifications", notifications); +setGlobalPMVariable("pushNotification", pushNotification); +setGlobalPMVariable("removeNotifications", removeNotifications); +setGlobalPMVariable("unreadNotifications", unreadNotifications); +setGlobalPMVariable("$notifications", $notifications); diff --git a/resources/js/next/config/openAI.js b/resources/js/next/config/openAI.js new file mode 100644 index 0000000000..06db7ceb56 --- /dev/null +++ b/resources/js/next/config/openAI.js @@ -0,0 +1,9 @@ +import { setGlobalPMVariable } from "../globalVariables"; + +const openAi = document.head.querySelector("meta[name=\"open-ai-nlq-to-pmql\"]"); + +setGlobalPMVariable("openAi", openAi ? { + enabled: openAi.content, +} : { + enabled: false, +}); diff --git a/resources/js/next/config/processmaker.js b/resources/js/next/config/processmaker.js new file mode 100644 index 0000000000..dba2ef3d43 --- /dev/null +++ b/resources/js/next/config/processmaker.js @@ -0,0 +1,137 @@ +import axios from "axios"; +import { setGlobalPMVariable, getGlobalPMVariable } from "../globalVariables"; + +const token = document.head.querySelector("meta[name=\"csrf-token\"]"); +const EventBus = getGlobalPMVariable("EventBus"); + +// Setup api versions +const apiVersionConfig = [ + { version: "1.0", baseURL: "/api/1.0/" }, + { version: "1.1", baseURL: "/api/1.1/" }, +]; + +// Set the default API timeout +let apiTimeout = 5000; + +/** + * Create a axios instance which any vue component can bring in to call + * REST api endpoints through oauth authentication + */ + +const apiClient = axios; + +apiClient.defaults.headers.common["X-Requested-With"] = "XMLHttpRequest"; + +apiClient.defaults.baseURL = apiVersionConfig[0].baseURL; + +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; +}); + +// flags print forms +apiClient.requestCount = 0; +apiClient.requestCountFlag = false; +apiClient.interceptors.request.use((request) => { + // flags print forms + if (apiClient.requestCountFlag) { + apiClient.requestCount += 1; + } + + EventBus.$emit("api-client-loading", request); + return request; +}); + +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) + EventBus.$emit("api-client-done", response); + + if (apiClient.requestCountFlag && apiClient.requestCount > 0) { + apiClient.requestCount -= 1; + } + 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); + } + 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")) { + apiClient.post("/debug", { + name: "Javascript ProcessMaker.apiClient Error", + message: JSON.stringify({ + message: error.message, + code: error.code, + config: error.config, + }), + }); + } + return Promise.reject(error); + } +}); + +/** + * 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) { + 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"); +} + +if (window.Processmaker && window.Processmaker.apiTimeout !== undefined) { + apiTimeout = window.Processmaker.apiTimeout; +} + +apiClient.defaults.timeout = apiTimeout; + +setGlobalPMVariable("apiClient", apiClient); + +// 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"); + } + } +}); diff --git a/resources/js/next/config/session.js b/resources/js/next/config/session.js new file mode 100644 index 0000000000..50aa68a264 --- /dev/null +++ b/resources/js/next/config/session.js @@ -0,0 +1,96 @@ +import { getGlobalPMVariable, setGlobalPMVariable, getGlobalVariable } from "../globalVariables"; + +const Vue = getGlobalVariable("Vue"); +const Echo = getGlobalVariable("Echo"); +const pushNotification = getGlobalVariable("pushNotification"); +const closeSessionModal = getGlobalVariable("closeSessionModal"); +const alert = getGlobalVariable("alert"); + +const timeoutScript = document.head.querySelector("meta[name=\"timeout-worker\"]")?.content; +const user = getGlobalPMVariable("user"); +const sessionModal = getGlobalPMVariable("sessionModal"); + +const isSameDevice = (e) => { + const localDeviceId = Vue.$cookies.get(e.device_variable); + const remoteDeviceId = e.device_id; + return localDeviceId && localDeviceId === remoteDeviceId; +}; + +if (user) { + // Session timeout + const AccountTimeoutLength = parseInt(eval(document.head.querySelector("meta[name=\"timeout-length\"]")?.content)); + const AccountTimeoutWarnSeconds = parseInt(document.head.querySelector("meta[name=\"timeout-warn-seconds\"]")?.content); + const AccountTimeoutEnabled = document.head.querySelector("meta[name=\"timeout-enabled\"]") ? parseInt(document.head.querySelector("meta[name=\"timeout-enabled\"]")?.content) : 1; + const AccountTimeoutWorker = new Worker(timeoutScript); + + AccountTimeoutWorker.addEventListener("message", (e) => { + if (e.data.method === "countdown") { + sessionModal( + "Session Warning", + "

Your user session is expiring. If your session expires, all of your unsaved data will be lost.

Would you like to stay connected?

", + e.data.data.time, + AccountTimeoutWarnSeconds, + ); + } + if (e.data.method === "timedOut") { + window.location = "/logout?timeout=true"; + } + }); + + // in some cases it's necessary to start manually + AccountTimeoutWorker.postMessage({ + method: "start", + data: { + timeout: AccountTimeoutLength, + warnSeconds: AccountTimeoutWarnSeconds, + enabled: AccountTimeoutEnabled, + }, + }); + + setGlobalPMVariable("AccountTimeoutWarnSeconds", AccountTimeoutLength); + setGlobalPMVariable("AccountTimeoutWarnSeconds", AccountTimeoutWarnSeconds); + setGlobalPMVariable("AccountTimeoutEnabled", AccountTimeoutEnabled); + setGlobalPMVariable("AccountTimeoutWorker", AccountTimeoutWorker); + + Echo.private(`ProcessMaker.Models.User.${user.id}`) + .notification((token) => { + pushNotification(token); + }) + .listen(".SessionStarted", (e) => { + const lifetime = parseInt(eval(e.lifetime)); + if (isSameDevice(e)) { + AccountTimeoutWorker.postMessage({ + method: "start", + data: { + timeout: lifetime, + warnSeconds: AccountTimeoutWarnSeconds, + enabled: AccountTimeoutEnabled, + }, + }); + if (closeSessionModal) { + closeSessionModal(); + } + } + }) + .listen(".Logout", (e) => { + if (isSameDevice(e) && window.location.pathname.indexOf("/logout") === -1) { + const localDeviceId = Vue.$cookies.get(e.device_variable); + const redirectLogoutinterval = setInterval(() => { + const newDeviceId = Vue.$cookies.get(e.device_variable); + if (localDeviceId !== newDeviceId) { + clearInterval(redirectLogoutinterval); + window.location.href = "/logout"; + } + }, 100); + } + }) + .listen(".SecurityLogDownloadJobCompleted", (e) => { + if (e.success) { + const { link } = e; + const { message } = e; + alert(message, "success", 0, false, false, link); + } else { + alert(e.message, "warning"); + } + }); +} diff --git a/resources/js/next/config/user.js b/resources/js/next/config/user.js new file mode 100644 index 0000000000..e3e36f835f --- /dev/null +++ b/resources/js/next/config/user.js @@ -0,0 +1,46 @@ +import { setGlobalPMVariable, getGlobalVariable } from "../globalVariables"; + +import datetime_format from "../../data/datetime_formats.json"; + +const moment = getGlobalVariable("moment"); +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\"]"); + +setGlobalPMVariable("app", appUrl ? { + url: appUrl.content, +} : null); + +if (userID) { + const 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) { + user.datetime_format = value.momentFormat; + user.calendar_format = value.calendarFormat; + } + }); + + if (user) { + moment.tz.setDefault(user.timezone); + moment.defaultFormat = user.datetime_format; + moment.defaultFormatUtc = user.datetime_format; + } + + if (document.documentElement.lang) { + moment.locale(document.documentElement.lang); + user.lang = document.documentElement.lang; + } + + setGlobalPMVariable("user", user); +} diff --git a/resources/js/next/globalVariables.js b/resources/js/next/globalVariables.js new file mode 100644 index 0000000000..281fd5cf7b --- /dev/null +++ b/resources/js/next/globalVariables.js @@ -0,0 +1,60 @@ +export default {}; + +export const setGlobalVariable = (key, value) => { + window[key] = value; +}; + +export const getGlobalVariable = (key) => window[key]; + +export const setGlobalPMVariable = (key, value) => { + if (!window.ProcessMaker) { + window.ProcessMaker = {}; + } + + window.ProcessMaker[key] = value; +}; + +export const setGlobalPMVariables = (variables) => { + if (!window.ProcessMaker) { + window.ProcessMaker = {}; + } + + Object.assign(window.ProcessMaker, variables); +}; + +export const getGlobalPMVariable = (key) => window.ProcessMaker[key]; + +export const addUses = (Vue, uses) => { + if (typeof uses === "object") { + Object.values(uses).forEach((use) => { + if (use) { + Vue.use(use); + } + }); + } +}; + +export const addMixin = (Vue, mixins) => { + if (typeof mixins === "object") { + Object.values(mixins).forEach((mixin) => { + if (mixin) { + Vue.mixin(mixin); + } + }); + } +}; + +export const loadModulesSequentially = async (modules) => { + const loadedModules = []; + + for (const modulePath of modules) { + try { + const module = await modulePath; + loadedModules.push(module); + } catch (error) { + console.error(`Error module: ${modulePath}`, error); + } + } + + return loadedModules; +}; diff --git a/resources/js/next/layout/navbar.js b/resources/js/next/layout/navbar.js new file mode 100644 index 0000000000..21edd620a7 --- /dev/null +++ b/resources/js/next/layout/navbar.js @@ -0,0 +1,259 @@ +import newRequestModal from "../../components/requests/requestModal.vue"; +import requestModal from "../../components/requests/modal.vue"; +import requestModalMobile from "../../components/requests/modalMobile.vue"; +import WelcomeModal from "../../Mobile/WelcomeModal.vue"; +import notifications from "../../notifications/components/notifications.vue"; +import sessionModalComponent from "../../components/Session.vue"; +import ConfirmationModal from "../../components/Confirm.vue"; +import MessageModal from "../../components/Message.vue"; +import NavbarProfile from "../../components/NavbarProfile.vue"; +import Menu from "../../components/Menu.vue"; +import { getGlobalVariable, getGlobalPMVariable, setGlobalPMVariable } from "../globalVariables"; + +const Vue = getGlobalVariable("Vue"); +const $ = getGlobalVariable("$"); +const events = getGlobalPMVariable("events"); + +Vue.component("LanguageSelectorButton", (resolve) => { + if (window.ProcessMaker.languageSelectorButtonComponent) { + resolve(window.ProcessMaker.languageSelectorButtonComponent); + } else { + window.ProcessMaker.languageSelectorButtonComponentResolve = resolve; + } +}); + +// Variables +const browser = navigator.userAgent; +const isMobileDevice = /Android|webOS|iPhone|iPad|iPod|BlackBerry|Windows Phone/i.test(browser); + +const mobileApp = !!isMobileDevice; +const isMobileNavbar = events.$cookies.get("isMobile"); // Verify is in mobile mode + +// Set our own specific alert function at the ProcessMaker global object that could +// potentially be overwritten by some custom theme support +const 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 +const 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; +}; + +const closeSessionModal = function () { + ProcessMaker.navbar.sessionShow = false; +}; + +// Set out own specific confirm modal. +const 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 own specific message modal. +const 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; +}; + +// Assign our navbar component to our global ProcessMaker object +const navbar = new Vue({ + el: "#navbar", + components: { + TopMenu: Menu, + requestModal, + notifications, + sessionModal: sessionModalComponent, + 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: "", + taskTitle: "", + isMobile: false, + isMobileDevice: mobileApp, + }; + }, + 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; // window.ProcessMaker.ScreenBuilder is not defined in the global scope + 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; + }, + }, +}); + +setGlobalPMVariable("mobileApp", mobileApp); +setGlobalPMVariable("alert", alert); +setGlobalPMVariable("sessionModal", sessionModal); +setGlobalPMVariable("closeSessionModal", closeSessionModal); +setGlobalPMVariable("confirmModal", confirmModal); +setGlobalPMVariable("messageModal", messageModal); +setGlobalPMVariable("navbar", navbar); +// Breadcrumbs are now part of the navbar component. Alias it here. +setGlobalPMVariable("breadcrumbs", navbar); + +// Assign our navbar component to our global ProcessMaker object +if (isMobileNavbar === "true") { + const 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; + }, + }, + }); + + setGlobalPMVariable("navbarMobile", navbarMobile); +} diff --git a/resources/js/next/layout/sidebar.js b/resources/js/next/layout/sidebar.js new file mode 100644 index 0000000000..882cb2693c --- /dev/null +++ b/resources/js/next/layout/sidebar.js @@ -0,0 +1,24 @@ +import { sanitizeUrl } from "@braintree/sanitize-url"; +// import VueHtml2Canvas from "vue-html2canvas"; +import Sidebaricon from "../../components/Sidebaricon.vue"; +import { getGlobalVariable } from "../globalVariables"; + +const Vue = getGlobalVariable("Vue"); + +// Vue.use(VueHtml2Canvas); +Vue.prototype.$sanitize = sanitizeUrl; + +new Vue({ + el: "#sidebar", + components: { + Sidebaricon, + }, + data() { + return { + expanded: false, + }; + }, + created() { + this.expanded === false; + }, +}); diff --git a/resources/js/next/libraries/bootstrap.js b/resources/js/next/libraries/bootstrap.js new file mode 100644 index 0000000000..c649b9221f --- /dev/null +++ b/resources/js/next/libraries/bootstrap.js @@ -0,0 +1,6 @@ +import * as bootstrap from "bootstrap"; +import { setGlobalVariable } from "../globalVariables"; + +import("bootstrap-vue/dist/bootstrap-vue.css"); + +setGlobalVariable("bootstrap", bootstrap); diff --git a/resources/js/next/libraries/broadcast.js b/resources/js/next/libraries/broadcast.js new file mode 100644 index 0000000000..0eabc0870b --- /dev/null +++ b/resources/js/next/libraries/broadcast.js @@ -0,0 +1,16 @@ +import Echo from "laravel-echo"; +import { setGlobalVariable, setGlobalPMVariable } from "../globalVariables"; + +// Verify if the broadcasting is enabled +if (window.Processmaker && window.Processmaker.broadcasting) { + const config = window.Processmaker.broadcasting; + + if (config.broadcaster === "pusher") { + const Pusher = require("pusher-js"); + Pusher.logToConsole = config.debug; + + setGlobalVariable("Pusher", Pusher); + } + + window.Echo = new Echo(config); +} diff --git a/resources/js/next/libraries/jquery.js b/resources/js/next/libraries/jquery.js new file mode 100644 index 0000000000..40bc81e825 --- /dev/null +++ b/resources/js/next/libraries/jquery.js @@ -0,0 +1,6 @@ +import * as jQuery from "jquery"; + +import { setGlobalVariable } from "../globalVariables"; + +setGlobalVariable("jQuery", jQuery); +setGlobalVariable("$", jQuery); diff --git a/resources/js/next/libraries/lodash.js b/resources/js/next/libraries/lodash.js new file mode 100644 index 0000000000..9ccc9f453e --- /dev/null +++ b/resources/js/next/libraries/lodash.js @@ -0,0 +1,3 @@ +import lodash from "lodash"; + +window._ = lodash; diff --git a/resources/js/next/libraries/popper.js b/resources/js/next/libraries/popper.js new file mode 100644 index 0000000000..519a53b597 --- /dev/null +++ b/resources/js/next/libraries/popper.js @@ -0,0 +1 @@ +window.Popper = require("popper.js").default; diff --git a/resources/js/next/libraries/sharedComponents.js b/resources/js/next/libraries/sharedComponents.js new file mode 100644 index 0000000000..81dc518e1b --- /dev/null +++ b/resources/js/next/libraries/sharedComponents.js @@ -0,0 +1,8 @@ +import { setGlobalVariable } from "../globalVariables"; +import * as SharedComponents from "../../components/shared"; + +setGlobalVariable("SharedComponents", SharedComponents); + +// import("../../components/shared").then((SharedComponents) => { +// setGlobalVariable("SharedComponents", SharedComponents); +// }); diff --git a/resources/js/next/libraries/vueCookies.js b/resources/js/next/libraries/vueCookies.js new file mode 100644 index 0000000000..a2d69e9509 --- /dev/null +++ b/resources/js/next/libraries/vueCookies.js @@ -0,0 +1,6 @@ +import VueCookies from "vue-cookies"; +import { getGlobalVariable } from "../globalVariables"; + +const Vue = getGlobalVariable("Vue"); + +Vue.use(VueCookies); diff --git a/resources/js/next/libraries/vueDeepset.js b/resources/js/next/libraries/vueDeepset.js new file mode 100644 index 0000000000..931acd4b9e --- /dev/null +++ b/resources/js/next/libraries/vueDeepset.js @@ -0,0 +1,3 @@ +import * as VueDeepSet from "vue-deepset"; + +window.Vue.use(VueDeepSet); diff --git a/resources/js/next/libraries/vueFormElements.js b/resources/js/next/libraries/vueFormElements.js new file mode 100644 index 0000000000..b09aa5e03a --- /dev/null +++ b/resources/js/next/libraries/vueFormElements.js @@ -0,0 +1,8 @@ +import VueFormElements from "@processmaker/vue-form-elements"; +import { setGlobalVariable, getGlobalVariable } from "../globalVariables"; + +const Vue = getGlobalVariable("Vue"); + +Vue.use(VueFormElements); + +setGlobalVariable("VueFormElements", VueFormElements); diff --git a/resources/js/next/libraries/vueRouter.js b/resources/js/next/libraries/vueRouter.js new file mode 100644 index 0000000000..9e7b8e0da6 --- /dev/null +++ b/resources/js/next/libraries/vueRouter.js @@ -0,0 +1,12 @@ +import Router from "vue-router"; + +import { setGlobalVariable, getGlobalVariable, setGlobalPMVariable } from "../globalVariables"; + +const Vue = getGlobalVariable("Vue"); + +setGlobalVariable("VueRouter", Router); +setGlobalPMVariable("Router", new Router({ + mode: "history", +})); + +Vue.use(Router); diff --git a/resources/js/next/libraries/vueTable.js b/resources/js/next/libraries/vueTable.js new file mode 100644 index 0000000000..a88c2fb50b --- /dev/null +++ b/resources/js/next/libraries/vueTable.js @@ -0,0 +1,3 @@ +import { install as VuetableInstall } from "vuetable-2"; + +VuetableInstall(window.Vue); diff --git a/resources/js/next/libraries/vuex.js b/resources/js/next/libraries/vuex.js new file mode 100644 index 0000000000..07d2942bc8 --- /dev/null +++ b/resources/js/next/libraries/vuex.js @@ -0,0 +1,8 @@ +import Vuex from "vuex"; +import GlobalStore from "../../globalStore"; +import { getGlobalVariable } from "../globalVariables"; + +const Vue = getGlobalVariable("Vue"); + +Vue.use(Vuex); +Vue.use(GlobalStore); diff --git a/resources/js/next/modeler.js b/resources/js/next/modeler.js new file mode 100644 index 0000000000..4a8f69bac5 --- /dev/null +++ b/resources/js/next/modeler.js @@ -0,0 +1,6 @@ +window.Modeler = require("@processmaker/modeler"); + +window.ProcessMaker.nodeTypes = []; +window.ProcessMaker.nodeTypes.get = function (id) { + return this.find((node) => node.id === id); +}; diff --git a/resources/js/next/monaco.js b/resources/js/next/monaco.js new file mode 100644 index 0000000000..0c2846644d --- /dev/null +++ b/resources/js/next/monaco.js @@ -0,0 +1,8 @@ +import MonacoEditor from "vue-monaco"; +import { getGlobalVariable, setGlobalVariable } from "./globalVariables"; + +const Vue = getGlobalVariable("Vue"); + +Vue.component("MonacoEditor", MonacoEditor); +setGlobalVariable("VueMonaco", MonacoEditor); +setGlobalVariable("monaco", MonacoEditor); diff --git a/resources/js/next/screenBuilder.js b/resources/js/next/screenBuilder.js new file mode 100644 index 0000000000..c6ec573fdf --- /dev/null +++ b/resources/js/next/screenBuilder.js @@ -0,0 +1,51 @@ +import { + getGlobalVariable, setGlobalVariable, getGlobalPMVariable, setGlobalPMVariable, +} from "./globalVariables"; + +const Vue = getGlobalVariable("Vue"); + +const addScriptsToDOM = async function (scripts) { + for (const script of scripts) { + await new Promise((resolve, reject) => { + const scriptElement = document.createElement("script"); + scriptElement.src = script; + scriptElement.async = false; + scriptElement.onload = resolve; + scriptElement.onerror = reject; + document.head.appendChild(scriptElement); + }); + } +}; + +const componentsScreenBuilder = ["VueFormRenderer", "Task"]; + +componentsScreenBuilder.forEach((component) => { + Vue.component(component, (resolve, reject) => { + import("@processmaker/screen-builder/dist/vue-form-builder.css"); + if (screenBuilderScripts) { + addScriptsToDOM(screenBuilderScripts).then(() => { + import("@processmaker/screen-builder").then((ScreenBuilder) => { + const apiClient = getGlobalPMVariable("apiClient"); + Vue.use(ScreenBuilder.default); + + const { initializeScreenCache } = ScreenBuilder; + // 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 screen = { + cacheEnabled: screenCacheEnabled === "true", + cacheTimeout: Number(screenCacheTimeout), + }; + + setGlobalVariable("ScreenBuilder", ScreenBuilder); + setGlobalPMVariable("screen", screen); + // Initialize screen-builder cache + initializeScreenCache(apiClient, screen);// TODO: Its a bad practice to use the apiClient here + + resolve(ScreenBuilder[component]); + }).catch(reject); + }); + } + }); +}); diff --git a/resources/js/processes/screen-builder/typeForm.js b/resources/js/processes/screen-builder/typeForm.js index 5df0c692ae..036ecbff46 100644 --- a/resources/js/processes/screen-builder/typeForm.js +++ b/resources/js/processes/screen-builder/typeForm.js @@ -1,11 +1,5 @@ -import Vue from "vue"; -import VueFormElements from "@processmaker/vue-form-elements"; -import { FormBuilderControls as initialControls, globalProperties } from "@processmaker/screen-builder"; -import { Multiselect } from "@processmaker/vue-multiselect"; - -Vue.use(VueFormElements); -Vue.component("Multiselect", Multiselect); - +const { FormBuilderControls, globalProperties } = window.ScreenBuilder; +const initialControls = FormBuilderControls; // The submit button has by default the 'submit' value const submitButton = initialControls.find((x) => x.control.label === "Submit"); if (submitButton) { diff --git a/resources/js/tasks/components/NavbarTaskMobile.vue b/resources/js/tasks/components/NavbarTaskMobile.vue index 063c89b604..f510b36f10 100644 --- a/resources/js/tasks/components/NavbarTaskMobile.vue +++ b/resources/js/tasks/components/NavbarTaskMobile.vue @@ -74,9 +74,10 @@ diff --git a/resources/js/tasks/components/TasksList.vue b/resources/js/tasks/components/TasksList.vue index 5f8e47d8f4..9d1fc24291 100644 --- a/resources/js/tasks/components/TasksList.vue +++ b/resources/js/tasks/components/TasksList.vue @@ -23,11 +23,11 @@