mirror of https://github.com/Budibase/budibase.git
22 changed files with 590 additions and 294 deletions
@ -1,62 +1,59 @@ |
|||
import { authenticate } from "./authenticate" |
|||
import { triggerWorkflow } from "./workflow" |
|||
|
|||
export const createApi = ({ setState, getState }) => { |
|||
const apiCall = method => async ({ url, body }) => { |
|||
const response = await fetch(url, { |
|||
method: method, |
|||
headers: { |
|||
"Content-Type": "application/json", |
|||
}, |
|||
body: body && JSON.stringify(body), |
|||
credentials: "same-origin", |
|||
}) |
|||
|
|||
switch (response.status) { |
|||
case 200: |
|||
import appStore from "../state/store" |
|||
|
|||
const apiCall = method => async ({ url, body }) => { |
|||
const response = await fetch(url, { |
|||
method: method, |
|||
headers: { |
|||
"Content-Type": "application/json", |
|||
}, |
|||
body: body && JSON.stringify(body), |
|||
credentials: "same-origin", |
|||
}) |
|||
|
|||
switch (response.status) { |
|||
case 200: |
|||
return response.json() |
|||
case 404: |
|||
return error(`${url} Not found`) |
|||
case 400: |
|||
return error(`${url} Bad Request`) |
|||
case 403: |
|||
return error(`${url} Forbidden`) |
|||
default: |
|||
if (response.status >= 200 && response.status < 400) { |
|||
return response.json() |
|||
case 404: |
|||
return error(`${url} Not found`) |
|||
case 400: |
|||
return error(`${url} Bad Request`) |
|||
case 403: |
|||
return error(`${url} Forbidden`) |
|||
default: |
|||
if (response.status >= 200 && response.status < 400) { |
|||
return response.json() |
|||
} |
|||
|
|||
return error(`${url} - ${response.statusText}`) |
|||
} |
|||
} |
|||
|
|||
return error(`${url} - ${response.statusText}`) |
|||
} |
|||
} |
|||
|
|||
const post = apiCall("POST") |
|||
const get = apiCall("GET") |
|||
const patch = apiCall("PATCH") |
|||
const del = apiCall("DELETE") |
|||
const post = apiCall("POST") |
|||
const get = apiCall("GET") |
|||
const patch = apiCall("PATCH") |
|||
const del = apiCall("DELETE") |
|||
|
|||
const ERROR_MEMBER = "##error" |
|||
const error = message => { |
|||
const err = { [ERROR_MEMBER]: message } |
|||
setState("##error_message", message) |
|||
return err |
|||
} |
|||
const ERROR_MEMBER = "##error" |
|||
const error = message => { |
|||
const err = { [ERROR_MEMBER]: message } |
|||
appStore.update(s => s["##error_message"], message) |
|||
return err |
|||
} |
|||
|
|||
const isSuccess = obj => !obj || !obj[ERROR_MEMBER] |
|||
|
|||
const apiOpts = { |
|||
setState, |
|||
getState, |
|||
isSuccess, |
|||
error, |
|||
post, |
|||
get, |
|||
patch, |
|||
delete: del, |
|||
} |
|||
const isSuccess = obj => !obj || !obj[ERROR_MEMBER] |
|||
|
|||
return { |
|||
authenticate: authenticate(apiOpts), |
|||
triggerWorkflow: triggerWorkflow(apiOpts), |
|||
} |
|||
const apiOpts = { |
|||
isSuccess, |
|||
error, |
|||
post, |
|||
get, |
|||
patch, |
|||
delete: del, |
|||
} |
|||
|
|||
export default { |
|||
authenticate: authenticate(apiOpts), |
|||
triggerWorkflow: triggerWorkflow(apiOpts), |
|||
} |
|||
|
|||
@ -1,9 +0,0 @@ |
|||
import { get } from "svelte/store" |
|||
import getOr from "lodash/fp/getOr" |
|||
import { appStore } from "./store" |
|||
|
|||
export const getState = (path, fallback) => { |
|||
if (!path || path.length === 0) return fallback |
|||
|
|||
return getOr(fallback, path, get(appStore)) |
|||
} |
|||
@ -0,0 +1 @@ |
|||
export default value => typeof value === "string" && value.includes("{{") |
|||
@ -0,0 +1,13 @@ |
|||
import appStore from "./store" |
|||
|
|||
export default treeNode => (propName, value) => { |
|||
if (!propName || propName.length === 0) return |
|||
if (!treeNode) return |
|||
const componentId = treeNode.props._id |
|||
|
|||
appStore.update(state => { |
|||
state[componentId] = state[componentId] || {} |
|||
state[componentId][propName] = value |
|||
return state |
|||
}, treeNode.contextStoreKey) |
|||
} |
|||
@ -1,11 +0,0 @@ |
|||
import set from "lodash/fp/set" |
|||
import { appStore } from "./store" |
|||
|
|||
export const setState = (path, value) => { |
|||
if (!path || path.length === 0) return |
|||
|
|||
appStore.update(state => { |
|||
state = set(path, value, state) |
|||
return state |
|||
}) |
|||
} |
|||
@ -1,9 +1,104 @@ |
|||
import { writable } from "svelte/store" |
|||
|
|||
const appStore = writable({}) |
|||
appStore.actions = {} |
|||
// we assume that the reference to this state object
|
|||
// will remain for the life of the application
|
|||
const rootState = {} |
|||
const rootStore = writable(rootState) |
|||
const contextStores = {} |
|||
|
|||
const routerStore = writable({}) |
|||
routerStore.actions = {} |
|||
// contextProviderId is the component id that provides the data for the context
|
|||
const contextStoreKey = (dataProviderId, childIndex) => |
|||
`${dataProviderId}${childIndex >= 0 ? ":" + childIndex : ""}` |
|||
|
|||
export { appStore, routerStore } |
|||
// creates a store for a datacontext (e.g. each item in a list component)
|
|||
const create = (data, dataProviderId, childIndex, parentContextStoreId) => { |
|||
const key = contextStoreKey(dataProviderId, childIndex) |
|||
const state = { data } |
|||
|
|||
// add reference to parent state object,
|
|||
// so we can use bindings like state.parent.parent
|
|||
// (if no parent, then parent is rootState )
|
|||
state.parent = parentContextStoreId |
|||
? contextStores[parentContextStoreId].state |
|||
: rootState |
|||
|
|||
if (!contextStores[key]) { |
|||
contextStores[key] = { |
|||
store: writable(state), |
|||
subscriberCount: 0, |
|||
state, |
|||
parentContextStoreId, |
|||
} |
|||
} |
|||
return key |
|||
} |
|||
|
|||
const subscribe = (subscription, storeKey) => { |
|||
if (!storeKey) { |
|||
return rootStore.subscribe(subscription) |
|||
} |
|||
const contextStore = contextStores[storeKey] |
|||
|
|||
// we are subscribing to multiple stores,
|
|||
// we dont want to each subscription the first time
|
|||
// as this could repeatedly run $set on the same component
|
|||
// ... which already has its initial properties set properly
|
|||
const ignoreFirstSubscription = () => { |
|||
let hasRunOnce = false |
|||
return () => { |
|||
if (hasRunOnce) subscription(contextStore.state) |
|||
hasRunOnce = true |
|||
} |
|||
} |
|||
|
|||
const unsubscribes = [rootStore.subscribe(ignoreFirstSubscription())] |
|||
|
|||
// we subscribe to all stores in the hierarchy
|
|||
const ancestorSubscribe = ctxStore => { |
|||
// unsubscribe func returned by svelte store
|
|||
const svelteUnsub = ctxStore.store.subscribe(ignoreFirstSubscription()) |
|||
|
|||
// we wrap the svelte unsubscribe, so we can
|
|||
// cleanup stores when they are no longer subscribed to
|
|||
const unsub = () => { |
|||
ctxStore.subscriberCount = contextStore.subscriberCount - 1 |
|||
// when no subscribers left, we delete the store
|
|||
if (ctxStore.subscriberCount === 0) { |
|||
delete ctxStore[storeKey] |
|||
} |
|||
svelteUnsub() |
|||
} |
|||
unsubscribes.push(unsub) |
|||
if (ctxStore.parentContextStoreId) { |
|||
ancestorSubscribe(contextStores[ctxStore.parentContextStoreId]) |
|||
} |
|||
} |
|||
|
|||
ancestorSubscribe(contextStore) |
|||
|
|||
// our final unsubscribe function calls unsubscribe on all stores
|
|||
return () => unsubscribes.forEach(u => u()) |
|||
} |
|||
|
|||
const findStore = (dataProviderId, childIndex) => |
|||
dataProviderId |
|||
? contextStores[contextStoreKey(dataProviderId, childIndex)].store |
|||
: rootStore |
|||
|
|||
const update = (updatefunc, dataProviderId, childIndex) => |
|||
findStore(dataProviderId, childIndex).update(updatefunc) |
|||
|
|||
const set = (value, dataProviderId, childIndex) => |
|||
findStore(dataProviderId, childIndex).set(value) |
|||
|
|||
const getState = contextStoreKey => |
|||
contextStoreKey ? contextStores[contextStoreKey].state : rootState |
|||
|
|||
export default { |
|||
subscribe, |
|||
update, |
|||
set, |
|||
getState, |
|||
create, |
|||
contextStoreKey, |
|||
} |
|||
|
|||
@ -0,0 +1,209 @@ |
|||
import { load, makePage, makeScreen } from "./testAppDef" |
|||
|
|||
describe("binding", () => { |
|||
|
|||
|
|||
it("should bind to data in context", async () => { |
|||
const { dom } = await load( |
|||
makePage({ |
|||
_component: "testlib/div", |
|||
_children: [ |
|||
{ |
|||
_component: "##builtin/screenslot", |
|||
text: "header one", |
|||
}, |
|||
], |
|||
}), |
|||
[ |
|||
makeScreen("/", { |
|||
_component: "testlib/list", |
|||
data: dataArray, |
|||
_children: [ |
|||
{ |
|||
_component: "testlib/h1", |
|||
text: "{{data.name}}", |
|||
} |
|||
], |
|||
}), |
|||
] |
|||
) |
|||
|
|||
const rootDiv = dom.window.document.body.children[0] |
|||
expect(rootDiv.children.length).toBe(1) |
|||
|
|||
const screenRoot = rootDiv.children[0] |
|||
|
|||
expect(screenRoot.children[0].children.length).toBe(2) |
|||
expect(screenRoot.children[0].children[0].innerText).toBe(dataArray[0].name) |
|||
expect(screenRoot.children[0].children[1].innerText).toBe(dataArray[1].name) |
|||
}) |
|||
|
|||
it("should bind to input in root", async () => { |
|||
const { dom } = await load( |
|||
makePage({ |
|||
_component: "testlib/div", |
|||
_children: [ |
|||
{ |
|||
_component: "##builtin/screenslot", |
|||
text: "header one", |
|||
}, |
|||
], |
|||
}), |
|||
[ |
|||
makeScreen("/", { |
|||
_component: "testlib/div", |
|||
_children: [ |
|||
{ |
|||
_component: "testlib/h1", |
|||
text: "{{inputid.value}}", |
|||
}, |
|||
{ |
|||
_id: "inputid", |
|||
_component: "testlib/input", |
|||
value: "hello" |
|||
} |
|||
], |
|||
}), |
|||
] |
|||
) |
|||
|
|||
const rootDiv = dom.window.document.body.children[0] |
|||
expect(rootDiv.children.length).toBe(1) |
|||
|
|||
const screenRoot = rootDiv.children[0] |
|||
|
|||
expect(screenRoot.children[0].children.length).toBe(2) |
|||
expect(screenRoot.children[0].children[0].innerText).toBe("hello") |
|||
|
|||
// change value of input
|
|||
const input = dom.window.document.getElementsByClassName("input-inputid")[0] |
|||
|
|||
changeInputValue(dom, input, "new value") |
|||
expect(screenRoot.children[0].children[0].innerText).toBe("new value") |
|||
|
|||
}) |
|||
|
|||
it("should bind to input in context", async () => { |
|||
const { dom } = await load( |
|||
makePage({ |
|||
_component: "testlib/div", |
|||
_children: [ |
|||
{ |
|||
_component: "##builtin/screenslot", |
|||
text: "header one", |
|||
}, |
|||
], |
|||
}), |
|||
[ |
|||
makeScreen("/", { |
|||
_component: "testlib/list", |
|||
data: dataArray, |
|||
_children: [ |
|||
{ |
|||
_component: "testlib/h1", |
|||
text: "{{inputid.value}}", |
|||
}, |
|||
{ |
|||
_id: "inputid", |
|||
_component: "testlib/input", |
|||
value: "hello" |
|||
} |
|||
], |
|||
}), |
|||
] |
|||
) |
|||
|
|||
const rootDiv = dom.window.document.body.children[0] |
|||
expect(rootDiv.children.length).toBe(1) |
|||
|
|||
const screenRoot = rootDiv.children[0] |
|||
expect(screenRoot.children[0].children.length).toBe(4) |
|||
|
|||
const firstHeader = screenRoot.children[0].children[0] |
|||
const firstInput = screenRoot.children[0].children[1] |
|||
const secondHeader = screenRoot.children[0].children[2] |
|||
const secondInput = screenRoot.children[0].children[3] |
|||
|
|||
expect(firstHeader.innerText).toBe("hello") |
|||
expect(secondHeader.innerText).toBe("hello") |
|||
|
|||
changeInputValue(dom, firstInput, "first input value") |
|||
expect(firstHeader.innerText).toBe("first input value") |
|||
|
|||
changeInputValue(dom, secondInput, "second input value") |
|||
expect(secondHeader.innerText).toBe("second input value") |
|||
|
|||
}) |
|||
|
|||
it("should bind contextual component, to input in root context", async () => { |
|||
const { dom } = await load( |
|||
makePage({ |
|||
_component: "testlib/div", |
|||
_children: [ |
|||
{ |
|||
_component: "##builtin/screenslot", |
|||
text: "header one", |
|||
}, |
|||
], |
|||
}), |
|||
[ |
|||
makeScreen("/", { |
|||
_component: "testlib/div", |
|||
_children: [ |
|||
{ |
|||
_id: "inputid", |
|||
_component: "testlib/input", |
|||
value: "hello" |
|||
}, |
|||
{ |
|||
_component: "testlib/list", |
|||
data: dataArray, |
|||
_children: [ |
|||
{ |
|||
_component: "testlib/h1", |
|||
text: "{{parent.inputid.value}}", |
|||
}, |
|||
], |
|||
} |
|||
] |
|||
}), |
|||
] |
|||
) |
|||
|
|||
const rootDiv = dom.window.document.body.children[0] |
|||
expect(rootDiv.children.length).toBe(1) |
|||
|
|||
const screenRoot = rootDiv.children[0] |
|||
expect(screenRoot.children[0].children.length).toBe(2) |
|||
|
|||
const input = screenRoot.children[0].children[0] |
|||
|
|||
const firstHeader = screenRoot.children[0].children[1].children[0] |
|||
const secondHeader = screenRoot.children[0].children[1].children[0] |
|||
|
|||
expect(firstHeader.innerText).toBe("hello") |
|||
expect(secondHeader.innerText).toBe("hello") |
|||
|
|||
changeInputValue(dom, input, "new input value") |
|||
expect(firstHeader.innerText).toBe("new input value") |
|||
expect(secondHeader.innerText).toBe("new input value") |
|||
|
|||
}) |
|||
|
|||
const changeInputValue = (dom, input, newValue) => { |
|||
var event = new dom.window.Event("change") |
|||
input.value = newValue |
|||
input.dispatchEvent(event) |
|||
} |
|||
|
|||
const dataArray = [ |
|||
{ |
|||
name: "katherine", |
|||
age: 30, |
|||
}, |
|||
{ |
|||
name: "steve", |
|||
age: 41, |
|||
}, |
|||
] |
|||
}) |
|||
@ -0,0 +1,16 @@ |
|||
<script> |
|||
export let _bb |
|||
export let className = "" |
|||
|
|||
let containerElement |
|||
let hasLoaded |
|||
|
|||
$: { |
|||
if (containerElement) { |
|||
_bb.attachChildren(containerElement) |
|||
hasLoaded = true |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<div bind:this={containerElement} class={className} /> |
|||
Loading…
Reference in new issue