1
0
mirror of https://github.com/PanJiaChen/vue-element-admin.git synced 2025-08-07 18:25:45 +08:00

Support workflow activities (#972)

* support workflow activities

* minimal changes

* add Workflows and services

* add state semantics

* rename fille

* add info the node

Co-authored-by: elsiosanchez <elsiossanches@gmail.com>
This commit is contained in:
Elsio Sanchez 2021-07-17 03:54:08 -04:00 committed by GitHub
parent 4364c01262
commit ebd568cfb6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 620 additions and 0 deletions

View File

@ -66,6 +66,7 @@
"vue-shortkey": "^3.1.7",
"vue-split-panel": "^1.0.4",
"vue-splitpane": "1.0.6",
"vue-workflow-chart": "^0.4.5",
"vuedraggable": "2.24.3",
"vuex": "3.6.2",
"xlsx": "0.16.9"

View File

@ -0,0 +1,56 @@
// ADempiere-Vue (Frontend) for ADempiere ERP & CRM Smart Business Solution
// Copyright (C) 2017-Present E.R.P. Consultores y Asociados, C.A.
// Contributor(s): Yamel Senih ysenih@erpya.com www.erpya.com
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
// Get Instance for connectionimport {
import { request } from '@/utils/ADempiere/request'
// List Activity
export function listActivity({
formUuid
}) {
return request({
url: '/form/addons/activitys',
method: 'get',
params: {
form_uuid: formUuid
}
})
.then(listActivityResponse => {
return listActivityResponse
})
}
// Send Activity
export function sendActivity({
formUuid,
activity,
message,
forward
}) {
return request({
url: '/form/addons/send-activity',
method: 'post',
data: {
form_uuid: formUuid,
activity,
message,
forward
}
})
.then(listActivityResponse => {
return listActivityResponse
})
}

View File

@ -92,3 +92,28 @@ export function requestListDocumentActions({
}
})
}
// Request a list of Activities from the user's Workflows
export function workflowActivities({
userUuid,
pageSize,
pageToken
}) {
return request({
url: '/workflow/workflow-activities',
method: 'get',
params: {
user_uuid: userUuid,
// Page Data
pageToken,
pageSize
}
})
.then(listWorkflowActivities => {
return {
nextPageToken: listWorkflowActivities.next_page_token,
recordCount: listWorkflowActivities.record_count,
listWorkflowActivities: listWorkflowActivities.records
}
})
}

View File

@ -0,0 +1,76 @@
// ADempiere-Vue (Frontend) for ADempiere ERP & CRM Smart Business Solution
// Copyright (C) 2017-Present E.R.P. Consultores y Asociados, C.A.
// Contributor(s): Elsio Sanchez elsiosanches@gmail.com www.erpya.com
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
// List of fields to send in search
// import language from '@/lang'
import language from '@/lang'
export default [
// History
{
elementColumnName: 'History',
isFromDictionary: true,
overwriteDefinition: {
handleFocusGained: true,
handleFocusLost: true,
handleKeyPressed: true,
handleKeyReleased: true,
handleActionKeyPerformed: true,
handleActionPerformed: true,
name: language.t('form.activity.filtersSearch.history'),
componentPath: 'FieldText',
size: 24,
sequence: 4,
isActiveLogics: true,
isMandatory: true,
isReadOnly: true
}
},
// Messages
{
elementColumnName: 'TextMsg',
isFromDictionary: true,
overwriteDefinition: {
handleFocusGained: true,
handleFocusLost: true,
handleKeyPressed: true,
handleKeyReleased: true,
handleActionKeyPerformed: true,
handleActionPerformed: true,
size: 24,
sequence: 5,
isActiveLogics: true,
isMandatory: true
}
},
// Forward
{
elementColumnName: 'Forward',
isFromDictionary: true,
overwriteDefinition: {
size: 24,
sequence: 6,
name: language.t('form.activity.filtersSearch.forward'),
handleFocusGained: true,
handleFocusLost: true,
handleKeyPressed: true,
handleKeyReleased: true,
handleActionKeyPerformed: true,
handleActionPerformed: true,
componentPath: 'FieldSelect'
}
}
]

View File

@ -0,0 +1,384 @@
<!--
ADempiere-Vue (Frontend) for ADempiere ERP & CRM Smart Business Solution
Copyright (C) 2017-Present E.R.P. Consultores y Asociados, C.A.
Contributor(s): Elsio Sanchez esanchez@erpya.com www.erpya.com
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https:www.gnu.org/licenses/>.
-->
<template>
<el-container style="height: 100% !important;">
<el-header class="header" :style="!collapse ? 'height: 35% !important;' : 'height: 10%!important'">
<el-card :style="!collapse ? 'height: 100% !important;' : 'height: auto'">
<div slot="header">
<span> {{ $t('form.activity.title') }} </span>
<el-button style="float: right; padding: 3px 0" type="text" :icon="collapse ? 'el-icon-arrow-down' : 'el-icon-arrow-up'" @click="collapse = !collapse" />
</div>
<el-table
v-show="!collapse"
ref="WorkflowActivity"
v-loading="isEmptyValue(activityList)"
:data="activityList"
highlight-current-row
style="width: 100%;height: 85% !important;"
border
height="90% !important"
@current-change="handleCurrentChange"
>
<el-table-column
v-for="(valueOrder) in orderLineDefinition"
:key="valueOrder.columnName"
:column-key="valueOrder.columnName"
:label="valueOrder.name"
:align="valueOrder.isNumeric ? 'right' : 'left'"
:prop="valueOrder.columnName"
/>
</el-table>
</el-card>
</el-header>
<el-main class="main">
<transition name="el-zoom-in-center">
<el-card v-show="show" :style="{position: 'absolute', zIndex: '5', left: leftContextualMenu + 'px', top: topContextualMenu + 'px'}" class="box-card">
<div slot="header" class="clearfix">
<span>
<b> {{ infoNode.name }} </b>
</span>
<el-button style="float: right; padding: 3px 0" type="text" icon="el-icon-close" @click="show = !show" />
</div>
<div class="text item" style="padding: 20px">
<el-timeline class="info">
<el-timeline-item :timestamp="currentWorkflow.created" placement="top">
<el-card style="padding: 20px!important;">
<b> Usuario: </b> {{ currentWorkflow.user_name }} <br>
<b> {{ $t('table.ProcessActivity.Description') }}: </b> {{ infoNode.description }}
</el-card>
</el-timeline-item>
</el-timeline>
</div>
</el-card>
</transition>
<workflow-chart
v-if="!isEmptyValue(node) && !isEmptyValue(currentWorkflow)"
:transitions="listWorkflowTransition"
:states="node"
:state-semantics="currentNode"
@state-click="onLabelClicked(node, $event)"
/>
<el-scrollbar v-if="!isEmptyValue(currentWorkflow)" wrap-class="scroll-child">
<el-timeline class="info">
<el-timeline-item
v-for="(nodes, key) in currentWorkflow.workflow_process.workflow_events"
:key="key"
:timestamp="translateDate(nodes.log_date)"
placement="top"
>
<b> {{ nodes.node_name }} </b> {{ nodes.text_message }}
</el-timeline-item>
</el-timeline>
</el-scrollbar>
</el-main>
<el-footer :class="styleFooter">
<el-card shadow="hover" class="search">
<el-form v-if="!isEmptyValue(fieldsList)" :disabled="isEmptyValue(currentActivity)" label-position="top" class="from-main">
<el-form-item>
<el-row>
<el-col v-for="(field, index) in fieldsList" :key="index" :span="6">
<field
:key="field.columnName"
:metadata-field="field"
:v-model="field.value"
/>
</el-col>
</el-row>
</el-form-item>
</el-form>
<el-button type="primary" icon="el-icon-check" style="float: right;" :disabled="isEmptyValue(currentActivity)" />
</el-card>
</el-footer>
</el-container>
</template>
<script>
import formMixin from '@/components/ADempiere/Form/formMixin.js'
import fieldsList from './fieldsList.js'
import WorkflowChart from 'vue-workflow-chart'
export default {
name: 'WorkflowActivity',
components: {
WorkflowChart
},
mixins: [
formMixin
],
props: {
metadata: {
type: Object,
default: () => {
return {
uuid: 'WF-Activity',
containerUuid: 'WF-Activity',
fieldsList
}
}
}
},
data() {
return {
fieldsList,
node: [],
transitions: [],
topContextualMenu: 0,
leftContextualMenu: 0,
infoNode: {},
show: false,
collapse: false,
currentNode: [{
classname: 'delete',
id: ''
}],
currentWorkflow: {},
listWorkflowTransition: [],
orderLineDefinition: [
{
columnName: 'workflow.name',
name: 'Nombre',
isNumeric: false
},
{
columnName: 'node.name',
name: this.$t('form.activity.table.node'),
isNumeric: false
},
{
columnName: 'node.description',
name: 'Descripcion',
isNumeric: false
}
]
}
},
computed: {
styleFooter() {
const showTitle = this.$store.getters.getIsShowTitleForm
if (showTitle) {
return 'show-title-footer'
}
return 'footer'
},
activityList() {
const list = this.$store.getters.getActivity
if (!this.isEmptyValue(list)) {
return list.filter(activity => !this.isEmptyValue(activity.uuid))
}
return []
},
currentActivity() {
return this.$store.getters.getCurrentActivity
}
},
mounted() {
this.$store.dispatch('serverListActivity')
if (!this.isEmptyValue(this.currentActivity)) {
this.setCurrent()
}
},
methods: {
setCurrent() {
const activity = this.activityList.find(activity => activity.node === this.currentActivity.node)
this.$refs.WorkflowActivity.setCurrentRow(activity)
},
handleCurrentChange(activity) {
this.listWorkflow(activity)
this.$store.dispatch('selectedActivity', activity)
},
onLabelClicked(type, id) {
this.infoNode = type.find(node => node.id === id)
const menuMinWidth = 105
const offsetLeft = this.$el.getBoundingClientRect().left // container margin left
const offsetWidth = this.$el.offsetWidth // container width
const maxLeft = offsetWidth - menuMinWidth // left boundary
const left = event.clientX - offsetLeft + 15 // 15: margin right
this.leftContextualMenu = left
if (left > maxLeft) {
this.leftContextualMenu = maxLeft
}
const offsetTop = this.$el.getBoundingClientRect().top
let top = event.clientY - offsetTop
if (this.panelType === 'browser' && this.panelMetadata.isShowedCriteria) {
top = event.clientY - 200
}
this.topContextualMenu = top
this.show = true
},
listWorkflow(activity) {
// Highlight Current Node
this.currentWorkflow = activity
this.transitions = []
if (!this.isEmptyValue(activity.node.uuid)) {
this.currentNode = [{
classname: 'delete',
id: activity.node.uuid
}]
}
const nodes = activity.workflow.workflow_nodes.filter(node => !this.isEmptyValue(node.uuid))
this.listNodeTransitions(nodes)
if (!this.isEmptyValue(nodes)) {
this.node = nodes.map((workflow, key) => {
return {
...workflow,
transitions: workflow.transitions,
id: workflow.uuid,
key,
label: workflow.name
}
})
} else {
this.node = []
}
},
listNodeTransitions(nodes) {
nodes.forEach(element => {
const uuid = element.uuid
const id = element.value
if (!this.isEmptyValue(element.transitions)) {
element.transitions.forEach((nextNode, key) => {
if (!this.isEmptyValue(nextNode.node_next_uuid)) {
if (this.isEmptyValue(nextNode.description)) {
this.transitions.push({
id: id + key,
target: uuid,
source: nextNode.node_next_uuid
})
} else {
this.transitions.push({
id: id + key,
label: nextNode.description,
target: uuid,
source: nextNode.node_next_uuid
})
}
}
})
}
})
const blon = nodes.map(item => {
return {
uuid: item.uuid
}
})
this.listWorkflowTransition = this.transitions.filter(data => {
const verificar = blon.find(mode => mode.uuid === data.source)
if (!this.isEmptyValue(verificar)) {
return data
}
})
},
translateDate(value) {
return this.$d(new Date(value), 'long', this.language)
}
}
}
</script>
<style lang="scss" scoped>
.from-main {
padding-right: 1% !important;
padding-bottom: 0px !important;
padding-top: 0px !important;
padding-left: 1% !important;
}
.card-form {
height: 100% !important;
overflow: auto;
}
.header {
padding-bottom: 2%;
padding-top: 1.5%;
box-sizing: border-box;
flex-shrink: 0;
padding-left: 1%;
padding-right: 1%;
}
.from-footer {
height: 5% !important;
box-sizing: border-box;
flex-shrink: 0;
}
.footer {
padding-top: 0px;
height: 10% !important;
padding-bottom: 0px;
}
.main {
padding-bottom: 0px;
padding-top: 0px;
}
.search {
height: 100%;
}
.show-title-footer {
padding-top: 0px;
height: 8% !important;
padding-bottom: 0px;
}
</style>
<style scoped>
.info {
margin: 0px;
font-size: 14px;
list-style: none;
padding: 0px;
}
.vue-workflow-chart-state {
background-color: #fff;
padding: 20px;
border-radius: 3px;
color: #11353d;
font-size: 15px;
font-family: Open Sans;
/* font-weight: 600; */
margin-right: 20px;
margin-bottom: 20px;
max-width: 15%;
text-align: center;
-webkit-box-shadow: 0 2px 4px 0 rgb(0 0 0 / 20%);
box-shadow: 0 2px 4px 0 rgb(0 0 0 / 20%);
}
.panel_main {
height: 100%;
width: 100%;
}
</style>
<style lang='scss'>
.el-card {
border-radius: 4px;
border: 1px solid #e6ebf5;
background-color: #FFFFFF;
overflow: hidden;
color: #303133;
-webkit-transition: 0.3s;
transition: 0.3s;
display: block;
}
@import '~vue-workflow-chart/dist/vue-workflow-chart.css';
.vue-workflow-chart-state-delete {
color: white;
background: #AED5FE;
}
.vue-workflow-chart-transition-arrow-delete {
fill: #AED5FE;
}
.vue-workflow-chart-transition-path-delete {
stroke: #AED5FE;
}
</style>

View File

@ -54,6 +54,9 @@ export default {
case 'ProductInfo':
form = import('@/components/ADempiere/Form/ProductInfo')
break
case 'WFActivity':
form = import('@/components/ADempiere/Form/WorkflowActivity')
break
case 'VPOS':
form = import('@/components/ADempiere/Form/VPOS')
break

View File

@ -556,6 +556,17 @@ export default {
toolsPoint: {
title: 'Point of Sale Tools'
}
},
activity: {
title: 'Your Workflow Activities',
filtersSearch: {
history: 'History records',
forward: 'Re-send'
},
table: {
priority: 'Priority',
node: 'Node'
}
}
}
}

View File

@ -531,6 +531,17 @@ export default {
toolsPoint: {
title: 'Herramientas del Punto de Venta'
}
},
activity: {
title: 'Sus Actividades de Flujo de Trabajo',
filtersSearch: {
history: 'Registros históricos',
forward: 'Re-enviar'
},
table: {
priority: 'Prioridad',
node: 'Nodo'
}
}
}
}

View File

@ -12,6 +12,7 @@ import enLang from 'element-ui/lib/locale/lang/en'// 如果使用中文语言包
import VueSplit from 'vue-split-panel'
import 'vue-resize/dist/vue-resize.css'
import VueResize from 'vue-resize'
import WorkflowChart from 'vue-workflow-chart'
/**
* TODO: Waiting for PR to:
* https://github.com/vue-extend/v-markdown/pull/4
@ -53,6 +54,7 @@ Vue.use(VMarkdown)
Vue.use(VueShortkey)
Vue.use(VueSplit)
Vue.use(VueResize)
Vue.use(WorkflowChart)
Vue.use(Element, {
size: Cookies.get('size') || 'medium', // set element-ui default size
i18n: (key, value) => i18n.t(key, value),

View File

@ -0,0 +1,51 @@
import {
workflowActivities
} from '@/api/ADempiere/workflow.js'
import { showMessage } from '@/utils/ADempiere/notification.js'
const activity = {
listActivity: [],
currentActivity: {}
}
export default {
state: activity,
mutations: {
setActivity(state, activity) {
state.listActivity = activity
},
setCurrentActivity(state, activity) {
state.currentActivity = activity
}
},
actions: {
serverListActivity({ commit, getters, rootGetters }) {
workflowActivities({
userUuid: rootGetters['user/getUserUuid']
})
.then(response => {
const { listWorkflowActivities } = response
commit('setActivity', listWorkflowActivities)
})
.catch(error => {
console.warn(`serverListActivity: ${error.message}. Code: ${error.code}.`)
showMessage({
type: 'error',
message: error.message,
showClose: true
})
})
},
selectedActivity({ commit }, activity) {
commit('setCurrentActivity', activity)
}
},
getters: {
getCurrentActivity: (state) => {
return state.currentActivity
},
getActivity: (state) => {
return state.listActivity
}
}
}