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 { authenticate } from "./authenticate" |
||||
import { triggerWorkflow } from "./workflow" |
import { triggerWorkflow } from "./workflow" |
||||
|
import appStore from "../state/store" |
||||
export const createApi = ({ setState, getState }) => { |
|
||||
const apiCall = method => async ({ url, body }) => { |
const apiCall = method => async ({ url, body }) => { |
||||
const response = await fetch(url, { |
const response = await fetch(url, { |
||||
method: method, |
method: method, |
||||
headers: { |
headers: { |
||||
"Content-Type": "application/json", |
"Content-Type": "application/json", |
||||
}, |
}, |
||||
body: body && JSON.stringify(body), |
body: body && JSON.stringify(body), |
||||
credentials: "same-origin", |
credentials: "same-origin", |
||||
}) |
}) |
||||
|
|
||||
switch (response.status) { |
switch (response.status) { |
||||
case 200: |
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() |
return response.json() |
||||
case 404: |
} |
||||
return error(`${url} Not found`) |
|
||||
case 400: |
return error(`${url} - ${response.statusText}`) |
||||
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}`) |
|
||||
} |
|
||||
} |
} |
||||
|
} |
||||
|
|
||||
const post = apiCall("POST") |
const post = apiCall("POST") |
||||
const get = apiCall("GET") |
const get = apiCall("GET") |
||||
const patch = apiCall("PATCH") |
const patch = apiCall("PATCH") |
||||
const del = apiCall("DELETE") |
const del = apiCall("DELETE") |
||||
|
|
||||
const ERROR_MEMBER = "##error" |
const ERROR_MEMBER = "##error" |
||||
const error = message => { |
const error = message => { |
||||
const err = { [ERROR_MEMBER]: message } |
const err = { [ERROR_MEMBER]: message } |
||||
setState("##error_message", message) |
appStore.update(s => s["##error_message"], message) |
||||
return err |
return err |
||||
} |
} |
||||
|
|
||||
const isSuccess = obj => !obj || !obj[ERROR_MEMBER] |
const isSuccess = obj => !obj || !obj[ERROR_MEMBER] |
||||
|
|
||||
const apiOpts = { |
|
||||
setState, |
|
||||
getState, |
|
||||
isSuccess, |
|
||||
error, |
|
||||
post, |
|
||||
get, |
|
||||
patch, |
|
||||
delete: del, |
|
||||
} |
|
||||
|
|
||||
return { |
const apiOpts = { |
||||
authenticate: authenticate(apiOpts), |
isSuccess, |
||||
triggerWorkflow: triggerWorkflow(apiOpts), |
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" |
import { writable } from "svelte/store" |
||||
|
|
||||
const appStore = writable({}) |
// we assume that the reference to this state object
|
||||
appStore.actions = {} |
// will remain for the life of the application
|
||||
|
const rootState = {} |
||||
|
const rootStore = writable(rootState) |
||||
|
const contextStores = {} |
||||
|
|
||||
const routerStore = writable({}) |
// contextProviderId is the component id that provides the data for the context
|
||||
routerStore.actions = {} |
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