Compare commits

...

10 Commits

Author SHA1 Message Date
76457b6667 UserDomain Add 2023-10-09 15:55:58 +09:00
e1f2afa942 sqlserver->postgresql 2023-09-30 15:16:05 +09:00
8d5dff60f1 Merge branch 'maxz-real-impl' into mvp_step2_dev 2023-09-30 13:33:39 +09:00
461cd26690 add app dialog 2023-09-30 13:12:08 +09:00
4c6b2ea844 DB連動実装 2023-09-27 14:35:24 +09:00
6aa057e590 flow api add 2023-09-21 07:22:12 +00:00
2721cd60d1 action bugfix 2023-09-18 02:54:53 +00:00
1f8d079d4d action bugfix 2023-09-18 02:54:35 +00:00
3ae685a0e2 action add subtitle 2023-09-17 08:54:54 +00:00
c1e50736e8 action table add 2023-09-16 01:52:15 +00:00
29 changed files with 1447 additions and 49 deletions

View File

@@ -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()
@@ -93,6 +94,20 @@ async def flow_details(
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(
request: Request,
@@ -123,3 +138,49 @@ async def flow_delete(
):
return delete_flow(db, flowid)
@r.get(
"/domain/{userid}",
response_model=List[Domain],
response_model_exclude_none=True,
)
async def domain_details(
request: Request,
userid: str,
db=Depends(get_db),
):
domains = get_domain(db, userid)
return domains
@r.post("/domain", response_model=Domain, response_model_exclude_none=True)
async def domain_create(
request: Request,
domain: DomainBase,
db=Depends(get_db),
):
return create_domain(db, domain)
@r.put(
"/domain", response_model=Domain, response_model_exclude_none=True
)
async def domain_edit(
request: Request,
domain: DomainBase,
db=Depends(get_db),
):
return edit_domain(db, domain)
@r.delete(
"/domain/{userid}/{id}", response_model=Domain, response_model_exclude_none=True
)
async def domain_delete(
request: Request,
userid: int,
id: int,
db=Depends(get_db),
):
return delete_domain(db, userid,id)

View File

@@ -2,7 +2,7 @@ import os
PROJECT_NAME = "KintoneAppBuilder"
SQLALCHEMY_DATABASE_URI = "mssql+pymssql://maxz64@maxzdb:m@xz1205@maxzdb.database.windows.net/alloc"
SQLALCHEMY_DATABASE_URI = "postgres://maxz64:m@xz1205@alicornkintone.postgres.database.azure.com/postgres"
BASE_URL = "https://mfu07rkgnb7c.cybozu.com"

View File

@@ -174,3 +174,54 @@ def get_flow(db: Session, flowid: str):
if not flow:
raise HTTPException(status_code=404, detail="Data not found")
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
def create_domain(db: Session, domain: schemas.DomainBase):
db_domain = models.UserDomain(
userid=domain.userid,
name=domain.name,
url=domain.url,
kintoneuser=domain.kintoneuser,
kintonepwd=domain.kintonepwd
)
db.add(db_domain)
db.commit()
db.refresh(db_domain)
return db_domain
def delete_domain(db: Session, userid: int,id: int):
db_domain = db.query(models.UserDomain).get(id)
if not db_domain or db_domain.userid != userid:
raise HTTPException(status.HTTP_404_NOT_FOUND, detail="Domain not found")
db.delete(db_domain)
db.commit()
return db_domain
def edit_domain(
db: Session, domain: schemas.DomainBase
) -> schemas.Domain:
db_domain = db.query(models.UserDomain).get(domain.id)
if not db_domain or db_domain.userid != domain.userid:
raise HTTPException(status.HTTP_404_NOT_FOUND, detail="Domain not found")
update_data = domain.dict(exclude_unset=True)
for key, value in update_data.items():
if(key != "id"):
setattr(db_domain, key, value)
db.add(db_domain)
db.commit()
db.refresh(db_domain)
return db_domain
def get_domain(db: Session, userid: str):
domains = db.query(models.UserDomain).filter(models.UserDomain.userid == userid).all()
if not domains:
raise HTTPException(status_code=404, detail="Data not found")
return domains

View File

@@ -1,4 +1,4 @@
from sqlalchemy import Boolean, Column, Integer, String, DateTime
from sqlalchemy import Boolean, Column, Integer, String, DateTime,ForeignKey
from sqlalchemy.ext.declarative import as_declarative
from datetime import datetime
@@ -49,3 +49,13 @@ class Flow(Base):
eventid = Column(String(100), index=True, nullable=False)
name = Column(String(200))
content = Column(String)
class UserDomain(Base):
__tablename__ = "userdomain"
userid = Column(Integer,ForeignKey("user.id"))
name = Column(String(100), nullable=False)
url = Column(String(200), nullable=False)
kintoneuser = Column(String(100), nullable=False)
kintonepwd = Column(String(100), nullable=False)
active = Column(Boolean, default=False)

View File

@@ -103,3 +103,24 @@ class Flow(Base):
class Config:
orm_mode = True
class DomainBase(BaseModel):
id: int
userid: int
name: str
url: str
kintoneuser: str
kintonepwd: str
active:bool = False
class Domain(Base):
id: int
userid: str
name: str
url: str
kintoneuser: str
kintonepwd: str
active:bool
class Config:
orm_mode = True

View File

@@ -21,3 +21,4 @@ pandas==2.0.3
openpyxl==3.1.2
deepdiff==6.3.1
pymssql==2.2.7
psycopg2==2.9.8

View File

@@ -1,6 +1,12 @@
<template>
<div class="q-pa-md">
<q-table :title="name+'一覧'" row-key="name" :selection="type" v-model:selected="selected" :columns="columns" :rows="rows" />
<q-table row-key="name" :selection="type" v-model:selected="selected" :columns="columns" :rows="rows"
class="action-table"
flat bordered
virtual-scroll
:pagination="pagination"
:rows-per-page-options="[0]"
/>
</div>
</template>
<script>
@@ -17,11 +23,11 @@ export default {
const columns = [
{ name: 'name', required: true,label: 'アクション名',align: 'left',field: 'name',sortable: true},
{ name: 'desc', align: 'left', label: '説明', field: 'desc', sortable: true },
{ name: 'content', label: '内容', field: 'content', sortable: true }
// { name: 'content', label: '内容', field: 'content', sortable: true }
]
const rows = reactive([])
onMounted(async () => {
await api.get('http://127.0.0.1:8000/api/kintone/2').then(res =>{
await api.get('http://127.0.0.1:8000/api/kintone/1').then(res =>{
res.data.forEach((item) =>
{
rows.push({name:item.name,desc:item.desc,content:item.content});
@@ -33,8 +39,16 @@ export default {
columns,
rows,
selected: ref([]),
pagination:ref({
rowsPerPage:0
})
}
},
}
</script>
<style lang="scss">
.action-table{
height: 100%;
}
</style>

View File

@@ -8,7 +8,7 @@ import { ref,onMounted,reactive } from 'vue'
import { api } from 'boot/axios';
export default {
name: 'appSelect',
name: 'AppSelect',
props: {
name: String,
type: String

View File

@@ -20,7 +20,7 @@
<script>
export default {
name: 'showDialog',
name: 'ShowDialog',
props: {
name:String,
visible: Boolean,

View File

@@ -0,0 +1,85 @@
<template>
<div
class="row"
style="
border-radius: 2px;
box-shadow: rgba(255, 255, 255, 0.1) 0px 0px 0px 1px inset,
rgba(0, 0, 0, 0.3) 0px 0px 0px 1px;
">
<q-icon
class="self-center q-ma-sm"
name="widgets"
color="grey-9"
style="font-size: 2em"
/>
<div class="col-7 self-center ellipsis">
{{ selectedApp.name }}
</div>
<div class="self-center">
<q-btn
outline
dense
label="変 更"
padding="none sm"
color="primary"
@click="showAppDialog"
></q-btn>
</div>
</div>
<ShowDialog v-model:visible="showSelectApp" name="アプリ" @close="closeDg">
<AppSelect ref="appDg" name="アプリ" type="single"></AppSelect>
</ShowDialog>
</template>
<script lang="ts">
import { defineComponent,ref } from 'vue';
import {AppInfo} from '../../types/ActionTypes'
import ShowDialog from '../../components/ShowDialog.vue';
import AppSelect from '../../components/AppSelect.vue';
import { useFlowEditorStore } from 'stores/flowEditor';
export default defineComponent({
name: 'AppSelector',
emits:[
"appSelected"
],
components:{
AppSelect,
ShowDialog
},
setup(props, context) {
const store = useFlowEditorStore();
const appDg = ref();
const showSelectApp=ref(false);
const selectedApp =ref<AppInfo>({
appId:"",
name:"",
});
const closeDg=(val :any)=>{
showSelectApp.value=false;
console.log("Dialog closed->",val);
if (val == 'OK') {
const data = appDg.value.selected[0];
console.log(data);
selectedApp.value={
appId:data.id ,
name:data.name
};
store.setApp(selectedApp.value);
store.setFlow();
}
}
const showAppDialog=()=>{
showSelectApp.value=true;
}
return {
store,
selectedApp,
showSelectApp,
showAppDialog,
closeDg,
appDg
}
}
});
</script>

View File

@@ -0,0 +1,72 @@
<template>
<div class="q-pa-md q-gutter-sm">
<q-tree
:nodes="eventTree.screens"
node-key="label"
children-key="events"
no-connectors
v-model:expanded="expanded"
:dense="true"
>
<template v-slot:default-header="prop">
<div class="row col items-start no-wrap event-node" @click="onSelected(prop.node)">
<q-icon v-if="prop.node.eventId"
name="play_circle"
:color="prop.node.hasFlow?'green':'grey'"
size="16px" class="q-mr-sm">
</q-icon>
<div class="no-wrap" :class="selectedEvent && prop.node.eventId===selectedEvent.eventId?'selected-node':''">{{ prop.node.label }}</div>
</div>
</template>
</q-tree>
</div>
</template>
<script lang="ts">
import { defineComponent, computed, ref } from 'vue';
import { kintoneEvents,KintoneEvent } from '../../types/KintoneEvents';
import { storeToRefs } from 'pinia';
import { useFlowEditorStore } from 'stores/flowEditor';
export default defineComponent({
name: 'EventTree',
setup(props, context) {
const store = useFlowEditorStore();
const eventTree=ref(kintoneEvents);
const selectedFlow = store.currentFlow;
const expanded=ref([
selectedFlow?.getRoot()?.title
]);
const selectedEvent = ref<KintoneEvent|null>(null);
const onSelected=(node:KintoneEvent)=>{
if(!node.eventId){
return;
}
selectedEvent.value=node;
}
return {
eventTree,
expanded,
onSelected,
selectedEvent,
store
}
}
});
</script>
<style lang="scss">
.nowrap{
flex-wrap:nowarp;
text-wrap:nowarp;
}
.event-node{
cursor:pointer;
}
.selected-node{
color: $primary;
font-weight: bolder;
}
.event-node:hover{
background-color: $light-blue-1;
}
</style>

View File

@@ -5,7 +5,7 @@
</template>
</q-input>
<show-dialog v-model:visible="show" name="フィールド一覧" @close="closeDg">
<field-select ref="appDg" name="フィールド" type="single" :appId="1"></field-select>
<field-select ref="appDg" name="フィールド" type="single" :appId="store.appInfo?.appId"></field-select>
</show-dialog>
</template>
@@ -13,7 +13,7 @@
import { defineComponent, ref ,watchEffect} from 'vue';
import ShowDialog from '../ShowDialog.vue';
import FieldSelect from '../FieldSelect.vue';
import { useFlowEditorStore } from 'stores/flowEditor';
export default defineComponent({
name: 'FieldInput',
components: {
@@ -35,6 +35,7 @@ export default defineComponent({
const appDg = ref();
const show = ref(false);
const selectedField = ref(props.modelValue);
const store = useFlowEditorStore();
const showDg = () => {
show.value = true;
@@ -51,6 +52,7 @@ export default defineComponent({
});
return {
store,
appDg,
show,
showDg,

View File

@@ -0,0 +1,21 @@
import { api } from 'boot/axios';
export class Auth
{
async login(user:string,pwd:string):Promise<boolean>
{
const params = new URLSearchParams();
params.append('username', user);
params.append('password', pwd);
try{
const result = await api.post(`http://127.0.0.1:8000/api/token`,params);
console.info(result);
localStorage.setItem('Token', result.data.access_token);
return true;
}catch(e)
{
console.info(e);
return false;
}
}
}

View File

@@ -1,8 +1,23 @@
import { api } from 'boot/axios';
import { ActionFlow } from 'src/types/ActionTypes';
export class FlowCtrl
{
async getFlows(appId:string):Promise<ActionFlow[]>
{
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<boolean>
{
const result = await api.post('http://127.0.0.1:8000/api/flow',jsonData);

View File

@@ -14,7 +14,9 @@
Kintone App Builder
<q-badge align="top" outline>V{{ env.version }}</q-badge>
</q-toolbar-title>
<q-btn flat round dense icon="logout" @click="authStore.logout()"/>
</q-toolbar>
</q-header>
<q-drawer
@@ -46,6 +48,9 @@
<script setup lang="ts">
import { ref } from 'vue';
import EssentialLink, { EssentialLinkProps } from 'components/EssentialLink.vue';
import { useAuthStore } from 'stores/useAuthStore';
const authStore = useAuthStore();
const essentialLinks: EssentialLinkProps[] = [
{

View File

@@ -0,0 +1,160 @@
<template>
<div class="q-pa-md q-gutter-sm event-tree">
<q-drawer
side="left"
overlay
bordered
v-model="drawerLeft"
:show-if-above="false"
elevated
>
<!-- <q-card class="column full-height" style="width: 300px">
<q-card-section> -->
<div class="flex-center fixd-top" >
<AppSelector />
</div>
<!-- </q-card-section> -->
<q-separator />
<!-- <q-card-section> -->
<div class="flex-center">
<EventTree />
</div>
<!-- </q-card-section> -->
<!-- </q-card> -->
</q-drawer>
</div>
<q-page>
<div class="q-pa-md q-gutter-sm">
<div class="flowchart" v-if="store.currentFlow">
<node-item v-for="(node,) in store.currentFlow.actionNodes" :key="node.id"
:isSelected="node===state.activeNode" :actionNode="node"
@addNode="addNode"
@nodeSelected="onNodeSelected"
@nodeEdit="onNodeEdit"
@deleteNode="onDeleteNode"
@deleteAllNextNodes="onDeleteAllNextNodes"
></node-item>
</div>
</div>
</q-page>
<PropertyPanel :actionNode="state.activeNode" v-model:drawerRight="drawerRight"></PropertyPanel>
<ShowDialog v-model:visible="showAddAction" name="アクション" @close="closeDg">
<action-select ref="appDg" name="model" type="single"></action-select>
</ShowDialog>
</template>
<script setup lang="ts">
import {ref,reactive,computed,onMounted} from 'vue';
import {IActionNode, ActionNode, IActionFlow, ActionFlow,RootAction, IActionProperty } from 'src/types/ActionTypes';
import { storeToRefs } from 'pinia';
import { useFlowEditorStore } from 'stores/flowEditor';
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';
import AppSelector from 'components/left/AppSelector.vue';
import EventTree from 'components/left/EventTree.vue';
import {FlowCtrl } from '../control/flowctrl';
const drawerLeft = ref(true);
const store = useFlowEditorStore();
// ref関数を使ってtemplateとバインド
const state=reactive({
activeNode:{
id:""
},
})
const appDg = ref();
const prevNodeIfo=ref({
prevNode:{} as IActionNode,
inputPoint:""
});
const refFlow = ref<ActionFlow|null>(null);
const showAddAction=ref(false);
const drawerRight=ref(false);
const model=ref("");
const addActionNode=(action:IActionNode)=>{
// refFlow.value?.actionNodes.push(action);
store.currentFlow?.actionNodes.push(action);
}
const addNode=(node:IActionNode,inputPoint:string)=>{
showAddAction.value=true;
prevNodeIfo.value.prevNode=node;
prevNodeIfo.value.inputPoint=inputPoint;
}
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)=>{
if(!store.currentFlow) return;
store.currentFlow?.removeNode(node);
}
const onDeleteAllNextNodes=(node:IActionNode)=>{
if(!store.currentFlow) return;
store.currentFlow?.removeAllNext(node.id);
}
const closeDg=(val :any)=>{
console.log("Dialog closed->",val);
if (val == 'OK') {
const data = appDg.value.selected[0];
const actionProps=JSON.parse(data.content);
const action = new ActionNode(data.name,data.desc,"",[],actionProps);
store.currentFlow?.addNode(action, prevNodeIfo.value.prevNode,prevNodeIfo.value.inputPoint);
}
}
const fetchData= async ()=>{
const flowCtrl = new FlowCtrl();
if(store.appInfo===undefined) return;
const actionFlows = await flowCtrl.getFlows(store.appInfo?.appId);
if(actionFlows && actionFlows.length>0){
store.setFlows(actionFlows);
}
if(actionFlows && actionFlows.length==1){
store.selectFlow(actionFlows[0]);
}
refFlow.value=actionFlows[0];
const root =refFlow.value.getRoot();
if(root){
state.activeNode=root;
}
}
onMounted(() => {
fetchData();
});
</script>
<style lang="scss">
.flowchart{
padding-top: 10px;
}
.flow-toolbar{
opacity: 50%;
}
.flow-container{
height: 91.5dvb;
overflow: hidden;
}
.event-tree .q-drawer {
top:50px;
z-index: 999;
}
</style>

View File

@@ -1,5 +1,6 @@
<template>
<q-page>
<div class="flowchart">
<node-item v-for="(node,) in refFlow.actionNodes" :key="node.id"
:isSelected="node===state.activeNode" :actionNode="node"
@@ -46,7 +47,6 @@ const saibanProps:IActionProperty[]=[{
}
}];
actionFlow.addNode(new ActionNode('自動採番','文書番号を自動採番する','',[],saibanProps));
actionFlow.addNode(new ActionNode('入力データ取得','電話番号を取得する',''));
const branchNode = actionFlow.addNode(new ActionNode('条件分岐','電話番号入力形式チャック','',['はい','いいえ'] ));

View File

@@ -45,7 +45,7 @@
<div class="q-mt-lg q-pa-sm">
<q-card-section>
<div class="flex-center">
<ItemSelector :actName="actName" />
<ItemSelector />
</div>
</q-card-section>
</div>

View File

@@ -0,0 +1,100 @@
<template>
<q-layout view="lHh Lpr fff">
<q-page-container>
<q-page class="window-height window-width row justify-center items-center">
<div class="column q-pa-lg">
<div class="row">
<q-card square class="shadow-24" style="width:400px;height:540px;">
<q-card-section class="bg-primary">
<h4 class="text-h5 text-white q-my-md">{{ title}}</h4>
</q-card-section>
<q-card-section>
<q-form class="q-px-sm q-pt-xl" ref="loginForm">
<q-input square clearable v-model="email" type="email" lazy-rules
:rules="[required,isEmail,short]" label="メール">
<template v-slot:prepend>
<q-icon name="email" />
</template>
</q-input>
<q-input square clearable v-model="password" :type="passwordFieldType" lazy-rules
:rules="[required, short]" label="パスワード">
<template v-slot:prepend>
<q-icon name="lock" />
</template>
<template v-slot:append>
<q-icon :name="visibilityIcon" @click="switchVisibility" class="cursor-pointer" />
</template>
</q-input>
</q-form>
</q-card-section>
<q-card-actions class="q-px-lg">
<q-btn unelevated size="lg" color="secondary" @click="submit" class="full-width text-white"
:label="btnLabel" />
</q-card-actions>
</q-card>
</div>
</div>
</q-page>
</q-page-container>
</q-layout>>
</template>
<script setup lang="ts">
import { useQuasar } from 'quasar'
// import { useRouter } from 'vue-router';
import { ref } from 'vue';
// import { Auth } from '../control/auth'
import { useAuthStore } from 'stores/useAuthStore';
const authStore = useAuthStore();
const $q = useQuasar()
const loginForm = ref(null);
let title = ref('ログイン');
let email = ref('');
let password = ref('');
let visibility = ref(false);
let passwordFieldType = ref('password');
let visibilityIcon = ref('visibility');
let btnLabel = ref('ログイン');
const required = (val:string) => {
return (val && val.length > 0 || '必須項目')
}
const isEmail = (val:string) => {
const emailPattern = /^(?=[a-zA-Z0-9@._%+-]{6,254}$)[a-zA-Z0-9._%+-]{1,64}@(?:[a-zA-Z0-9-]{1,63}\.){1,8}[a-zA-Z]{2,63}$/
return (emailPattern.test(val) || '無効なメールアドレス')
}
const short = (val:string) => {
return (val && val.length > 3 || '値が短く過ぎる')
}
const switchVisibility = () => {
visibility.value = !visibility.value
passwordFieldType.value = visibility.value ? 'text' : 'password'
visibilityIcon.value = visibility.value ? 'visibility_off' : 'visibility'
}
const submit = () =>{
authStore.login(email.value,password.value).then((result)=>{
if(result)
{
$q.notify({
icon: 'done',
color: 'positive',
message: 'ログイン成功'
})
}
else
{
$q.notify({
icon: 'error',
color: 'negative',
message: 'ログイン失敗'
})
}
});
}
</script>

View File

@@ -0,0 +1,354 @@
<!-- <template>
<div class="q-pa-md" style="max-width: 400px">
<q-form
@submit="onSubmit"
@reset="onReset"
class="q-gutter-md"
>
<q-input
filled
v-model="name"
label="Your name *"
hint="Kintone envirment name"
lazy-rules
:rules="[ val => val && val.length > 0 || 'Please type something']"
/>
<q-input
filled type="url"
v-model="url"
label="Kintone url"
hint="Kintone domain address"
lazy-rules
:rules="[ val => val && val.length > 0,isDomain || 'Please type something']"
/>
<q-input
filled
v-model="username"
label="Login user "
hint="Kintone user name"
lazy-rules
:rules="[ val => val && val.length > 0 || 'Please type something']"
/>
<q-input v-model="password" filled :type="isPwd ? 'password' : 'text'" hint="Password with toggle" label="User password">
<template v-slot:append>
<q-icon
:name="isPwd ? 'visibility_off' : 'visibility'"
class="cursor-pointer"
@click="isPwd = !isPwd"
/>
</template>
</q-input>
<q-toggle v-model="accept" label="Active Domain" />
<div>
<q-btn label="Submit" type="submit" color="primary"/>
<q-btn label="Reset" type="reset" color="primary" flat class="q-ml-sm" />
</div>
</q-form>
</div>
</template>
<script>
import { useQuasar } from 'quasar'
import { ref } from 'vue'
export default {
setup () {
const $q = useQuasar()
const name = ref(null)
const age = ref(null)
const accept = ref(false)
const isPwd =ref(true)
return {
name,
age,
accept,
isPwd,
isDomain(val) {
const domainPattern = /^https?\/\/:([a-zA-Z] +\.){1}([a-zA-Z]+)\.([a-zA-Z]+)$/;
return (domainPattern.test(val) || '無効なURL')
},
onSubmit () {
if (accept.value !== true) {
$q.notify({
color: 'red-5',
textColor: 'white',
icon: 'warning',
message: 'You need to accept the license and terms first'
})
}
else {
$q.notify({
color: 'green-4',
textColor: 'white',
icon: 'cloud_done',
message: 'Submitted'
})
}
},
onReset () {
name.value = null
age.value = null
accept.value = false
}
}
}
}
</script> -->
<template>
<div class="q-pa-md">
<q-table grid grid-header title="Domain" selection="single" :rows="rows" :columns="columns" v-model:selected="selected" row-key="name" :filter="filter" hide-header>
<template v-slot:top>
<div class="q-pa-md q-gutter-sm">
<q-btn color="primary" size="sm" label=" 新規 " @click="newDomain()" dense />
</div>
<q-space />
<q-input borderless dense debounce="300" v-model="filter" placeholder="Search">
<template v-slot:append>
<q-icon name="search" />
</template>
</q-input>
</template>
<template v-slot:item="props">
<div class="q-pa-xs col-xs-12 col-sm-6 col-md-4">
<q-card>
<q-card-section>
<div class="q-table__grid-item-row">
<div class="q-table__grid-item-title">Name</div>
<div class="q-table__grid-item-value">{{ props.row.name }}</div>
</div>
<div class="q-table__grid-item-row">
<div class="q-table__grid-item-title">Domain</div>
<div class="q-table__grid-item-value">{{ props.row.url }}</div>
</div>
<div class="q-table__grid-item-row">
<div class="q-table__grid-item-title">Account</div>
<div class="q-table__grid-item-value">{{ props.row.kintoneuser }}</div>
</div>
</q-card-section>
<q-separator />
<q-card-actions align="right">
<q-btn flat @click = "editDomain(props.row)">編集</q-btn>
<q-btn flat @click = "deleteConfirm(props.row)">削除</q-btn>
</q-card-actions>
</q-card>
</div>
</template>
</q-table>
<q-dialog :model-value="show" persistent>
<q-card style="min-width: 400px">
<q-card-section>
<div class="text-h6">Kintone Account</div>
</q-card-section>
<q-card-section class="q-pt-none">
<q-form class="q-gutter-md">
<q-input filled v-model="name" label="Your name *" hint="Kintone envirment name" lazy-rules
:rules="[val => val && val.length > 0 || 'Please type something']" />
<q-input filled type="url" v-model="url" label="Kintone url" hint="Kintone domain address" lazy-rules
:rules="[val => val && val.length > 0, isDomain || 'Please type something']" />
<q-input filled v-model="kintoneuser" label="Login user " hint="Kintone user name" lazy-rules
:rules="[val => val && val.length > 0 || 'Please type something']" />
<q-input v-model="kintonepwd" filled :type="isPwd ? 'password' : 'text'" hint="Password with toggle"
label="User password">
<template v-slot:append>
<q-icon :name="isPwd ? 'visibility_off' : 'visibility'" class="cursor-pointer" @click="isPwd = !isPwd" />
</template>
</q-input>
<q-toggle v-model="active" label="Active Domain" />
</q-form>
</q-card-section>
<q-card-actions align="right" class="text-primary">
<q-btn label="Save" type="submit" color="primary" @click="onSubmit"/>
<q-btn label="Cancel" type="cancel" color="primary" flat class="q-ml-sm" @click="closeDg()"/>
</q-card-actions>
</q-card>
</q-dialog>
<q-dialog v-model="confirm" persistent>
<q-card>
<q-card-section class="row items-center">
<q-avatar icon="confirm" color="primary" text-color="white" />
<span class="q-ml-sm">削除してもよろしいですか</span>
</q-card-section>
<q-card-actions align="right">
<q-btn flat label="Cancel" color="primary" v-close-popup />
<q-btn flat label="OK" color="primary" v-close-popup @click = "deleteDomain()"/>
</q-card-actions>
</q-card>
</q-dialog>
</div>
</template>
<script setup lang="ts">
import { useQuasar } from 'quasar'
import { ref, onMounted, reactive } from 'vue'
import { api } from 'boot/axios';
const $q = useQuasar()
const selected = ref([])
const name = ref('')
const active = ref(false)
const isPwd =ref(true)
const url =ref('')
const kintoneuser =ref('')
const kintonepwd =ref('')
const show = ref(false);
const confirm = ref(false)
let editId = ref(0);
const columns = [
{ name: 'id'},
{
name: 'name',
required: true,
label: 'Name',
align: 'left',
field: row => row.name,
sortable: true
},
{ name: 'url', align: 'center', label: 'Domain', field: 'url', sortable: true },
{ name: 'kintoneuser', label: 'User', field: 'kintoneuser', sortable: true },
{ name: 'kintonepwd' }
]
const rows = reactive([])
const newDomain = () => {
editId.value = 0;
show.value = true;
};
const editDomain = (row:object) => {
editId.value = row.id;
name.value = row.name;
url.value = row.url;
kintoneuser.value = row.kintoneuser;
kintonepwd.value = row.kintonepwd;
isPwd.value = true;
active.value = false;
show.value = true;
};
const deleteConfirm = (row:object) => {
confirm.value = true;
editId.value = row.id;
};
const deleteDomain = () => {
api.delete(`http://127.0.0.1:8000/api/domain/1/`+ editId.value).then(() =>{
getDomain();
})
editId.value = 0;
};
const closeDg = () => {
show.value = false;
onReset();
};
const getDomain = () => {
api.get(`http://127.0.0.1:8000/api/domain/1`).then(res => {
rows.length = 0;
res.data.forEach((item) => {
rows.push({ id:item.id,name: item.name, url: item.url, kintoneuser: item.kintoneuser, kintonepwd: item.kintonepwd });
}
)
});
}
onMounted(() => {
getDomain();
})
const isDomain = (val) =>{
// const domainPattern = /^https\/\/:([a-zA-Z] +\.){1}([a-zA-Z]+)\.([a-zA-Z]+)$/;
// return (domainPattern.test(val) || '無効なURL')
return true;
};
const onSubmit = () =>{
if(editId.value !== 0)
{
api.put(`http://127.0.0.1:8000/api/domain`,{
'id': editId.value,
'userid': 1,
'name': name.value,
'url': url.value,
'kintoneuser': kintoneuser.value,
'kintonepwd': kintonepwd.value,
'active': active.value
}).then(() =>{
getDomain();
closeDg();
onReset();
})
}
else
{
api.post(`http://127.0.0.1:8000/api/domain`,{
'id': 0,
'userid': 1,
'name': name.value,
'url': url.value,
'kintoneuser': kintoneuser.value,
'kintonepwd': kintonepwd.value,
'active': active.value
}).then(() =>{
getDomain();
closeDg();
onReset();
})
}
// if (accept.value !== true) {
// $q.notify({
// color: 'red-5',
// textColor: 'white',
// icon: 'warning',
// message: 'You need to accept the license and terms first'
// })
// }
// else {
// $q.notify({
// color: 'green-4',
// textColor: 'white',
// icon: 'cloud_done',
// message: 'Submitted'
// })
// }
};
const onReset = () => {
name.value = '';
url.value = '';
kintoneuser.value = '';
kintonepwd.value ='';
isPwd.value = true;
active.value = false;
editId.value = 0;
}
</script>

View File

@@ -7,7 +7,7 @@ import {
} from 'vue-router';
import routes from './routes';
import { useAuthStore } from 'stores/useAuthStore';
/*
* If not building with SSR mode, you can
* directly export the Router instantiation;
@@ -17,12 +17,31 @@ import routes from './routes';
* with the Router instance.
*/
export default route(function (/* { store, ssrContext } */) {
// export default route(function (/* { store, ssrContext } */) {
// const createHistory = process.env.SERVER
// ? createMemoryHistory
// : (process.env.VUE_ROUTER_MODE === 'history' ? createWebHistory : createWebHashHistory);
// const Router = createRouter({
// scrollBehavior: () => ({ left: 0, top: 0 }),
// routes,
// // Leave this as is and make changes in quasar.conf.js instead!
// // quasar.conf.js -> build -> vueRouterMode
// // quasar.conf.js -> build -> publicPath
// history: createHistory(process.env.VUE_ROUTER_BASE),
// });
// return Router;
// });
const createHistory = process.env.SERVER
? createMemoryHistory
: (process.env.VUE_ROUTER_MODE === 'history' ? createWebHistory : createWebHashHistory);
const Router = createRouter({
export const Router = createRouter({
scrollBehavior: () => ({ left: 0, top: 0 }),
routes,
@@ -32,5 +51,23 @@ export default route(function (/* { store, ssrContext } */) {
history: createHistory(process.env.VUE_ROUTER_BASE),
});
export default route(function (/* { store, ssrContext } */) {
return Router;
});
Router.beforeEach(async (to) => {
// clear alert on route change
//const alertStore = useAlertStore();
//alertStore.clear();
// redirect to login page if not logged in and trying to access a restricted page
const publicPages = ['/login'];
const authRequired = !publicPages.includes(to.path);
const authStore = useAuthStore();
if (authRequired && !authStore.token) {
authStore.returnUrl = to.fullPath;
return '/login';
}
});

View File

@@ -1,6 +1,11 @@
import { RouteRecordRaw } from 'vue-router';
const routes: RouteRecordRaw[] = [
{
path: '/login',
component: () => import('pages/LoginPage.vue')
},
{
path: '/',
component: () => import('layouts/MainLayout.vue'),
@@ -11,8 +16,10 @@ 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') },
{ path: 'domain', component: () => import('pages/UserDomain.vue') }
],
},
// Always leave this as last one,

View File

@@ -1,25 +1,47 @@
import { defineStore } from 'pinia';
import { ActionFlow,AppInfo } from 'src/types/ActionTypes';
import {FlowCtrl } from '../control/flowCtrl';
export const useFlowEditorStore = defineStore('flowEditor', {
state: () => ({
counter: 0,
flowNames: [],
flowNames1: ''
export interface FlowEditorState{
flowNames1:string;
appInfo?:AppInfo;
flows?:ActionFlow[];
selectedFlow?:ActionFlow|undefined;
}
const flowCtrl=new FlowCtrl();
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;
},
setApp(app:AppInfo){
this.appInfo=app;
},
async setFlow(){
if(this.appInfo===undefined) return;
const actionFlows = await flowCtrl.getFlows(this.appInfo?.appId);
if(actionFlows && actionFlows.length>0){
this.setFlows(actionFlows);
}
if(actionFlows && actionFlows.length==1){
this.selectFlow(actionFlows[0]);
}
}
}
});

View File

@@ -0,0 +1,36 @@
import { defineStore } from 'pinia';
import { api } from 'boot/axios';
import { Router } from '../router';
export const useAuthStore = defineStore({
id: 'auth',
state: () => ({
token: localStorage.getItem('token'),
returnUrl: ''
}),
actions: {
async login(username:string, password:string) {
const params = new URLSearchParams();
params.append('username', username);
params.append('password', password);
try{
const result = await api.post(`http://127.0.0.1:8000/api/token`,params);
console.info(result);
this.token =result.data.access_token;
localStorage.setItem('token', result.data.access_token);
Router.push(this.returnUrl || '/');
return true;
}catch(e)
{
console.info(e);
return false;
}
},
logout() {
this.token = null;
localStorage.removeItem('token');
Router.push('/login');
}
}
});

View File

@@ -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<IActionNode>
}
@@ -112,7 +121,7 @@ export class ActionNode implements IActionNode {
title:string,
inputPoint:string,
outputPoint: Array<string> = [],
actionProps: Array<IActionProperty> =[ActionProperty.defaultProperty()]
actionProps: Array<IActionProperty> =[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<IActionNode>;
constructor(actionNodes:Array<IActionNode>|RootAction){
if(actionNodes instanceof Array){
@@ -176,6 +185,7 @@ export class ActionFlow implements IActionFlow {
}else{
this.actionNodes=[actionNodes];
}
this.id=uuidv4();
}
/**
* ノードを追加する
@@ -194,13 +204,18 @@ export class ActionFlow implements IActionFlow {
if(inputPoint!==undefined){
newNode.inputPoint=inputPoint;
}
if(prevNode){
if(prevNode!==undefined){
this.connectNodes(prevNode,newNode,inputPoint||'');
}else{
prevNode=this.actionNodes[this.actionNodes.length-1];
this.connectNodes(prevNode,newNode,inputPoint||'');
}
const index=this.actionNodes.findIndex(node=>node.id===prevNode?.id)
if(index>=0){
this.actionNodes.splice(index+1,0,newNode);
}else{
this.actionNodes.push(newNode);
}
return newNode;
}
/**
@@ -354,5 +369,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;
}
}

View File

@@ -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
},
]
}
]);

138
sample.json Normal file
View File

@@ -0,0 +1,138 @@
{
"id": "681ecde3-4439-4210-9fdf-424c6af98f09",
"actionNodes": [
{
"id": "34dfd32e-ba1a-440f-bb46-a8a1999109cd",
"name": "app.record.create.submit",
"title": "レコード追加画面",
"subTitle": "保存するとき",
"inputPoint": "",
"outputPoints": [],
"isRoot": true,
"actionProps": [],
"ActionValue": {},
"nextNodeIds": [
[
"",
"ce07775d-9729-4516-a88c-78ee8f2f851e"
]
]
},
{
"id": "ce07775d-9729-4516-a88c-78ee8f2f851e",
"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": "34dfd32e-ba1a-440f-bb46-a8a1999109cd",
"nextNodeIds": [
[
"",
"0d18c3c9-abee-44e5-83eb-82074316219b"
]
]
},
{
"id": "0d18c3c9-abee-44e5-83eb-82074316219b",
"name": "入力データ取得",
"title": "電話番号を取得する",
"inputPoint": "",
"outputPoints": [],
"actionProps": [
{
"component": "InputText",
"props": {
"name": "displayName",
"displayName": "表示名",
"placeholder": "表示を入力してください",
"modelValue": ""
}
}
],
"prevNodeId": "ce07775d-9729-4516-a88c-78ee8f2f851e",
"nextNodeIds": [
[
"",
"399d7c04-5345-4bf6-8da3-d745df554524"
]
]
},
{
"id": "399d7c04-5345-4bf6-8da3-d745df554524",
"name": "条件分岐",
"title": "電話番号入力形式チャック",
"inputPoint": "",
"outputPoints": [
"はい",
"いいえ"
],
"actionProps": [
{
"component": "InputText",
"props": {
"name": "displayName",
"displayName": "表示名",
"placeholder": "表示を入力してください",
"modelValue": ""
}
}
],
"prevNodeId": "0d18c3c9-abee-44e5-83eb-82074316219b",
"nextNodeIds": [
[
"いいえ",
"8173e6bc-3fa2-4403-b973-9368884e2dfa"
]
]
},
{
"id": "8173e6bc-3fa2-4403-b973-9368884e2dfa",
"name": "エラー表示",
"title": "エラー表示して保存しない",
"inputPoint": "いいえ",
"outputPoints": [],
"actionProps": [
{
"component": "InputText",
"props": {
"name": "displayName",
"displayName": "表示名",
"placeholder": "表示を入力してください",
"modelValue": ""
}
}
],
"prevNodeId": "399d7c04-5345-4bf6-8da3-d745df554524",
"nextNodeIds": []
}
]
}