diff --git a/ProcessMaker/Helpers/MobileHelper.php b/ProcessMaker/Helpers/MobileHelper.php index f577f4284f..2d8254f763 100644 --- a/ProcessMaker/Helpers/MobileHelper.php +++ b/ProcessMaker/Helpers/MobileHelper.php @@ -13,4 +13,9 @@ public static function isMobile($userAgent) return false; } + + public static function detectMobile() + { + return isset($_SERVER['HTTP_USER_AGENT']) && self::isMobile($_SERVER['HTTP_USER_AGENT']); + } } diff --git a/ProcessMaker/Http/Controllers/Api/ProcessController.php b/ProcessMaker/Http/Controllers/Api/ProcessController.php index b55cd6a597..e6000c96bb 100644 --- a/ProcessMaker/Http/Controllers/Api/ProcessController.php +++ b/ProcessMaker/Http/Controllers/Api/ProcessController.php @@ -45,7 +45,7 @@ class ProcessController extends Controller { const CAROUSEL_TYPES = [ 'IMAGE' => 'image', - 'EMBED' => 'embed' + 'EMBED' => 'embed', ]; /** @@ -128,6 +128,7 @@ public function index(Request $request) 'id' => $item['id'], 'events'=> $item['start_events']]; }); + return new ApiCollection($modifiedCollection); } @@ -402,7 +403,7 @@ public function store(Request $request) * @param Process $process * @return ResponseFactory|Response * - * @throws \Throwable + * @throws Throwable * * @OA\Put( * path="/processes/{processId}", @@ -520,7 +521,7 @@ public function update(Request $request, Process $process) public function updateBpmn(Request $request, Process $process) { $request->validate(Process::rules($process)); - + // bpmn validation if ($schemaErrors = $this->validateBpmn($request)) { $warnings = []; @@ -557,7 +558,8 @@ public function updateBpmn(Request $request, Process $process) ], 200); } - private function updateSubprocessElement($parentProcess, $request, $process) { + private function updateSubprocessElement($parentProcess, $request, $process) + { $definitions = $parentProcess->getDefinitions(); $elements = $definitions->getElementsByTagName('callActivity'); foreach ($elements as $element) { @@ -584,7 +586,7 @@ private function updateSubprocessElement($parentProcess, $request, $process) { * @param Process $process * @return ResponseFactory|Response * - * @throws \Throwable + * @throws Throwable * * @OA\Put( * path="/processes/{processId}/draft", @@ -948,7 +950,7 @@ public function startProcesses(Request $request) * @param Process $process * @return ResponseFactory|Response * - * @throws \Throwable + * @throws Throwable * * @OA\Put( * path="/processes/{processId}/restore", @@ -1294,7 +1296,7 @@ public function import_ready($code) * * @return resource * - * @throws \Throwable + * @throws Throwable * * @OA\Post( * path="/processes/{process_id}/import/assignments", @@ -1440,7 +1442,7 @@ public function importAssignments(Process $process, Request $request) * @param Process $process * @param Request $request * - * @return \ProcessMaker\Http\Resources\ProcessRequests + * @return ProcessRequests * * @OA\Post( * path="/process_events/{process_id}", @@ -1778,7 +1780,7 @@ public function deleteMediaImages(Request $request, Process $process) * @param string $uuid * @param string $collectionName * @return bool - */ + */ private function deleteImage(Process $process, $uuid, $collectionName) { // Retrieve the media image by UUID and collection name @@ -1788,6 +1790,7 @@ private function deleteImage(Process $process, $uuid, $collectionName) // If the media image exists, delete it and return true if ($mediaImagen) { $mediaImagen->delete(); + return true; } else { // Otherwise, return false diff --git a/ProcessMaker/Http/Controllers/Api/ProcessLaunchpadController.php b/ProcessMaker/Http/Controllers/Api/ProcessLaunchpadController.php index a25e7452e6..40e7cdd9dd 100644 --- a/ProcessMaker/Http/Controllers/Api/ProcessLaunchpadController.php +++ b/ProcessMaker/Http/Controllers/Api/ProcessLaunchpadController.php @@ -12,6 +12,7 @@ use ProcessMaker\Models\Media; use ProcessMaker\Models\Process; use ProcessMaker\Models\ProcessLaunchpad; +use ProcessMaker\Models\ProcessRequest; class ProcessLaunchpadController extends Controller { @@ -24,7 +25,9 @@ public function getProcesses(Request $request) $processes = Process::nonSystem()->active(); // Filter by category $category = $request->input('category', null); - if (!empty($category)) { + if ($category === 'recent') { + $processes->orderByRecentRequests(); + } elseif (!empty($category)) { $processes->processCategory($category); } // Filter pmql @@ -54,9 +57,30 @@ public function getProcesses(Request $request) $process->launchpad = ProcessLaunchpad::getLaunchpad($launchpad, $process->id); } + $process = $processes->map(function ($process) { + $process->counts = $this->getCounts($process->id); + }); + return new ProcessCollection($processes); } + protected function getCounts($processId) + { + $result = ProcessRequest::where('process_id', $processId) + ->selectRaw('status, count(*) as count') + ->groupBy('status') + ->get(); + + $completed = $result->where('status', 'COMPLETED')->first()?->count ?? 0; + $in_progress = $result->where('status', 'ACTIVE')->first()?->count ?? 0; + + return [ + 'completed' => $completed, + 'in_progress' => $in_progress, + 'total' => $completed + $in_progress, + ]; + } + /** * Get the size of the page. * per_page=# (integer, the page requested) (Default: 10). @@ -83,7 +107,12 @@ public function index(Request $request, Process $process) }]) ->where('id', $process->id) ->get() - ->toArray(); + ->map(function ($process) use ($request) { + $process->counts = $this->getCounts($process->id); + $process->bookmark_id = Bookmark::getBookmarked(true, $process->id, $request->user()->id); + + return $process; + }); return new ApiResource($processes); } @@ -120,9 +149,9 @@ public function destroy(ProcessLaunchpad $launch) return response([], 204); } - /** - * Store the elements related to the carousel [IMAGE, EMBED URL] - */ + /** + * Store the elements related to the carousel [IMAGE, EMBED URL] + */ public function saveContentCarousel(Request $request, Process $process) { $contentCarousel = $request->input('imagesCarousel'); @@ -144,7 +173,6 @@ public function saveContentCarousel(Request $request, Process $process) // Nothing break; } - } } } diff --git a/ProcessMaker/Http/Controllers/Api/TaskDraftController.php b/ProcessMaker/Http/Controllers/Api/TaskDraftController.php index d9b4c6bae8..a721e6c434 100644 --- a/ProcessMaker/Http/Controllers/Api/TaskDraftController.php +++ b/ProcessMaker/Http/Controllers/Api/TaskDraftController.php @@ -12,6 +12,19 @@ class TaskDraftController extends Controller { + public function index(Request $request, ProcessRequestToken $task) + { + $search = ['task_id' => $task->id]; + $draft = TaskDraft::where($search)->first(); + + if ($draft) { + $draftData = $draft->data; + return new ApiResource($draftData); + } + + return new ApiResource(null); + } + public function update(Request $request, ProcessRequestToken $task) { $search = ['task_id' => $task->id]; @@ -24,7 +37,6 @@ public function update(Request $request, ProcessRequestToken $task) } $draft->data = $data; $draft->saveOrFail(); - return new ApiResource($draft); } diff --git a/ProcessMaker/Http/Controllers/Auth/LoginController.php b/ProcessMaker/Http/Controllers/Auth/LoginController.php index 4b3d7cf925..61ed085d2b 100644 --- a/ProcessMaker/Http/Controllers/Auth/LoginController.php +++ b/ProcessMaker/Http/Controllers/Auth/LoginController.php @@ -277,11 +277,11 @@ public function loggedOut(Request $request) * Handle a login request to the application. * Overrides the original login action. * - * @param \Illuminate\Http\Request $request - * @param \ProcessMaker\Models\User $user + * @param Request $request + * @param User $user * @return \Illuminate\Http\RedirectResponse|\Illuminate\Http\Response|\Illuminate\Http\JsonResponse * - * @throws \Illuminate\Validation\ValidationException + * @throws ValidationException */ public function login(Request $request, User $user) { @@ -340,7 +340,7 @@ public function login(Request $request, User $user) /** * Throws locked error message * - * @throws \Illuminate\Validation\ValidationException + * @throws ValidationException */ protected function throwLockedLoginResponse() { diff --git a/ProcessMaker/Http/Controllers/ProcessController.php b/ProcessMaker/Http/Controllers/ProcessController.php index 9da007d074..74bb1600bd 100644 --- a/ProcessMaker/Http/Controllers/ProcessController.php +++ b/ProcessMaker/Http/Controllers/ProcessController.php @@ -124,7 +124,7 @@ public function edit(Process $process) 'canEditData', 'addons', 'assignedProjects', - 'isDraft' + 'isDraft', ])); } diff --git a/ProcessMaker/Http/Controllers/ProcessesCatalogueController.php b/ProcessMaker/Http/Controllers/ProcessesCatalogueController.php index 7e333e24a7..4c394861f8 100644 --- a/ProcessMaker/Http/Controllers/ProcessesCatalogueController.php +++ b/ProcessMaker/Http/Controllers/ProcessesCatalogueController.php @@ -5,12 +5,13 @@ use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; use ProcessMaker\Events\ScreenBuilderStarting; +use ProcessMaker\Helpers\MobileHelper; use ProcessMaker\Http\Controllers\Controller; use ProcessMaker\Managers\ScreenBuilderManager; -use ProcessMaker\Models\Process; use ProcessMaker\Models\Bookmark; -use ProcessMaker\Models\ProcessLaunchpad; +use ProcessMaker\Models\Process; use ProcessMaker\Models\ProcessCategory; +use ProcessMaker\Models\ProcessLaunchpad; use ProcessMaker\Traits\HasControllerAddons; /** @@ -22,7 +23,7 @@ class ProcessesCatalogueController extends Controller { use HasControllerAddons; - + public function index(Request $request, Process $process = null) { $manager = app(ScreenBuilderManager::class); @@ -33,6 +34,13 @@ public function index(Request $request, Process $process = null) $process->launchpad = ProcessLaunchpad::getLaunchpad(true, $process->id); $process->bookmark_id = Bookmark::getBookmarked(true, $process->id, $currentUser['id']); } + + if (MobileHelper::detectMobile()) { + $title = __('Process Browser'); + + return view('processes-catalogue.mobile', compact('title', 'process', 'currentUser', 'manager')); + } + return view('processes-catalogue.index', compact('process', 'currentUser', 'manager')); } } diff --git a/ProcessMaker/Http/Controllers/TaskController.php b/ProcessMaker/Http/Controllers/TaskController.php index f66d143828..cb8ccdc17d 100644 --- a/ProcessMaker/Http/Controllers/TaskController.php +++ b/ProcessMaker/Http/Controllers/TaskController.php @@ -165,8 +165,12 @@ public function edit(ProcessRequestToken $task, string $preview = '') public function quickFillEdit(ProcessRequestToken $task) { + $screenVersion = $task->getScreenVersion(); + $screenFields = $screenVersion ? $screenVersion->screenFilteredFields() : []; + return view('tasks.editQuickFill', [ 'task' => $task, + 'screenFields' => $screenFields, ]); } } diff --git a/ProcessMaker/Models/Process.php b/ProcessMaker/Models/Process.php index efa6d91c04..35526657be 100644 --- a/ProcessMaker/Models/Process.php +++ b/ProcessMaker/Models/Process.php @@ -1162,29 +1162,34 @@ public function manageCustomRoutes() break; default: - if ($webEntryProperties->webentryRouteConfig->firstUrlSegment !== '') { - $webentryRouteConfig = $webEntryProperties->webentryRouteConfig; - try { - WebentryRoute::updateOrCreate( - [ - 'process_id' => $this->id, - 'node_id' => $webentryRouteConfig->nodeId, - ], - [ - 'first_segment' => $webentryRouteConfig->firstUrlSegment, - 'params' => $webentryRouteConfig->parameters, - ] - ); - } catch (\Exception $e) { - \Log::info('*** Error: ' . $e->getMessage()); - } - } + $this->manageWebentryRoute($webEntryProperties); break; } } } } + private function manageWebentryRoute($webEntryProperties) + { + if ($webEntryProperties->webentryRouteConfig->firstUrlSegment !== '') { + $webentryRouteConfig = $webEntryProperties->webentryRouteConfig; + try { + WebentryRoute::updateOrCreate( + [ + 'process_id' => $this->id, + 'node_id' => $webentryRouteConfig->nodeId, + ], + [ + 'first_segment' => $webentryRouteConfig->firstUrlSegment, + 'params' => $webentryRouteConfig->parameters, + ] + ); + } catch (Exception $e) { + \Log::info('*** Error: ' . $e->getMessage()); + } + } + } + /** * Get node element attributes * @@ -1250,7 +1255,7 @@ private function getStartEventPermissions(User $user) /** * Process events relationship. * - * @return \ProcessMaker\Models\ProcessEvents + * @return ProcessEvents */ public function events() { @@ -1287,7 +1292,7 @@ public function launchpad() /** * Assignments of the process. * - * @return \Illuminate\Database\Eloquent\Relations\HasMany + * @return HasMany */ public function assignments() { @@ -1616,7 +1621,7 @@ private function validateSchema(BpmnDocument $document) private function deleteUnusedCustomRoutes($url, $processId, $nodeId) { // Delete unused custom routes - $customRoute = webentryRoute::where('process_id', $processId)->where('node_id', $nodeId)->first(); + $customRoute = WebentryRoute::where('process_id', $processId)->where('node_id', $nodeId)->first(); if ($customRoute) { $customRoute->delete(); } @@ -1751,9 +1756,9 @@ public function scopeFilter($query, $filterStr) ->orWhere('processes.description', 'like', $filter) ->orWhere('processes.status', '=', $filterStr) ->orWhereHas('user', function ($query) use ($filter) { - $query->where('firstname', 'like', $filter) - ->orWhere('lastname', 'like', $filter); - }) + $query->where('firstname', 'like', $filter) + ->orWhere('lastname', 'like', $filter); + }) ->orWhereIn('processes.id', function ($qry) use ($filter) { $qry->select('assignable_id') ->from('category_assignments') @@ -1797,4 +1802,18 @@ public function hasAlternative() { return true; } + + public function scopeOrderByRecentRequests($query) + { + return $query->orderByDesc( + ProcessRequest::select('id') + // User has participated + ->whereHas('tokens', function ($q) { + $q->where('user_id', Auth::user()->id); + }) + ->whereColumn('process_id', 'processes.id') + ->orderByDesc('id') // using ID because created_at is not indexed + ->limit(1) + ); + } } diff --git a/ProcessMaker/Models/ProcessRequest.php b/ProcessMaker/Models/ProcessRequest.php index 5ab2a1f681..accec25261 100644 --- a/ProcessMaker/Models/ProcessRequest.php +++ b/ProcessMaker/Models/ProcessRequest.php @@ -527,10 +527,11 @@ public function summary() $result[] = [ 'key' => $key, 'value' => $value, - 'type' => $type + 'type' => $type, ]; } } + return $result; } diff --git a/resources/img/pagination-images/first.svg b/resources/img/pagination-images/first.svg new file mode 100644 index 0000000000..7399ee56fa --- /dev/null +++ b/resources/img/pagination-images/first.svg @@ -0,0 +1,3 @@ + + + diff --git a/resources/img/pagination-images/last.svg b/resources/img/pagination-images/last.svg new file mode 100644 index 0000000000..621fd4a98e --- /dev/null +++ b/resources/img/pagination-images/last.svg @@ -0,0 +1,3 @@ + + + diff --git a/resources/img/pagination-images/next.svg b/resources/img/pagination-images/next.svg new file mode 100644 index 0000000000..99343901d6 --- /dev/null +++ b/resources/img/pagination-images/next.svg @@ -0,0 +1,3 @@ + + + diff --git a/resources/img/pagination-images/previous.svg b/resources/img/pagination-images/previous.svg new file mode 100644 index 0000000000..499314c292 --- /dev/null +++ b/resources/img/pagination-images/previous.svg @@ -0,0 +1,3 @@ + + + diff --git a/resources/img/smartinbox-images/open-case.svg b/resources/img/smartinbox-images/open-case.svg new file mode 100644 index 0000000000..463e6a30fe --- /dev/null +++ b/resources/img/smartinbox-images/open-case.svg @@ -0,0 +1,3 @@ + + + diff --git a/resources/js/app-layout.js b/resources/js/app-layout.js index fd51785742..68e845aeae 100644 --- a/resources/js/app-layout.js +++ b/resources/js/app-layout.js @@ -220,6 +220,7 @@ if (isMobileNavbar === "true") { }, data() { return { + display: true, }; }, mounted() { diff --git a/resources/js/common/PMColumnFilterPopoverCommonMixin.js b/resources/js/common/PMColumnFilterPopoverCommonMixin.js index b2f6aa73ce..ba50c02a6f 100644 --- a/resources/js/common/PMColumnFilterPopoverCommonMixin.js +++ b/resources/js/common/PMColumnFilterPopoverCommonMixin.js @@ -2,6 +2,10 @@ import { get, cloneDeep } from "lodash"; const PMColumnFilterCommonMixin = { props: { + autosaveFilter: { + type: Boolean, + default: true + }, advancedFilterProp: { type: Object, default: null @@ -55,7 +59,9 @@ const PMColumnFilterCommonMixin = { order }; - ProcessMaker.apiClient.put(url, config); + if (!this.autosaveFilter) { + ProcessMaker.apiClient.put(url, config); + } window.ProcessMaker.advanced_filter = config; window.ProcessMaker.EventBus.$emit("advanced-filter-updated"); }, @@ -266,7 +272,13 @@ const PMColumnFilterCommonMixin = { this.tableHeaders[i].sortDesc = false; } for (let i in this.tableHeaders) { - if (this.tableHeaders[i].field === this.orderBy) { + if (this.tableHeaders[i].order_column !== undefined) { + if (this.orderBy === this.tableHeaders[i].order_column) { + let sort = this.sortOrder[0].direction; + this.tableHeaders[i].sortAsc = (sort.toLowerCase() === "asc"); + this.tableHeaders[i].sortDesc = (sort.toLowerCase() === "desc"); + } + } else if (this.orderBy.endsWith(this.tableHeaders[i].field)) { let sort = this.sortOrder[0].direction; this.tableHeaders[i].sortAsc = (sort.toLowerCase() === "asc"); this.tableHeaders[i].sortDesc = (sort.toLowerCase() === "desc"); diff --git a/resources/js/components/PMColumnFilterPopover/PMColumnFilterIconAsc.vue b/resources/js/components/PMColumnFilterPopover/PMColumnFilterIconAsc.vue index 5b8f1f8f81..b87a22d0dd 100644 --- a/resources/js/components/PMColumnFilterPopover/PMColumnFilterIconAsc.vue +++ b/resources/js/components/PMColumnFilterPopover/PMColumnFilterIconAsc.vue @@ -1,5 +1,5 @@ diff --git a/resources/js/components/PMColumnFilterPopover/PMColumnFilterIconDesc.vue b/resources/js/components/PMColumnFilterPopover/PMColumnFilterIconDesc.vue index 7e6adc61a1..14ff423fbf 100644 --- a/resources/js/components/PMColumnFilterPopover/PMColumnFilterIconDesc.vue +++ b/resources/js/components/PMColumnFilterPopover/PMColumnFilterIconDesc.vue @@ -1,5 +1,5 @@ diff --git a/resources/js/components/PMColumnFilterPopover/PMColumnFilterIconThreeDots.vue b/resources/js/components/PMColumnFilterPopover/PMColumnFilterIconThreeDots.vue index f8348637a3..4b01a48d1b 100644 --- a/resources/js/components/PMColumnFilterPopover/PMColumnFilterIconThreeDots.vue +++ b/resources/js/components/PMColumnFilterPopover/PMColumnFilterIconThreeDots.vue @@ -1,5 +1,27 @@ + diff --git a/resources/js/components/PMColumnFilterPopover/PMColumnFilterPopover.vue b/resources/js/components/PMColumnFilterPopover/PMColumnFilterPopover.vue index 30577c932b..a7acaa5bab 100644 --- a/resources/js/components/PMColumnFilterPopover/PMColumnFilterPopover.vue +++ b/resources/js/components/PMColumnFilterPopover/PMColumnFilterPopover.vue @@ -4,7 +4,11 @@ variant="link" size="sm" class="pm-filter-popover-button"> - +
@@ -293,4 +297,31 @@ export default { .static-header { position: static !important; } +.contracted-menu { + width: 40px; + height: 40px; + box-shadow: 0px 4px 6px 0px rgba(0, 0, 0, 0.15); + border-radius: 4px; + background-color: #FFFFFF; +} +.static-header:hover { + background-color: #EBEEF2; + border-radius: 4px; +} +.expanded-menu { + color: #556271; + text-transform: none; + border-radius: 4px; + font-size: 16px; +} +.ellipsis-icon-v { + height: 16px; + width: 16px; +} +.ellipsis-tooltip { + border-radius: 4px; +} +.ellipsis-tooltip .arrow { + display: none; +} diff --git a/resources/js/components/shared/FilterTable.vue b/resources/js/components/shared/FilterTable.vue index c7f6215aef..e982e14da5 100644 --- a/resources/js/components/shared/FilterTable.vue +++ b/resources/js/components/shared/FilterTable.vue @@ -21,7 +21,8 @@ :id="`${tableName}-column-${index}`" :key="index" class="pm-table-ellipsis-column" - :class="{ 'pm-table-filter-applied': column.filterApplied }" + :class="{ 'pm-table-filter-applied': column.filterApplied || column.sortAsc || column.sortDesc }" + :style="{ width: column.fixed_width + 'px' }" >