diff --git a/ProcessMaker/Http/Controllers/Api/UserConfigurationController.php b/ProcessMaker/Http/Controllers/Api/UserConfigurationController.php index 64ea095fd1..d325c1636b 100644 --- a/ProcessMaker/Http/Controllers/Api/UserConfigurationController.php +++ b/ProcessMaker/Http/Controllers/Api/UserConfigurationController.php @@ -25,6 +25,9 @@ class UserConfigurationController extends Controller 'tasks' => [ 'isMenuCollapse' => true, ], + 'tasks_inbox' => [ + 'isMenuCollapse' => false, + ], ]; public function index() @@ -54,6 +57,7 @@ public function store(Request $request) 'ui_configuration.cases' => 'required|array', 'ui_configuration.requests' => 'required|array', 'ui_configuration.tasks' => 'required|array', + 'ui_configuration.tasks_inbox' => 'required|array', ]); $uiConfiguration = json_encode($request->input('ui_configuration')); diff --git a/ProcessMaker/Http/Controllers/HomeController.php b/ProcessMaker/Http/Controllers/HomeController.php index df81ab10f9..40ca2e9eed 100644 --- a/ProcessMaker/Http/Controllers/HomeController.php +++ b/ProcessMaker/Http/Controllers/HomeController.php @@ -14,14 +14,22 @@ public function index(Request $request) if (Auth::check()) { // Redirect to home dynamic only if the package was enable if (hasPackage('package-dynamic-ui')) { - $user = \Auth::user(); - $homePage = \ProcessMaker\Package\PackageDynamicUI\Models\DynamicUI::getHomePage($user); + $user = Auth::user(); - return redirect($homePage); + // Check if there is at least one custom dashboard per user + $customDashboardExists = \ProcessMaker\Package\PackageDynamicUI\Models\DynamicUI::where('type', 'DASHBOARD') + ->where('assignable_id', $user->id) + ->count() > 0; + + if ($customDashboardExists) { + $homePage = \ProcessMaker\Package\PackageDynamicUI\Models\DynamicUI::getHomePage($user); + + return redirect($homePage); + } } // Redirect to the default view - return redirect('/requests'); + return redirect('/inbox'); } } diff --git a/ProcessMaker/Http/Controllers/TaskController.php b/ProcessMaker/Http/Controllers/TaskController.php index fa4c945454..415bece910 100755 --- a/ProcessMaker/Http/Controllers/TaskController.php +++ b/ProcessMaker/Http/Controllers/TaskController.php @@ -44,6 +44,8 @@ public function index() { $title = 'To Do Tasks'; + $showOldTaskScreen = Request::path() !== 'inbox'; + if (Request::input('status') == 'CLOSED') { $title = 'Completed Tasks'; } @@ -58,7 +60,9 @@ public function index() $taskDraftsEnabled = TaskDraft::draftsEnabled(); - return view('tasks.index', compact('title', 'userFilter', 'defaultColumns', 'taskDraftsEnabled')); + $userConfiguration = (new UserConfigurationController())->index()['ui_configuration'] ?? []; + + return view('tasks.index', compact('title', 'userFilter', 'defaultColumns', 'taskDraftsEnabled', 'userConfiguration', 'showOldTaskScreen')); } public function edit(ProcessRequestToken $task, string $preview = '') diff --git a/ProcessMaker/Traits/TaskControllerIndexMethods.php b/ProcessMaker/Traits/TaskControllerIndexMethods.php index 078843c950..d0a351478f 100644 --- a/ProcessMaker/Traits/TaskControllerIndexMethods.php +++ b/ProcessMaker/Traits/TaskControllerIndexMethods.php @@ -23,7 +23,20 @@ private function indexBaseQuery($request) // Determine if the data should be included $includeData = in_array('data', $includes); - $query = ProcessRequestToken::exclude(['data'])->with([ + $query = ProcessRequestToken::exclude(['data']); + + // If all_inbox is true and user has process requests, filter to only show the latest process + if ($request->has('all_inbox') && $request->input('all_inbox') === 'false') { + $latestProcessRequest = ProcessRequestToken::where('user_id', auth()->id()) + ->orderBy('created_at', 'desc') + ->first(); + + if ($latestProcessRequest) { + $query->where('process_id', $latestProcessRequest->process_id); + } + } + + $query = $query->with([ 'processRequest' => function ($q) use ($includeData) { if (!$includeData) { return $q->exclude(['data']); diff --git a/devhub/pm-font/svg/tachometer-alt-average.svg b/devhub/pm-font/svg/tachometer-alt-average.svg new file mode 100644 index 0000000000..768e8904a4 --- /dev/null +++ b/devhub/pm-font/svg/tachometer-alt-average.svg @@ -0,0 +1,3 @@ + + + diff --git a/resources/fonts/pm-font/index.html b/resources/fonts/pm-font/index.html index 281a2aa4c4..c91af38eb9 100644 --- a/resources/fonts/pm-font/index.html +++ b/resources/fonts/pm-font/index.html @@ -103,7 +103,7 @@
-

ProcessMaker Icons4.12.0+alpha-3

+

ProcessMaker Icons4.12.0

Icons generated with svgtofont. For add new icons, please check the README file
@@ -115,7 +115,7 @@

ProcessMaker Icons4.12.0+alpha-3

-

ProcessMaker Icons4.12.0+alpha-3

+

ProcessMaker Icons4.12.0

Icons generated with svgtofont. For add new icons, please check the README file
@@ -387,6 +387,13 @@

fp-slideshow

fp-table

+
  • + +

    fp-tachometer-alt-average

    +
  • +
  • -

    ProcessMaker Icons4.12.0+alpha-3

    +

    ProcessMaker Icons4.12.0

    Icons generated with svgtofont. For add new icons, please check the README file
    @@ -134,7 +134,7 @@

    ProcessMaker Icons4.12.0+alpha-3

      -
    • bpmn-action-by-email

      
    • bpmn-data-connector

      
    • bpmn-data-object

      
    • bpmn-data-store

      
    • bpmn-docusign

      
    • bpmn-end-event

      
    • bpmn-flowgenie

      
    • bpmn-gateway

      
    • bpmn-generic-gateway

      
    • bpmn-idp

      
    • bpmn-intermediate-event

      
    • bpmn-pool

      
    • bpmn-send-email

      
    • bpmn-start-event

      
    • bpmn-task

      
    • bpmn-text-annotation

      
    • brush-icon

      
    • close

      
    • cloud-download-outline

      
    • copy

      
    • desktop

      
    • eye

      
    • fields-icon

      
    • flowgenie-outline

      
    • folder-outline

      
    • fullscreen

      
    • github

      
    • layout-icon

      
    • map

      
    • mobile

      
    • pdf

      
    • play-outline

      
    • plus

      
    • screen-outline

      
    • script-outline

      
    • slack-notification

      
    • slack

      
    • slideshow

      
    • table

      
    • trash

      
    • unlink

      
    • +
    • bpmn-action-by-email

      
    • bpmn-data-connector

      
    • bpmn-data-object

      
    • bpmn-data-store

      
    • bpmn-docusign

      
    • bpmn-end-event

      
    • bpmn-flowgenie

      
    • bpmn-gateway

      
    • bpmn-generic-gateway

      
    • bpmn-idp

      
    • bpmn-intermediate-event

      
    • bpmn-pool

      
    • bpmn-send-email

      
    • bpmn-start-event

      
    • bpmn-task

      
    • bpmn-text-annotation

      
    • brush-icon

      
    • close

      
    • cloud-download-outline

      
    • copy

      
    • desktop

      
    • eye

      
    • fields-icon

      
    • flowgenie-outline

      
    • folder-outline

      
    • fullscreen

      
    • github

      
    • layout-icon

      
    • map

      
    • mobile

      
    • pdf

      
    • play-outline

      
    • plus

      
    • screen-outline

      
    • script-outline

      
    • slack-notification

      
    • slack

      
    • slideshow

      
    • table

      
    • tachometer-alt-average

      
    • trash

      
    • unlink

      
  • @@ -44,7 +46,7 @@ import ProcessDescription from "./optionsMenu/ProcessDescription.vue"; import ProcessCounter from "./optionsMenu/ProcessCounter.vue"; export default { - props: ["process", "processId"], + props: ["process", "processId", "ellipsisPermission"], components: { ProcessInfo, ProcessScreen, MiniPieChart, Bookmark, ProcessDescription, ProcessCounter }, diff --git a/resources/js/processes-catalogue/components/ProcessCollapseInfo.vue b/resources/js/processes-catalogue/components/ProcessCollapseInfo.vue index 6dd2ef0f41..5439cf254a 100644 --- a/resources/js/processes-catalogue/components/ProcessCollapseInfo.vue +++ b/resources/js/processes-catalogue/components/ProcessCollapseInfo.vue @@ -3,6 +3,7 @@
    + :permission="$root.permission || ellipsisPermission" />
    @@ -62,6 +62,10 @@ export default { enableCollapse: { type: Boolean, default: true + }, + ellipsisPermission: { + type: Array, + default: () => [] } }, data() { diff --git a/resources/js/processes-catalogue/components/ProcessInfo.vue b/resources/js/processes-catalogue/components/ProcessInfo.vue index b16fc7b969..77669f2ab6 100644 --- a/resources/js/processes-catalogue/components/ProcessInfo.vue +++ b/resources/js/processes-catalogue/components/ProcessInfo.vue @@ -4,6 +4,7 @@ v-show="hideLaunchpad" :process="process" :current-user-id="currentUserId" + :ellipsis-permission="ellipsisPermission" @goBackCategory="$emit('goBackCategory')" /> { this.firstImage = pos + 1; }); - }, computed: { }, diff --git a/resources/js/processes-catalogue/components/ProcessScreen.vue b/resources/js/processes-catalogue/components/ProcessScreen.vue index 1b6870cbfe..1431c6f40f 100644 --- a/resources/js/processes-catalogue/components/ProcessScreen.vue +++ b/resources/js/processes-catalogue/components/ProcessScreen.vue @@ -2,6 +2,7 @@
    permissionsNeeded.includes(permission)); + const permissions = (this.$root && this.$root.permission) ? this.$root.permission : this.ellipsisPermission || []; + this.showEllipsis = permissions.some((permission) => permissionsNeeded.includes(permission)); }, /** * Return a process cards from process info diff --git a/resources/js/tasks/components/DashboardViewer.vue b/resources/js/tasks/components/DashboardViewer.vue new file mode 100644 index 0000000000..aa5fd80457 --- /dev/null +++ b/resources/js/tasks/components/DashboardViewer.vue @@ -0,0 +1,19 @@ + + + \ No newline at end of file diff --git a/resources/js/tasks/components/ListMixin.js b/resources/js/tasks/components/ListMixin.js index a0a2827bf9..9fc0a93ea0 100644 --- a/resources/js/tasks/components/ListMixin.js +++ b/resources/js/tasks/components/ListMixin.js @@ -94,6 +94,11 @@ const ListMixin = { if (this.additionalIncludes) { include.push(...this.additionalIncludes); } + + let getAllTasksInbox = "&all_inbox=false"; + if (this.$parent.allInbox) { + getAllTasksInbox = "&all_inbox=true"; + } // Load from our api client ProcessMaker.apiClient .get( @@ -108,6 +113,7 @@ const ListMixin = { }${this.getSortParam() }&non_system=true` + `&processesIManage=${(this.processesIManage ? 'true' : 'false')}` + + getAllTasksInbox + advancedFilter + this.columnsQuery, { diff --git a/resources/js/tasks/components/ParticipantHomeScreen.vue b/resources/js/tasks/components/ParticipantHomeScreen.vue new file mode 100644 index 0000000000..aa6176c7a0 --- /dev/null +++ b/resources/js/tasks/components/ParticipantHomeScreen.vue @@ -0,0 +1,596 @@ + + + + + \ No newline at end of file diff --git a/resources/js/tasks/components/ProcessBrowser.vue b/resources/js/tasks/components/ProcessBrowser.vue new file mode 100644 index 0000000000..1f5d293f4b --- /dev/null +++ b/resources/js/tasks/components/ProcessBrowser.vue @@ -0,0 +1,19 @@ + + + \ No newline at end of file diff --git a/resources/js/tasks/components/ProcessesDashboardsMenu.vue b/resources/js/tasks/components/ProcessesDashboardsMenu.vue new file mode 100644 index 0000000000..6fd5dd39f7 --- /dev/null +++ b/resources/js/tasks/components/ProcessesDashboardsMenu.vue @@ -0,0 +1,213 @@ + + + + + diff --git a/resources/js/tasks/index.js b/resources/js/tasks/index.js index 41ad99016b..dea8677c70 100644 --- a/resources/js/tasks/index.js +++ b/resources/js/tasks/index.js @@ -2,12 +2,18 @@ import Vue from "vue"; import TasksList from "./components/TasksList"; import TasksListCounter from "./components/TasksListCounter.vue"; import setDefaultAdvancedFilterStatus from "../common/setDefaultAdvancedFilterStatus"; +import ParticipantHomeScreen from './components/ParticipantHomeScreen.vue'; Vue.component("TasksList", TasksList); +Vue.component('participant-home-screen', ParticipantHomeScreen); new Vue({ el: "#tasks", data: { + showOldTaskScreen: window.ProcessMaker.showOldTaskScreen, + userConfiguration: window.ProcessMaker.userConfiguration, + urlConfiguration: "users/configuration", + showMenu: true, columns: window.Processmaker.defaultColumns || null, filter: "", pmql: "", @@ -62,7 +68,9 @@ new Vue({ if (!window.location.search.includes("filter_user_recommendation")) { this.$nextTick(() => { - this.$refs.taskList.fetch(); + if (this.$refs.taskList) { + this.$refs.taskList.fetch(); + } }); } }, diff --git a/resources/js/tasks/mixins/TasksMixin.js b/resources/js/tasks/mixins/TasksMixin.js new file mode 100644 index 0000000000..ad96cc0a3e --- /dev/null +++ b/resources/js/tasks/mixins/TasksMixin.js @@ -0,0 +1,191 @@ +export default { + data() { + return { + showOldTaskScreen: false, + urlConfiguration: "users/configuration", + showMenu: false, + columns: 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, + }, + ], + }; + }, + computed: { + effectiveSavedsearchDefaultsEditRoute() { + return ( + this.savedsearchDefaultsEditRoute || + window.ProcessMaker.savedsearchDefaultsEditRoute + ); + }, + }, + methods: { + defineUserConfiguration() { + this.localUserConfiguration = JSON.parse( + window.ProcessMaker.userConfiguration || "{}" + ); + if (this.localUserConfiguration.tasks_inbox) { + this.showMenu = this.localUserConfiguration.tasks_inbox.isMenuCollapse; + } else { + this.showMenu = false; + this.localUserConfiguration.tasks_inbox = { + isMenuCollapse: false, + }; + } + }, + hideMenu() { + this.showMenu = !this.showMenu; + this.updateUserConfiguration(); + }, + updateUserConfiguration() { + this.localUserConfiguration.tasks_inbox.isMenuCollapse = this.showMenu; + ProcessMaker.apiClient + .put(this.urlConfiguration, { + ui_configuration: this.localUserConfiguration, + }) + .catch((error) => { + console.error("Error", error); + }); + }, + 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/js/tasks/router.js b/resources/js/tasks/router.js new file mode 100644 index 0000000000..ab96b9b45b --- /dev/null +++ b/resources/js/tasks/router.js @@ -0,0 +1,32 @@ +import Vue from "vue"; +import VueRouter from "vue-router"; +import ProcessBrowser from "./components/ProcessBrowser.vue"; +import DashboardViewer from "./components/DashboardViewer.vue"; + +Vue.use(VueRouter); + +const router = new VueRouter({ + mode: "history", + base: "/tasks", + routes: [ + { + path: "/process/:processId", + name: "process-browser", + component: ProcessBrowser, + props: (route) => ({ + processId: parseInt(route.params.processId) || null, + process: null, + }), + }, + { + path: "/dashboard/:dashboardId", + name: "dashboard", + component: DashboardViewer, + props: (route) => ({ + dashboardId: route.params.dashboardId || null, + }), + }, + ], +}); + +export default router; diff --git a/resources/views/tasks/index.blade.php b/resources/views/tasks/index.blade.php index cc3082c845..08b4050ead 100644 --- a/resources/views/tasks/index.blade.php +++ b/resources/views/tasks/index.blade.php @@ -1,7 +1,7 @@ @extends('layouts.layout') @section('title') - {{__($title)}} + {{ __($title) }} @endsection @section('sidebar') @@ -9,175 +9,163 @@ @endsection @section('breadcrumbs') - @include('shared.breadcrumbs', ['routes' => [ - __('Tasks') => route('tasks.index'), - __($title) => null, - ]]) + @include('shared.breadcrumbs', [ + 'routes' => [ + __('Tasks') => route('tasks.index'), + __($title) => null, + ], + ]) @endsection + @section('content') -
    -
    -
    - - @{{ inOverdueMessage }} - -
    -
    +
    +
    +
    +
    +
    + + @{{ inOverdueMessage }} + +
    +
    -
    -
    - - -
    -
    -
    -
    -
    -
    - -
    - -
    +
    + +
    -
    @endsection @section('js') - + + @endsection @section('css') @@ -222,14 +210,17 @@ class="ml-md-2" min-height: 25px; border-radius: 50%; } + .task-nav { border-bottom: 0 !important; } + .task-nav-link.active { color: #1572C2 !important; font-weight: 700; font-size: 15px; } + .task-nav-link { color: #556271; font-weight: 400; @@ -237,25 +228,227 @@ class="ml-md-2" border-top-left-radius: 5px !important; border-top-right-radius: 5px !important; } + .task-list-body { border-radius: 5px; } + .task-inbox-rules { - width: max-content; + width: max-content; } + .task-inbox-rules-content { - display: flex; - justify-content: space-between; - padding: 15px; + display: flex; + justify-content: space-between; + padding: 15px; } + .task-inbox-rules-content-text { - width: 310px; - padding-left: 10px; + width: 310px; + padding-left: 10px; } + @endsection diff --git a/routes/web.php b/routes/web.php index e9240fc6b3..fc00a596cc 100644 --- a/routes/web.php +++ b/routes/web.php @@ -136,6 +136,7 @@ Route::get('modeler/{process}/inflight/{request?}', [ModelerController::class, 'inflight'])->name('modeler.inflight')->middleware('can:view,request'); Route::get('/', [HomeController::class, 'index'])->name('home'); + Route::get('/inbox', [TaskController::class, 'index'])->name('inbox')->middleware('no-cache'); Route::get('/redirect-to-intended', [HomeController::class, 'redirectToIntended'])->name('redirect_to_intended'); Route::post('/keep-alive', [LoginController::class, 'keepAlive'])->name('keep-alive'); diff --git a/tests/Feature/Api/UserConfigurationTest.php b/tests/Feature/Api/UserConfigurationTest.php index 0a2b4a0c8d..95fdce0acc 100644 --- a/tests/Feature/Api/UserConfigurationTest.php +++ b/tests/Feature/Api/UserConfigurationTest.php @@ -55,6 +55,9 @@ public function testStoreUserConfigurationAndGetNewValues() 'tasks' => [ 'isMenuCollapse' => false, ], + 'tasks_inbox' => [ + 'isMenuCollapse' => false, + ], ]; $response = $this->apiCall('PUT', self::API_TEST_URL, ['ui_configuration' => $values]); @@ -74,6 +77,7 @@ public function testStoreUserConfigurationAndGetNewValues() $this->assertEquals($uiConfig->cases->isMenuCollapse, $values['cases']['isMenuCollapse']); $this->assertEquals($uiConfig->requests->isMenuCollapse, $values['requests']['isMenuCollapse']); $this->assertEquals($uiConfig->tasks->isMenuCollapse, $values['tasks']['isMenuCollapse']); + $this->assertEquals($uiConfig->tasks_inbox->isMenuCollapse, $values['tasks_inbox']['isMenuCollapse']); } /** @@ -86,7 +90,7 @@ public function testStoreUserConfigurationWithInvalidValues() // Validate the header status code $response->assertStatus(422); - $this->assertEquals('The Ui configuration field is required. (and 4 more errors)', $response->json()['message']); + $this->assertEquals('The Ui configuration field is required. (and 5 more errors)', $response->json()['message']); // An incomplete ui_configuration $values = [ @@ -99,6 +103,9 @@ public function testStoreUserConfigurationWithInvalidValues() 'tasks' => [ 'isMenuCollapse' => false, ], + 'tasks_inbox' => [ + 'isMenuCollapse' => false, + ], ]; $response = $this->apiCall('PUT', self::API_TEST_URL, ['ui_configuration' => $values]); // Validate the header status code diff --git a/webpack.mix.js b/webpack.mix.js index ef6c05502a..96d065d9c7 100644 --- a/webpack.mix.js +++ b/webpack.mix.js @@ -131,6 +131,7 @@ mix .js("resources/js/tasks/index.js", "public/js/tasks/index.js") .js("resources/js/tasks/mobile.js", "public/js/tasks/mobile.js") .js("resources/js/tasks/show.js", "public/js/tasks/show.js") + .js("resources/js/tasks/router.js", "public/js/tasks/router.js") .js("resources/js/notifications/index.js", "public/js/notifications/index.js") .js('resources/js/inbox-rules/index.js', 'public/js/inbox-rules')