diff --git a/cibseven-webclient-core/src/main/java/org/cibseven/webapp/providers/TaskProvider.java b/cibseven-webclient-core/src/main/java/org/cibseven/webapp/providers/TaskProvider.java index 050766076..83fba33b3 100644 --- a/cibseven-webclient-core/src/main/java/org/cibseven/webapp/providers/TaskProvider.java +++ b/cibseven-webclient-core/src/main/java/org/cibseven/webapp/providers/TaskProvider.java @@ -19,11 +19,14 @@ import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.stream.Collectors; import org.cibseven.webapp.auth.CIBUser; import org.cibseven.webapp.exception.SystemException; @@ -35,6 +38,7 @@ import org.cibseven.webapp.rest.model.TaskForm; import org.cibseven.webapp.rest.model.TaskHistory; import org.cibseven.webapp.rest.model.Variable; +import org.cibseven.webapp.rest.model.VariableInstance; import org.cibseven.webapp.providers.utils.URLUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.MediaType; @@ -184,7 +188,12 @@ public Object form(String taskId, CIBUser user) { public Collection findTasksByFilter(TaskFiltering filters, String filterId, CIBUser user, Integer firstResult, Integer maxResults) { String url = getEngineRestUrl(user) + "/filter/" + filterId + "/list?firstResult=" + firstResult + "&maxResults=" + maxResults; try { - return Arrays.asList(((ResponseEntity) doPost(url, filters.json(), Task[].class, user)).getBody()); + List tasks = new ArrayList<>(Arrays.asList(((ResponseEntity) doPost(url, filters.json(), Task[].class, user)).getBody())); + List variableNames = filters.getVariableNames(); + if (variableNames != null && !variableNames.isEmpty()) { + enrichTasksWithVariables(tasks, variableNames, user); + } + return tasks; } catch (JsonProcessingException e) { SystemException se = new SystemException(e); log.info("Exception in getTasksFiltered(...):", se); @@ -192,6 +201,51 @@ public Collection findTasksByFilter(TaskFiltering filters, String filterId } } + private void enrichTasksWithVariables(List tasks, List variableNames, CIBUser user) { + String processInstanceIds = tasks.stream() + .map(Task::getProcessInstanceId) + .filter(id -> id != null && !id.isEmpty()) + .distinct() + .collect(Collectors.joining(",")); + + if (processInstanceIds.isEmpty()) return; + + String url = getEngineRestUrl(user) + "/variable-instance?processInstanceIdIn=" + + processInstanceIds + "&deserializeValues=false"; + try { + VariableInstance[] instances = ((ResponseEntity) + doGet(url, VariableInstance[].class, user, false)).getBody(); + if (instances == null) return; + + Map> varsByInstance = new HashMap<>(); + Map> typesByInstance = new HashMap<>(); + for (VariableInstance vi : instances) { + if (variableNames.contains(vi.getName()) && vi.getProcessInstanceId() != null) { + Object displayValue = vi.getValue(); + if ("Object".equals(vi.getType()) && vi.getValueInfo() != null) { + Object typeName = vi.getValueInfo().get("objectTypeName"); + if (typeName != null) displayValue = typeName; + } + varsByInstance + .computeIfAbsent(vi.getProcessInstanceId(), k -> new HashMap<>()) + .put(vi.getName(), displayValue); + typesByInstance + .computeIfAbsent(vi.getProcessInstanceId(), k -> new HashMap<>()) + .put(vi.getName(), vi.getType()); + } + } + + for (Task task : tasks) { + if (task.getProcessInstanceId() != null) { + task.setVariables(varsByInstance.getOrDefault(task.getProcessInstanceId(), new HashMap<>())); + task.setVariableTypes(typesByInstance.getOrDefault(task.getProcessInstanceId(), new HashMap<>())); + } + } + } catch (Exception e) { + log.warn("Could not enrich tasks with variables: {}", e.getMessage()); + } + } + @Override public Integer findTasksCountByFilter(String filterId, CIBUser user, TaskFiltering filters) { String url = getEngineRestUrl(user) + "/filter/" + filterId + "/count"; diff --git a/cibseven-webclient-core/src/main/java/org/cibseven/webapp/rest/model/FilterProperties.java b/cibseven-webclient-core/src/main/java/org/cibseven/webapp/rest/model/FilterProperties.java index 79c662733..de63caf46 100644 --- a/cibseven-webclient-core/src/main/java/org/cibseven/webapp/rest/model/FilterProperties.java +++ b/cibseven-webclient-core/src/main/java/org/cibseven/webapp/rest/model/FilterProperties.java @@ -16,6 +16,8 @@ */ package org.cibseven.webapp.rest.model; +import java.util.List; + import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import lombok.AllArgsConstructor; @@ -29,4 +31,5 @@ public class FilterProperties { private String description; private boolean refresh; private int priority; + private List variables; } diff --git a/cibseven-webclient-core/src/main/java/org/cibseven/webapp/rest/model/FilterVariable.java b/cibseven-webclient-core/src/main/java/org/cibseven/webapp/rest/model/FilterVariable.java new file mode 100644 index 000000000..e8758645b --- /dev/null +++ b/cibseven-webclient-core/src/main/java/org/cibseven/webapp/rest/model/FilterVariable.java @@ -0,0 +1,29 @@ +/* + * Copyright CIB software GmbH and/or licensed to CIB software GmbH + * under one or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information regarding copyright + * ownership. CIB software licenses this file to you under the Apache License, + * Version 2.0; you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.cibseven.webapp.rest.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data @AllArgsConstructor @NoArgsConstructor @JsonIgnoreProperties(ignoreUnknown = true) +public class FilterVariable { + private String name; + private String label; +} diff --git a/cibseven-webclient-core/src/main/java/org/cibseven/webapp/rest/model/Task.java b/cibseven-webclient-core/src/main/java/org/cibseven/webapp/rest/model/Task.java index 3db660f7a..fa200ba87 100644 --- a/cibseven-webclient-core/src/main/java/org/cibseven/webapp/rest/model/Task.java +++ b/cibseven-webclient-core/src/main/java/org/cibseven/webapp/rest/model/Task.java @@ -16,6 +16,8 @@ */ package org.cibseven.webapp.rest.model; +import java.util.Map; + import com.fasterxml.jackson.annotation.JsonAlias; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; @@ -47,14 +49,16 @@ public class Task { String suspended; String tenantId; CamundaForm camundaFormRef; - + Map variables; + Map variableTypes; + @JsonProperty("created") @JsonAlias({"creationDate"}) String created; @JsonProperty("due") @JsonAlias({"dueDate"}) String due; @JsonProperty("followUp") @JsonAlias({"followUpDate"}) String followUp; @JsonProperty("taskDefinitionKey") @JsonAlias({"taskDefinitionId"}) String taskDefinitionKey; @JsonProperty("processDefinitionId") @JsonAlias({"processDefinitionKey"}) String processDefinitionId; @JsonProperty("processInstanceId") @JsonAlias({"processInstanceKey"}) String processInstanceId; - + public String json() throws JsonProcessingException { return new ObjectMapper().writeValueAsString(this); } diff --git a/cibseven-webclient-core/src/main/java/org/cibseven/webapp/rest/model/TaskFiltering.java b/cibseven-webclient-core/src/main/java/org/cibseven/webapp/rest/model/TaskFiltering.java index b3934539d..10ecc8bb1 100644 --- a/cibseven-webclient-core/src/main/java/org/cibseven/webapp/rest/model/TaskFiltering.java +++ b/cibseven-webclient-core/src/main/java/org/cibseven/webapp/rest/model/TaskFiltering.java @@ -35,6 +35,8 @@ public class TaskFiltering { List processVariables; List orQueries; Boolean likePatternIgnoreCase; + Boolean variableValuesIgnoreCase; + List variableNames; public String json() throws JsonProcessingException { return new ObjectMapper().writeValueAsString(this); diff --git a/cibseven-webclient-core/src/test/java/org/cibseven/webapp/providers/FilterProviderIT.java b/cibseven-webclient-core/src/test/java/org/cibseven/webapp/providers/FilterProviderIT.java index c938d6d4c..734665341 100644 --- a/cibseven-webclient-core/src/test/java/org/cibseven/webapp/providers/FilterProviderIT.java +++ b/cibseven-webclient-core/src/test/java/org/cibseven/webapp/providers/FilterProviderIT.java @@ -100,7 +100,7 @@ void testCreateFilter() throws Exception { "New Filter", "user1", new FilterCriterias(), - new FilterProperties("blue", false, "Test Description", true, 10) + new FilterProperties("blue", false, "Test Description", true, 10, null) ); // Load the mock response from a file @@ -131,7 +131,7 @@ void testUpdateFilter() throws Exception { "Updated Filter", "user1", new FilterCriterias(), - new FilterProperties("red", true, "Updated Description", true, 20) + new FilterProperties("red", true, "Updated Description", true, 20, null) ); mockWebServer.enqueue(new MockResponse().setResponseCode(204)); diff --git a/frontend/src/__tests__/filter.modal.test.js b/frontend/src/__tests__/filter.modal.test.js index 0d0e11232..16b311e60 100644 --- a/frontend/src/__tests__/filter.modal.test.js +++ b/frontend/src/__tests__/filter.modal.test.js @@ -62,7 +62,8 @@ function getWrapper() { 'b-form-select': true, 'b-button': true, 'b-form-checkbox': true, - 'b-modal': true + 'b-modal': true, + 'b-badge': true }, plugins: [i18n], mocks: { diff --git a/frontend/src/assets/translations_de.json b/frontend/src/assets/translations_de.json index 3ff9542cb..cede1813b 100644 --- a/frontend/src/assets/translations_de.json +++ b/frontend/src/assets/translations_de.json @@ -930,6 +930,16 @@ "legendExpression": "Mit \"*\" endende Schlüssel akzeptieren Ausdrücke als Wert. Z.B.: '${ currentUser() }' oder '${ currentUserGroups() }'", "legendMultiple": "Mit \"in\" endende Schlüssel akzeptieren mehrere durch Komma getrennte Werte. Z.B.: `SchlüsselC, SchlüsselA, SchlüsselB`", "create": "Filter erstellen", + "variablesTitle": "Variablen", + "variablesLegend": "Sie können Variablen definieren, die in der Aufgabenliste angezeigt werden.", + "showUndefinedVariable": "Undefinierte Variablen anzeigen", + "addVariable": "Variable hinzufügen", + "variable": { + "name": "Name", + "label": "Bezeichnung", + "namePlaceholder": "Variablenname", + "labelPlaceholder": "Lesbarer Name" + }, "operators": { "eq": "=", "neq": "≠", diff --git a/frontend/src/assets/translations_en.json b/frontend/src/assets/translations_en.json index 2503cf286..42437ca8a 100644 --- a/frontend/src/assets/translations_en.json +++ b/frontend/src/assets/translations_en.json @@ -930,6 +930,16 @@ "legendExpression": "Keys ending with \"*\" accept expressions as value. E.g.: '${ currentUser() }' or '${ currentUserGroups() }'", "legendMultiple": "Keys ending with \"in\" accept multiple values separated by comma. E.g.: `keyC, keyA, keyB`", "create": "Create filter", + "variablesTitle": "Variables", + "variablesLegend": "You can define variables shown in the tasks list.", + "showUndefinedVariable": "Show undefined variables", + "addVariable": "Add variable", + "variable": { + "name": "Name", + "label": "Label", + "namePlaceholder": "Variable name", + "labelPlaceholder": "Readable label" + }, "operators": { "eq": "=", "neq": "≠", diff --git a/frontend/src/assets/translations_es.json b/frontend/src/assets/translations_es.json index 232cbe801..5ff414f0f 100644 --- a/frontend/src/assets/translations_es.json +++ b/frontend/src/assets/translations_es.json @@ -930,6 +930,16 @@ "legendExpression": "Las claves que terminan en \"*\" aceptan expresiones como valor. Por ejemplo: '${ currentUser() }' o '${ currentUserGroups() }'", "legendMultiple": "Las claves que terminan en \"in\" aceptan múltiples valores separados por coma. Por ejemplo: `claveC, claveA, claveB`", "create": "Crear filtro", + "variablesTitle": "Variables", + "variablesLegend": "Puede definir variables que se muestran en la lista de tareas.", + "showUndefinedVariable": "Mostrar variables no definidas", + "addVariable": "Añadir variable", + "variable": { + "name": "Nombre", + "label": "Etiqueta", + "namePlaceholder": "Nombre de variable", + "labelPlaceholder": "Etiqueta legible" + }, "operators": { "eq": "=", "neq": "≠", diff --git a/frontend/src/assets/translations_ru.json b/frontend/src/assets/translations_ru.json index 8035952e7..e3b92248b 100644 --- a/frontend/src/assets/translations_ru.json +++ b/frontend/src/assets/translations_ru.json @@ -930,6 +930,16 @@ "legendExpression": "Свойства с окончанием \"*\" в качестве значения могут иметь выражения, например, '${ currentUser() }' или '${ currentUserGroups() }'", "legendMultiple": "Свойства с окончанием \"in\" могут иметь несколько значений, например, список значений, разделённый запятыми, например, `keyC, keyA, keyB`", "create": "Создать фильтр", + "variablesTitle": "Переменные", + "variablesLegend": "Вы можете задать переменные, отображаемые в списке задач.", + "showUndefinedVariable": "Показывать неопределённые переменные", + "addVariable": "Добавить переменную", + "variable": { + "name": "Имя", + "label": "Метка", + "namePlaceholder": "Имя переменной", + "labelPlaceholder": "Читаемое название" + }, "operators": { "eq": "=", "neq": "≠", diff --git a/frontend/src/assets/translations_ua.json b/frontend/src/assets/translations_ua.json index 2d34b6c72..7b283173c 100644 --- a/frontend/src/assets/translations_ua.json +++ b/frontend/src/assets/translations_ua.json @@ -930,6 +930,16 @@ "legendExpression": "Властивості з закінченням \"*\" як значення можуть мати вирази, наприклад '${ currentUser() }' або '${ currentUserGroups() }'", "legendMultiple": "Властивості з закінченням \"in\" можуть мати кілька значень, наприклад список значень, розділений комами: `keyC, keyA, keyB`", "create": "Створити фільтр", + "variablesTitle": "Змінні", + "variablesLegend": "Ви можете задати змінні, які відображаються у списку завдань.", + "showUndefinedVariable": "Показувати невизначені змінні", + "addVariable": "Додати змінну", + "variable": { + "name": "Ім'я", + "label": "Мітка", + "namePlaceholder": "Ім'я змінної", + "labelPlaceholder": "Читабельна назва" + }, "operators": { "eq": "=", "neq": "≠", diff --git a/frontend/src/components/process/mixins/variableUtils.js b/frontend/src/components/process/mixins/variableUtils.js index 0f7af3b72..d36499a82 100644 --- a/frontend/src/components/process/mixins/variableUtils.js +++ b/frontend/src/components/process/mixins/variableUtils.js @@ -132,6 +132,12 @@ export default { return '' }, + shortValue(value) { + const str = '' + value + const dot = str.lastIndexOf('.') + return dot >= 0 && dot < str.length - 1 && /^[a-z]/.test(str) ? str.substring(dot + 1) : str + }, + getFileObjects() { return [ 'de.cib.cibflow.api.files.FileValueDataFlowSource', diff --git a/frontend/src/components/task/TasksContent.vue b/frontend/src/components/task/TasksContent.vue index 9bf6c8753..a45be338c 100644 --- a/frontend/src/components/task/TasksContent.vue +++ b/frontend/src/components/task/TasksContent.vue @@ -275,6 +275,13 @@ export default { filters.orQueries.push(this.$store.getters.formatedCriteriaData) } } + if (filters.processVariables || (filters.orQueries && filters.orQueries.some(q => q.processVariables))) { + filters.variableValuesIgnoreCase = true + } + const filterVariables = this.$store.state.filter.selected.properties?.variables + if (filterVariables && filterVariables.length > 0) { + filters.variableNames = filterVariables.map(v => v.name) + } TaskService.findTasksByFilter(this.$store.state.filter.selected.id, filters, { firstResult: firstResult, maxResults: maxResults }).then(result => { const tasks = this.tasksByPermissions(this.$root.config.permissions.displayTasks, result) diff --git a/frontend/src/components/task/TasksNavBar.vue b/frontend/src/components/task/TasksNavBar.vue index 9f16e753a..140f86d74 100644 --- a/frontend/src/components/task/TasksNavBar.vue +++ b/frontend/src/components/task/TasksNavBar.vue @@ -62,7 +62,8 @@
+ ($root.config.taskFilter.advancedSearch.processVariables.length > 0 && $root.config.taskFilter.advancedSearch.filterEnabled) || + filterVariables.length > 0" class="ms-auto col-auto"> @@ -84,13 +85,22 @@
-
+
{{ criteria.displayName }}
+
+
+
+ +
{{ criteria.displayName }}
+
+ +
+
@@ -134,13 +144,41 @@
- + {{ $t('task.assignToMe') }}
+
+
+
+
+
{{ filterVar.label }}:
+
+ {{ displayValue(task, filterVar) }} +
+
<undefined>
+
+
+
+
+ +
+
+ +
+
@@ -194,12 +232,13 @@ import { moment } from '@/globals.js' import { TaskService, AdminService } from '@/services.js' import { debounce } from '@/utils/debounce.js' -import { formatDateForTooltips } from '@/utils/dates.js' +import { formatDate, formatDateForTooltips } from '@/utils/dates.js' import StartProcess from '@/components/start-process/StartProcess.vue' import AdvancedSearchModal from '@/components/task/AdvancedSearchModal.vue' import SmartSearch from '@/components/task/SmartSearch.vue' import { ConfirmDialog, BWaitingBox, HighlightedText } from '@cib/common-frontend' import { mapActions } from 'vuex' +import variableUtils from '@/components/process/mixins/variableUtils.js' export default { name: 'TasksNavBar', @@ -220,6 +259,7 @@ export default { pauseRefreshButton: false, advancedFilter: [], advancedFilterAux: null, + expandedTasks: {}, justSelectedFromList: false, pendingScrollToTaskId: null } @@ -255,6 +295,16 @@ export default { this.updateAdvancedFilters() } } + }, + 'filterVariables': function() { + const cleaned = this.$store.state.advancedSearch.criterias.filter(c => !c.id?.startsWith('fv_')) + if (cleaned.length !== this.$store.state.advancedSearch.criterias.length) { + this.$store.dispatch('updateAdvancedSearch', { + matchAllCriteria: this.$store.state.advancedSearch.matchAllCriteria, + criterias: cleaned + }) + } + this.loadAdvancedFilters() } }, computed: { @@ -273,6 +323,12 @@ export default { }, filteredFields() { return this.$root.config.taskSorting.fields.filter(item => this.showFields(item)) + }, + filterVariables: function() { + return this.$store.state.filter.selected.properties?.variables || [] + }, + showUndefinedVariable: function() { + return this.$store.state.filter.selected.properties?.showUndefinedVariable || false } }, created: function () { @@ -285,6 +341,37 @@ export default { methods: { ...mapActions('task', ['setSelectedAssignee']), formatDateForTooltips, + displayValue: function(task, filterVar) { + const variable = { + name: filterVar.name, + value: task.variables?.[filterVar.name], + type: task.variableTypes?.[filterVar.name] + } + if (variable.type === 'Date') return formatDate(variable.value) + return variableUtils.shortValue(variableUtils.displayValue(variable)) + }, + displayTooltip: function(task, filterVar) { + const header = filterVar.label + ' (' + filterVar.name + '):' + const variable = { + name: filterVar.name, + value: task.variables?.[filterVar.name], + type: task.variableTypes?.[filterVar.name] + } + if (variable.value == null) return header + '\n' + const value = variable.type === 'Date' + ? formatDateForTooltips(variable.value) + : variableUtils.displayValue(variable) + return header + '\n' + value + }, + toggleTaskVariables: function(taskId) { + this.expandedTasks[taskId] = !this.expandedTasks[taskId] + }, + visibleFilterVariables: function(task) { + return this.filterVariables.filter(filterVar => + (task.variables && task.variables[filterVar.name] !== undefined && task.variables[filterVar.name] !== null) || + this.showUndefinedVariable + ) + }, loadAdvancedFilters: function() { this.advancedFilter = [] this.$root.config.taskFilter.advancedSearch.processVariables.forEach(pv => { @@ -308,8 +395,24 @@ export default { advancedFilterObj.value = pv.type === 'Boolean' ? '' : pv.value } this.advancedFilter.push(advancedFilterObj) - this.advancedFilterAux = JSON.stringify(this.advancedFilter) }) + this.filterVariables.forEach(fv => { + const key = 'fv_' + fv.name + const criteria = this.$store.state.advancedSearch.criterias + .find(obj => obj.id === key && obj.operator === 'like') + this.advancedFilter.push({ + key: key, + variableName: fv.name, + displayName: fv.label || fv.name, + type: 'String', + defaultValue: '', + operator: 'like', + source: 'filter', + check: !!criteria, + value: criteria ? criteria.value.slice(1, -1) : '' + }) + }) + this.advancedFilterAux = JSON.stringify(this.advancedFilter) }, updateAdvancedFilters: debounce(800, function() { const criterias = this.advancedFilter @@ -536,6 +639,16 @@ export default {