Compare commits

...

17 Commits

Author SHA1 Message Date
df59bff6ae ActionFlow bug fix 2023-09-17 20:21:39 +09:00
59e6d33656 右側プロパティ開くとののバグfix 2023-09-11 23:14:05 +09:00
b641c729c2 bug fix 2023-09-11 22:16:14 +09:00
142cdcda38 プロパティ属性設定連動実装 2023-09-10 01:15:40 +09:00
fc2669dabf Merge remote-tracking branch 'origin/fang' into mvp_step2_dev 2023-09-09 01:25:06 +09:00
8e095b51e3 FlowChart削除メニュー追加 2023-09-08 21:17:20 +09:00
ff03490209 UI美化 2023-09-08 20:04:34 +09:00
40cd9998d0 FlowEditor初期合体 2023-09-08 14:28:45 +09:00
973ba159b4 flowChart初期実装 2023-09-08 13:31:41 +09:00
063a5af822 add right drawer 2023-09-08 03:52:18 +00:00
dt
6a06c71104 add flow editor left component 2023-09-07 07:54:53 +08:00
cccff1d16d fang create 2023-09-06 13:02:40 +00:00
dt
100d8de54f fix extra code after merge 2023-09-06 00:16:47 +08:00
dt
7c667660c0 Merge branch 'dt' into daitian
# Conflicts:
#	frontend/src/router/routes.ts
2023-09-05 23:56:39 +08:00
daitian
4eb56372a5 add FlowEditorPage 2023-09-02 08:14:24 +08:00
daitian
16edd398be add node v20, pnpm support 2023-09-02 05:40:53 +08:00
daitian
4e08159e6d add pnpm-lock file ignore 2023-09-02 05:14:34 +08:00
28 changed files with 1631 additions and 30 deletions

3
frontend/.gitignore vendored
View File

@@ -35,3 +35,6 @@ yarn-error.log*
# local .env files
.env.local*
# pnpm
pnpm-lock.yaml

View File

@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html>
<html lang="ja-jp">
<head>
<title><%= productName %></title>

View File

@@ -11,12 +11,14 @@
"@quasar/extras": "^1.16.4",
"axios": "^1.4.0",
"quasar": "^2.6.0",
"uuid": "^9.0.0",
"vue": "^3.0.0",
"vue-router": "^4.0.0"
},
"devDependencies": {
"@quasar/app-vite": "^1.3.0",
"@types/node": "^12.20.21",
"@types/uuid": "^9.0.3",
"@typescript-eslint/eslint-plugin": "^5.10.0",
"@typescript-eslint/parser": "^5.10.0",
"autoprefixer": "^10.4.2",
@@ -28,8 +30,9 @@
"typescript": "^4.5.4"
},
"engines": {
"node": "^18 || ^16 || ^14.19",
"node": "^20 ||^18 || ^16 || ^14.19",
"npm": ">= 6.13.4",
"pnpm": ">=8.6.0",
"yarn": ">= 1.21.1"
}
},
@@ -544,6 +547,12 @@
"@types/node": "*"
}
},
"node_modules/@types/uuid": {
"version": "9.0.3",
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.3.tgz",
"integrity": "sha512-taHQQH/3ZyI3zP8M/puluDEIEvtQHVYcC6y3N8ijFtAd28+Ey/G4sg1u2gB01S8MwybLOKAp9/yCMu/uR5l3Ug==",
"dev": true
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "5.61.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.61.0.tgz",
@@ -5045,6 +5054,14 @@
"node": ">= 0.4.0"
}
},
"node_modules/uuid": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz",
"integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==",
"bin": {
"uuid": "dist/bin/uuid"
}
},
"node_modules/vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",

View File

@@ -16,12 +16,14 @@
"@quasar/extras": "^1.16.4",
"axios": "^1.4.0",
"quasar": "^2.6.0",
"uuid": "^9.0.0",
"vue": "^3.0.0",
"vue-router": "^4.0.0"
},
"devDependencies": {
"@quasar/app-vite": "^1.3.0",
"@types/node": "^12.20.21",
"@types/uuid": "^9.0.3",
"@typescript-eslint/eslint-plugin": "^5.10.0",
"@typescript-eslint/parser": "^5.10.0",
"autoprefixer": "^10.4.2",
@@ -33,8 +35,9 @@
"typescript": "^4.5.4"
},
"engines": {
"node": "^18 || ^16 || ^14.19",
"node": "^20 ||^18 || ^16 || ^14.19",
"npm": ">= 6.13.4",
"yarn": ">= 1.21.1"
"yarn": ">= 1.21.1",
"pnpm": ">=8.6.0"
}
}

View File

@@ -1,9 +0,0 @@
export interface Rule{
id:number;
name:string;
condtion:CondtionTree
}
export interface CondtionTree{
}

View File

@@ -0,0 +1,73 @@
<template>
<div class="q-py-md">
<q-list>
<q-expansion-item
group="somegroup"
label="レコードを追加画面"
default-opened
>
<q-card-section>
<q-checkbox v-model="setting.v1" label="追加画面表示した時" />
<q-checkbox v-model="setting.v2" label="保存をクリックした時" />
<q-checkbox v-model="setting.v3" label="保存成功した時" />
</q-card-section>
</q-expansion-item>
<q-expansion-item group="somegroup" label="レコード編集画面">
<q-card>
<q-card-section>
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Quidem,
eius reprehenderit eos corrupti commodi magni quaerat ex numquam,
dolorum officiis modi facere maiores architecto suscipit iste
eveniet doloribus ullam aliquid.
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item group="somegroup" label="レコード詳細画面">
<q-card>
<q-card-section>
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Quidem,
eius reprehenderit eos corrupti commodi magni quaerat ex numquam,
dolorum officiis modi facere maiores architecto suscipit iste
eveniet doloribus ullam aliquid.
</q-card-section>
</q-card>
</q-expansion-item>
<q-expansion-item group="somegroup" label="レコード一覧画面">
<q-card class="bg-teal-2">
<q-card-section>
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Quidem,
eius reprehenderit eos corrupti commodi magni quaerat ex numquam,
dolorum officiis modi facere maiores architecto suscipit iste
eveniet doloribus ullam aliquid.
</q-card-section>
</q-card>
</q-expansion-item>
</q-list>
</div>
<q-btn @click="clear" label="clear"/>
</template>
<script setup lang="ts">
import { ref, Ref } from 'vue';
interface Setting {
v1: boolean;
v2: boolean;
v3: boolean;
}
const setting: Ref<Setting> = ref({
v1: true,
v2: true,
v3: false,
});
let clear = () => {
setting.value.v1 = false
setting.value.v2 = false
setting.value.v3 = false
}
</script>

View File

@@ -0,0 +1,24 @@
<template>
<div class="q-py-md">
<q-tree :nodes="LeftDataBus.root" node-key="label">
<template #header-rg="p">
<ControlPanelTreeRadio
:node="p.node"
:dataBus="LeftDataBus"
></ControlPanelTreeRadio>
</template>
</q-tree>
</div>
</template>
<script setup lang="ts">
import {
LeftDataBus,
setControlPanelE,
} from 'components/flowEditor/left/DataBus';
import ControlPanelTreeRadio from './ControlPanelTreeRadio.vue';
// 应该在page中用网络请求获取值并初始化组件
// 然后在page中执行setControlPane设置databus
setControlPanelE();
</script>

View File

@@ -0,0 +1,23 @@
<template>
<q-radio v-model="model" :val="node.value" :label="node.label" />
</template>
<script setup lang="ts">
import { computed } from 'vue';
import { LeftData, ControlPanelData } from 'components/flowEditor/left/DataBus';
const props = defineProps(['node', 'dataBus']);
const node = computed(() => props.node as ControlPanelData);
const model = computed({
get() {
return (props.dataBus as LeftData).data?.get(node.value.group ?? 'n');
},
set(newValue) {
(props.dataBus as LeftData).data?.set(
node.value.group ?? 'n',
newValue ?? ''
);
},
});
</script>

View File

@@ -0,0 +1,72 @@
import { reactive } from 'vue'
export const LeftDataBus = reactive<LeftData>({})
const defaultData = {
root: [
{
label: 'レコードを追加画面',
children: [
{
label: '追加画面表示した時',
header: 'rg',
value: '1-1',
group: 'g1',
children: []
},
{
label: '保存をクリックした時',
header: 'rg',
value: '1-2',
group: 'g1',
children: []
},
{
label: '保存成功した時',
header: 'rg',
value: '1-3',
group: 'g1',
children: []
},
]
},
{
label: 'レコード編集画面',
},
{
label: 'レコード詳細画面',
},
{
label: 'レコード一覧画面',
},
],
data: new Map([['g1', '1-1']])
}
export const setControlPanel = (rootData: LeftData) => {
const { root: dr, data: dd } = defaultData
LeftDataBus.title = rootData.title
LeftDataBus.root = rootData.root ?? dr
LeftDataBus.data = rootData.data ?? dd
}
export const setControlPanelE = () => {
const { root: dr, data: dd } = defaultData
// LeftDataBus.title = rootData.title
LeftDataBus.root = dr
LeftDataBus.data = dd
}
export interface LeftData {
title?: string
root?: ControlPanelData[]
data?: Map<string, string>
}
export interface ControlPanelData {
label: string,
header?: string,
value?: string,
group?: string,
children?: ControlPanelData[]
}

View File

@@ -0,0 +1,36 @@
<template>
<div class="ItemSelector q-pa-sm">
<div class="row">
<div class="col-auto">
<q-icon name="widgets" color="primary" size="2.5em" />
</div>
<div class="col flex">
<div class="q-pa-sm flex" style="align-items: center">{{title}}</div>
</div>
<div class="col-auto flex">
<div class="flex" style="align-items: center">
<q-btn
class="q-px-sm"
color="white"
size="sm"
text-color="black"
label="変 更"
dense
/>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue';
const title = ref('勤怠管理')
</script>
<style lang="sass">
.ItemSelector
border: 0.15em solid rgba(#999, .4)
border-radius: 0.4em
</style>

View File

@@ -0,0 +1,186 @@
<template>
<div class="row justify-center" :style="{ marginLeft: node.inputPoint !== '' ? '240px' : '' }" >
<div class="row">
<q-card class="action-node" :class="nodeStyle" :square="false" @click="onNodeClick" >
<q-toolbar class="col" >
<div class="text-subtitle2">{{ node.subTitle }}</div>
<q-space></q-space>
<q-btn flat round dense icon="more_horiz" size="sm" >
<q-menu auto-close anchor="top right">
<q-list>
<q-item clickable v-if="!isRoot" @click="onEditNode">
<q-item-section avatar><q-icon name="edit" ></q-icon></q-item-section>
<q-item-section >編集する</q-item-section>
</q-item>
<q-item clickable v-if="!isRoot" @click="onDeleteNode">
<q-item-section avatar><q-icon name="delete" ></q-icon></q-item-section>
<q-item-section>削除する</q-item-section>
</q-item>
<q-item clickable @click="onDeleteAllNode">
<q-item-section avatar><q-icon name="delete_sweep" ></q-icon></q-item-section>
<q-item-section >以下すべて削除する</q-item-section>
</q-item>
</q-list>
</q-menu>
</q-btn>
</q-toolbar>
<q-separator />
<q-card-section>
<div class="text-h7">{{ node.title }}</div>
</q-card-section>
<template v-if="hasBranch">
<q-separator />
<q-card-actions align="around">
<q-btn flat v-for="(point, index) in node.outputPoints" :key="index">
{{ point }}
</q-btn>
</q-card-actions>
</template>
</q-card>
</div>
</div>
<template v-if="hasBranch">
<div class="row justify-center" :style="{ marginLeft: node.inputPoint !== '' ? '240px' : '' }">
<div v-for="(point, index) in node.outputPoints" :key="index">
<node-line :action-node="node" :mode="getMode(point)" @addNode="addNode" :input-point="point"></node-line>
</div>
</div>
</template>
<template v-if="!hasBranch">
<div class="row justify-center" :style="{ marginLeft: node.inputPoint !== '' ? '240px' : '' }">
<node-line :action-node="node" :mode="getMode('')" @addNode="addNode" input-point=""></node-line>
</div>
</template>
</template>
<script lang="ts">
import { defineComponent, computed, ref } from 'vue';
import { IActionNode } from '../../types/ActionTypes';
import NodeLine, { Direction } from '../main/NodeLine.vue';
export default defineComponent({
name: 'NodeItem',
components: {
NodeLine
},
props: {
actionNode: {
type: Object as () => IActionNode,
required: true
},
isSelected: {
type: Boolean
}
},
emits: [
'addNode',
"nodeSelected",
"nodeEdit",
"deleteNode",
"deleteAllNextNodes",
],
setup(props, context) {
const hasBranch = computed(() => props.actionNode.outputPoints.length > 0);
const nodeStyle = computed(() => {
return {
'root-node': props.actionNode.isRoot,
'text-white': props.actionNode.isRoot,
'selected': props.isSelected && !props.actionNode.isRoot
};
});
const getMode = (point: string) => {
if (point === '' || props.actionNode.outputPoints.length === 0) {
return Direction.Default;
}
if (point === props.actionNode.outputPoints[0]) {
if (props.actionNode.nextNodeIds.get(point)) {
return Direction.Left;
} else {
return Direction.LeftNotNext;
}
} else {
if (props.actionNode.nextNodeIds.get(point)) {
return Direction.Right;
} else {
return Direction.RightNotNext;
}
}
}
/**
* アクションノード追加イベントを
* @param point 入力ポイント
*/
const addNode = (point: string) => {
context.emit('addNode', props.actionNode, point);
}
/**
* ノード選択状態
*/
const onNodeClick = () => {
context.emit('nodeSelected', props.actionNode);
}
const onEditNode=()=>{
context.emit('nodeEdit', props.actionNode);
}
/**
* ノードを削除する
*/
const onDeleteNode=()=>{
context.emit('deleteNode', props.actionNode);
}
/**
* ノードの以下すべて削除する
*/
const onDeleteAllNode=()=>{
context.emit('deleteAllNextNodes', props.actionNode);
}
return {
node: props.actionNode,
isRoot: props.actionNode.isRoot,
hasBranch,
nodeStyle,
getMode,
addNode,
onNodeClick,
onEditNode,
onDeleteNode,
onDeleteAllNode
}
}
});
</script>
<style lang="scss">
.action-node {
min-width: 300px !important;
}
.line {
height: 20px;
}
.line:after {
content: '';
background-color: $blue-7;
display: block;
width: 3px;
}
.add-icon {
font-size: 2em;
color: $blue-7;
}
.root-node {
background-color: $blue-7;
border-radius: 20px;
}
.action-node:not(.root-node):hover{
background-color: $light-blue-1;
}
.selected{
background-color: $yellow-1;
}
</style>

View File

@@ -0,0 +1,109 @@
<template>
<div>
<svg class="node-line">
<polyline :points="points.linePoints" class="line" ></polyline>
<text class="add-icon" @click="addNode(node)" :x="points.iconPoint.x" :y="points.iconPoint.y" font-family="Arial" font-size="25"
text-anchor="middle" dy=".3em" style="cursor: pointer;" >
</text>
</svg>
</div>
</template>
<script lang="ts">
import { ref, defineComponent, computed, PropType } from 'vue';
import { IActionNode, ActionNode, ActionFlow, RootAction } from '../../types/ActionTypes';
export enum Direction {
Default = "None",
Left = "LEFT",
Right = "RIGHT",
LeftNotNext = "LEFTNOTNEXT",
RightNotNext = "RIGHTNOTNEXT",
}
export default defineComponent({
name: 'NodeLine',
props: {
actionNode: {
type: Object as PropType<IActionNode>,
required: true
},
mode: {
type: String as PropType<Direction>,
required: true
},
inputPoint:{
type:String
}
},
emits: ['addNode'],
setup(props,context) {
const hasBranch = computed(() => props.actionNode.outputPoints.length > 0);
const points = computed(() => {
switch (props.mode) {
case Direction.Left:
return {
linePoints: '180, 0, 180, 40, 120, 40, 120, 60',
iconPoint: { x: 180, y: 20 }
};
case Direction.Right:
return {
linePoints: '60, 0, 60, 40, 120, 40, 120, 60',
iconPoint: { x: 60, y: 20 }
};
case Direction.LeftNotNext:
return {
linePoints: '180, 0, 180, 40',
iconPoint: { x: 180, y: 20 }
};
case Direction.RightNotNext:
return {
linePoints: '60, 0, 60, 40',
iconPoint: { x: 60, y: 30 }
};
default:
return {
linePoints: '120, 0, 120, 60',
iconPoint: { x: 120, y: 30 }
};
}
});
const addNode=(prveNode:IActionNode)=>{
context.emit('addNode',props.inputPoint);
}
return {
node: props.actionNode,
hasBranch,
points,
addNode
}
}
});
</script>
<style lang="scss">
.node-line {
height: 60px;
width: 240px;
}
.line {
stroke: $blue-7;
fill: none;
stroke-width: 2;
}
.add-icon {
stroke: $blue-8;
fill: $blue-8;
font-family: Arial;
pointer-events: all;
font-size: 2.0em;
}
.add-icon:hover{
stroke: $blue-8;
fill:$blue-8;
font-weight: bold;
font-size: 2.4em;
}
</style>

View File

@@ -32,4 +32,3 @@ export interface AppInfo {
creator?:User;
modifier?:User;
}

View File

@@ -0,0 +1,57 @@
<template>
<div>
<div v-for="(item, index) in componentData" :key="index">
<component :is="item.component" v-bind="item.props" v-model="item.props.modelValue"></component>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import InputText from '../right/InputText.vue';
import SelectBox from '../right/SelectBox.vue';
import DatePicker from '../right/DatePicker.vue';
import FieldInput from '../right/FieldInput.vue';
export default defineComponent({
name: 'ActionProperty',
components: {
InputText,
SelectBox,
DatePicker,
FieldInput
},
props: {
jsonData: {
type: Object,
required: true,
},
jsonValue:{
type: Object,
required: false,
}
},
computed: {
componentData() {
return this.jsonData.elements.map((element: any) => {
if(this.jsonValue != undefined )
{
if(this.jsonValue.hasOwnProperty(element.props.name))
{
element.props.modelValue = this.jsonValue[element.props.name];
}
else
{
element.props.modelValue = '';
}
}
return {
component: element.component,
props: element.props,
};
});
},
},
});
</script>

View File

@@ -0,0 +1,45 @@
<template>
<q-input v-model="selectedDate" :label="placeholder" mask="date" :rules="['date']">
<template v-slot:append>
<q-icon name="event" class="cursor-pointer">
<q-popup-proxy cover transition-show="scale" transition-hide="scale">
<q-date v-model="selectedDate">
<div class="row items-center justify-end">
<q-btn v-close-popup label="Close" color="primary" flat />
</div>
</q-date>
</q-popup-proxy>
</q-icon>
</template>
</q-input>
</template>
<script lang="ts">
import { defineComponent, ref ,watchEffect} from 'vue';
export default defineComponent({
name: 'DatePicker',
props: {
placeholder: {
type: String,
default: '',
},
modelValue: {
type: String,
default: '',
},
},
setup(props, { emit }) {
const selectedDate = ref(props.modelValue);
watchEffect(() => {
emit('update:modelValue', selectedDate.value);
});
return {
selectedDate
};
}
});
</script>

View File

@@ -0,0 +1,62 @@
<template>
<q-input v-model="selectedField" :label="placeholder">
<template v-slot:append>
<q-icon name="search" class="cursor-pointer" @click="showDg"/>
</template>
</q-input>
<show-dialog v-model:visible="show" name="フィールド一覧" @close="closeDg">
<field-select ref="appDg" name="フィールド" type="single" :appId="1"></field-select>
</show-dialog>
</template>
<script lang="ts">
import { defineComponent, ref ,watchEffect} from 'vue';
import ShowDialog from '../ShowDialog.vue';
import FieldSelect from '../FieldSelect.vue';
export default defineComponent({
name: 'FieldInput',
components: {
ShowDialog,
FieldSelect,
},
props: {
placeholder: {
type: String,
default: '',
},
modelValue: {
type: String,
default: '',
},
},
setup(props, { emit }) {
const appDg = ref();
const show = ref(false);
const selectedField = ref(props.modelValue);
const showDg = () => {
show.value = true;
};
const closeDg = (val:string) => {
if (val == 'OK') {
selectedField.value = appDg.value.selected[0].name;
}
};
watchEffect(() => {
emit('update:modelValue', selectedField.value);
});
return {
appDg,
show,
showDg,
closeDg,
selectedField,
};
}
});
</script>

View File

@@ -0,0 +1,33 @@
<template>
<q-input :label="placeholder" v-model="inputValue"/>
</template>
<script lang="ts">
import { defineComponent,ref,watchEffect } from 'vue';
export default defineComponent({
name: 'InputText',
props: {
placeholder: {
type: String,
default: '',
},
modelValue: {
type: String,
default: '',
},
},
setup(props, { emit }) {
const inputValue = ref(props.modelValue);
watchEffect(() => {
emit('update:modelValue', inputValue.value);
});
return {
inputValue,
};
},
});
</script>

View File

@@ -0,0 +1,45 @@
<template>
<div>
<div v-for="(item, index) in properties" :key="index">
<component :is="item.component" v-bind="item.props" v-model="item.props.modelValue"></component>
</div>
</div>
</template>
<script lang="ts">
/**
* プロパティ属性設定生成する
*/
import { PropType, defineComponent,ref } from 'vue';
import InputText from '../right/InputText.vue';
import SelectBox from '../right/SelectBox.vue';
import DatePicker from '../right/DatePicker.vue';
import FieldInput from '../right/FieldInput.vue';
import { IActionNode,IActionProperty } from 'src/types/ActionTypes';
export default defineComponent({
name: 'PropertyList',
components: {
InputText,
SelectBox,
DatePicker,
FieldInput
},
props: {
nodeProps: {
type: Object as PropType<Array<IActionProperty>>,
required: true,
},
jsonValue:{
type: Object,
required: false,
}
},
setup(props, context) {
const properties=ref(props.nodeProps)
return {
properties
}
}
});
</script>

View File

@@ -0,0 +1,80 @@
<template>
<div class="q-pa-md q-gutter-sm">
<q-drawer
side="right"
:show-if-above="false"
bordered
:width="301"
:breakpoint="500"
class="bg-grey-3"
:model-value="showPanel"
elevated
overlay
>
<q-card class="column full-height" style="width: 300px">
<q-card-section>
<div class="text-h6">プロパティ</div>
</q-card-section>
<q-card-section class="col q-pt-none">
<property-list :node-props="actionProps" v-if="showPanel" ></property-list>
</q-card-section>
<q-card-actions align="right" class="bg-white text-teal">
<q-btn flat label="Save" @click="save"/>
<q-btn flat label="Cancel" @click="cancel" />
</q-card-actions>
</q-card>
</q-drawer>
</div>
</template>
<script lang="ts">
import { reactive, ref,defineComponent, defineProps,PropType ,watchEffect} from 'vue'
import PropertyList from 'components/right/PropertyList.vue';
import { IActionNode } from 'src/types/ActionTypes';
export default defineComponent({
name: 'PropertyPanel',
components: {
PropertyList
},
props: {
actionNode:{
type:Object as PropType<IActionNode>,
required:true
},
drawerRight:{
type:Boolean,
required:true
}
},
emits: [
"update:drawerRight"
],
setup(props,{emit}) {
const showPanel =ref(props.drawerRight);
const actionProps =ref(props.actionNode.actionProps);
watchEffect(() => {
showPanel.value = props.drawerRight;
actionProps.value= props.actionNode.actionProps;
});
const cancel = async() =>{
showPanel.value = false;
emit("update:drawerRight",false )
}
const save = async () =>{
showPanel.value=false;
emit("update:drawerRight",false )
}
return {
cancel,
save,
actionProps,
showPanel
}
}
});
</script>
<style lang="scss">
</style>

View File

@@ -0,0 +1,36 @@
<template>
<q-select v-model="selectedValue" :label="placeholder" :options="options"/>
</template>
<script lang="ts">
import { defineComponent,ref,watchEffect } from 'vue';
export default defineComponent({
name: 'SelectBox',
props: {
placeholder: {
type: String,
default: '',
},
options: {
type: Array,
required: true,
},
modelValue: {
type: String,
default: '',
},
},
setup(props, { emit }) {
const selectedValue = ref(props.modelValue);
watchEffect(() => {
emit('update:modelValue', selectedValue.value);
});
return {
selectedValue
};
},
});
</script>

View File

@@ -56,10 +56,17 @@ const essentialLinks: EssentialLinkProps[] = [
target:'_self'
},
{
title: 'ルールエディター',
caption: 'rule',
icon: 'rule',
link: '/#/ruleEditor',
title: 'フローエディター',
caption: 'flowChart',
icon: 'account_tree',
link: '/#/flowChart',
target:'_self'
},
{
title: 'FlowEditor',
caption: 'FlowEditor',
icon: 'account_tree',
link: '/#/flowEditor',
target:'_self'
},
{

View File

@@ -0,0 +1,102 @@
<template>
<q-page>
<div class="flowchart">
<node-item v-for="(node,) in refFlow.actionNodes" :key="node.id"
:isSelected="node===state.activeNode" :actionNode="node"
@addNode="addNode"
@nodeSelected="onNodeSelected"
@nodeEdit="onNodeEdit"
@deleteNode="onDeleteNode"
@deleteAllNextNodes="onDeleteAllNextNodes"
></node-item>
</div>
</q-page>
<PropertyPanel :actionNode="state.activeNode" v-model:drawerRight="drawerRight"></PropertyPanel>
<show-dialog v-model:visible="showAddAction" name="アクション" @close="closeDg">
<action-select ref="appDg" name="アクション" type="single"></action-select>
</show-dialog>
</template>
<script setup lang="ts">
import {ref,reactive,computed} from 'vue';
import {IActionNode, ActionNode, IActionFlow, ActionFlow,RootAction, IActionProperty } from 'src/types/ActionTypes';
import NodeItem from 'src/components/main/NodeItem.vue';
import ShowDialog from 'components/ShowDialog.vue';
import ActionSelect from 'components/ActionSelect.vue';
import PropertyPanel from 'components/right/PropertyPanel.vue';
const rootNode:RootAction =new RootAction("app.record.create.submit","レコード追加画面","保存するとき");
const actionFlow: ActionFlow = new ActionFlow(rootNode);
const saibanProps:IActionProperty[]=[{
component:"InputText",
props:{
displayName:"フォーマット",
modelValue:"",
name:"format",
placeholder:"フォーマットを入力してください",
}
},{
component:"FieldInput",
props:{
displayName:"採番項目",
modelValue:"",
name:"field",
placeholder:"採番項目を選択してください",
}
}];
actionFlow.addNode(new ActionNode('自動採番','文書番号を自動採番する','',[],saibanProps));
actionFlow.addNode(new ActionNode('入力データ取得','電話番号を取得する',''));
const branchNode = actionFlow.addNode(new ActionNode('条件分岐','電話番号入力形式チャック','',['はい','いいえ'] ));
actionFlow.addNode(new ActionNode('入力データ取得','住所を取得する',''),branchNode,'はい');
actionFlow.addNode(new ActionNode('エラー表示','エラー表示して保存しない',''),branchNode,'いいえ' );
// ref関数を使ってtemplateとバインド
const state=reactive({
activeNode:rootNode,
})
const refFlow = ref(actionFlow);
const showAddAction=ref(false);
const drawerRight=ref(false);
const addActionNode=(action:IActionNode)=>{
refFlow.value.actionNodes.push(action);
}
const addNode=(node:IActionNode,inputPoint:string)=>{
showAddAction.value=true;
}
const onNodeSelected=(node:IActionNode)=>{
//右パネルが開いている場合、自動閉じる
if(drawerRight.value && state.activeNode.id!==node.id){
drawerRight.value=false;
}
state.activeNode = node;
}
const onNodeEdit=(node:IActionNode)=>{
state.activeNode = node;
drawerRight.value=true;
}
const onDeleteNode=(node:IActionNode)=>{
refFlow.value.removeNode(node);
}
const onDeleteAllNextNodes=(node:IActionNode)=>{
refFlow.value.removeAllNext(node.id);
}
const closeDg=(val :any)=>{
console.log("Dialog closed->",val);
}
</script>
<style lang="scss">
.flowchart{
padding-top: 10px;
}
</style>

View File

@@ -0,0 +1,37 @@
<template>
<q-page>
<div class="q-pa-md">
<div class="q-gutter-sm row items-start">
<q-breadcrumbs>
<q-breadcrumbs-el icon="home" to="/" />
<q-breadcrumbs-el :label="title" icon="rule" />
</q-breadcrumbs>
</div>
<div class="q-pa-md">
<div class="row">
<div class="col-2 column">
<ItemSelector />
<div class="col-auto"><ControlPanel /></div>
</div>
<!-- <div class="col">
</div> -->
</div>
</div>
</div>
</q-page>
</template>
<script setup lang="ts">
import ItemSelector from 'components/flowEditor/left/ItemSelector.vue';
import ControlPanel from 'components/flowEditor/left/ControlPanelC.vue';
interface FlowEditorPageProps {
title: string;
}
const props = withDefaults(defineProps<FlowEditorPageProps>(), {
title: 'FlowEditor',
});
</script>
<style lang="sass"></style>

View File

@@ -0,0 +1,96 @@
<template>
<div class="q-pa-md q-gutter-sm">
<q-btn label="プロパティ" icon="keyboard_arrow_right" color="primary" @click="open('right')" />
<!-- <q-btn label="Readプロパティ" icon="keyboard_arrow_right" color="primary" @click="write('right')" /> -->
<q-dialog v-model="dialog" :position="position">
<q-card class="column full-height" style="width: 300px">
<q-card-section>
<div class="text-h6">プロパティ</div>
</q-card-section>
<q-card-section class="col q-pt-none">
<ActionProperty :jsonData="jsonData" :jsonValue="jsonValue"/>
</q-card-section>
<q-card-actions align="right" class="bg-white text-teal">
<q-btn flat label="Save" v-close-popup @click="save"/>
<q-btn flat label="Cancel" v-close-popup />
</q-card-actions>
</q-card>
</q-dialog>
</div>
</template>
<script setup lang="ts">
import { ref,onMounted } from 'vue'
import ActionProperty from 'components/right/ActionProperty.vue';
const dialog = ref(false)
const position = ref('top')
const jsonData = {
elements: [
{
component: 'InputText',
props: {
name:'1',
placeholder: 'Enter some text',
modelValue: '',
},
},
{
component: 'SelectBox',
props: {
name:'2',
placeholder: 'Choose an option',
modelValue: '',
options: [
'option1',
'option2',
'option3'
],
},
},
{
component: 'DatePicker',
props: {
name:'3',
placeholder: 'Choose a date',
modelValue: '',
},
},
{
component: 'FieldInput',
props: {
name:'4',
placeholder: 'Choose a field',
modelValue: '',
},
},
]
};
let jsonValue = {
1:'abc',
2:'option2',
3:'2023/09/04',
4:'6666'
};
const open = (pos:string) => {
position.value = pos
dialog.value = true
};
const save = async () =>{
jsonData.elements.forEach(property => {
if(jsonValue != undefined)
{
jsonValue[property.props.name] = property.props.modelValue;
}
});
console.log(jsonValue);
}
</script>

View File

@@ -0,0 +1,101 @@
<template>
<div class="q-pa-md q-gutter-sm">
<q-btn label="プロパティ" icon="keyboard_arrow_right" color="primary" @click="drawerRight = !drawerRight" />
<!-- <q-btn label="Readプロパティ" icon="keyboard_arrow_right" color="primary" @click="write('right')" /> -->
<q-drawer
side="right"
v-model="drawerRight"
show-if-above
bordered
:width="301"
:breakpoint="500"
class="bg-grey-3"
>
<q-card class="column full-height" style="width: 300px">
<q-card-section>
<div class="text-h6">プロパティ</div>
</q-card-section>
<q-card-section class="col q-pt-none">
<ActionProperty :jsonData="jsonData" :jsonValue="jsonValue" v-if="drawerRight"/>
</q-card-section>
<q-card-actions align="right" class="bg-white text-teal">
<q-btn flat label="Save" @click="save"/>
<q-btn flat label="Cancel" @click="cancel" />
</q-card-actions>
</q-card>
</q-drawer>
</div>
</template>
<script setup lang="ts">
import { reactive, ref } from 'vue'
import ActionProperty from 'components/right/ActionProperty.vue';
const drawerRight = ref(false);
const jsonData = {
elements: [
{
component: 'InputText',
props: {
name:'1',
placeholder: 'Enter some text',
modelValue: '',
},
},
{
component: 'SelectBox',
props: {
name:'2',
placeholder: 'Choose an option',
modelValue: '',
options: [
'option1',
'option2',
'option3'
],
},
},
{
component: 'DatePicker',
props: {
name:'3',
placeholder: 'Choose a date',
modelValue: '',
},
},
{
component: 'FieldInput',
props: {
name:'4',
placeholder: 'Choose a field',
modelValue: '',
},
},
]
};
let jsonValue = {
1:'abc',
2:'option2',
3:'2023/09/04',
4:'6666'
};
const cancel = async() =>{
drawerRight.value = false;
}
const save = async () =>{
jsonData.elements.forEach(property => {
if(jsonValue != undefined)
{
jsonValue[property.props.name] = property.props.modelValue;
}
});
console.log(jsonValue);
drawerRight.value=false;
}
</script>

View File

@@ -4,19 +4,16 @@ const routes: RouteRecordRaw[] = [
{
path: '/',
component: () => import('layouts/MainLayout.vue'),
children: [{ path: '', component: () => import('pages/IndexPage.vue') }],
children: [
{ path: '', component: () => import('pages/IndexPage.vue') },
{ path: 'ruleEditor', component: () => import('pages/RuleEditor.vue') },
{ path: 'test', component: () => import('pages/testQursar.vue') },
{ path: 'flow', component: () => import('pages/testFlow.vue') },
{ path: 'flowchart', component: () => import('pages/FlowChartTest.vue') },
{ path: 'flowEditor', component: () => import('pages/FlowEditorPage.vue') },
{ path: 'right', component: () => import('pages/testRight.vue') },
],
},
{
path: '/ruleEditor/',
component: () => import('layouts/MainLayout.vue'),
children: [{ path: '', component: () => import('pages/RuleEditor.vue') }],
},
{
path: '/test/',
component: () => import('layouts/MainLayout.vue'),
children: [{ path: '', component: () => import('pages/testQursar.vue') }],
},
// Always leave this as last one,
// but you can also remove it
{

View File

@@ -0,0 +1,357 @@
import { v4 as uuidv4 } from 'uuid';
/**
* アクションのプロパティ定義
*/
export interface IActionProperty {
component: string;
props: {
//プロパティ名
name: string;
//プロパティ表示名
displayName:string;
placeholder: string;
//プロパティ設定値
modelValue: any;
};
}
/**
* アクションタイプ定義
*/
export interface IActionNode{
id:string;
//アクション名
name:string;
title:string;
subTitle:string;
inputPoint:string;
//出力ポイント(条件分岐以外未使用)
outputPoints:Array<string>;
//ルートアクションKintone event
isRoot:boolean;
//アクションのプロパティ定義
actionProps:Array<IActionProperty>;
//アクションのプロパティ設定値抽出
ActionValue:object
prevNodeId?: string;
nextNodeIds: Map<string, string>;
}
/**
* アクションフローの定義
*/
export interface IActionFlow {
actionNodes:Array<IActionNode>
}
/**
* アクションのプロパティ定義に基づいたクラス
*/
class ActionProperty implements IActionProperty {
component: string;
props: {
// プロパティ名
name: string;
// プロパティ表示名
displayName: string;
placeholder: string;
// プロパティ設定値
modelValue: any;
};
static defaultProperty():IActionProperty{
return new ActionProperty('InputText','displayName','表示名','表示を入力してください','');
};
constructor(
component: string,
name: string,
displayName: string,
placeholder: string,
modelValue: any
) {
this.component = component;
this.props = {
name: name,
displayName: displayName,
placeholder: placeholder,
modelValue: modelValue
};
}
}
/**
* IActionNodeの実装、RootActionNode以外のアクション定義
*/
export class ActionNode implements IActionNode {
id:string;
name: string;
title:string;
get subTitle():string{
return this.name;
};
inputPoint:string;
//出力ポイント(条件分岐以外未使用)
outputPoints:Array<string>;
actionProps: Array<IActionProperty>;
get isRoot(): boolean{
return false;
};
get ActionValue():object{
const propValue:any={};
this.actionProps.forEach((value)=>{
propValue[value.props.name]=value.props.modelValue
});
return propValue;
};
prevNodeId?: string;
nextNodeIds: Map<string, string>;
constructor(
name: string,
title:string,
inputPoint:string,
outputPoint: Array<string> = [],
actionProps: Array<IActionProperty> =[ActionProperty.defaultProperty()]
) {
this.id=uuidv4();
this.name = name;
this.title=title;
this.inputPoint=inputPoint;
this.outputPoints = outputPoint;
const defProp =ActionProperty.defaultProperty();
defProp.props.displayName=title;
this.actionProps =actionProps;
const prop = this.actionProps.find((prop)=>prop.props.name===defProp.props.name);
if(prop===undefined){
this.actionProps.unshift(defProp);
}
this.nextNodeIds=new Map<string,string>();
}
}
/**
* ルートアクション定義
*/
export class RootAction implements IActionNode {
id:string;
name: string;
title:string;
subTitle:string;
inputPoint:string;
//出力ポイント(条件分岐以外未使用)
outputPoints:Array<string>;
isRoot: boolean;
actionProps: Array<IActionProperty>;
ActionValue:object;
prevNodeId?: string = undefined;
nextNodeIds: Map<string, string>;
constructor(
name: string,
title:string,
subTitle:string,
) {
this.id=uuidv4();
this.name = name;
this.title=title;
this.subTitle=subTitle;
this.inputPoint='';
this.outputPoints = [];
this.isRoot = true;
this.actionProps=[];
this.ActionValue={};
this.nextNodeIds=new Map<string,string>();
}
}
/**
* アクションフローの定義
*/
export class ActionFlow implements IActionFlow {
actionNodes:Array<IActionNode>;
constructor(actionNodes:Array<IActionNode>|RootAction){
if(actionNodes instanceof Array){
this.actionNodes=actionNodes;
}else{
this.actionNodes=[actionNodes];
}
}
/**
* ノードを追加する
* 1.ID採番する
* 2.前のノードを関連付け
* @param newNode 新規追加するノード
* @param prevNode 前のノード
* @param inputPoint 入力ポイント
* @returns 追加されたノード
*/
addNode(
newNode:IActionNode,
prevNode?:IActionNode,
inputPoint?:string):IActionNode
{
if(inputPoint!==undefined){
newNode.inputPoint=inputPoint;
}
if(prevNode){
this.connectNodes(prevNode,newNode,inputPoint||'');
}else{
prevNode=this.actionNodes[this.actionNodes.length-1];
this.connectNodes(prevNode,newNode,inputPoint||'');
}
this.actionNodes.push(newNode);
return newNode;
}
/**
* ノードを削除する
* @param delNode
*/
removeNode(targetNode :IActionNode):boolean{
if (!targetNode ) {
return false;
}
if(targetNode.isRoot){
return false;
}
this.disconnectFromPrevNode(targetNode);
this.reconnectOrRemoveNextNodes(targetNode);
this.removeFromActionNodes(targetNode.id);
return true;
}
/***
* 目標ノードの次のノードを全部削除する
*/
removeAllNext(targetNodeId :string){
if (!targetNodeId || targetNodeId==='') {
return false;
}
const targetNode=this.findNodeById(targetNodeId);
if(!targetNode){
return false;
}
if(targetNode.nextNodeIds.size==0){
return false;
}
for (const [, id] of targetNode.nextNodeIds) {
this.removeAllNext(id);
this.removeFromActionNodes(id);
}
}
// 断开与前一个节点的连接
disconnectFromPrevNode(targetNode: IActionNode): void {
const prevNodeId = targetNode.prevNodeId;
if (prevNodeId) {
const prevNode = this.findNodeById(prevNodeId);
if (prevNode) {
for (const [key, value] of prevNode.nextNodeIds) {
if (value === targetNode.id) {
prevNode.nextNodeIds.delete(key);
}
}
}
}
}
// 从 actionNodes 数组中移除节点
private removeFromActionNodes(targetNodeId: string): void {
const index = this.actionNodes.findIndex(node => node.id === targetNodeId);
if (index > -1) {
this.actionNodes.splice(index, 1);
}
}
/**
* ノード削除時、前のノードと次のノードを接続する
* @param targetNode
*/
reconnectOrRemoveNextNodes(targetNode: IActionNode): void {
if(!targetNode || !targetNode.prevNodeId ){
return;
}
//前のノードを取得
const prevNode = this.findNodeById(targetNode.prevNodeId);
if(!prevNode) return;
//次のノード取得
const nextNodeIds = targetNode.nextNodeIds;
if(nextNodeIds.size==0){
return;
}
//次のノード一つの場合
if(nextNodeIds.size==1){
const nextNodeId = nextNodeIds.get('');
if(!nextNodeId) return;
const nextNode = this.findNodeById(nextNodeId) ;
if(!nextNode) return;
nextNode.prevNodeId=prevNode.id;
prevNode.nextNodeIds.set(targetNode.inputPoint||'',nextNodeId);
return;
}
//二つ以上の場合
for(const [point,nextid] of nextNodeIds){
const nextNode = this.findNodeById(nextid);
if(!this.connectNodes(prevNode,nextNode,point)){
this.removeAllNext(nextid);
this.removeFromActionNodes(nextid);
}
}
}
/**
* 二つノードを接続する
* @param prevNode
* @param nextNodeId
* @param point
* @returns
*/
connectNodes(prevNode:IActionNode,nextNode:IActionNode,point:string):boolean{
if(!prevNode || !nextNode){
return false;
}
if(!nextNode) return false;
prevNode.nextNodeIds.set(point,nextNode.id);
nextNode.prevNodeId=prevNode.id;
nextNode.inputPoint=point;
return true;
}
resetNodeRelation(prevNode: IActionNode, newNode: IActionNode, inputPoint?: string) {
// 设置新节点和前节点的关联
prevNode.nextNodeIds.set(inputPoint || '', newNode.id);
newNode.prevNodeId = prevNode.id;
// 保存前节点原有的后节点ID
const originalNextNodeId = prevNode.nextNodeIds.get(inputPoint || '');
this.setNewNodeNextId(newNode,originalNextNodeId,inputPoint);
}
/**
* 後ノードと新ノードの関連付け
* @param newNode
* @param originalNextNodeId
* @param inputPoint
*/
private setNewNodeNextId(newNode: IActionNode, originalNextNodeId: string | undefined, inputPoint?: string) {
// 如果原先的后节点存在
if (originalNextNodeId) {
// 检查新节点的 outputPoints 是否包含该 inputPoint
if (newNode.outputPoints.includes(inputPoint || '')) {
newNode.nextNodeIds.set(inputPoint || '', originalNextNodeId);
} else {
// 如果不包含,选择新节点的一个 outputPoint
const alternativeOutputPoint = newNode.outputPoints.length > 0 ? newNode.outputPoints[0] : '';
newNode.nextNodeIds.set(alternativeOutputPoint, originalNextNodeId);
}
}
}
/***
* IDでActionNodeを取得する
*/
findNodeById(id: string): IActionNode | undefined {
return this.actionNodes.find((node) => node.id === id);
}
}

View File

@@ -278,6 +278,11 @@
"@types/mime" "*"
"@types/node" "*"
"@types/uuid@^9.0.3":
version "9.0.3"
resolved "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.3.tgz"
integrity sha512-taHQQH/3ZyI3zP8M/puluDEIEvtQHVYcC6y3N8ijFtAd28+Ey/G4sg1u2gB01S8MwybLOKAp9/yCMu/uR5l3Ug==
"@typescript-eslint/eslint-plugin@^5.10.0":
version "5.61.0"
resolved "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.61.0.tgz"
@@ -2707,6 +2712,11 @@ utils-merge@1.0.1:
resolved "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz"
integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==
uuid@^9.0.0:
version "9.0.0"
resolved "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz"
integrity sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==
vary@~1.1.2:
version "1.1.2"
resolved "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz"