Merge branch 'master' into feature/remove-add-row-button-in-grid-component
This commit is contained in:
commit
e9f97f3445
|
@ -55,6 +55,11 @@ context("Create a View", () => {
|
||||||
cy.get(".menu-container")
|
cy.get(".menu-container")
|
||||||
.find("select")
|
.find("select")
|
||||||
.eq(0)
|
.eq(0)
|
||||||
|
.select("Statistics")
|
||||||
|
cy.wait(50)
|
||||||
|
cy.get(".menu-container")
|
||||||
|
.find("select")
|
||||||
|
.eq(1)
|
||||||
.select("age")
|
.select("age")
|
||||||
cy.contains("Save").click()
|
cy.contains("Save").click()
|
||||||
cy.get("thead th div").should($headers => {
|
cy.get("thead th div").should($headers => {
|
||||||
|
|
|
@ -24,6 +24,7 @@ import {
|
||||||
saveCurrentPreviewItem as _saveCurrentPreviewItem,
|
saveCurrentPreviewItem as _saveCurrentPreviewItem,
|
||||||
saveScreenApi as _saveScreenApi,
|
saveScreenApi as _saveScreenApi,
|
||||||
regenerateCssForCurrentScreen,
|
regenerateCssForCurrentScreen,
|
||||||
|
regenerateCssForScreen,
|
||||||
generateNewIdsForComponent,
|
generateNewIdsForComponent,
|
||||||
getComponentDefinition,
|
getComponentDefinition,
|
||||||
} from "../storeUtils"
|
} from "../storeUtils"
|
||||||
|
@ -98,6 +99,29 @@ const setPackage = (store, initial) => async pkg => {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// if the app has just been created
|
||||||
|
// we need to build the CSS and save
|
||||||
|
if (pkg.justCreated) {
|
||||||
|
const generateInitialPageCss = async name => {
|
||||||
|
const page = pkg.pages[name]
|
||||||
|
regenerateCssForScreen(page)
|
||||||
|
for (let screen of page._screens) {
|
||||||
|
regenerateCssForScreen(screen)
|
||||||
|
}
|
||||||
|
|
||||||
|
await api.post(`/_builder/api/${pkg.application._id}/pages/${name}`, {
|
||||||
|
page: {
|
||||||
|
componentLibraries: pkg.application.componentLibraries,
|
||||||
|
...page,
|
||||||
|
},
|
||||||
|
screens: page._screens,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
generateInitialPageCss("main")
|
||||||
|
generateInitialPageCss("unauthenticated")
|
||||||
|
pkg.justCreated = false
|
||||||
|
}
|
||||||
|
|
||||||
initial.libraries = pkg.application.componentLibraries
|
initial.libraries = pkg.application.componentLibraries
|
||||||
initial.components = await fetchComponentLibDefinitions(pkg.application._id)
|
initial.components = await fetchComponentLibDefinitions(pkg.application._id)
|
||||||
initial.name = pkg.application.name
|
initial.name = pkg.application.name
|
||||||
|
@ -156,8 +180,8 @@ const createScreen = store => async screen => {
|
||||||
state.currentPreviewItem = screen
|
state.currentPreviewItem = screen
|
||||||
state.currentComponentInfo = screen.props
|
state.currentComponentInfo = screen.props
|
||||||
state.currentFrontEndType = "screen"
|
state.currentFrontEndType = "screen"
|
||||||
savePromise = _saveScreen(store, state, screen)
|
|
||||||
regenerateCssForCurrentScreen(state)
|
regenerateCssForCurrentScreen(state)
|
||||||
|
savePromise = _saveScreen(store, state, screen)
|
||||||
return state
|
return state
|
||||||
})
|
})
|
||||||
await savePromise
|
await savePromise
|
||||||
|
|
|
@ -79,10 +79,12 @@ export const walkProps = (props, action, cancelToken = null) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const regenerateCssForScreen = screen => {
|
||||||
|
screen._css = generate_screen_css([screen.props])
|
||||||
|
}
|
||||||
|
|
||||||
export const regenerateCssForCurrentScreen = state => {
|
export const regenerateCssForCurrentScreen = state => {
|
||||||
state.currentPreviewItem._css = generate_screen_css([
|
regenerateCssForScreen(state.currentPreviewItem)
|
||||||
state.currentPreviewItem.props,
|
|
||||||
])
|
|
||||||
return state
|
return state
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,21 +9,23 @@
|
||||||
export let view = {}
|
export let view = {}
|
||||||
|
|
||||||
let data = []
|
let data = []
|
||||||
|
let loading = false
|
||||||
|
|
||||||
$: name = view.name
|
$: name = view.name
|
||||||
|
|
||||||
// Fetch rows for specified view
|
// Fetch rows for specified view
|
||||||
$: {
|
$: {
|
||||||
if (!name.startsWith("all_")) {
|
if (!name.startsWith("all_")) {
|
||||||
fetchViewData(name, view.field, view.groupBy)
|
loading = true
|
||||||
|
fetchViewData(name, view.field, view.groupBy, view.calculation)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchViewData(name, field, groupBy) {
|
async function fetchViewData(name, field, groupBy, calculation) {
|
||||||
const params = new URLSearchParams()
|
const params = new URLSearchParams()
|
||||||
if (field) {
|
if (calculation) {
|
||||||
params.set("field", field)
|
params.set("field", field)
|
||||||
params.set("stats", true)
|
params.set("calculation", calculation)
|
||||||
}
|
}
|
||||||
if (groupBy) {
|
if (groupBy) {
|
||||||
params.set("group", groupBy)
|
params.set("group", groupBy)
|
||||||
|
@ -31,10 +33,11 @@
|
||||||
const QUERY_VIEW_URL = `/api/views/${name}?${params}`
|
const QUERY_VIEW_URL = `/api/views/${name}?${params}`
|
||||||
const response = await api.get(QUERY_VIEW_URL)
|
const response = await api.get(QUERY_VIEW_URL)
|
||||||
data = await response.json()
|
data = await response.json()
|
||||||
|
loading = false
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Table title={decodeURI(name)} schema={view.schema} {data}>
|
<Table title={decodeURI(name)} schema={view.schema} {data} {loading}>
|
||||||
<FilterButton {view} />
|
<FilterButton {view} />
|
||||||
<CalculateButton {view} />
|
<CalculateButton {view} />
|
||||||
{#if view.calculation}
|
{#if view.calculation}
|
||||||
|
|
|
@ -9,7 +9,11 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div bind:this={anchor}>
|
<div bind:this={anchor}>
|
||||||
<TextButton text small on:click={dropdown.show} active={!!view.field}>
|
<TextButton
|
||||||
|
text
|
||||||
|
small
|
||||||
|
on:click={dropdown.show}
|
||||||
|
active={view.field && view.calculation}>
|
||||||
<Icon name="calculate" />
|
<Icon name="calculate" />
|
||||||
Calculate
|
Calculate
|
||||||
</TextButton>
|
</TextButton>
|
||||||
|
|
|
@ -9,6 +9,14 @@
|
||||||
name: "Statistics",
|
name: "Statistics",
|
||||||
key: "stats",
|
key: "stats",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "Count",
|
||||||
|
key: "count",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Sum",
|
||||||
|
key: "sum",
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
export let view = {}
|
export let view = {}
|
||||||
|
@ -20,11 +28,12 @@
|
||||||
$: fields =
|
$: fields =
|
||||||
viewTable &&
|
viewTable &&
|
||||||
Object.keys(viewTable.schema).filter(
|
Object.keys(viewTable.schema).filter(
|
||||||
field => viewTable.schema[field].type === "number"
|
field =>
|
||||||
|
view.calculation === "count" ||
|
||||||
|
viewTable.schema[field].type === "number"
|
||||||
)
|
)
|
||||||
|
|
||||||
function saveView() {
|
function saveView() {
|
||||||
if (!view.calculation) view.calculation = "stats"
|
|
||||||
backendUiStore.actions.views.save(view)
|
backendUiStore.actions.views.save(view)
|
||||||
notifier.success(`View ${view.name} saved.`)
|
notifier.success(`View ${view.name} saved.`)
|
||||||
onClosed()
|
onClosed()
|
||||||
|
@ -35,25 +44,26 @@
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<h5>Calculate</h5>
|
<h5>Calculate</h5>
|
||||||
<div class="input-group-row">
|
<div class="input-group-row">
|
||||||
<!-- <p>The</p>
|
<p>The</p>
|
||||||
<Select secondary thin bind:value={view.calculation}>
|
<Select secondary thin bind:value={view.calculation}>
|
||||||
<option value="">Choose an option</option>
|
<option value={''}>Choose an option</option>
|
||||||
{#each CALCULATIONS as calculation}
|
{#each CALCULATIONS as calculation}
|
||||||
<option value={calculation.key}>{calculation.name}</option>
|
<option value={calculation.key}>{calculation.name}</option>
|
||||||
{/each}
|
{/each}
|
||||||
</Select>
|
</Select>
|
||||||
<p>of</p> -->
|
{#if view.calculation}
|
||||||
<p>The statistics of</p>
|
<p>of</p>
|
||||||
<Select secondary thin bind:value={view.field}>
|
<Select secondary thin bind:value={view.field}>
|
||||||
<option value="">Choose an option</option>
|
<option value={''}>You must choose an option</option>
|
||||||
{#each fields as field}
|
{#each fields as field}
|
||||||
<option value={field}>{field}</option>
|
<option value={field}>{field}</option>
|
||||||
{/each}
|
{/each}
|
||||||
</Select>
|
</Select>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="footer">
|
<div class="footer">
|
||||||
<Button secondary on:click={onClosed}>Cancel</Button>
|
<Button secondary on:click={onClosed}>Cancel</Button>
|
||||||
<Button primary on:click={saveView}>Save</Button>
|
<Button primary on:click={saveView} disabled={!view.field}>Save</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -72,18 +72,17 @@
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<Input label="Name" thin bind:value={field.name} />
|
<Input label="Name" thin bind:value={field.name} />
|
||||||
|
|
||||||
{#if !originalName}
|
<Select
|
||||||
<Select
|
disabled={originalName}
|
||||||
secondary
|
secondary
|
||||||
thin
|
thin
|
||||||
label="Type"
|
label="Type"
|
||||||
on:change={handleFieldConstraints}
|
on:change={handleFieldConstraints}
|
||||||
bind:value={field.type}>
|
bind:value={field.type}>
|
||||||
{#each Object.values(fieldDefinitions) as field}
|
{#each Object.values(fieldDefinitions) as field}
|
||||||
<option value={field.type}>{field.name}</option>
|
<option value={field.type}>{field.name}</option>
|
||||||
{/each}
|
{/each}
|
||||||
</Select>
|
</Select>
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if field.type !== 'link'}
|
{#if field.type !== 'link'}
|
||||||
<Toggle
|
<Toggle
|
||||||
|
|
|
@ -9,6 +9,10 @@
|
||||||
name: "Equals",
|
name: "Equals",
|
||||||
key: "EQUALS",
|
key: "EQUALS",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "Not Equals",
|
||||||
|
key: "NOT_EQUALS",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "Less Than",
|
name: "Less Than",
|
||||||
key: "LT",
|
key: "LT",
|
||||||
|
|
|
@ -84,7 +84,7 @@
|
||||||
<li>No Users found</li>
|
<li>No Users found</li>
|
||||||
{/each}
|
{/each}
|
||||||
</ul>
|
</ul>
|
||||||
{:catch error}
|
{:catch err}
|
||||||
Something went wrong when trying to fetch users. Please refresh (CMD + R /
|
Something went wrong when trying to fetch users. Please refresh (CMD + R /
|
||||||
CTRL + R) the page and try again.
|
CTRL + R) the page and try again.
|
||||||
{/await}
|
{/await}
|
||||||
|
|
|
@ -154,6 +154,7 @@
|
||||||
const pkg = await applicationPkg.json()
|
const pkg = await applicationPkg.json()
|
||||||
if (applicationPkg.ok) {
|
if (applicationPkg.ok) {
|
||||||
backendUiStore.actions.reset()
|
backendUiStore.actions.reset()
|
||||||
|
pkg.justCreated = true
|
||||||
await store.setPackage(pkg)
|
await store.setPackage(pkg)
|
||||||
automationStore.actions.fetch()
|
automationStore.actions.fetch()
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -11,6 +11,9 @@
|
||||||
|
|
||||||
export let screens = []
|
export let screens = []
|
||||||
|
|
||||||
|
$: sortedScreens = screens.sort(
|
||||||
|
(s1, s2) => s1.props._instanceName > s2.props._instanceName
|
||||||
|
)
|
||||||
/*
|
/*
|
||||||
Using a store here seems odd....
|
Using a store here seems odd....
|
||||||
have a look in the <ComponentsHierarchyChildren /> code file to find out why.
|
have a look in the <ComponentsHierarchyChildren /> code file to find out why.
|
||||||
|
@ -24,12 +27,15 @@
|
||||||
const joinPath = join("/")
|
const joinPath = join("/")
|
||||||
|
|
||||||
const normalizedName = name =>
|
const normalizedName = name =>
|
||||||
pipe(name, [
|
pipe(
|
||||||
trimCharsStart("./"),
|
name,
|
||||||
trimCharsStart("~/"),
|
[
|
||||||
trimCharsStart("../"),
|
trimCharsStart("./"),
|
||||||
trimChars(" "),
|
trimCharsStart("~/"),
|
||||||
])
|
trimCharsStart("../"),
|
||||||
|
trimChars(" "),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
const changeScreen = screen => {
|
const changeScreen = screen => {
|
||||||
store.setCurrentScreen(screen.props._instanceName)
|
store.setCurrentScreen(screen.props._instanceName)
|
||||||
|
@ -38,7 +44,7 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="root">
|
<div class="root">
|
||||||
{#each screens as screen}
|
{#each sortedScreens as screen}
|
||||||
<div
|
<div
|
||||||
class="budibase__nav-item screen-header-row"
|
class="budibase__nav-item screen-header-row"
|
||||||
class:selected={$store.currentComponentInfo._id === screen.props._id}
|
class:selected={$store.currentComponentInfo._id === screen.props._id}
|
||||||
|
|
|
@ -13,6 +13,7 @@
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
|
|
||||||
export let label = ""
|
export let label = ""
|
||||||
|
export let bindable = true
|
||||||
export let componentInstance = {}
|
export let componentInstance = {}
|
||||||
export let control = null
|
export let control = null
|
||||||
export let key = ""
|
export let key = ""
|
||||||
|
@ -93,7 +94,7 @@
|
||||||
{...props}
|
{...props}
|
||||||
name={key} />
|
name={key} />
|
||||||
</div>
|
</div>
|
||||||
{#if control === Input && !key.startsWith('_')}
|
{#if bindable && control === Input && !key.startsWith('_')}
|
||||||
<button data-cy={`${key}-binding-button`} on:click={dropdown.show}>
|
<button data-cy={`${key}-binding-button`} on:click={dropdown.show}>
|
||||||
<Icon name="edit" />
|
<Icon name="edit" />
|
||||||
</button>
|
</button>
|
||||||
|
|
|
@ -88,6 +88,7 @@
|
||||||
{#if screenOrPageInstance}
|
{#if screenOrPageInstance}
|
||||||
{#each screenOrPageDefinition as def}
|
{#each screenOrPageDefinition as def}
|
||||||
<PropertyControl
|
<PropertyControl
|
||||||
|
bindable={false}
|
||||||
control={def.control}
|
control={def.control}
|
||||||
label={def.label}
|
label={def.label}
|
||||||
key={def.key}
|
key={def.key}
|
||||||
|
|
|
@ -327,6 +327,12 @@ export default {
|
||||||
key: "datasource",
|
key: "datasource",
|
||||||
control: TableViewSelect,
|
control: TableViewSelect,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: "Table URL",
|
||||||
|
key: "detailUrl",
|
||||||
|
placeholder: "tableName",
|
||||||
|
control: ScreenSelect,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: "Editable",
|
label: "Editable",
|
||||||
key: "editable",
|
key: "editable",
|
||||||
|
|
|
@ -80,7 +80,7 @@
|
||||||
{#await promise}
|
{#await promise}
|
||||||
<!-- This should probably be some kind of loading state? -->
|
<!-- This should probably be some kind of loading state? -->
|
||||||
<div />
|
<div />
|
||||||
{:then results}
|
{:then _}
|
||||||
<slot />
|
<slot />
|
||||||
{:catch error}
|
{:catch error}
|
||||||
<p>Something went wrong: {error.message}</p>
|
<p>Something went wrong: {error.message}</p>
|
||||||
|
|
|
@ -3,6 +3,8 @@ export const parseAppIdFromCookie = docCookie => {
|
||||||
docCookie.split(";").find(c => c.trim().startsWith("budibase:token")) ||
|
docCookie.split(";").find(c => c.trim().startsWith("budibase:token")) ||
|
||||||
docCookie.split(";").find(c => c.trim().startsWith("builder:token"))
|
docCookie.split(";").find(c => c.trim().startsWith("builder:token"))
|
||||||
|
|
||||||
|
if (!cookie) return location.pathname.replace(/\//g, "")
|
||||||
|
|
||||||
const base64Token = cookie.substring(lengthOfKey)
|
const base64Token = cookie.substring(lengthOfKey)
|
||||||
|
|
||||||
const user = JSON.parse(atob(base64Token.split(".")[1]))
|
const user = JSON.parse(atob(base64Token.split(".")[1]))
|
||||||
|
|
|
@ -31,7 +31,9 @@ export const screenRouter = ({ screens, onScreenSelected, window }) => {
|
||||||
function route(url) {
|
function route(url) {
|
||||||
const _url = makeRootedPath(url.state || url)
|
const _url = makeRootedPath(url.state || url)
|
||||||
current = routes.findIndex(
|
current = routes.findIndex(
|
||||||
p => p !== "*" && new RegExp("^" + p + "$").test(_url)
|
p =>
|
||||||
|
p !== "*" &&
|
||||||
|
new RegExp("^" + p.toLowerCase() + "$").test(_url.toLowerCase())
|
||||||
)
|
)
|
||||||
|
|
||||||
const params = {}
|
const params = {}
|
||||||
|
|
|
@ -2,6 +2,7 @@ const fs = require("fs")
|
||||||
const { join } = require("../../../utilities/centralPath")
|
const { join } = require("../../../utilities/centralPath")
|
||||||
const AWS = require("aws-sdk")
|
const AWS = require("aws-sdk")
|
||||||
const fetch = require("node-fetch")
|
const fetch = require("node-fetch")
|
||||||
|
const uuid = require("uuid")
|
||||||
const { budibaseAppsDir } = require("../../../utilities/budibaseDir")
|
const { budibaseAppsDir } = require("../../../utilities/budibaseDir")
|
||||||
const PouchDB = require("../../../db")
|
const PouchDB = require("../../../db")
|
||||||
const environment = require("../../../environment")
|
const environment = require("../../../environment")
|
||||||
|
@ -13,7 +14,7 @@ async function invalidateCDN(cfDistribution, appId) {
|
||||||
.createInvalidation({
|
.createInvalidation({
|
||||||
DistributionId: cfDistribution,
|
DistributionId: cfDistribution,
|
||||||
InvalidationBatch: {
|
InvalidationBatch: {
|
||||||
CallerReference: appId,
|
CallerReference: `${appId}-${uuid.v4()}`,
|
||||||
Paths: {
|
Paths: {
|
||||||
Quantity: 1,
|
Quantity: 1,
|
||||||
Items: [`/assets/${appId}/*`],
|
Items: [`/assets/${appId}/*`],
|
||||||
|
|
|
@ -11,6 +11,12 @@ const { cloneDeep } = require("lodash")
|
||||||
|
|
||||||
const TABLE_VIEW_BEGINS_WITH = `all${SEPARATOR}${DocumentTypes.TABLE}${SEPARATOR}`
|
const TABLE_VIEW_BEGINS_WITH = `all${SEPARATOR}${DocumentTypes.TABLE}${SEPARATOR}`
|
||||||
|
|
||||||
|
const CALCULATION_TYPES = {
|
||||||
|
SUM: "sum",
|
||||||
|
COUNT: "count",
|
||||||
|
STATS: "stats",
|
||||||
|
}
|
||||||
|
|
||||||
validateJs.extend(validateJs.validators.datetime, {
|
validateJs.extend(validateJs.validators.datetime, {
|
||||||
parse: function(value) {
|
parse: function(value) {
|
||||||
return new Date(value).getTime()
|
return new Date(value).getTime()
|
||||||
|
@ -137,7 +143,7 @@ exports.save = async function(ctx) {
|
||||||
exports.fetchView = async function(ctx) {
|
exports.fetchView = async function(ctx) {
|
||||||
const instanceId = ctx.user.instanceId
|
const instanceId = ctx.user.instanceId
|
||||||
const db = new CouchDB(instanceId)
|
const db = new CouchDB(instanceId)
|
||||||
const { stats, group, field } = ctx.query
|
const { calculation, group, field } = ctx.query
|
||||||
const viewName = ctx.params.viewName
|
const viewName = ctx.params.viewName
|
||||||
|
|
||||||
// if this is a table view being looked for just transfer to that
|
// if this is a table view being looked for just transfer to that
|
||||||
|
@ -148,22 +154,35 @@ exports.fetchView = async function(ctx) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await db.query(`database/${viewName}`, {
|
const response = await db.query(`database/${viewName}`, {
|
||||||
include_docs: !stats,
|
include_docs: !calculation,
|
||||||
group,
|
group,
|
||||||
})
|
})
|
||||||
|
|
||||||
if (stats) {
|
if (!calculation) {
|
||||||
|
response.rows = response.rows.map(row => row.doc)
|
||||||
|
ctx.body = await linkRows.attachLinkInfo(instanceId, response.rows)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (calculation === CALCULATION_TYPES.STATS) {
|
||||||
response.rows = response.rows.map(row => ({
|
response.rows = response.rows.map(row => ({
|
||||||
group: row.key,
|
group: row.key,
|
||||||
field,
|
field,
|
||||||
...row.value,
|
...row.value,
|
||||||
avg: row.value.sum / row.value.count,
|
avg: row.value.sum / row.value.count,
|
||||||
}))
|
}))
|
||||||
} else {
|
ctx.body = response.rows
|
||||||
response.rows = response.rows.map(row => row.doc)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.body = await linkRows.attachLinkInfo(instanceId, response.rows)
|
if (
|
||||||
|
calculation === CALCULATION_TYPES.COUNT ||
|
||||||
|
calculation === CALCULATION_TYPES.SUM
|
||||||
|
) {
|
||||||
|
ctx.body = response.rows.map(row => ({
|
||||||
|
group: row.key,
|
||||||
|
field,
|
||||||
|
value: row.value,
|
||||||
|
}))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.fetchTableRows = async function(ctx) {
|
exports.fetchTableRows = async function(ctx) {
|
||||||
|
|
|
@ -45,7 +45,7 @@ exports[`viewBuilder Filter creates a view with multiple filters and conjunction
|
||||||
Object {
|
Object {
|
||||||
"map": "function (doc) {
|
"map": "function (doc) {
|
||||||
if (doc.tableId === \\"14f1c4e94d6a47b682ce89d35d4c78b0\\" && doc[\\"Name\\"] === \\"Test\\" || doc[\\"Yes\\"] > \\"Value\\") {
|
if (doc.tableId === \\"14f1c4e94d6a47b682ce89d35d4c78b0\\" && doc[\\"Name\\"] === \\"Test\\" || doc[\\"Yes\\"] > \\"Value\\") {
|
||||||
emit(doc._id);
|
emit(doc[\\"_id\\"], doc[\\"undefined\\"]);
|
||||||
}
|
}
|
||||||
}",
|
}",
|
||||||
"meta": Object {
|
"meta": Object {
|
||||||
|
@ -86,6 +86,5 @@ Object {
|
||||||
"schema": null,
|
"schema": null,
|
||||||
"tableId": "14f1c4e94d6a47b682ce89d35d4c78b0",
|
"tableId": "14f1c4e94d6a47b682ce89d35d4c78b0",
|
||||||
},
|
},
|
||||||
"reduce": "_stats",
|
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
const TOKEN_MAP = {
|
const TOKEN_MAP = {
|
||||||
EQUALS: "===",
|
EQUALS: "===",
|
||||||
|
NOT_EQUALS: "!==",
|
||||||
LT: "<",
|
LT: "<",
|
||||||
LTE: "<=",
|
LTE: "<=",
|
||||||
MT: ">",
|
MT: ">",
|
||||||
|
@ -22,6 +23,14 @@ const FIELD_PROPERTY = {
|
||||||
}
|
}
|
||||||
|
|
||||||
const SCHEMA_MAP = {
|
const SCHEMA_MAP = {
|
||||||
|
sum: {
|
||||||
|
field: "string",
|
||||||
|
value: "number",
|
||||||
|
},
|
||||||
|
count: {
|
||||||
|
field: "string",
|
||||||
|
value: "number",
|
||||||
|
},
|
||||||
stats: {
|
stats: {
|
||||||
sum: {
|
sum: {
|
||||||
type: "number",
|
type: "number",
|
||||||
|
@ -80,8 +89,7 @@ function parseFilterExpression(filters) {
|
||||||
* @param {String?} groupBy - field to group calculation results on, if any
|
* @param {String?} groupBy - field to group calculation results on, if any
|
||||||
*/
|
*/
|
||||||
function parseEmitExpression(field, groupBy) {
|
function parseEmitExpression(field, groupBy) {
|
||||||
if (field) return `emit(doc["${groupBy || "_id"}"], doc["${field}"]);`
|
return `emit(doc["${groupBy || "_id"}"], doc["${field}"]);`
|
||||||
return `emit(doc._id);`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -101,7 +109,7 @@ function viewTemplate({ field, tableId, groupBy, filters = [], calculation }) {
|
||||||
|
|
||||||
const emitExpression = parseEmitExpression(field, groupBy)
|
const emitExpression = parseEmitExpression(field, groupBy)
|
||||||
|
|
||||||
const reduction = field ? { reduce: "_stats" } : {}
|
const reduction = field && calculation ? { reduce: `_${calculation}` } : {}
|
||||||
|
|
||||||
let schema = null
|
let schema = null
|
||||||
|
|
||||||
|
|
|
@ -18,6 +18,7 @@ describe("/views", () => {
|
||||||
const createView = async (config = {
|
const createView = async (config = {
|
||||||
name: "TestView",
|
name: "TestView",
|
||||||
field: "Price",
|
field: "Price",
|
||||||
|
calculation: "stats",
|
||||||
tableId: table._id
|
tableId: table._id
|
||||||
}) =>
|
}) =>
|
||||||
await request
|
await request
|
||||||
|
@ -65,20 +66,30 @@ describe("/views", () => {
|
||||||
expect(updatedTable.views).toEqual({
|
expect(updatedTable.views).toEqual({
|
||||||
TestView: {
|
TestView: {
|
||||||
field: "Price",
|
field: "Price",
|
||||||
|
calculation: "stats",
|
||||||
tableId: table._id,
|
tableId: table._id,
|
||||||
filters: [],
|
filters: [],
|
||||||
schema: {
|
schema: {
|
||||||
name: {
|
sum: {
|
||||||
type: "string",
|
type: "number",
|
||||||
constraints: {
|
|
||||||
type: "string"
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
description: {
|
min: {
|
||||||
|
type: "number",
|
||||||
|
},
|
||||||
|
max: {
|
||||||
|
type: "number",
|
||||||
|
},
|
||||||
|
count: {
|
||||||
|
type: "number",
|
||||||
|
},
|
||||||
|
sumsqr: {
|
||||||
|
type: "number",
|
||||||
|
},
|
||||||
|
avg: {
|
||||||
|
type: "number",
|
||||||
|
},
|
||||||
|
field: {
|
||||||
type: "string",
|
type: "string",
|
||||||
constraints: {
|
|
||||||
type: "string"
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -123,7 +134,7 @@ describe("/views", () => {
|
||||||
Price: 4000
|
Price: 4000
|
||||||
})
|
})
|
||||||
const res = await request
|
const res = await request
|
||||||
.get(`/api/views/TestView?stats=true`)
|
.get(`/api/views/TestView?calculation=stats`)
|
||||||
.set(defaultHeaders(app._id, instance._id))
|
.set(defaultHeaders(app._id, instance._id))
|
||||||
.expect('Content-Type', /json/)
|
.expect('Content-Type', /json/)
|
||||||
.expect(200)
|
.expect(200)
|
||||||
|
@ -133,6 +144,7 @@ describe("/views", () => {
|
||||||
|
|
||||||
it("returns data for the created view using a group by", async () => {
|
it("returns data for the created view using a group by", async () => {
|
||||||
await createView({
|
await createView({
|
||||||
|
calculation: "stats",
|
||||||
name: "TestView",
|
name: "TestView",
|
||||||
field: "Price",
|
field: "Price",
|
||||||
groupBy: "Category",
|
groupBy: "Category",
|
||||||
|
@ -154,10 +166,11 @@ describe("/views", () => {
|
||||||
Category: "Two"
|
Category: "Two"
|
||||||
})
|
})
|
||||||
const res = await request
|
const res = await request
|
||||||
.get(`/api/views/TestView?stats=true&group=Category`)
|
.get(`/api/views/TestView?calculation=stats&group=Category`)
|
||||||
.set(defaultHeaders(app._id, instance._id))
|
.set(defaultHeaders(app._id, instance._id))
|
||||||
.expect('Content-Type', /json/)
|
.expect('Content-Type', /json/)
|
||||||
.expect(200)
|
.expect(200)
|
||||||
|
|
||||||
expect(res.body.length).toBe(2)
|
expect(res.body.length).toBe(2)
|
||||||
expect(res.body).toMatchSnapshot()
|
expect(res.body).toMatchSnapshot()
|
||||||
})
|
})
|
||||||
|
|
|
@ -34,13 +34,15 @@ module.exports = async (ctx, next) => {
|
||||||
|
|
||||||
let appId = process.env.CLOUD ? ctx.subdomains[1] : ctx.params.appId
|
let appId = process.env.CLOUD ? ctx.subdomains[1] : ctx.params.appId
|
||||||
|
|
||||||
if (!appId) {
|
// if appId can't be determined from path param or subdomain
|
||||||
appId = ctx.referer && ctx.referer.split("/").pop()
|
if (!appId && ctx.request.headers.referer) {
|
||||||
|
const url = new URL(ctx.request.headers.referer)
|
||||||
|
// remove leading and trailing slashes from appId
|
||||||
|
appId = url.pathname.replace(/\//g, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.user = {
|
ctx.user = {
|
||||||
// if appId can't be determined from path param or subdomain
|
appId,
|
||||||
appId: appId,
|
|
||||||
}
|
}
|
||||||
await next()
|
await next()
|
||||||
return
|
return
|
||||||
|
|
|
@ -246,6 +246,7 @@
|
||||||
"type": "bool",
|
"type": "bool",
|
||||||
"default": true
|
"default": true
|
||||||
},
|
},
|
||||||
|
"detailUrl": "string"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dataform": {
|
"dataform": {
|
||||||
|
|
|
@ -3,4 +3,4 @@
|
||||||
export let files
|
export let files
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<AttachmentList {files} on:delete />
|
<AttachmentList {files} on:delete />
|
||||||
|
|
|
@ -19,12 +19,13 @@
|
||||||
export let theme = "alpine"
|
export let theme = "alpine"
|
||||||
export let height = 500
|
export let height = 500
|
||||||
export let pagination
|
export let pagination
|
||||||
|
export let detailUrl
|
||||||
|
|
||||||
// These can never change at runtime so don't need to be reactive
|
// These can never change at runtime so don't need to be reactive
|
||||||
let canEdit = editable && datasource && datasource.type !== "view"
|
let canEdit = editable && datasource && datasource.type !== "view"
|
||||||
let canAddDelete = editable && datasource && datasource.type === "table"
|
let canAddDelete = editable && datasource && datasource.type === "table"
|
||||||
|
|
||||||
let modal;
|
let modal
|
||||||
|
|
||||||
let store = _bb.store
|
let store = _bb.store
|
||||||
let dataLoaded = false
|
let dataLoaded = false
|
||||||
|
@ -74,6 +75,19 @@
|
||||||
autoHeight: true,
|
autoHeight: true,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
columnDefs = [...columnDefs, {
|
||||||
|
headerName: 'Details',
|
||||||
|
field: '_id',
|
||||||
|
width: 25,
|
||||||
|
flex: 0,
|
||||||
|
editable: false,
|
||||||
|
sortable: false,
|
||||||
|
cellRenderer: getRenderer({
|
||||||
|
type: '_id',
|
||||||
|
options: detailUrl
|
||||||
|
}),
|
||||||
|
autoHeight: true,
|
||||||
|
}]
|
||||||
dataLoaded = true
|
dataLoaded = true
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -140,7 +154,10 @@
|
||||||
on:select={({ detail }) => (selectedRows = detail)} />
|
on:select={({ detail }) => (selectedRows = detail)} />
|
||||||
{/if}
|
{/if}
|
||||||
<Modal bind:this={modal}>
|
<Modal bind:this={modal}>
|
||||||
<ModalContent title="Confirm Row Deletion" confirmText="Delete" onConfirm={deleteRows} >
|
<ModalContent
|
||||||
|
title="Confirm Row Deletion"
|
||||||
|
confirmText="Delete"
|
||||||
|
onConfirm={deleteRows}>
|
||||||
<span>Are you sure you want to delete {selectedRows.length} row(s)?</span>
|
<span>Are you sure you want to delete {selectedRows.length} row(s)?</span>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
<script>
|
||||||
|
import { Icon } from '@budibase/bbui'
|
||||||
|
export let url
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<a href={url}><Icon name="view" /></a>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
a {
|
||||||
|
color: var(--grey-6)
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -2,6 +2,7 @@
|
||||||
// https://www.ag-grid.com/javascript-grid-cell-rendering-components/
|
// https://www.ag-grid.com/javascript-grid-cell-rendering-components/
|
||||||
|
|
||||||
import AttachmentCell from "./AttachmentCell/Button.svelte"
|
import AttachmentCell from "./AttachmentCell/Button.svelte"
|
||||||
|
import ViewDetails from "./ViewDetails/Cell.svelte"
|
||||||
import Select from "./Select/Wrapper.svelte"
|
import Select from "./Select/Wrapper.svelte"
|
||||||
import DatePicker from "./DateTime/Wrapper.svelte"
|
import DatePicker from "./DateTime/Wrapper.svelte"
|
||||||
import RelationshipDisplay from "./Relationship/RelationshipDisplay.svelte"
|
import RelationshipDisplay from "./Relationship/RelationshipDisplay.svelte"
|
||||||
|
@ -11,18 +12,19 @@ const renderers = new Map([
|
||||||
["attachment", attachmentRenderer],
|
["attachment", attachmentRenderer],
|
||||||
["options", optionsRenderer],
|
["options", optionsRenderer],
|
||||||
["link", linkedRowRenderer],
|
["link", linkedRowRenderer],
|
||||||
|
["_id", viewDetailsRenderer],
|
||||||
])
|
])
|
||||||
|
|
||||||
export function getRenderer({ type, constraints }, editable) {
|
export function getRenderer(schema, editable) {
|
||||||
if (renderers.get(type)) {
|
if (renderers.get(schema.type)) {
|
||||||
return renderers.get(type)(constraints, editable)
|
return renderers.get(schema.type)(schema.options, editable)
|
||||||
} else {
|
} else {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* eslint-disable no-unused-vars */
|
/* eslint-disable no-unused-vars */
|
||||||
function booleanRenderer(constraints, editable) {
|
function booleanRenderer(options, editable) {
|
||||||
return params => {
|
return params => {
|
||||||
const toggle = e => {
|
const toggle = e => {
|
||||||
params.value = !params.value
|
params.value = !params.value
|
||||||
|
@ -44,7 +46,7 @@ function booleanRenderer(constraints, editable) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/* eslint-disable no-unused-vars */
|
/* eslint-disable no-unused-vars */
|
||||||
function attachmentRenderer(constraints, editable) {
|
function attachmentRenderer(options, editable) {
|
||||||
return params => {
|
return params => {
|
||||||
const container = document.createElement("div")
|
const container = document.createElement("div")
|
||||||
|
|
||||||
|
@ -66,7 +68,7 @@ function attachmentRenderer(constraints, editable) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/* eslint-disable no-unused-vars */
|
/* eslint-disable no-unused-vars */
|
||||||
function dateRenderer(constraints, editable) {
|
function dateRenderer(options, editable) {
|
||||||
return function(params) {
|
return function(params) {
|
||||||
const container = document.createElement("div")
|
const container = document.createElement("div")
|
||||||
const toggle = e => {
|
const toggle = e => {
|
||||||
|
@ -111,7 +113,7 @@ function optionsRenderer({ inclusion }, editable) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/* eslint-disable no-unused-vars */
|
/* eslint-disable no-unused-vars */
|
||||||
function linkedRowRenderer(constraints, editable) {
|
function linkedRowRenderer(options, editable) {
|
||||||
return params => {
|
return params => {
|
||||||
let container = document.createElement("div")
|
let container = document.createElement("div")
|
||||||
container.style.display = "grid"
|
container.style.display = "grid"
|
||||||
|
@ -129,3 +131,22 @@ function linkedRowRenderer(constraints, editable) {
|
||||||
return container
|
return container
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* eslint-disable no-unused-vars */
|
||||||
|
function viewDetailsRenderer(options) {
|
||||||
|
return params => {
|
||||||
|
let container = document.createElement("div")
|
||||||
|
container.style.display = "grid"
|
||||||
|
container.style.placeItems = "center"
|
||||||
|
container.style.height = "100%"
|
||||||
|
|
||||||
|
new ViewDetails({
|
||||||
|
target: container,
|
||||||
|
props: {
|
||||||
|
url: `${options}/${params.data._id}`,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return container
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -35,7 +35,7 @@
|
||||||
|
|
||||||
{#await _appPromise}
|
{#await _appPromise}
|
||||||
loading
|
loading
|
||||||
{:then _bb}
|
{:then _}
|
||||||
<div id="current_component" bind:this={currentComponent} />
|
<div id="current_component" bind:this={currentComponent} />
|
||||||
{/await}
|
{/await}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import { Modal, ModalContent, Icon } from '@budibase/bbui'
|
import { Modal, ModalContent, Icon } from "@budibase/bbui"
|
||||||
import { createEventDispatcher } from "svelte";
|
import { createEventDispatcher } from "svelte"
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
import { FILE_TYPES } from "./fileTypes"
|
import { FILE_TYPES } from "./fileTypes"
|
||||||
|
@ -9,16 +9,16 @@
|
||||||
export let height = "70"
|
export let height = "70"
|
||||||
export let width = "70"
|
export let width = "70"
|
||||||
|
|
||||||
let modal;
|
let modal
|
||||||
let currentFile;
|
let currentFile
|
||||||
|
|
||||||
const openModal = (file) => {
|
const openModal = file => {
|
||||||
currentFile = file
|
currentFile = file
|
||||||
modal.show()
|
modal.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleConfirm = () => {
|
const handleConfirm = () => {
|
||||||
dispatch('delete', currentFile)
|
dispatch("delete", currentFile)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -31,12 +31,19 @@
|
||||||
{:else}<i class="far fa-file" />{/if}
|
{:else}<i class="far fa-file" />{/if}
|
||||||
</a>
|
</a>
|
||||||
<span>{file.name}</span>
|
<span>{file.name}</span>
|
||||||
<div class="button-placement"><button primary on:click|stopPropagation={() => openModal(file)}>×</button></div>
|
<div class="button-placement">
|
||||||
|
<button primary on:click|stopPropagation={() => openModal(file)}>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
<Modal bind:this={modal}>
|
<Modal bind:this={modal}>
|
||||||
<ModalContent title="Confirm File Deletion" confirmText="Delete" onConfirm={handleConfirm} >
|
<ModalContent
|
||||||
|
title="Confirm File Deletion"
|
||||||
|
confirmText="Delete"
|
||||||
|
onConfirm={handleConfirm}>
|
||||||
<span>Are you sure you want to delete this attachment?</span>
|
<span>Are you sure you want to delete this attachment?</span>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
@ -67,7 +74,7 @@
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
display: block;
|
display: block;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
@ -85,18 +92,18 @@
|
||||||
border-radius: var(--border-radius-xl);
|
border-radius: var(--border-radius-xl);
|
||||||
background: black;
|
background: black;
|
||||||
transition: transform 0.2s cubic-bezier(0.25, 0.1, 0.25, 1),
|
transition: transform 0.2s cubic-bezier(0.25, 0.1, 0.25, 1),
|
||||||
background 0.2s cubic-bezier(0.25, 0.1, 0.25, 1);
|
background 0.2s cubic-bezier(0.25, 0.1, 0.25, 1);
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
button:hover {
|
button:hover {
|
||||||
background-color: var(--grey-8);
|
background-color: var(--grey-8);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
button:active {
|
button:active {
|
||||||
background-color: var(--grey-9);
|
background-color: var(--grey-9);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.file {
|
.file {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
Loading…
Reference in New Issue