From b4938316c4a76202553380a297d638b5d72c80a2 Mon Sep 17 00:00:00 2001 From: Henry Jonas Date: Mon, 20 Jul 2026 14:53:17 -0400 Subject: [PATCH] VITE INBOX: POC to test the ease of migrating other views to vite --- .../Http/Controllers/TaskController.php | 8 +- package.json | 1 + resources/js/Mobile/WelcomeModal.vue | 6 +- .../PMColumnFilterPopover/PMColumnFilterOp.js | 24 +- .../PMColumnFilterOpBetweenDatepicker.vue | 2 +- .../PMColumnFilterOpInDatepicker.vue | 2 +- .../PMColumnFilterPopover.vue | 4 +- .../PMColumnFilterToggleAscDesc.vue | 4 +- resources/js/components/TimelineItem.vue | 2 +- .../common/mixins/apiDataLoading.js | 2 +- .../js/components/common/mixins/datatable.js | 4 +- .../js/components/requests/requestModal.vue | 3 +- .../components/shared/AddToProjectModal.vue | 6 +- .../js/components/shared/ColumnChooser.vue | 6 +- .../js/components/shared/ColumnConfig.vue | 4 +- .../components/shared/DownloadSvgButton.vue | 3 +- .../js/components/shared/IconSelector.vue | 2 +- resources/js/components/shared/Icons.js | 2 +- resources/js/components/shared/Modal.vue | 7 +- resources/js/components/shared/PTabs.vue | 4 +- .../js/components/shared/PaginationTable.vue | 12 +- resources/js/components/shared/SidebarNav.vue | 2 +- resources/js/components/shared/index.js | 58 +-- .../templates/TemplateExistsModal.vue | 3 +- resources/js/next/components/index.js | 5 +- .../components/notification-card.vue | 4 +- .../components/notification-item.vue | 4 +- .../components/notification-message.vue | 4 +- .../components/notification-user.vue | 2 +- .../components/notifications.vue | 20 +- .../components/charts/ListChart.vue | 2 +- .../components/utils/index.js | 2 +- .../export/components/ExportSuccessModal.vue | 2 +- .../export/components/MainAssetView.vue | 2 +- .../export/components/SetPasswordModal.vue | 2 +- .../import/components/EnterPasswordModal.vue | 2 +- .../import/components/ImportProcessModal.vue | 4 +- .../components/CreateScreenTemplateForm.vue | 5 +- .../requests/components/RequestsListing.vue | 2 +- .../js/tasks/components/TaskDetailsMobile.vue | 3 +- resources/js/tasks/components/TaskLoading.vue | 6 +- resources/js/tasks/components/TasksList.vue | 3 +- .../js/tasks/components/TasksPreview.vue | 6 +- resources/js/tasks/router.js | 2 +- resources/js/vite/tasks/loaderTasks.js | 34 ++ resources/js/vite/tasks/tasks.js | 207 ++++++++ resources/views/layouts/layoutnext.blade.php | 4 +- resources/views/vite/tasks/index.blade.php | 462 ++++++++++++++++++ vite.config.js | 39 +- webpack.mix.js | 8 + 50 files changed, 895 insertions(+), 112 deletions(-) create mode 100644 resources/js/vite/tasks/loaderTasks.js create mode 100644 resources/js/vite/tasks/tasks.js create mode 100644 resources/views/vite/tasks/index.blade.php diff --git a/ProcessMaker/Http/Controllers/TaskController.php b/ProcessMaker/Http/Controllers/TaskController.php index 213d9374ab..b62ea5d75a 100755 --- a/ProcessMaker/Http/Controllers/TaskController.php +++ b/ProcessMaker/Http/Controllers/TaskController.php @@ -83,7 +83,13 @@ public function index() $metricsApiEndpoint = EnvironmentVariable::getMetricsApiEndpoint(); } - return view('tasks.index', compact( + $viteView = json_decode(config('app.vite_view') ?: '{}', true); + $tasksView = 'tasks.index'; + if (!empty($viteView['tasks.index'])) { + $tasksView = $viteView['tasks.index']; + } + + return view($tasksView, compact( // User and Authentication related 'currentUser', 'userFilter', diff --git a/package.json b/package.json index 629b565e6f..7a453986b5 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "devDependencies": { "@babel/eslint-parser": "^7.15.8", "@babel/preset-env": "^7.23.9", + "@rollup/plugin-yaml": "^5.0.0", "@vitejs/plugin-vue2": "^2.3.4", "accounting": "^0.4.1", "autoprefixer": "^10.4.20", diff --git a/resources/js/Mobile/WelcomeModal.vue b/resources/js/Mobile/WelcomeModal.vue index ba52aa307b..47ef09d5c3 100644 --- a/resources/js/Mobile/WelcomeModal.vue +++ b/resources/js/Mobile/WelcomeModal.vue @@ -20,7 +20,7 @@

recent projects

@@ -45,7 +45,9 @@ export default { props: ["username"], data() { - return {}; + return { + welcomeMobileImage: '/img/welcome_mobile.svg', + }; }, methods: { hideModal() { diff --git a/resources/js/components/PMColumnFilterPopover/PMColumnFilterOp.js b/resources/js/components/PMColumnFilterPopover/PMColumnFilterOp.js index 6f804bd9f0..30f74b5fdd 100644 --- a/resources/js/components/PMColumnFilterPopover/PMColumnFilterOp.js +++ b/resources/js/components/PMColumnFilterPopover/PMColumnFilterOp.js @@ -1,15 +1,15 @@ -import PMColumnFilterIconMinus from "./PMColumnFilterIconMinus"; -import PMColumnFilterIconPlus from "./PMColumnFilterIconPlus"; -import PMColumnFilterOpBetween from "./PMColumnFilterOpBetween"; -import PMColumnFilterOpIn from "./PMColumnFilterOpIn"; -import PMColumnFilterOpBetweenDatepicker from "./PMColumnFilterOpBetweenDatepicker"; -import PMColumnFilterOpInDatepicker from "./PMColumnFilterOpInDatepicker"; -import PMColumnFilterOpDatetime from "./PMColumnFilterOpDatetime"; -import PMColumnFilterOpInput from "./PMColumnFilterOpInput"; -import PMColumnFilterOpSelect from "./PMColumnFilterOpSelect"; -import PMColumnFilterOpSelectMultiple from "./PMColumnFilterOpSelectMultiple"; -import PMColumnFilterOpBoolean from "./PMColumnFilterOpBoolean"; -import PMColumnFilterToggleAscDesc from "./PMColumnFilterToggleAscDesc"; +import PMColumnFilterIconMinus from "./PMColumnFilterIconMinus.vue"; +import PMColumnFilterIconPlus from "./PMColumnFilterIconPlus.vue"; +import PMColumnFilterOpBetween from "./PMColumnFilterOpBetween.vue"; +import PMColumnFilterOpIn from "./PMColumnFilterOpIn.vue"; +import PMColumnFilterOpBetweenDatepicker from "./PMColumnFilterOpBetweenDatepicker.vue"; +import PMColumnFilterOpInDatepicker from "./PMColumnFilterOpInDatepicker.vue"; +import PMColumnFilterOpDatetime from "./PMColumnFilterOpDatetime.vue"; +import PMColumnFilterOpInput from "./PMColumnFilterOpInput.vue"; +import PMColumnFilterOpSelect from "./PMColumnFilterOpSelect.vue"; +import PMColumnFilterOpSelectMultiple from "./PMColumnFilterOpSelectMultiple.vue"; +import PMColumnFilterOpBoolean from "./PMColumnFilterOpBoolean.vue"; +import PMColumnFilterToggleAscDesc from "./PMColumnFilterToggleAscDesc.vue"; export { PMColumnFilterIconMinus, diff --git a/resources/js/components/PMColumnFilterPopover/PMColumnFilterOpBetweenDatepicker.vue b/resources/js/components/PMColumnFilterPopover/PMColumnFilterOpBetweenDatepicker.vue index e12c4ed61a..908c12aa24 100644 --- a/resources/js/components/PMColumnFilterPopover/PMColumnFilterOpBetweenDatepicker.vue +++ b/resources/js/components/PMColumnFilterPopover/PMColumnFilterOpBetweenDatepicker.vue @@ -8,7 +8,7 @@ diff --git a/resources/js/tasks/components/TasksList.vue b/resources/js/tasks/components/TasksList.vue index aae5f56de2..0f1e6e2a5a 100644 --- a/resources/js/tasks/components/TasksList.vue +++ b/resources/js/tasks/components/TasksList.vue @@ -32,7 +32,7 @@ > priority-header @@ -136,7 +136,7 @@ @click="showQuickFillPreview = true" > @@ -286,6 +286,8 @@ export default { props: ["tooltipButton", "propPreview"], data() { return { + eraserImage: '/img/smartinbox-images/eraser.svg', + fillImage: '/img/smartinbox-images/fill.svg', }; }, watch: { diff --git a/resources/js/tasks/router.js b/resources/js/tasks/router.js index 1444aba313..064197e5ea 100644 --- a/resources/js/tasks/router.js +++ b/resources/js/tasks/router.js @@ -1,7 +1,7 @@ import Vue from "vue"; import VueRouter from "vue-router"; import DashboardViewer from "./components/DashboardViewer.vue"; -import Process from "../processes-catalogue/components/Process"; +import Process from "../processes-catalogue/components/Process.vue"; Vue.use(VueRouter); const screen = JSON.parse(sessionStorage.getItem('dashboard_screen')); diff --git a/resources/js/vite/tasks/loaderTasks.js b/resources/js/vite/tasks/loaderTasks.js new file mode 100644 index 0000000000..20787250f8 --- /dev/null +++ b/resources/js/vite/tasks/loaderTasks.js @@ -0,0 +1,34 @@ +import { setupMain } from "../../../js/next/setupMain"; +import vueFormElements from "../../../js/next/libraries/vueFormElements"; +import modelerInspector from "../../../js/next/libraries/modelerInspector"; +import modeler from "../../../js/next/modeler"; +import screenBuilderNext from "../../../js/next/screenBuilder"; +// Load screen-builder for PMBLOCKs +import * as ScreenBuilder from "@processmaker/screen-builder"; + +window.ScreenBuilder = ScreenBuilder; + +setupMain(); +screenBuilderNext(); +modeler(); + +window.ProcessMaker.taskDraftsEnabled = window.temporal.taskDraftsEnabled; +window.ProcessMaker.advanced_filter = window.temporal.advanced_filter; +window.ProcessMaker.defaultColumns = window.temporal.defaultColumns; +window.ProcessMaker.isDefaultColumns = window.temporal.isDefaultColumns; +window.ProcessMaker.userConfiguration = window.temporal.userConfiguration; +window.ProcessMaker.showOldTaskScreen = window.temporal.showOldTaskScreen; +window.ProcessMaker.user = window.temporal.user; +window.ProcessMaker.selectedProcess = window.temporal.selectedProcess; +window.ProcessMaker.defaultSavedSearchId = window.temporal.defaultSavedSearchId; +window.ProcessMaker.isTceCustomization = window.temporal.isTceCustomization; +window.ProcessMaker.metricsApiEndpoint = window.temporal.metricsApiEndpoint; +window.ProcessMaker.ellipsisPermission = window.temporal.ellipsisPermission; +window.ProcessMaker.packages = window.temporal.packages; + +// Legacy Mix keys (lowercase "m") used by TasksMixin / ParticipantHomeScreen +window.Processmaker = window.Processmaker || {}; +window.Processmaker.defaultColumns = window.temporal.defaultColumns; +window.Processmaker.user = window.temporal.user; +window.Processmaker.selectedProcess = window.temporal.selectedProcess; +window.Processmaker.defaultSavedSearchId = window.temporal.defaultSavedSearchId; diff --git a/resources/js/vite/tasks/tasks.js b/resources/js/vite/tasks/tasks.js new file mode 100644 index 0000000000..f49c98ef1e --- /dev/null +++ b/resources/js/vite/tasks/tasks.js @@ -0,0 +1,207 @@ +import Vue from "vue"; +import TasksList from "../../tasks/components/TasksList.vue"; +import TasksListCounter from "../../tasks/components/TasksListCounter.vue"; +import setDefaultAdvancedFilterStatus from "../../common/setDefaultAdvancedFilterStatus"; +import ParticipantHomeScreen from "../../tasks/components/ParticipantHomeScreen.vue"; +import PmqlInput from "../../components/shared/PmqlInput.vue"; + +Vue.component("TasksList", TasksList); +Vue.component("participant-home-screen", ParticipantHomeScreen); + +// Component used in the tasks list +Vue.component("PmqlInput", PmqlInput); + +const main = new Vue({ + el: "#tasks", + components: { + TasksList, + TasksListCounter, + }, + data: { + showOldTaskScreen: window.ProcessMaker.showOldTaskScreen, + userConfiguration: window.ProcessMaker.userConfiguration, + urlConfiguration: "users/configuration", + showMenu: true, + // Legacy Mix spelling used by TasksMixin / ParticipantHomeScreen + columns: window.Processmaker?.defaultColumns || window.ProcessMaker?.defaultColumns || null, + filter: "", + pmql: "", + urlPmql: "", + filtersPmql: "", + fullPmql: "", + status: [], + inOverdueMessage: "", + additions: [], + priorityField: "is_priority", + draftField: "draft", + isDataLoading: false, + inbox: true, + priority: false, + draft: false, + tab: "inbox", + inboxCount: null, + draftCount: null, + priorityCount: null, + priorityFilter: [ + { + subject: { + type: "Field", + value: "is_priority", + }, + operator: "=", + value: true, + _column_field: "is_priority", + _column_label: "Priority", + _hide_badge: true, + }, + ], + draftFilter: [ + { + subject: { + type: "Relationship", + value: "draft.id", + }, + operator: ">", + value: 0, + _column_field: "draft", + _column_label: "Draft", + _hide_badge: true, + }, + ], + taskDraftsEnabled: window.ProcessMaker.taskDraftsEnabled, + }, + created() { + const params = new URL(document.location).searchParams; + const statusParam = params.get("status"); + this.urlPmql = params.get("pmql"); + + let status = ""; + + switch (statusParam) { + case "CLOSED": + status = "Completed"; + break; + case "SELF_SERVICE": + status = "Self Service"; + break; + default: + status = "In Progress"; + break; + } + setDefaultAdvancedFilterStatus(status); + + if (this.urlPmql && this.urlPmql !== "") { + this.onSearch(); + } + }, + mounted() { + window.ProcessMaker.EventBus.$on("advanced-search-addition", (component) => { + this.additions.push(component); + }); + + // Blade uses :fetch-on-created="false"; list only loads when we fetch here. + if (!window.location.search.includes("filter_user_recommendation")) { + this.$nextTick(() => { + if (this.$refs.taskList) { + this.$refs.taskList.fetch(); + } + }); + } + }, + methods: { + switchTab(tab) { + this.tab = tab; + const taskListComponent = this.$refs.taskList; + taskListComponent.advancedFilter[this.priorityField] = []; + taskListComponent.advancedFilter[this.draftField] = []; + switch (tab) { + case "priority": + taskListComponent.advancedFilter.is_priority = this.priorityFilter; + break; + case "draft": + taskListComponent.advancedFilter.draft = this.draftFilter; + break; + } + taskListComponent.markStyleWhenColumnSetAFilter(); + taskListComponent.storeFilterConfiguration(); + taskListComponent.fetch(true); + }, + dataLoading(value) { + this.isDataLoading = value; + }, + onFetchTask() { + this.inbox = true; + this.priority = this.draft = false; + let filters = window.ProcessMaker.advanced_filter?.filters; + if (!Array.isArray(filters)) { + filters = []; + } + filters.forEach((item) => { + if (item._column_field === "is_priority") { + this.priority = true; + this.inbox = this.draft = false; + } + if (item._column_field === "draft") { + this.draft = true; + this.inbox = this.priority = false; + } + }); + }, + handleTabCount(value) { + if (this.tab === "inbox") { + this.inboxCount = value; + } + if (this.tab === "draft") { + this.draftCount = value; + } + if (this.tab === "priority") { + this.priorityCount = value; + } + }, + onFiltersPmqlChange(value) { + this.filtersPmql = value[0]; + this.fullPmql = this.getFullPmql(); + }, + onNLQConversion(query) { + this.onChange(query); + this.onSearch(); + }, + onChange(query) { + this.pmql = query; + this.fullPmql = this.getFullPmql(); + }, + onSearch() { + if (this.$refs.taskList) { + this.$refs.taskList.fetch(true); + } + }, + onInboxRules() { + window.location.href = "/tasks/rules"; + }, + setInOverdueMessage(inOverdue) { + let inOverdueMessage = ""; + if (inOverdue) { + const taskText = (inOverdue > 1) ? this.$t("Tasks").toLowerCase() : this.$t("Task").toLowerCase(); + inOverdueMessage = this.$t("You have {{ inOverDue }} overdue {{ taskText }} pending", { inOverDue: inOverdue, taskText }); + } + this.inOverdueMessage = inOverdueMessage; + }, + getFullPmql() { + let fullPmqlString = ""; + + if (this.filtersPmql && this.filtersPmql !== "") { + fullPmqlString = this.filtersPmql; + } + + if (fullPmqlString !== "" && this.pmql && this.pmql !== "") { + fullPmqlString = `${fullPmqlString} AND ${this.pmql}`; + } + + if (fullPmqlString === "" && this.pmql && this.pmql !== "") { + fullPmqlString = this.pmql; + } + + return fullPmqlString; + }, + }, +}); diff --git a/resources/views/layouts/layoutnext.blade.php b/resources/views/layouts/layoutnext.blade.php index a78ca97aee..5516ab138b 100644 --- a/resources/views/layouts/layoutnext.blade.php +++ b/resources/views/layouts/layoutnext.blade.php @@ -135,7 +135,9 @@ class="main flex-grow-1 h-100 @yield('js') @stack('scripts') - +@foreach(GlobalScripts::getScripts() as $script) + +@endforeach @isset($addons) @foreach ($addons as $addon) @if (!empty($addon['script_mix'])) diff --git a/resources/views/vite/tasks/index.blade.php b/resources/views/vite/tasks/index.blade.php new file mode 100644 index 0000000000..b60ae83dee --- /dev/null +++ b/resources/views/vite/tasks/index.blade.php @@ -0,0 +1,462 @@ +@extends('layouts.layoutnext', ['content_margin' => '', 'overflow-auto' => '']) + +@section('title') + {{ __($title) }} +@endsection + +@section('sidebar') + @include('layouts.sidebar', ['sidebar' => Menu::get('sidebar_task')]) +@endsection + +@section('breadcrumbs') + @include('shared.breadcrumbs', [ + 'routes' => [ + Route::currentRouteName() === 'inbox' ? __('Inbox') : __('Tasks') => Route::currentRouteName() === 'inbox' ? route('inbox') : route('tasks.index') + ], + ]) +@endsection + +@section('content') +
+
+
+
+
+ + @{{ inOverdueMessage }} + +
+
+ +
+
+ + +
+
+
+ + +
+
+
+
+
+
+
+ +
+@endsection + +@section('js') + {{-- temporal must exist before loaderTasks (Vite modules run after deferred classic scripts) --}} + + @vite(['resources/js/vite/tasks/loaderTasks.js']) + @foreach($manager->getScripts() as $script) + + @endforeach + @vite(['resources/js/vite/tasks/tasks.js']) +@endsection + +@section('css') + + + +@endsection diff --git a/vite.config.js b/vite.config.js index 2cf030a65b..94215f730d 100644 --- a/vite.config.js +++ b/vite.config.js @@ -2,6 +2,27 @@ import { defineConfig } from 'vite'; import laravel from 'laravel-vite-plugin'; import vue from '@vitejs/plugin-vue2'; import path from 'path'; +import { pathToFileURL } from 'url'; +import yaml from '@rollup/plugin-yaml'; + +const stylesPath = path.resolve(__dirname, 'resources/sass'); + +/** + * Resolve Webpack-style `~styles/...` imports used in Vue SFC