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

support Product Configuration (#969)

* support Product Configuration

* paint flowchart dynamically

* minimal changes Workflow

* remove comment

Co-authored-by: elsiosanchez <elsiossanches@gmail.com>
This commit is contained in:
Elsio Sanchez 2021-08-06 19:55:12 -04:00 committed by GitHub
parent 07bb5bd692
commit 44ad3521ba
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 584 additions and 75 deletions

View File

@ -0,0 +1,41 @@
// 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 connection
import { request } from '@/utils/ADempiere/request'
/**
* Request dictionary workflow metadata
* @param {string} uuid universally unique identifier
* @param {number} id, identifier
*/
export function requestWorkflowMetadata({
uuid,
id
}) {
return request({
url: '/dictionary/workflow',
method: 'get',
params: {
uuid,
id
}
})
.then(workflowResponse => {
const { convertWorkflow } = require('@/utils/ADempiere/apiConverts/dictionary.js')
return convertWorkflow(workflowResponse)
})
}

View File

@ -117,3 +117,29 @@ export function workflowActivities({
}
})
}
// GET Workflows
/**
* Request Document Status List
* @param {string} tableName
* @param {number} pageSize
* @param {string} pageToken
*/
export function getWorkflow({
tableName,
pageSize,
pageToken
}) {
return request({
url: '/workflow/workflows',
method: 'get',
params: {
table_name: tableName,
// Page Data
pageToken,
pageSize
}
})
.then(listWorkflowActivities => {
return listWorkflowActivities
})
}

View File

@ -69,7 +69,16 @@ export default {
},
computed: {
getRecordNotification() {
return this.$store.getters.getNotificationProcess
return this.$store.getters.getNotificationProcess.map(item => {
if (item.typeActivity) {
return {
...item,
name: item.name + ' ' + item.quantityActivities
}
} else {
return item
}
})
}
},
watch: {

View File

@ -45,37 +45,12 @@
<el-main class="main">
<el-container style="height: 100%;">
<el-aside v-if="!isEmptyValue(currentActivity)" id="workflow" width="70%" style="background: white;">
<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>
{{ infoNode.description }}
</span>
<el-button style="float: right; padding: 3px 0" type="text" icon="el-icon-close" @click="show = !show" />
</div>
<div v-if="!isEmptyValue(infoNode.nodeLogs)" class="text item" style="padding: 20px">
<el-timeline class="info">
<el-timeline-item
v-for="(logs, key) in infoNode.nodeLogs"
:key="key"
:timestamp="translateDate(logs.log_date)"
placement="top"
>
<el-card style="padding: 20px!important;">
<b> {{ $t('login.userName') }} </b> {{ logs.user_name }} <br>
{{ logs.text_message }}
</el-card>
</el-timeline-item>
</el-timeline>
</div>
</el-card>
</transition>
<workflow-chart
<workflow
v-if="!isEmptyValue(node) && !isEmptyValue(currentActivity)"
:transitions="listWorkflowTransition"
:states="node"
:state-semantics="currentNode"
@state-click="onLabelClicked(node, $event)"
:node-transition-list="listWorkflowTransition"
:node-list="node"
:current-node="currentNode"
:workflow-logs="listProcessWorkflow"
/>
</el-aside>
<el-main v-if="!isEmptyValue(currentActivity)" style="overflow: hidden;">
@ -105,12 +80,12 @@
<script>
import formMixin from '@/components/ADempiere/Form/formMixin.js'
import fieldsList from './fieldsList.js'
import WorkflowChart from 'vue-workflow-chart'
import Workflow from '@/components/ADempiere/Workflow'
export default {
name: 'WorkflowActivity',
components: {
WorkflowChart
Workflow
},
mixins: [
formMixin
@ -184,9 +159,6 @@ export default {
}
},
watch: {
activityList(list) {
this.SendActivityListNotifier(list)
},
currentActivity(value) {
this.listWorkflow(value)
this.setCurrent()
@ -199,9 +171,6 @@ export default {
}
},
methods: {
SendActivityListNotifier() {
this.$store.commit('addNotificationProcess', { name: this.$t('navbar.badge.activity') + ' ' + this.activityList.length, typeActivity: true })
},
setCurrent() {
const activity = this.activityList.find(activity => activity.node === this.currentActivity.node)
this.$refs.WorkflowActivity.setCurrentRow(activity)
@ -347,27 +316,6 @@ export default {
}
</style>
<style scoped>
.info {
margin: 0px;
font-size: 14px;
list-style: none;
padding: 10px;
}
.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%;
@ -387,15 +335,3 @@ export default {
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

@ -0,0 +1,173 @@
<!--
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-main style="overflow: hidden;">
<transition name="el-zoom-in-bottom">
<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>
{{ infoNode.description }}
</span>
<el-button style="float: right; padding: 3px 0" type="text" icon="el-icon-close" @click="show = !show" />
</div>
<div v-if="!isEmptyValue(infoNode.nodeLogs)" class="text item" style="padding: 20px">
<el-timeline class="info">
<el-timeline-item
v-for="(logs, key) in infoNode.nodeLogs"
:key="key"
:timestamp="translateDate(logs.log_date)"
placement="top"
>
<el-card style="padding: 20px!important;">
<b> {{ $t('login.userName') }} </b> {{ logs.user_name }} <br>
{{ logs.text_message }}
</el-card>
</el-timeline-item>
</el-timeline>
</div>
</el-card>
</transition>
<workflow-chart
v-if="!isEmptyValue(nodeList)"
id="Diagrama"
:transitions="nodeTransitionList"
:states="nodeList"
:state-semantics="currentNode"
@state-click="onLabelClicked(nodeList, $event)"
/>
</el-main>
</el-container>
</template>
<script>
import WorkflowChart from 'vue-workflow-chart'
export default {
name: 'Workflow',
components: {
WorkflowChart
},
props: {
nodeList: {
type: Array,
default: () => []
},
nodeTransitionList: {
type: Array,
default: () => []
},
currentNode: {
type: Array,
default: () => [{
classname: 'delete',
id: ''
}]
},
workflowLogs: {
type: Array,
default: () => []
}
},
data() {
return {
show: false,
infoNode: {},
topContextualMenu: 0,
leftContextualMenu: 0
}
},
methods: {
onLabelClicked(type, id) {
this.infoNode = type.find(node => node.id === id)
const nodeLogs = this.workflowLogs.filter(node => node.node_uuid === this.infoNode.uuid)
this.infoNode.nodeLogs = nodeLogs
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
const top = event.clientY - offsetTop + 500
this.topContextualMenu = top
this.show = true
},
translateDate(value) {
return this.$d(new Date(value), 'long', this.language)
}
}
}
</script>
<style scoped>
.info {
margin: 0px;
font-size: 14px;
list-style: none;
padding: 10px;
}
.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'>
.scroll-child {
max-height: 450px;
}
.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

@ -1,7 +1,9 @@
import {
workflowActivities
} from '@/api/ADempiere/workflow.js'
import { isEmptyValue } from '@/utils/ADempiere'
import { showMessage } from '@/utils/ADempiere/notification.js'
import language from '@/lang'
const activity = {
listActivity: [],
@ -19,9 +21,11 @@ export default {
}
},
actions: {
serverListActivity({ commit, getters, rootGetters }) {
serverListActivity({ commit, state, dispatch, rootGetters }, params) {
const userUuid = isEmptyValue(params) ? rootGetters['user/getUserUuid'] : params
const name = language.t('navbar.badge.activity')
workflowActivities({
userUuid: rootGetters['user/getUserUuid']
userUuid
})
.then(response => {
const { listWorkflowActivities } = response
@ -35,6 +39,26 @@ export default {
showClose: true
})
})
.finally(() => {
const notification = rootGetters.getNotificationProcess.find(notification => {
if (notification.typeActivity && notification.quantityActivities === state.listActivity.length) {
return notification
}
})
if (isEmptyValue(notification)) {
commit('addNotificationProcess', {
name,
typeActivity: true,
quantityActivities: state.listActivity.length
})
} else {
dispatch('updateNotifications', {
name,
typeActivity: true,
quantityActivities: state.listActivity.length
})
}
})
},
selectedActivity({ commit }, activity) {
commit('setCurrentActivity', activity)

View File

@ -783,6 +783,9 @@ const actions = {
default:
executeAction = 'getFieldsFromTab'
break
case 'workflow':
executeAction = 'getWorkflowFromServer'
break
}
return dispatch(executeAction, {

View File

@ -1220,5 +1220,17 @@ export default {
dispatch('updateOrderPos', false)
})
})
},
updateNotifications({ commit, state },
update
) {
const notification = state.notificationProcess.map(notification => {
if (notification.name === update.name && notification.typeActivity && notification.quantityActivities !== update.quantityActivities) {
return {
...update
}
}
})
commit('updateNotificationProcess', notification)
}
}

View File

@ -14,6 +14,9 @@ export default {
addNotificationProcess(state, payload) {
state.notificationProcess.push(payload)
},
updateNotificationProcess(state, update) {
state.notificationProcess = update
},
// Delete process in execution afther some response from server
deleteInExecution(state, payload) {
state.inExecution = state.inExecution.filter(item => item.containerUuid !== payload.containerUuid)

View File

@ -0,0 +1,75 @@
import { requestWorkflowMetadata } from '@/api/ADempiere/dictionary/workflow'
import { showMessage } from '@/utils/ADempiere/notification'
// import router from '@/router'
import language from '@/lang'
const workflow = {
state: {
workflow: []
},
mutations: {
addWorkflow(state, payload) {
state.workflow.push(payload)
},
dictionaryResetCacheWorkflow(state) {
state.workflow = []
}
},
actions: {
getWorkflowFromServer({ commit, dispatch }, {
id,
containerUuid,
routeToDelete
}) {
return new Promise(resolve => {
requestWorkflowMetadata({
uuid: containerUuid,
id
})
.then(workflowResponse => {
const panelType = 'workflow'
// Panel for save on store
const newWorkflow = {
...workflowResponse,
containerUuid,
fieldsList: [],
panelType
}
commit('addWorkflow', newWorkflow)
resolve(newWorkflow)
const actions = []
// Add process menu
dispatch('setContextMenu', {
containerUuid,
actions
})
})
.catch(error => {
// router.push({
// path: '/dashboard'
// }, () => {})
// dispatch('tagsView/delView', routeToDelete)
showMessage({
message: language.t('login.unexpectedError'),
type: 'error'
})
console.warn(`Dictionary Workflow - Error ${error.code}: ${error.message}.`)
})
})
}
},
getters: {
getWorkflowUuid: (state) => (workflowUuid) => {
return state.workflow.find(
item => item.uuid === workflowUuid
)
}
}
}
export default workflow

View File

@ -672,7 +672,6 @@ export function convertAction(action) {
isIndex: false,
component: () => import('@/views/ADempiere/Unsupported')
}
switch (action) {
case 'B':
actionAttributes.name = 'workbech'
@ -681,6 +680,7 @@ export function convertAction(action) {
case 'F':
actionAttributes.name = 'workflow'
actionAttributes.icon = 'example'
actionAttributes.component = () => import('@/views/ADempiere/Workflow')
break
case 'P':
actionAttributes.name = 'process'

View File

@ -0,0 +1,206 @@
<!--
ADempiere-Vue (Frontend) for ADempiere ERP & CRM Smart Business Solution
Copyright (C) 2017-Present E.R.P. Consultores y Asociados, C.A.
Contributor(s): Edwin Betancourt EdwinBetanc0urt@outlook.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 class="panel_main">
<el-header>
<title-and-help
:name="workflowFileName"
:help="$route.meta.description"
/>
</el-header>
<el-main v-if="isLoadedMetadata">
<workflow
v-if="!isEmptyValue(node)"
:node-transition-list="listWorkflowTransition"
:node-list="node"
:current-node="currentNode"
/>
</el-main>
<div
v-else
key="form-loading"
v-loading="!isLoadedMetadata"
:element-loading-text="$t('notifications.loading')"
element-loading-spinner="el-icon-loading"
element-loading-background="rgba(255, 255, 255, 0.8)"
class="view-loading"
/>
</el-container>
</template>
<script>
// When supporting the workflow, smart browser and reports,
// the ContextMenu and sticky must be placed in the layout
// import ContextMenu from '@/components/ADempiere/ContextMenu'
// import MainPanel from '@/components/ADempiere/Panel'
import TitleAndHelp from '@/components/ADempiere/TitleAndHelp'
import Workflow from '@/components/ADempiere/Workflow'
import { getWorkflow } from '@/api/ADempiere/workflow.js'
export default {
name: 'Workflow',
components: {
Workflow,
TitleAndHelp
},
props: {
isEdit: {
type: Boolean,
default: false
}
},
data() {
return {
size: {
width: '20px',
height: '2px'
},
workflowMetadata: {},
node: [],
listWorkflowTransition: [],
isLoadedMetadata: false,
panelType: 'workflow'
}
},
computed: {
workflowUuid() {
return this.$route.meta.uuid
},
workflowFileName() {
return this.workflowMetadata.fileName || this.$route.meta.title
},
getWorkflow() {
return this.$store.getters.getWorkflowUuid(this.workflowUuid)
},
nodoWorkflow() {
return this.workflowMetadata.node.map(node => {
return {
id: node.id,
label: node.name
}
})
}
},
created() {
this.gettWorkflow()
},
methods: {
gettWorkflow() {
const workflow = this.getWorkflow
if (workflow) {
this.workflowMetadata = workflow
this.isLoadedMetadata = true
} else {
this.$store.dispatch('getPanelAndFields', {
containerUuid: this.workflowUuid,
panelType: this.panelType,
routeToDelete: this.$route
}).then(workflowResponse => {
this.workflowMetadata = workflowResponse
this.listWorkflow(this.workflowMetadata)
}).finally(() => {
this.isLoadedMetadata = true
})
}
this.serverWorkflow(this.workflowMetadata)
},
serverWorkflow({ tableName }) {
if (this.isEmptyValue(tableName)) {
return ''
}
getWorkflow({
tableName
})
.then(response => {
this.listWorkflow(response.records)
})
.catch(error => {
console.warn(`serverWorkflow: ${error.message}. Code: ${error.code}.`)
})
},
listWorkflow(workflow) {
// Highlight Current Node
this.transitions = []
if (!this.isEmptyValue(workflow.node.uuid)) {
this.currentNode = [{
classname: 'delete',
id: workflow.start_node.uuid
}]
}
const nodes = 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
}
})
}
}
}
</script>
<style scoped>
.panel_main {
height: 100%;
width: 100%;
}
</style>

View File

@ -196,6 +196,7 @@ export default {
})
.finally(() => {
this.loading = false
this.$store.dispatch('serverListActivity')
})
} else {
console.log('error submit!!')