diff --git a/backend/app/api/api_v1/routers/platform.py b/backend/app/api/api_v1/routers/platform.py index c6f4c8f..d309d72 100644 --- a/backend/app/api/api_v1/routers/platform.py +++ b/backend/app/api/api_v1/routers/platform.py @@ -3,6 +3,7 @@ from app.db import Base,engine from app.db.session import get_db from app.db.crud import * from app.db.schemas import * +from typing import List platform_router = r = APIRouter() @@ -91,7 +92,21 @@ async def flow_details( ): app = get_flow(db, flowid) return app - + + +@r.get( + "/flows/{appid}", + response_model=List[Flow], + response_model_exclude_none=True, +) +async def flow_list( + request: Request, + appid: str, + db=Depends(get_db), +): + flows = get_flows_by_app(db, appid) + return flows + @r.post("/flow", response_model=Flow, response_model_exclude_none=True) async def flow_create( diff --git a/backend/app/db/crud.py b/backend/app/db/crud.py index b0cf96d..c8dfea3 100644 --- a/backend/app/db/crud.py +++ b/backend/app/db/crud.py @@ -173,4 +173,10 @@ def get_flow(db: Session, flowid: str): flow = db.query(models.Flow).filter(models.Flow.flowid == flowid).first() if not flow: raise HTTPException(status_code=404, detail="Data not found") - return flow \ No newline at end of file + return flow + +def get_flows_by_app(db: Session, appid: str): + flows = db.query(models.Flow).filter(models.Flow.appid == appid).all() + if not flows: + raise HTTPException(status_code=404, detail="Data not found") + return flows \ No newline at end of file diff --git a/document/kintone開発自動化ツール UIデザイン案.pptx b/document/kintone開発自動化ツール UIデザイン案.pptx new file mode 100644 index 0000000..48781f1 Binary files /dev/null and b/document/kintone開発自動化ツール UIデザイン案.pptx differ diff --git a/frontend/src/components/ActionList.vue b/frontend/src/components/ActionList.vue deleted file mode 100644 index e69de29..0000000 diff --git a/frontend/src/components/ActionSelect.vue b/frontend/src/components/ActionSelect.vue index 9bf184a..bd8dfd0 100644 --- a/frontend/src/components/ActionSelect.vue +++ b/frontend/src/components/ActionSelect.vue @@ -1,6 +1,12 @@ - + + diff --git a/frontend/src/components/left/AppSelector.vue b/frontend/src/components/left/AppSelector.vue new file mode 100644 index 0000000..369386b --- /dev/null +++ b/frontend/src/components/left/AppSelector.vue @@ -0,0 +1,50 @@ + + + + + + {{ selectedApp.name }} + + + + + + + + + diff --git a/frontend/src/components/left/EventTree.vue b/frontend/src/components/left/EventTree.vue new file mode 100644 index 0000000..04f96a4 --- /dev/null +++ b/frontend/src/components/left/EventTree.vue @@ -0,0 +1,72 @@ + + + + + + + + {{ prop.node.label }} + + + + + + + + diff --git a/frontend/src/control/flowctrl.ts b/frontend/src/control/flowctrl.ts index 883ceb9..81e7d67 100644 --- a/frontend/src/control/flowctrl.ts +++ b/frontend/src/control/flowctrl.ts @@ -1,8 +1,23 @@ import { api } from 'boot/axios'; +import { ActionFlow } from 'src/types/ActionTypes'; export class FlowCtrl { + async getFlows(appId:string):Promise + { + const result = await api.get(`http://127.0.0.1:8000/api/flows/${appId}`); + //console.info(result.data); + if(!result.data || !Array.isArray(result.data)){ + return []; + } + const flows:ActionFlow[]=[]; + for(const flow of result.data){ + flows.push(ActionFlow.fromJSON(flow.content)); + } + return flows; + } + async SaveFlow(jsonData:any):Promise { const result = await api.post('http://127.0.0.1:8000/api/flow',jsonData); diff --git a/frontend/src/pages/FlowChart.vue b/frontend/src/pages/FlowChart.vue new file mode 100644 index 0000000..f430317 --- /dev/null +++ b/frontend/src/pages/FlowChart.vue @@ -0,0 +1,165 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/src/pages/FlowChartTest.vue b/frontend/src/pages/FlowChartTest.vue index 1d0917f..b98881e 100644 --- a/frontend/src/pages/FlowChartTest.vue +++ b/frontend/src/pages/FlowChartTest.vue @@ -1,5 +1,6 @@ + - + diff --git a/frontend/src/router/routes.ts b/frontend/src/router/routes.ts index f8464b6..a3eb0e2 100644 --- a/frontend/src/router/routes.ts +++ b/frontend/src/router/routes.ts @@ -11,7 +11,8 @@ const routes: RouteRecordRaw[] = [ { path: 'flow', component: () => import('pages/testFlow.vue') }, { path: 'flowchart', component: () => import('pages/FlowChartTest.vue') }, { path: 'flowEditor', component: () => import('pages/FlowEditorPage.vue') }, - { path: 'flowEditor2', component: () => import('pages/FlowEditorPage2.vue') }, + { path: 'flowEditor2', component: () => import('pages/FlowChart.vue') }, + { path: 'flowChart2', component: () => import('pages/FlowEditorPage2.vue') }, { path: 'right', component: () => import('pages/testRight.vue') }, ], }, diff --git a/frontend/src/stores/flowEditor.ts b/frontend/src/stores/flowEditor.ts index 8ca0fa3..c89a384 100644 --- a/frontend/src/stores/flowEditor.ts +++ b/frontend/src/stores/flowEditor.ts @@ -1,25 +1,33 @@ import { defineStore } from 'pinia'; +import { ActionFlow,AppInfo } from 'src/types/ActionTypes'; -export const useFlowEditorStore = defineStore('flowEditor', { - state: () => ({ - counter: 0, - flowNames: [], - flowNames1: '' - }), +export interface FlowEditorState{ + flowNames1:string; + appInfo?:AppInfo; + flows?:ActionFlow[]; + selectedFlow?:ActionFlow|undefined; +} +export const useFlowEditorStore = defineStore("flowEditor",{ + state: ():FlowEditorState => ({ + flowNames1: '', + appInfo:undefined, + flows:undefined, + selectedFlow:undefined + }), getters: { - doubleCount(state) { - return state.counter * 2; + currentFlow():ActionFlow|undefined{ + return this.selectedFlow; } }, actions: { - increment() { - this.counter++; + setFlows(flows:ActionFlow[]){ + this.flows=flows; }, - - setDefaultFlow() { - this.counter++ + selectFlow(flow:ActionFlow){ + this.selectedFlow=flow; } + } }); diff --git a/frontend/src/types/ActionTypes.ts b/frontend/src/types/ActionTypes.ts index 88ff97f..06e2dab 100644 --- a/frontend/src/types/ActionTypes.ts +++ b/frontend/src/types/ActionTypes.ts @@ -1,5 +1,13 @@ import { v4 as uuidv4 } from 'uuid'; - +/** + * アプリ情報 + */ +export interface AppInfo { + appId:string; + code?:string; + name:string; + description?:string; +} /** * アクションのプロパティ定義 @@ -41,6 +49,7 @@ export interface IActionNode{ * アクションフローの定義 */ export interface IActionFlow { + id:string; actionNodes:Array } @@ -112,7 +121,7 @@ export class ActionNode implements IActionNode { title:string, inputPoint:string, outputPoint: Array = [], - actionProps: Array =[ActionProperty.defaultProperty()] + actionProps: Array =[ActionProperty.defaultProperty()], ) { this.id=uuidv4(); this.name = name; @@ -168,7 +177,7 @@ export class RootAction implements IActionNode { * アクションフローの定義 */ export class ActionFlow implements IActionFlow { - + id:string; actionNodes:Array; constructor(actionNodes:Array|RootAction){ if(actionNodes instanceof Array){ @@ -176,6 +185,7 @@ export class ActionFlow implements IActionFlow { }else{ this.actionNodes=[actionNodes]; } + this.id=uuidv4(); } /** * ノードを追加する @@ -354,5 +364,38 @@ reconnectOrRemoveNextNodes(targetNode: IActionNode): void { return this.actionNodes.find((node) => node.id === id); } + toJSON() { + return { + id:this.id, + actionNodes: this.actionNodes.map(node => { + const { nextNodeIds, ...rest } = node; + return { + ...rest, + nextNodeIds: Array.from(nextNodeIds.entries()) + }; + }) + }; + } + + getRoot():IActionNode|undefined{ + return this.actionNodes.find(node=>node.isRoot) + } + + static fromJSON(json: string): ActionFlow { + const parsedObject = JSON.parse(json); + + const actionNodes = parsedObject.actionNodes.map((node: any) => { + const nodeClass = !node.isRoot? new ActionNode(node.name,node.title,node.inputPoint,node.outputPoint,node.actionProps) + :new RootAction(node.name,node.title,node.subTitle); + nodeClass.nextNodeIds=new Map(node.nextNodeIds); + nodeClass.prevNodeId=node.prevNodeId; + return nodeClass; + }); + const actionFlow = new ActionFlow(actionNodes); + actionFlow.id=parsedObject.id; + return actionFlow; + } + + } diff --git a/frontend/src/types/KintoneEvents.ts b/frontend/src/types/KintoneEvents.ts new file mode 100644 index 0000000..48025fc --- /dev/null +++ b/frontend/src/types/KintoneEvents.ts @@ -0,0 +1,138 @@ +import {IActionFlow} from './ActionTypes'; +export interface TreeNode { + label: string; +} + +export interface KintoneEvent extends TreeNode { + eventId: string; + hasFlow: boolean; + flowData?: IActionFlow; +} + +export interface KintoneScreen extends TreeNode { + label: string; + events: KintoneEvent[]; +} + + +export class KintoneEventManager { + public screens: KintoneScreen[]; + + constructor(screens: KintoneScreen[]) { + this.screens = screens; + } + + public findEventById(eventId: string): KintoneEvent | null { + for (const screen of this.screens) { + for (const event of screen.events) { + if (event.eventId === eventId) { + return event; + } + } + } + return null; + } +} + +export const kintoneEvents:KintoneEventManager = new KintoneEventManager([ + { + label:"レコード追加画面", + events:[ + { + label:"レコード追加画面を表示した後", + eventId:"app.record.create.show", + hasFlow:false + }, + { + label:"保存をクリックしたとき", + eventId:"app.record.create.submit", + hasFlow:true + }, + { + label:"保存が成功したとき", + eventId:"app.record.create.submit.success ", + hasFlow:false + }, + { + label:"フィールドの値を変更したとき", + eventId:"app.record.create.change", + hasFlow:false + }, + ] + }, + { + label:"レコード詳細画面", + events:[ + { + label:"レコード詳細画面を表示した後", + eventId:"app.record.detail.show", + hasFlow:false + }, + { + label:"レコードを削除するとき", + eventId:"app.record.detail.delete.submit", + hasFlow:false + }, + { + label:"プロセス管理のアクションを実行したとき", + eventId:"app.record.detail.process.proceed", + hasFlow:false + }, + ] + }, + { + label:"レコード編集画面", + events:[ + { + label:"レコード編集画面を表示した後", + eventId:"app.record.edit.show", + hasFlow:false + }, + { + label:"保存をクリックしたとき", + eventId:"app.record.edit.submit", + hasFlow:false + }, + { + label:"保存が成功したとき", + eventId:"app.record.edit.submit.success", + hasFlow:false + }, + { + label:"フィールドの値を変更したとき", + eventId:"app.record.edit.change", + hasFlow:false + }, + ] + }, + { + label:"レコード一覧画面", + events:[ + { + label:"一覧画面を表示した後", + eventId:"app.record.index.show", + hasFlow:false + }, + { + label:"インライン編集を開始したとき", + eventId:"app.record.index.edit.show", + hasFlow:false + }, + { + label:"インライン編集のフィールド値を変更したとき", + eventId:"app.record.index.edit.change", + hasFlow:false + }, + { + label:"インライン編集の【保存】をクリックしたとき", + eventId:"app.record.index.edit.submit", + hasFlow:false + }, + { + label:"インライン編集の保存が成功したとき", + eventId:"app.record.index.edit.submit.success", + hasFlow:false + }, + ] + } +]); diff --git a/sample.json b/sample.json new file mode 100644 index 0000000..a5f9984 --- /dev/null +++ b/sample.json @@ -0,0 +1,137 @@ +{ + "actionNodes": [ + { + "id": "c9fa2f6a-a05b-4a2b-af8b-00addae2de6f", + "name": "app.record.create.submit", + "title": "レコード追加画面", + "subTitle": "保存するとき", + "inputPoint": "", + "outputPoints": [], + "isRoot": true, + "actionProps": [], + "ActionValue": {}, + "nextNodeIds": [ + [ + "", + "4b39f388-f7f2-4ccd-ace2-995e60893fd3" + ] + ] + }, + { + "id": "4b39f388-f7f2-4ccd-ace2-995e60893fd3", + "name": "自動採番", + "title": "文書番号を自動採番する", + "inputPoint": "", + "outputPoints": [], + "actionProps": [ + { + "component": "InputText", + "props": { + "name": "displayName", + "displayName": "文書番号を自動採番する", + "placeholder": "表示を入力してください", + "modelValue": "" + } + }, + { + "component": "InputText", + "props": { + "displayName": "フォーマット", + "modelValue": "", + "name": "format", + "placeholder": "フォーマットを入力してください" + } + }, + { + "component": "FieldInput", + "props": { + "displayName": "採番項目", + "modelValue": "", + "name": "field", + "placeholder": "採番項目を選択してください" + } + } + ], + "prevNodeId": "c9fa2f6a-a05b-4a2b-af8b-00addae2de6f", + "nextNodeIds": [ + [ + "", + "22cac85a-005b-434a-8ef6-25eb536e0ad3" + ] + ] + }, + { + "id": "22cac85a-005b-434a-8ef6-25eb536e0ad3", + "name": "入力データ取得", + "title": "電話番号を取得する", + "inputPoint": "", + "outputPoints": [], + "actionProps": [ + { + "component": "InputText", + "props": { + "name": "displayName", + "displayName": "表示名", + "placeholder": "表示を入力してください", + "modelValue": "" + } + } + ], + "prevNodeId": "4b39f388-f7f2-4ccd-ace2-995e60893fd3", + "nextNodeIds": [ + [ + "", + "c39e6d90-0063-4fdb-b3b6-9db396e402fe" + ] + ] + }, + { + "id": "c39e6d90-0063-4fdb-b3b6-9db396e402fe", + "name": "条件分岐", + "title": "電話番号入力形式チャック", + "inputPoint": "", + "outputPoints": [ + "はい", + "いいえ" + ], + "actionProps": [ + { + "component": "InputText", + "props": { + "name": "displayName", + "displayName": "表示名", + "placeholder": "表示を入力してください", + "modelValue": "" + } + } + ], + "prevNodeId": "22cac85a-005b-434a-8ef6-25eb536e0ad3", + "nextNodeIds": [ + [ + "いいえ", + "e5464858-b536-4cb0-be08-396daf5092f5" + ] + ] + }, + { + "id": "e5464858-b536-4cb0-be08-396daf5092f5", + "name": "エラー表示", + "title": "エラー表示して保存しない", + "inputPoint": "いいえ", + "outputPoints": [], + "actionProps": [ + { + "component": "InputText", + "props": { + "name": "displayName", + "displayName": "表示名", + "placeholder": "表示を入力してください", + "modelValue": "" + } + } + ], + "prevNodeId": "c39e6d90-0063-4fdb-b3b6-9db396e402fe", + "nextNodeIds": [] + } + ] +} \ No newline at end of file