Merge branch 'develop' of github.com:Budibase/budibase into feature/map-actions
This commit is contained in:
commit
bedbf4bcae
29
README.md
29
README.md
|
@ -102,6 +102,35 @@ Budibase is made to scale. With Budibase, you can self-host on your own infrastr
|
|||
|
||||
- Checkout the promo video: https://youtu.be/xoljVpty_Kw
|
||||
|
||||
<br />
|
||||
|
||||
---
|
||||
|
||||
<br />
|
||||
|
||||
|
||||
## Budibase Public API
|
||||
As with anything that we build in Budibase, our new public API is simple to use, flexible, and introduces new extensibility. To summarize, the Budibase API enables:
|
||||
|
||||
- Budibase as a backend
|
||||
- Interoperability
|
||||
|
||||
|
||||
#### Docs
|
||||
You can learn more about the Budibase API at the following places:
|
||||
|
||||
- [General documentation](https://docs.budibase.com/docs/public-api) : Learn how to get your API key, how to use spec, and how to use with Postman
|
||||
- [Interactive API documentation](https://docs.budibase.com/reference/post_applications) : Learn how to interact with the API
|
||||
|
||||
#### Guides
|
||||
|
||||
- [Build an app with Budibase and Next.js](https://budibase.com/blog/building-a-crud-app-with-budibase-and-next.js/)
|
||||
|
||||
<p align="center">
|
||||
<img alt="Budibase data" src="https://res.cloudinary.com/daog6scxm/image/upload/v1647858558/Feb%20release/Start_building_with_Budibase_s_API_3_rhlzhv.png">
|
||||
</p>
|
||||
<br /><br />
|
||||
|
||||
<br /><br /><br />
|
||||
|
||||
## 🏁 Get started
|
||||
|
|
|
@ -12,7 +12,7 @@ All ports are BLOCKED except 22 (SSH), 80 (HTTP), 443 (HTTPS), and 10000
|
|||
|
||||
* Budibase website: http://budibase.com
|
||||
|
||||
For help and more information, visit https://docs.budibase.com/self-hosting/hosting-methods/digitalocean
|
||||
For help and more information, visit https://docs.budibase.com/docs/digitalocean
|
||||
|
||||
********************************************************************************
|
||||
To delete this message of the day: rm -rf $(readlink -f ${0})
|
||||
|
|
|
@ -39,7 +39,7 @@
|
|||
</p>
|
||||
|
||||
<h3 align="center">
|
||||
<a href="https://docs.budibase.com/getting-started">Los Geht's</a>
|
||||
<a href="https://docs.budibase.com/docs/quickstart-tutorials">Los Geht's</a>
|
||||
<span> · </span>
|
||||
<a href="https://docs.budibase.com">Dokumentation</a>
|
||||
<span> · </span>
|
||||
|
@ -109,7 +109,7 @@ $ budi hosting --start
|
|||
4. Lege einen Admin-Benutzer an.
|
||||
Gib die E-Mail und das Passwort für den neuen Admin-Benutzer ein.
|
||||
|
||||
Schon geschafft! Jetzt kann es losgehen mit der minutenschnellen Entwicklung deiner Tools. Für weitere Informationen und Tipps schau doch mal in unsere [Dokumentation](https://docs.budibase.com/getting-started).
|
||||
Schon geschafft! Jetzt kann es losgehen mit der minutenschnellen Entwicklung deiner Tools. Für weitere Informationen und Tipps schau doch mal in unsere [Dokumentation](https://docs.budibase.com/docs/quickstart-tutorials).
|
||||
|
||||
<br />
|
||||
|
||||
|
|
|
@ -112,7 +112,7 @@ The Budibase builder runs in Electron, on Mac, PC and Linux. Follow the steps be
|
|||
|
||||
Budibase wants to make sure anyone can use the tools we develop and we know a lot of people need to be able to host the apps they make on their own systems - that is why we've decided to try and make self hosting as easy as possible!
|
||||
|
||||
Currently, you can host your apps using Docker or Digital Ocean. The documentation for self-hosting can be found [here](https://docs.budibase.com/self-hosting/introduction-to-self-hosting).
|
||||
Currently, you can host your apps using Docker or Digital Ocean. The documentation for self-hosting can be found [here](https://docs.budibase.com/docs/hosting-methods).
|
||||
|
||||
[![Deploy to DO](https://www.deploytodo.com/do-btn-blue.svg)](https://cloud.digitalocean.com/droplets/new?onboarding_origin=marketplace&i=09038e&fleetUuid=bb04f9c8-1de8-4687-b2ae-1d5177a0535b&appId=77729671&type=applications&size=s-4vcpu-8gb®ion=nyc1&refcode=0caaa6085a82&image=budibase-20-04)
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"version": "1.0.91-alpha.0",
|
||||
"version": "1.0.91-alpha.16",
|
||||
"npmClient": "yarn",
|
||||
"packages": [
|
||||
"packages/*"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@budibase/backend-core",
|
||||
"version": "1.0.91-alpha.0",
|
||||
"version": "1.0.91-alpha.16",
|
||||
"description": "Budibase backend core libraries used in server and worker",
|
||||
"main": "src/index.js",
|
||||
"author": "Budibase",
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "@budibase/bbui",
|
||||
"description": "A UI solution used in the different Budibase projects.",
|
||||
"version": "1.0.91-alpha.0",
|
||||
"version": "1.0.91-alpha.16",
|
||||
"license": "MPL-2.0",
|
||||
"svelte": "src/index.js",
|
||||
"module": "dist/bbui.es.js",
|
||||
|
@ -38,7 +38,7 @@
|
|||
],
|
||||
"dependencies": {
|
||||
"@adobe/spectrum-css-workflow-icons": "^1.2.1",
|
||||
"@budibase/string-templates": "^1.0.91-alpha.0",
|
||||
"@budibase/string-templates": "^1.0.91-alpha.16",
|
||||
"@spectrum-css/actionbutton": "^1.0.1",
|
||||
"@spectrum-css/actiongroup": "^1.0.1",
|
||||
"@spectrum-css/avatar": "^3.0.2",
|
||||
|
|
|
@ -56,6 +56,7 @@
|
|||
$: if (!loading) loaded = true
|
||||
$: fields = getFields(schema, showAutoColumns, autoSortColumns)
|
||||
$: rows = fields?.length ? data || [] : []
|
||||
$: totalRowCount = rows?.length || 0
|
||||
$: visibleRowCount = getVisibleRowCount(
|
||||
loaded,
|
||||
height,
|
||||
|
@ -63,7 +64,12 @@
|
|||
rowCount,
|
||||
rowHeight
|
||||
)
|
||||
$: contentStyle = getContentStyle(visibleRowCount, rowCount, rowHeight)
|
||||
$: heightStyle = getHeightStyle(
|
||||
visibleRowCount,
|
||||
rowCount,
|
||||
totalRowCount,
|
||||
rowHeight
|
||||
)
|
||||
$: sortedRows = sortRows(rows, sortColumn, sortOrder)
|
||||
$: gridStyle = getGridStyle(fields, schema, showEditColumn)
|
||||
$: showEditColumn = allowEditRows || allowSelectRows
|
||||
|
@ -107,11 +113,16 @@
|
|||
return Math.min(allRows, Math.ceil(height / rowHeight))
|
||||
}
|
||||
|
||||
const getContentStyle = (visibleRows, rowCount, rowHeight) => {
|
||||
if (!rowCount || !visibleRows) {
|
||||
const getHeightStyle = (
|
||||
visibleRowCount,
|
||||
rowCount,
|
||||
totalRowCount,
|
||||
rowHeight
|
||||
) => {
|
||||
if (!rowCount || !visibleRowCount || totalRowCount <= rowCount) {
|
||||
return ""
|
||||
}
|
||||
return `height: ${headerHeight + visibleRows * rowHeight}px;`
|
||||
return `height: ${headerHeight + visibleRowCount * rowHeight}px;`
|
||||
}
|
||||
|
||||
const getGridStyle = (fields, schema, showEditColumn) => {
|
||||
|
@ -264,11 +275,11 @@
|
|||
style={`--row-height: ${rowHeight}px; --header-height: ${headerHeight}px;`}
|
||||
>
|
||||
{#if !loaded}
|
||||
<div class="loading" style={contentStyle}>
|
||||
<div class="loading" style={heightStyle}>
|
||||
<ProgressCircle />
|
||||
</div>
|
||||
{:else}
|
||||
<div class="spectrum-Table" style={`${contentStyle}${gridStyle}`}>
|
||||
<div class="spectrum-Table" style={`${heightStyle}${gridStyle}`}>
|
||||
{#if fields.length}
|
||||
<div class="spectrum-Table-head">
|
||||
{#if showEditColumn}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -247,7 +247,7 @@ Cypress.Commands.add("createScreen", (screenName, route) => {
|
|||
cy.get("[aria-label=AddCircle]").click()
|
||||
cy.get(".spectrum-Modal").within(() => {
|
||||
cy.get(".item").contains("Blank").click()
|
||||
cy.get(".spectrum-Button").contains("Add Screens").click({ force: true })
|
||||
cy.get(".spectrum-Button").contains("Add screens").click({ force: true })
|
||||
cy.wait(500)
|
||||
})
|
||||
cy.get(".spectrum-Dialog-grid").within(() => {
|
||||
|
@ -265,7 +265,7 @@ Cypress.Commands.add("createAutogeneratedScreens", screenNames => {
|
|||
for (let i = 0; i < screenNames.length; i++) {
|
||||
cy.get(".item").contains(screenNames[i]).click()
|
||||
}
|
||||
cy.get(".spectrum-Button").contains("Add Screens").click({ force: true })
|
||||
cy.get(".spectrum-Button").contains("Add screens").click({ force: true })
|
||||
cy.wait(4000)
|
||||
})
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@budibase/builder",
|
||||
"version": "1.0.91-alpha.0",
|
||||
"version": "1.0.91-alpha.16",
|
||||
"license": "GPL-3.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
@ -65,10 +65,10 @@
|
|||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@budibase/bbui": "^1.0.91-alpha.0",
|
||||
"@budibase/client": "^1.0.91-alpha.0",
|
||||
"@budibase/frontend-core": "^1.0.91-alpha.0",
|
||||
"@budibase/string-templates": "^1.0.91-alpha.0",
|
||||
"@budibase/bbui": "^1.0.91-alpha.16",
|
||||
"@budibase/client": "^1.0.91-alpha.16",
|
||||
"@budibase/frontend-core": "^1.0.91-alpha.16",
|
||||
"@budibase/string-templates": "^1.0.91-alpha.16",
|
||||
"@sentry/browser": "5.19.1",
|
||||
"@spectrum-css/page": "^3.0.1",
|
||||
"@spectrum-css/vars": "^3.0.1",
|
||||
|
|
|
@ -1,4 +1,10 @@
|
|||
import { store } from "./index"
|
||||
import { Helpers } from "@budibase/bbui"
|
||||
import {
|
||||
decodeJSBinding,
|
||||
encodeJSBinding,
|
||||
findHBSBlocks,
|
||||
} from "@budibase/string-templates"
|
||||
|
||||
/**
|
||||
* Recursively searches for a specific component ID
|
||||
|
@ -161,3 +167,58 @@ export const getComponentSettings = componentType => {
|
|||
|
||||
return settings
|
||||
}
|
||||
|
||||
/**
|
||||
* Randomises a components ID's, including all child component IDs, and also
|
||||
* updates all data bindings to still be valid.
|
||||
* This mutates the object in place.
|
||||
* @param component the component to randomise
|
||||
*/
|
||||
export const makeComponentUnique = component => {
|
||||
if (!component) {
|
||||
return
|
||||
}
|
||||
|
||||
// Replace component ID
|
||||
const oldId = component._id
|
||||
const newId = Helpers.uuid()
|
||||
component._id = newId
|
||||
|
||||
if (component._children?.length) {
|
||||
let children = JSON.stringify(component._children)
|
||||
|
||||
// Replace all instances of this ID in child HBS bindings
|
||||
children = children.replace(new RegExp(oldId, "g"), newId)
|
||||
|
||||
// Replace all instances of this ID in child JS bindings
|
||||
const bindings = findHBSBlocks(children)
|
||||
bindings.forEach(binding => {
|
||||
// JSON.stringify will have escaped double quotes, so we need
|
||||
// to account for that
|
||||
let sanitizedBinding = binding.replace(/\\"/g, '"')
|
||||
|
||||
// Check if this is a valid JS binding
|
||||
let js = decodeJSBinding(sanitizedBinding)
|
||||
if (js != null) {
|
||||
// Replace ID inside JS binding
|
||||
js = js.replace(new RegExp(oldId, "g"), newId)
|
||||
|
||||
// Create new valid JS binding
|
||||
let newBinding = encodeJSBinding(js)
|
||||
|
||||
// Replace escaped double quotes
|
||||
newBinding = newBinding.replace(/"/g, '\\"')
|
||||
|
||||
// Insert new JS back into binding.
|
||||
// A single string replace here is better than a regex as
|
||||
// the binding contains special characters, and we only need
|
||||
// to replace a single instance.
|
||||
children = children.replace(binding, newBinding)
|
||||
}
|
||||
})
|
||||
|
||||
// Recurse on all children
|
||||
component._children = JSON.parse(children)
|
||||
component._children.forEach(makeComponentUnique)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -126,7 +126,7 @@ export const getDatasourceForProvider = (asset, component) => {
|
|||
if (dataProviderSetting) {
|
||||
const settingValue = component[dataProviderSetting.key]
|
||||
const providerId = extractLiteralHandlebarsID(settingValue)
|
||||
const provider = findComponent(asset.props, providerId)
|
||||
const provider = findComponent(asset?.props, providerId)
|
||||
return getDatasourceForProvider(asset, provider)
|
||||
}
|
||||
|
||||
|
@ -486,7 +486,7 @@ export const getSchemaForDatasource = (asset, datasource, options) => {
|
|||
// Determine the entity which backs this datasource.
|
||||
// "provider" datasources are those targeting another data provider
|
||||
if (type === "provider") {
|
||||
const component = findComponent(asset.props, datasource.providerId)
|
||||
const component = findComponent(asset?.props, datasource.providerId)
|
||||
const source = getDatasourceForProvider(asset, component)
|
||||
return getSchemaForDatasource(asset, source, options)
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ import { getAutomationStore } from "./store/automation"
|
|||
import { getThemeStore } from "./store/theme"
|
||||
import { derived, writable } from "svelte/store"
|
||||
import { FrontendTypes, LAYOUT_NAMES } from "../constants"
|
||||
import { findComponent } from "./componentUtils"
|
||||
import { findComponent, findComponentPath } from "./componentUtils"
|
||||
|
||||
export const store = getFrontendStore()
|
||||
export const automationStore = getAutomationStore()
|
||||
|
@ -25,7 +25,17 @@ export const selectedComponent = derived(
|
|||
if (!$currentAsset || !$store.selectedComponentId) {
|
||||
return null
|
||||
}
|
||||
return findComponent($currentAsset.props, $store.selectedComponentId)
|
||||
return findComponent($currentAsset?.props, $store.selectedComponentId)
|
||||
}
|
||||
)
|
||||
|
||||
export const selectedComponentPath = derived(
|
||||
[store, currentAsset],
|
||||
([$store, $currentAsset]) => {
|
||||
return findComponentPath(
|
||||
$currentAsset?.props,
|
||||
$store.selectedComponentId
|
||||
).map(component => component._id)
|
||||
}
|
||||
)
|
||||
|
||||
|
|
|
@ -24,9 +24,9 @@ import {
|
|||
findAllMatchingComponents,
|
||||
findComponent,
|
||||
getComponentSettings,
|
||||
makeComponentUnique,
|
||||
} from "../componentUtils"
|
||||
import { Helpers } from "@budibase/bbui"
|
||||
import { removeBindings } from "../dataBinding"
|
||||
|
||||
const INITIAL_FRONTEND_STATE = {
|
||||
apps: [],
|
||||
|
@ -400,11 +400,11 @@ export const getFrontendStore = () => {
|
|||
parentComponent = selected
|
||||
} else {
|
||||
// Otherwise we need to use the parent of this component
|
||||
parentComponent = findComponentParent(asset.props, selected._id)
|
||||
parentComponent = findComponentParent(asset?.props, selected._id)
|
||||
}
|
||||
} else {
|
||||
// Use screen or layout if no component is selected
|
||||
parentComponent = asset.props
|
||||
parentComponent = asset?.props
|
||||
}
|
||||
|
||||
// Attach component
|
||||
|
@ -490,37 +490,22 @@ export const getFrontendStore = () => {
|
|||
}
|
||||
}
|
||||
},
|
||||
paste: async (targetComponent, mode, preserveBindings = false) => {
|
||||
paste: async (targetComponent, mode) => {
|
||||
let promises = []
|
||||
store.update(state => {
|
||||
// Stop if we have nothing to paste
|
||||
if (!state.componentToPaste) {
|
||||
return state
|
||||
}
|
||||
|
||||
// defines if this is a copy or a cut
|
||||
const cut = state.componentToPaste.isCut
|
||||
|
||||
// immediately need to remove bindings, currently these aren't valid when pasted
|
||||
if (!cut && !preserveBindings) {
|
||||
state.componentToPaste = removeBindings(state.componentToPaste, "")
|
||||
}
|
||||
|
||||
// Clone the component to paste
|
||||
// Retain the same ID if cutting as things may be referencing this component
|
||||
// Clone the component to paste and make unique if copying
|
||||
delete state.componentToPaste.isCut
|
||||
let componentToPaste = cloneDeep(state.componentToPaste)
|
||||
if (cut) {
|
||||
state.componentToPaste = null
|
||||
} else {
|
||||
const randomizeIds = component => {
|
||||
if (!component) {
|
||||
return
|
||||
}
|
||||
component._id = Helpers.uuid()
|
||||
component._children?.forEach(randomizeIds)
|
||||
}
|
||||
randomizeIds(componentToPaste)
|
||||
makeComponentUnique(componentToPaste)
|
||||
}
|
||||
|
||||
if (mode === "inside") {
|
||||
|
|
|
@ -10,17 +10,18 @@ const allTemplates = tables => [
|
|||
]
|
||||
|
||||
// Allows us to apply common behaviour to all create() functions
|
||||
const createTemplateOverride = (frontendState, create) => () => {
|
||||
const screen = create()
|
||||
const createTemplateOverride = (frontendState, template) => () => {
|
||||
const screen = template.create()
|
||||
screen.name = screen.props._id
|
||||
screen.routing.route = screen.routing.route.toLowerCase()
|
||||
screen.template = template.id
|
||||
return screen
|
||||
}
|
||||
|
||||
export default (frontendState, tables) => {
|
||||
const enrichTemplate = template => ({
|
||||
...template,
|
||||
create: createTemplateOverride(frontendState, template.create),
|
||||
create: createTemplateOverride(frontendState, template),
|
||||
})
|
||||
|
||||
const fromScratch = enrichTemplate(createFromScratchScreen)
|
||||
|
|
|
@ -10,12 +10,10 @@
|
|||
<div class="title">
|
||||
<Tabs selected="Automations">
|
||||
<Tab title="Automations">
|
||||
<div class="tab-content-padding">
|
||||
<AutomationList />
|
||||
<Modal bind:this={modal}>
|
||||
<CreateAutomationModal {webhookModal} />
|
||||
</Modal>
|
||||
</div>
|
||||
<AutomationList />
|
||||
<Modal bind:this={modal}>
|
||||
<CreateAutomationModal {webhookModal} />
|
||||
</Modal>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
<div class="add-button" data-cy="new-screen">
|
||||
|
@ -24,9 +22,6 @@
|
|||
</div>
|
||||
|
||||
<style>
|
||||
.tab-content-padding {
|
||||
padding: 0 var(--spacing-xl);
|
||||
}
|
||||
.add-button {
|
||||
position: absolute;
|
||||
top: var(--spacing-l);
|
||||
|
|
|
@ -56,7 +56,7 @@
|
|||
<a
|
||||
slot="footer"
|
||||
target="_blank"
|
||||
href="https://docs.budibase.com/automate/introduction-to-automate"
|
||||
href="https://docs.budibase.com/docs/automation-steps"
|
||||
>
|
||||
<Icon name="InfoOutline" />
|
||||
<span>Learn about automations</span>
|
||||
|
|
|
@ -69,7 +69,7 @@
|
|||
<a
|
||||
slot="footer"
|
||||
target="_blank"
|
||||
href="https://docs.budibase.com/automate/steps/triggers"
|
||||
href="https://docs.budibase.com/docs/trigger"
|
||||
>
|
||||
<Icon name="InfoOutline" />
|
||||
<span>Learn about webhooks</span>
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
Modal,
|
||||
notifications,
|
||||
} from "@budibase/bbui"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import { createEventDispatcher, onMount } from "svelte"
|
||||
import { cloneDeep } from "lodash/fp"
|
||||
import { tables } from "stores/backend"
|
||||
import { TableNames, UNEDITABLE_USER_FIELDS } from "constants"
|
||||
|
@ -321,6 +321,12 @@
|
|||
}
|
||||
return newError
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
if (primaryDisplay) {
|
||||
field.constraints.presence = { allowEmpty: false }
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<ModalContent
|
||||
|
|
|
@ -22,10 +22,11 @@
|
|||
const selected = $datasources.selected === datasource._id
|
||||
const open = openDataSources.includes(datasource._id)
|
||||
const containsSelected = containsActiveEntity(datasource)
|
||||
const onlySource = $datasources.list.length === 1
|
||||
return {
|
||||
...datasource,
|
||||
selected,
|
||||
open: selected || open || containsSelected,
|
||||
open: selected || open || containsSelected || onlySource,
|
||||
}
|
||||
})
|
||||
: []
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
<script>
|
||||
export let width = "100"
|
||||
export let height = "100"
|
||||
</script>
|
||||
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
viewBox="23 6 469 132"
|
||||
{width}
|
||||
{height}
|
||||
>
|
||||
<defs id="defs202">
|
||||
<linearGradient id="a" x1="-3.49%" x2="100.83%" y1="17.02%" y2="92.9%">
|
||||
<stop offset="0%" stop-color="#fff" stop-opacity=".1" id="stop192" />
|
||||
<stop offset="14%" stop-color="#fff" stop-opacity=".08" id="stop194" />
|
||||
<stop offset="61%" stop-color="#fff" stop-opacity=".02" id="stop196" />
|
||||
<stop offset="100%" stop-color="#fff" stop-opacity="0" id="stop198" />
|
||||
</linearGradient>
|
||||
<path
|
||||
id="b"
|
||||
d="M106.687 35.2742c-.186-1.0977-.967-2-2.0244-2.338s-2.2148-.057-3.0002.73L86.2473 49.166l-12.12-23.1455c-.5133-.9786-1.525-1.5914-2.6273-1.5914s-2.114.6128-2.6273 1.5914l-6.6277 12.656L45.62 7.5726c-.603-1.1297-1.8588-1.746-3.118-1.5297s-2.2394 1.216-2.4335 2.4827L24 111.701l42.9727 24.1654c2.6985 1.5113 5.985 1.5113 8.6836 0L119 111.701l-12.313-76.427z"
|
||||
/>
|
||||
</defs>
|
||||
<g id="g305" transform="matrix(2.9011579,0,0,2.9011579,43.533284,-135.93685)">
|
||||
<path
|
||||
fill="#ffa000"
|
||||
d="M 23.8266,111.7182 39.9588,8.4901 c 0.1972,-1.266 1.1818,-2.264 2.445,-2.4786 1.2632,-0.2146 2.522,0.4028 3.126,1.5327 L 62.2133,38.6615 68.8633,26 c 0.515,-0.979 1.5303,-1.592 2.6366,-1.592 1.1063,0 2.1215,0.613 2.6366,1.592 l 45.0227,85.718 H 23.8266 Z"
|
||||
id="path204"
|
||||
/>
|
||||
<path
|
||||
fill="#f57c00"
|
||||
d="M 79.566,71.5074 62.2124,38.6472 23.8334,111.7187 Z"
|
||||
id="path206"
|
||||
/>
|
||||
<path
|
||||
fill="#ffca28"
|
||||
d="m 119.1666,111.7187 -12.356,-76.4603 c -0.1867,-1.098 -0.9703,-2 -2.0315,-2.34 -1.0612,-0.34 -2.2226,-0.057 -3.0107,0.7302 l -77.935,78.069 43.1234,24.1834 c 2.708,1.512 6.006,1.512 8.714,0 l 43.4958,-24.1834 z"
|
||||
id="path208"
|
||||
/>
|
||||
<path
|
||||
fill="#ffffff"
|
||||
fill-opacity="0.2"
|
||||
d="m 106.8105,35.2584 c -0.1867,-1.098 -0.9703,-2 -2.0315,-2.34 -1.0612,-0.34 -2.2226,-0.057 -3.0107,0.7302 L 86.3,49.1562 74.1365,26 c -0.515,-0.979 -1.5303,-1.592 -2.6366,-1.592 -1.1063,0 -2.1215,0.613 -2.6366,1.592 L 62.2133,38.6615 45.529,7.5447 C 44.924,6.4145 43.6637,5.7981 42.399,6.0143 41.1343,6.2305 40.153,7.231 39.958,8.498 L 23.8333,111.7187 h -0.052 l 0.052,0.0596 0.4245,0.2085 77.488,-77.5775 c 0.7877,-0.7915 1.952,-1.076 3.016,-0.737 1.064,0.339 1.849,1.2445 2.0338,2.3457 l 12.2518,75.775 0.1192,-0.0745 -12.356,-76.4603 z M 23.9748,111.5772 39.9655,9.228 c 0.1948,-1.267 1.1784,-2.2675 2.442,-2.4837 1.2636,-0.2162 2.524,0.4 3.13,1.5304 L 62.22,39.392 68.87,26.7305 c 0.515,-0.979 1.5303,-1.592 2.6366,-1.592 1.1063,0 2.1215,0.613 2.6366,1.592 l 11.9167,22.664 -62.0858,62.1827 z"
|
||||
id="path210"
|
||||
/>
|
||||
<path
|
||||
fill="#a52714"
|
||||
opacity="0.2"
|
||||
d="m 75.6708,135.1722 c -2.708,1.512 -6.006,1.512 -8.714,0 l -43.0192,-24.1162 -0.1043,0.663 43.1234,24.176 c 2.708,1.512 6.006,1.512 8.714,0 l 43.4958,-24.176 -0.1117,-0.6852 -43.384,24.1387 z"
|
||||
id="path212"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
|
@ -0,0 +1,67 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
viewBox="23 6 469 132"
|
||||
width="100"
|
||||
height="100"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs202">
|
||||
<linearGradient
|
||||
id="a"
|
||||
x1="-3.49%"
|
||||
x2="100.83%"
|
||||
y1="17.02%"
|
||||
y2="92.9%">
|
||||
<stop
|
||||
offset="0%"
|
||||
stop-color="#fff"
|
||||
stop-opacity=".1"
|
||||
id="stop192" />
|
||||
<stop
|
||||
offset="14%"
|
||||
stop-color="#fff"
|
||||
stop-opacity=".08"
|
||||
id="stop194" />
|
||||
<stop
|
||||
offset="61%"
|
||||
stop-color="#fff"
|
||||
stop-opacity=".02"
|
||||
id="stop196" />
|
||||
<stop
|
||||
offset="100%"
|
||||
stop-color="#fff"
|
||||
stop-opacity="0"
|
||||
id="stop198" />
|
||||
</linearGradient>
|
||||
<path
|
||||
id="b"
|
||||
d="M106.687 35.2742c-.186-1.0977-.967-2-2.0244-2.338s-2.2148-.057-3.0002.73L86.2473 49.166l-12.12-23.1455c-.5133-.9786-1.525-1.5914-2.6273-1.5914s-2.114.6128-2.6273 1.5914l-6.6277 12.656L45.62 7.5726c-.603-1.1297-1.8588-1.746-3.118-1.5297s-2.2394 1.216-2.4335 2.4827L24 111.701l42.9727 24.1654c2.6985 1.5113 5.985 1.5113 8.6836 0L119 111.701l-12.313-76.427z" />
|
||||
</defs>
|
||||
<g
|
||||
id="g305"
|
||||
transform="matrix(2.9011579,0,0,2.9011579,43.533284,-135.93685)">
|
||||
<path
|
||||
fill="#ffa000"
|
||||
d="M 23.8266,111.7182 39.9588,8.4901 c 0.1972,-1.266 1.1818,-2.264 2.445,-2.4786 1.2632,-0.2146 2.522,0.4028 3.126,1.5327 L 62.2133,38.6615 68.8633,26 c 0.515,-0.979 1.5303,-1.592 2.6366,-1.592 1.1063,0 2.1215,0.613 2.6366,1.592 l 45.0227,85.718 H 23.8266 Z"
|
||||
id="path204" />
|
||||
<path
|
||||
fill="#f57c00"
|
||||
d="M 79.566,71.5074 62.2124,38.6472 23.8334,111.7187 Z"
|
||||
id="path206" />
|
||||
<path
|
||||
fill="#ffca28"
|
||||
d="m 119.1666,111.7187 -12.356,-76.4603 c -0.1867,-1.098 -0.9703,-2 -2.0315,-2.34 -1.0612,-0.34 -2.2226,-0.057 -3.0107,0.7302 l -77.935,78.069 43.1234,24.1834 c 2.708,1.512 6.006,1.512 8.714,0 l 43.4958,-24.1834 z"
|
||||
id="path208" />
|
||||
<path
|
||||
fill="#ffffff"
|
||||
fill-opacity="0.2"
|
||||
d="m 106.8105,35.2584 c -0.1867,-1.098 -0.9703,-2 -2.0315,-2.34 -1.0612,-0.34 -2.2226,-0.057 -3.0107,0.7302 L 86.3,49.1562 74.1365,26 c -0.515,-0.979 -1.5303,-1.592 -2.6366,-1.592 -1.1063,0 -2.1215,0.613 -2.6366,1.592 L 62.2133,38.6615 45.529,7.5447 C 44.924,6.4145 43.6637,5.7981 42.399,6.0143 41.1343,6.2305 40.153,7.231 39.958,8.498 L 23.8333,111.7187 h -0.052 l 0.052,0.0596 0.4245,0.2085 77.488,-77.5775 c 0.7877,-0.7915 1.952,-1.076 3.016,-0.737 1.064,0.339 1.849,1.2445 2.0338,2.3457 l 12.2518,75.775 0.1192,-0.0745 -12.356,-76.4603 z M 23.9748,111.5772 39.9655,9.228 c 0.1948,-1.267 1.1784,-2.2675 2.442,-2.4837 1.2636,-0.2162 2.524,0.4 3.13,1.5304 L 62.22,39.392 68.87,26.7305 c 0.515,-0.979 1.5303,-1.592 2.6366,-1.592 1.1063,0 2.1215,0.613 2.6366,1.592 l 11.9167,22.664 -62.0858,62.1827 z"
|
||||
id="path210" />
|
||||
<path
|
||||
fill="#a52714"
|
||||
opacity="0.2"
|
||||
d="m 75.6708,135.1722 c -2.708,1.512 -6.006,1.512 -8.714,0 l -43.0192,-24.1162 -0.1043,0.663 43.1234,24.176 c 2.708,1.512 6.006,1.512 8.714,0 l 43.4958,-24.176 -0.1117,-0.6852 -43.384,24.1387 z"
|
||||
id="path212" />
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 3.1 KiB |
|
@ -12,6 +12,7 @@ import Rest from "./Rest.svelte"
|
|||
import Budibase from "./Budibase.svelte"
|
||||
import Oracle from "./Oracle.svelte"
|
||||
import GoogleSheets from "./GoogleSheets.svelte"
|
||||
import Firebase from "./Firebase.svelte"
|
||||
|
||||
export default {
|
||||
BUDIBASE: Budibase,
|
||||
|
@ -28,4 +29,5 @@ export default {
|
|||
REST: Rest,
|
||||
ORACLE: Oracle,
|
||||
GOOGLE_SHEETS: GoogleSheets,
|
||||
FIREBASE: Firebase,
|
||||
}
|
||||
|
|
|
@ -7,7 +7,6 @@
|
|||
Layout,
|
||||
Tabs,
|
||||
Tab,
|
||||
Input,
|
||||
Heading,
|
||||
TextArea,
|
||||
Dropzone,
|
||||
|
@ -98,15 +97,16 @@
|
|||
<Body size="XS"
|
||||
>Import your rest collection using one of the options below</Body
|
||||
>
|
||||
<Tabs selected="Link">
|
||||
<Tab title="Link">
|
||||
<Tabs selected="File">
|
||||
<!-- Commenting until nginx csp issue resolved -->
|
||||
<!-- <Tab title="Link">
|
||||
<Input
|
||||
bind:value={$data.url}
|
||||
on:change={() => (lastTouched = "url")}
|
||||
label="Enter a URL"
|
||||
placeholder="e.g. https://petstore.swagger.io/v2/swagger.json"
|
||||
/>
|
||||
</Tab>
|
||||
</Tab> -->
|
||||
<Tab title="File">
|
||||
<Dropzone
|
||||
gallery={false}
|
||||
|
@ -115,7 +115,14 @@
|
|||
$data.file = e.detail?.[0]
|
||||
lastTouched = "file"
|
||||
}}
|
||||
fileTags={["OpenAPI 2.0", "Swagger 2.0", "cURL", "YAML", "JSON"]}
|
||||
fileTags={[
|
||||
"OpenAPI 3.0",
|
||||
"OpenAPI 2.0",
|
||||
"Swagger 2.0",
|
||||
"cURL",
|
||||
"YAML",
|
||||
"JSON",
|
||||
]}
|
||||
maximum={1}
|
||||
/>
|
||||
</Tab>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<script>
|
||||
import { Icon } from "@budibase/bbui"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import { createEventDispatcher, getContext } from "svelte"
|
||||
|
||||
export let icon
|
||||
export let withArrow = false
|
||||
|
@ -14,29 +14,46 @@
|
|||
export let iconText
|
||||
export let iconColor
|
||||
|
||||
const scrollApi = getContext("scroll")
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
function onIconClick(event) {
|
||||
event.stopPropagation()
|
||||
let contentRef
|
||||
$: selected && contentRef && scrollToView()
|
||||
|
||||
const onClick = () => {
|
||||
scrollToView()
|
||||
dispatch("click")
|
||||
}
|
||||
|
||||
const onIconClick = e => {
|
||||
e.stopPropagation()
|
||||
dispatch("iconClick")
|
||||
}
|
||||
|
||||
const scrollToView = () => {
|
||||
if (!scrollApi || !contentRef) {
|
||||
return
|
||||
}
|
||||
const bounds = contentRef.getBoundingClientRect()
|
||||
scrollApi.scrollTo(bounds)
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="nav-item"
|
||||
class:border
|
||||
class:selected
|
||||
style={`padding-left: ${indentLevel * 14}px`}
|
||||
style={`padding-left: ${20 + indentLevel * 14}px`}
|
||||
{draggable}
|
||||
on:dragend
|
||||
on:dragstart
|
||||
on:dragover
|
||||
on:drop
|
||||
on:click
|
||||
on:click={onClick}
|
||||
ondragover="return false"
|
||||
ondragenter="return false"
|
||||
>
|
||||
<div class="content">
|
||||
<div class="nav-item-content" bind:this={contentRef}>
|
||||
{#if withArrow}
|
||||
<div class:opened class="icon arrow" on:click={onIconClick}>
|
||||
<Icon size="S" name="ChevronRight" />
|
||||
|
@ -64,11 +81,16 @@
|
|||
|
||||
<style>
|
||||
.nav-item {
|
||||
border-radius: var(--border-radius-s);
|
||||
cursor: pointer;
|
||||
color: var(--grey-7);
|
||||
transition: background-color
|
||||
var(--spectrum-global-animation-duration-100, 130ms) ease-in-out;
|
||||
padding: 0 var(--spacing-m) 0 var(--spacing-xl);
|
||||
height: 32px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.nav-item.selected {
|
||||
background-color: var(--grey-2);
|
||||
|
@ -81,14 +103,14 @@
|
|||
visibility: visible;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 0 var(--spacing-s);
|
||||
height: 32px;
|
||||
.nav-item-content {
|
||||
flex: 1 1 auto;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
gap: var(--spacing-xs);
|
||||
width: max-content;
|
||||
}
|
||||
|
||||
.icon {
|
||||
|
@ -111,12 +133,13 @@
|
|||
}
|
||||
|
||||
.text {
|
||||
flex: 1 1 auto;
|
||||
font-weight: 600;
|
||||
font-size: var(--spectrum-global-dimension-font-size-75);
|
||||
white-space: nowrap;
|
||||
max-width: 160px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.actions {
|
||||
|
@ -125,9 +148,9 @@
|
|||
height: 20px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
display: grid;
|
||||
margin-left: var(--spacing-s);
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
.iconText {
|
||||
|
|
|
@ -238,6 +238,7 @@
|
|||
border: var(--border-light);
|
||||
transition: background-color 130ms ease-in-out, color 130ms ease-in-out,
|
||||
border-color 130ms ease-in-out;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
li:not(:last-of-type) {
|
||||
margin-bottom: var(--spacing-s);
|
||||
|
|
|
@ -40,7 +40,7 @@
|
|||
<a
|
||||
slot="footer"
|
||||
target="_blank"
|
||||
href="https://docs.budibase.com/automate/steps/triggers"
|
||||
href="https://docs.budibase.com/docs/trigger"
|
||||
>
|
||||
<i class="ri-information-line" />
|
||||
<span>Learn about webhooks</span>
|
||||
|
|
|
@ -68,6 +68,7 @@
|
|||
customTheme: $store.customTheme,
|
||||
previewDevice: $store.previewDevice,
|
||||
messagePassing: $store.clientFeatures.messagePassing,
|
||||
isBudibaseEvent: true
|
||||
}
|
||||
$: json = JSON.stringify(previewData)
|
||||
|
||||
|
@ -160,6 +161,11 @@
|
|||
await store.actions.components.updateProp(data.prop, data.value)
|
||||
} else if (type === "delete-component" && data.id) {
|
||||
confirmDeleteComponent(data.id)
|
||||
} else if (type === "duplicate-component" && data.id) {
|
||||
const rootComponent = get(currentAsset).props
|
||||
const component = findComponent(rootComponent, data.id)
|
||||
store.actions.components.copy(component)
|
||||
await store.actions.components.paste(component)
|
||||
} else if (type === "preview-loaded") {
|
||||
// Wait for this event to show the client library if intelligent
|
||||
// loading is supported
|
||||
|
|
|
@ -52,7 +52,7 @@ export default `
|
|||
console.error("Client received invalid JSON")
|
||||
// Ignore
|
||||
}
|
||||
if (!parsed) {
|
||||
if (!parsed || !parsed.isBudibaseEvent) {
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
|
||||
const moveUpComponent = () => {
|
||||
const asset = get(currentAsset)
|
||||
const parent = findComponentParent(asset.props, component._id)
|
||||
const parent = findComponentParent(asset?.props, component._id)
|
||||
if (!parent) {
|
||||
return
|
||||
}
|
||||
|
@ -41,7 +41,7 @@
|
|||
|
||||
const moveDownComponent = () => {
|
||||
const asset = get(currentAsset)
|
||||
const parent = findComponentParent(asset.props, component._id)
|
||||
const parent = findComponentParent(asset?.props, component._id)
|
||||
if (!parent) {
|
||||
return
|
||||
}
|
||||
|
@ -61,7 +61,7 @@
|
|||
|
||||
const duplicateComponent = () => {
|
||||
storeComponentForCopy(false)
|
||||
pasteComponent("below", true)
|
||||
pasteComponent("below")
|
||||
}
|
||||
|
||||
const deleteComponent = async () => {
|
||||
|
@ -73,14 +73,12 @@
|
|||
}
|
||||
|
||||
const storeComponentForCopy = (cut = false) => {
|
||||
// lives in store - also used by drag drop
|
||||
store.actions.components.copy(component, cut)
|
||||
}
|
||||
|
||||
const pasteComponent = (mode, preserveBindings = false) => {
|
||||
const pasteComponent = mode => {
|
||||
try {
|
||||
// lives in store - also used by drag drop
|
||||
store.actions.components.paste(component, mode, preserveBindings)
|
||||
store.actions.components.paste(component, mode)
|
||||
} catch (error) {
|
||||
notifications.error("Error saving component")
|
||||
}
|
||||
|
@ -140,3 +138,10 @@
|
|||
onOk={deleteComponent}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.icon {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
}
|
||||
</style>
|
|
@ -1,10 +1,11 @@
|
|||
<script>
|
||||
import { store } from "builderStore"
|
||||
import { DropEffect, DropPosition } from "./dragDropStore"
|
||||
import ComponentDropdownMenu from "../ComponentDropdownMenu.svelte"
|
||||
import ComponentDropdownMenu from "./ComponentDropdownMenu.svelte"
|
||||
import NavItem from "components/common/NavItem.svelte"
|
||||
import { capitalise } from "helpers"
|
||||
import { notifications } from "@budibase/bbui"
|
||||
import { selectedComponentPath } from "builderStore"
|
||||
|
||||
export let components = []
|
||||
export let currentComponent
|
||||
|
@ -71,10 +72,20 @@
|
|||
notifications.error("Error saving component")
|
||||
}
|
||||
}
|
||||
|
||||
const isOpen = (component, selectedComponentPath, closedNodes) => {
|
||||
if (!component?._children?.length) {
|
||||
return false
|
||||
}
|
||||
if (selectedComponentPath.includes(component._id)) {
|
||||
return true
|
||||
}
|
||||
return !closedNodes[component._id]
|
||||
}
|
||||
</script>
|
||||
|
||||
<ul>
|
||||
{#each components as component, index (component._id)}
|
||||
{#each components || [] as component, index (component._id)}
|
||||
<li on:click|stopPropagation={() => selectComponent(component)}>
|
||||
{#if $dragDropStore?.targetComponent === component && $dragDropStore.dropPosition === DropPosition.ABOVE}
|
||||
<div
|
||||
|
@ -97,12 +108,12 @@
|
|||
withArrow
|
||||
indentLevel={level + 1}
|
||||
selected={$store.selectedComponentId === component._id}
|
||||
opened={!closedNodes[component._id] && component?._children?.length}
|
||||
opened={isOpen(component, $selectedComponentPath, closedNodes)}
|
||||
>
|
||||
<ComponentDropdownMenu {component} />
|
||||
</NavItem>
|
||||
|
||||
{#if component._children && !closedNodes[component._id]}
|
||||
{#if isOpen(component, $selectedComponentPath, closedNodes)}
|
||||
<svelte:self
|
||||
components={component._children}
|
||||
{currentComponent}
|
||||
|
@ -133,6 +144,10 @@
|
|||
padding-left: 0;
|
||||
margin: 0;
|
||||
}
|
||||
ul,
|
||||
li {
|
||||
min-width: max-content;
|
||||
}
|
||||
|
||||
.drop-item {
|
||||
border-radius: var(--border-radius-m);
|
||||
|
|
|
@ -51,7 +51,7 @@
|
|||
bind:this={confirmDeleteDialog}
|
||||
title="Confirm Deletion"
|
||||
body={"Are you sure you wish to delete this layout?"}
|
||||
okText="Delete Layout"
|
||||
okText="Delete layout"
|
||||
onOk={deleteLayout}
|
||||
/>
|
||||
|
||||
|
@ -65,3 +65,10 @@
|
|||
<Input thin type="text" label="Name" bind:value={name} />
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
|
||||
<style>
|
||||
.icon {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -0,0 +1,82 @@
|
|||
<script>
|
||||
import { goto } from "@roxi/routify"
|
||||
import { store } from "builderStore"
|
||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||
import {
|
||||
ActionMenu,
|
||||
MenuItem,
|
||||
Icon,
|
||||
Layout,
|
||||
notifications,
|
||||
} from "@budibase/bbui"
|
||||
import { get } from "svelte/store"
|
||||
|
||||
export let path
|
||||
export let screens
|
||||
|
||||
let confirmDeleteDialog
|
||||
|
||||
const deleteScreens = async () => {
|
||||
if (!screens?.length) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
for (let { id } of screens) {
|
||||
// We have to fetch the screen to be deleted immediately before deleting
|
||||
// as otherwise we're very likely to 409
|
||||
const screen = get(store).screens.find(screen => screen._id === id)
|
||||
if (!screen) {
|
||||
continue
|
||||
}
|
||||
await store.actions.screens.delete(screen)
|
||||
}
|
||||
notifications.success("Screens deleted successfully")
|
||||
$goto("../")
|
||||
} catch (error) {
|
||||
notifications.error("Error deleting screens")
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<ActionMenu>
|
||||
<div slot="control" class="icon">
|
||||
<Icon size="S" hoverable name="MoreSmallList" />
|
||||
</div>
|
||||
<MenuItem icon="Delete" on:click={confirmDeleteDialog.show}>
|
||||
Delete all screens
|
||||
</MenuItem>
|
||||
</ActionMenu>
|
||||
|
||||
<ConfirmDialog
|
||||
bind:this={confirmDeleteDialog}
|
||||
title="Confirm Deletion"
|
||||
okText="Delete screens"
|
||||
onOk={deleteScreens}
|
||||
>
|
||||
<Layout noPadding gap="S">
|
||||
<div>
|
||||
Are you sure you want to delete all screens under the <b>{path}</b> route?
|
||||
</div>
|
||||
<div>The following screens will be deleted:</div>
|
||||
<div class="to-delete">
|
||||
{#each screens as screen}
|
||||
<div>{screen.route}</div>
|
||||
{/each}
|
||||
</div>
|
||||
</Layout>
|
||||
</ConfirmDialog>
|
||||
|
||||
<style>
|
||||
.to-delete {
|
||||
font-weight: bold;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: flex-start;
|
||||
padding-left: var(--spacing-xl);
|
||||
}
|
||||
.icon {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
}
|
||||
</style>
|
|
@ -8,6 +8,7 @@
|
|||
import instantiateStore from "./dragDropStore"
|
||||
import ComponentTree from "./ComponentTree.svelte"
|
||||
import NavItem from "components/common/NavItem.svelte"
|
||||
import PathDropdownMenu from "./PathDropdownMenu.svelte"
|
||||
import ScreenDropdownMenu from "./ScreenDropdownMenu.svelte"
|
||||
import { get } from "svelte/store"
|
||||
|
||||
|
@ -28,6 +29,7 @@
|
|||
export let border
|
||||
|
||||
let routeManuallyOpened = false
|
||||
|
||||
$: selectedScreen = $currentAsset
|
||||
$: allScreens = getAllScreens(route)
|
||||
$: filteredScreens = getFilteredScreens(allScreens, $screenSearchString)
|
||||
|
@ -73,14 +75,17 @@
|
|||
opened={routeOpened}
|
||||
{border}
|
||||
withArrow={route.subpaths}
|
||||
/>
|
||||
>
|
||||
<PathDropdownMenu screens={allScreens} {path} />
|
||||
</NavItem>
|
||||
|
||||
{#if routeOpened}
|
||||
{#each filteredScreens as screen (screen.id)}
|
||||
<NavItem
|
||||
icon="WebPage"
|
||||
indentLevel={indent || 1}
|
||||
selected={$store.selectedScreenId === screen.id}
|
||||
selected={$store.selectedScreenId === screen.id &&
|
||||
$store.currentView === "detail"}
|
||||
opened={$store.selectedScreenId === screen.id}
|
||||
text={ROUTE_NAME_MAP[screen.route]?.[screen.role] || screen.route}
|
||||
withArrow={route.subpaths}
|
||||
|
|
|
@ -2,14 +2,57 @@
|
|||
import { goto } from "@roxi/routify"
|
||||
import { store, allScreens } from "builderStore"
|
||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||
import { ActionMenu, MenuItem, Icon, notifications } from "@budibase/bbui"
|
||||
import {
|
||||
ActionMenu,
|
||||
MenuItem,
|
||||
Icon,
|
||||
Modal,
|
||||
Helpers,
|
||||
notifications,
|
||||
} from "@budibase/bbui"
|
||||
import ScreenDetailsModal from "../ScreenDetailsModal.svelte"
|
||||
import sanitizeUrl from "builderStore/store/screenTemplates/utils/sanitizeUrl"
|
||||
import analytics, { Events } from "analytics"
|
||||
import { makeComponentUnique } from "builderStore/componentUtils"
|
||||
|
||||
export let screenId
|
||||
|
||||
let confirmDeleteDialog
|
||||
let screenDetailsModal
|
||||
|
||||
$: screen = $allScreens.find(screen => screen._id === screenId)
|
||||
|
||||
const duplicateScreen = () => {
|
||||
screenDetailsModal.show()
|
||||
}
|
||||
|
||||
const createDuplicateScreen = async ({ screenName, screenUrl }) => {
|
||||
// Create a dupe and ensure it is unique
|
||||
let duplicateScreen = Helpers.cloneDeep(screen)
|
||||
delete duplicateScreen._id
|
||||
delete duplicateScreen._rev
|
||||
makeComponentUnique(duplicateScreen.props)
|
||||
|
||||
// Attach the new name and URL
|
||||
duplicateScreen.routing.route = sanitizeUrl(screenUrl)
|
||||
duplicateScreen.props._instanceName = screenName
|
||||
|
||||
try {
|
||||
// Create the screen
|
||||
await store.actions.screens.save(duplicateScreen)
|
||||
|
||||
// Analytics
|
||||
if (screen.template) {
|
||||
analytics.captureEvent(Events.SCREEN.CREATED, {
|
||||
template: "createFromScratch",
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
notifications.error("Error duplicating screen")
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
const deleteScreen = async () => {
|
||||
try {
|
||||
await store.actions.screens.delete(screen)
|
||||
|
@ -19,12 +62,28 @@
|
|||
notifications.error("Error deleting screen")
|
||||
}
|
||||
}
|
||||
|
||||
const pasteComponent = mode => {
|
||||
try {
|
||||
store.actions.components.paste(screen?.props, mode)
|
||||
} catch (error) {
|
||||
notifications.error("Error saving component")
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<ActionMenu>
|
||||
<div slot="control" class="icon">
|
||||
<Icon size="S" hoverable name="MoreSmallList" />
|
||||
</div>
|
||||
<MenuItem icon="Duplicate" on:click={duplicateScreen}>Duplicate</MenuItem>
|
||||
<MenuItem
|
||||
icon="ShowOneLayer"
|
||||
on:click={() => pasteComponent("inside")}
|
||||
disabled={!$store.componentToPaste}
|
||||
>
|
||||
Paste inside
|
||||
</MenuItem>
|
||||
<MenuItem icon="Delete" on:click={confirmDeleteDialog.show}>Delete</MenuItem>
|
||||
</ActionMenu>
|
||||
|
||||
|
@ -32,6 +91,22 @@
|
|||
bind:this={confirmDeleteDialog}
|
||||
title="Confirm Deletion"
|
||||
body={"Are you sure you wish to delete this screen?"}
|
||||
okText="Delete Screen"
|
||||
okText="Delete screen"
|
||||
onOk={deleteScreen}
|
||||
/>
|
||||
|
||||
<Modal bind:this={screenDetailsModal}>
|
||||
<ScreenDetailsModal
|
||||
onConfirm={createDuplicateScreen}
|
||||
screenName={screen?.props._instanceName}
|
||||
screenUrl={screen?.routing.route}
|
||||
confirmText="Duplicate"
|
||||
/>
|
||||
</Modal>
|
||||
|
||||
<style>
|
||||
.icon {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -55,11 +55,10 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<div class="root">
|
||||
<div class="root" class:has-screens={!!paths?.length}>
|
||||
{#each paths as path, idx (path)}
|
||||
<PathTree border={idx > 0} {path} route={routes[path]} />
|
||||
{/each}
|
||||
|
||||
{#if !paths.length}
|
||||
<div class="empty">
|
||||
There aren't any screens configured with this access role.
|
||||
|
@ -68,9 +67,12 @@
|
|||
</div>
|
||||
|
||||
<style>
|
||||
.root.has-screens {
|
||||
min-width: max-content;
|
||||
}
|
||||
div.empty {
|
||||
font-size: var(--font-size-xs);
|
||||
font-size: var(--font-size-s);
|
||||
color: var(--grey-5);
|
||||
padding-top: var(--spacing-xs);
|
||||
padding: var(--spacing-xs) var(--spacing-xl);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import { onMount } from "svelte"
|
||||
import { onMount, setContext } from "svelte"
|
||||
import { goto, params } from "@roxi/routify"
|
||||
import {
|
||||
store,
|
||||
|
@ -18,11 +18,63 @@
|
|||
Search,
|
||||
Tabs,
|
||||
Tab,
|
||||
Layout as BBUILayout,
|
||||
notifications,
|
||||
} from "@budibase/bbui"
|
||||
|
||||
export let showModal
|
||||
|
||||
let scrollRef
|
||||
|
||||
const scrollTo = bounds => {
|
||||
if (!bounds) {
|
||||
return
|
||||
}
|
||||
|
||||
const sidebarWidth = 259
|
||||
const navItemHeight = 32
|
||||
const { scrollLeft, scrollTop, offsetHeight } = scrollRef
|
||||
|
||||
let scrollBounds = scrollRef.getBoundingClientRect()
|
||||
let newOffsets = {}
|
||||
|
||||
// Calculate left offset
|
||||
const offsetX = bounds.left + bounds.width + scrollLeft + 20
|
||||
if (offsetX > sidebarWidth) {
|
||||
newOffsets.left = offsetX - sidebarWidth
|
||||
} else {
|
||||
newOffsets.left = 0
|
||||
}
|
||||
if (newOffsets.left === scrollLeft) {
|
||||
delete newOffsets.left
|
||||
}
|
||||
|
||||
// Calculate top offset
|
||||
const offsetY = bounds.top - scrollBounds?.top + scrollTop
|
||||
if (offsetY > scrollTop + offsetHeight - 2 * navItemHeight) {
|
||||
newOffsets.top = offsetY - offsetHeight + 2 * navItemHeight
|
||||
} else if (offsetY < scrollTop + navItemHeight) {
|
||||
newOffsets.top = offsetY - navItemHeight
|
||||
} else {
|
||||
delete newOffsets.top
|
||||
}
|
||||
|
||||
// Skip if offset is unchanged
|
||||
if (newOffsets.left == null && newOffsets.top == null) {
|
||||
return
|
||||
}
|
||||
|
||||
// Smoothly scroll to the offset
|
||||
scrollRef.scroll({
|
||||
...newOffsets,
|
||||
behavior: "smooth",
|
||||
})
|
||||
}
|
||||
|
||||
setContext("scroll", {
|
||||
scrollTo,
|
||||
})
|
||||
|
||||
const tabs = [
|
||||
{
|
||||
title: "Screens",
|
||||
|
@ -79,7 +131,7 @@
|
|||
<Tabs {selected} on:select={navigate}>
|
||||
<Tab title="Screens">
|
||||
<div class="tab-content-padding">
|
||||
<div class="role-select">
|
||||
<BBUILayout noPadding gap="XS">
|
||||
<Select
|
||||
on:change={updateAccessRole}
|
||||
value={$selectedAccessRole}
|
||||
|
@ -93,17 +145,24 @@
|
|||
label="Search Screens"
|
||||
bind:value={$screenSearchString}
|
||||
/>
|
||||
</div>
|
||||
<div class="nav-items-container">
|
||||
</BBUILayout>
|
||||
<div class="nav-items-container" bind:this={scrollRef}>
|
||||
<ComponentNavigationTree />
|
||||
</div>
|
||||
</div>
|
||||
</Tab>
|
||||
<Tab title="Layouts">
|
||||
<div class="tab-content-padding">
|
||||
{#each $store.layouts as layout, idx (layout._id)}
|
||||
<Layout {layout} border={idx > 0} />
|
||||
{/each}
|
||||
<div
|
||||
class="nav-items-container nav-items-container--layouts"
|
||||
bind:this={scrollRef}
|
||||
>
|
||||
<div class="layouts-container">
|
||||
{#each $store.layouts as layout, idx (layout._id)}
|
||||
<Layout {layout} border={idx > 0} />
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
<Modal bind:this={newLayoutModal}>
|
||||
<NewLayoutModal />
|
||||
</Modal>
|
||||
|
@ -126,23 +185,45 @@
|
|||
justify-content: flex-start;
|
||||
align-items: stretch;
|
||||
position: relative;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
.title :global(.spectrum-Tabs-content),
|
||||
.title :global(.spectrum-Tabs-content > div),
|
||||
.title :global(.spectrum-Tabs-content > div > div) {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.add-button {
|
||||
position: absolute;
|
||||
top: var(--spacing-l);
|
||||
right: var(--spacing-xl);
|
||||
}
|
||||
|
||||
.role-select {
|
||||
.tab-content-padding {
|
||||
padding: 0 var(--spacing-xl);
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: stretch;
|
||||
margin-bottom: var(--spacing-m);
|
||||
gap: var(--spacing-m);
|
||||
gap: var(--spacing-xl);
|
||||
}
|
||||
|
||||
.tab-content-padding {
|
||||
padding: 0 var(--spacing-xl);
|
||||
.nav-items-container {
|
||||
border-top: var(--border-light);
|
||||
margin: 0 calc(-1 * var(--spacing-xl));
|
||||
padding: var(--spacing-m) 0;
|
||||
flex: 1 1 auto;
|
||||
overflow: auto;
|
||||
height: 0;
|
||||
position: relative;
|
||||
}
|
||||
.nav-items-container--layouts {
|
||||
border-top: none;
|
||||
margin-top: calc(-1 * var(--spectrum-global-dimension-static-size-150));
|
||||
}
|
||||
|
||||
.layouts-container {
|
||||
min-width: max-content;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -10,39 +10,19 @@
|
|||
ProgressCircle,
|
||||
} from "@budibase/bbui"
|
||||
import getTemplates from "builderStore/store/screenTemplates"
|
||||
import { onDestroy } from "svelte"
|
||||
|
||||
import { createEventDispatcher } from "svelte"
|
||||
|
||||
export let chooseModal
|
||||
export let save
|
||||
export let onConfirm
|
||||
export let onCancel
|
||||
export let showProgressCircle = false
|
||||
|
||||
let selectedScreens = []
|
||||
|
||||
const blankScreen = "createFromScratch"
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
function setScreens() {
|
||||
dispatch("save", {
|
||||
screens: selectedScreens,
|
||||
})
|
||||
}
|
||||
let selectedScreens = []
|
||||
let templates = getTemplates($store, $tables.list)
|
||||
|
||||
$: blankSelected = selectedScreens?.length === 1
|
||||
$: autoSelected = selectedScreens?.length > 0 && !blankSelected
|
||||
|
||||
let templates = getTemplates($store, $tables.list)
|
||||
|
||||
const confirm = async () => {
|
||||
if (autoSelected) {
|
||||
setScreens()
|
||||
await save()
|
||||
} else {
|
||||
setScreens()
|
||||
chooseModal(1)
|
||||
}
|
||||
}
|
||||
const toggleScreenSelection = table => {
|
||||
if (selectedScreens.find(s => s.table === table.name)) {
|
||||
selectedScreens = selectedScreens.filter(
|
||||
|
@ -56,25 +36,25 @@
|
|||
}
|
||||
}
|
||||
|
||||
onDestroy(() => {
|
||||
selectedScreens = []
|
||||
})
|
||||
const confirmScreenSelection = async () => {
|
||||
await onConfirm(selectedScreens)
|
||||
}
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<ModalContent
|
||||
title="Add screens"
|
||||
confirmText="Add Screens"
|
||||
confirmText="Add screens"
|
||||
cancelText="Cancel"
|
||||
onConfirm={() => confirm()}
|
||||
onConfirm={confirmScreenSelection}
|
||||
{onCancel}
|
||||
disabled={!selectedScreens.length}
|
||||
size="L"
|
||||
>
|
||||
<Body size="S"
|
||||
>Please select the screens you would like to add to your application.
|
||||
Autogenerated screens come with CRUD functionality.</Body
|
||||
>
|
||||
|
||||
<Body size="S">
|
||||
Please select the screens you would like to add to your application.
|
||||
Autogenerated screens come with CRUD functionality.
|
||||
</Body>
|
||||
<Layout noPadding gap="S">
|
||||
<Detail size="S">Blank screen</Detail>
|
||||
<div
|
||||
|
|
|
@ -2,58 +2,62 @@
|
|||
import { ModalContent, Input, ProgressCircle } from "@budibase/bbui"
|
||||
import sanitizeUrl from "builderStore/store/screenTemplates/utils/sanitizeUrl"
|
||||
import { selectedAccessRole, allScreens } from "builderStore"
|
||||
import { onDestroy } from "svelte"
|
||||
import { get } from "svelte/store"
|
||||
|
||||
export let screenName
|
||||
export let url
|
||||
export let chooseModal
|
||||
export let save
|
||||
export let onConfirm
|
||||
export let onCancel
|
||||
export let showProgressCircle = false
|
||||
export let screenName
|
||||
export let screenUrl
|
||||
export let confirmText = "Continue"
|
||||
|
||||
let routeError
|
||||
let roleId = $selectedAccessRole || "BASIC"
|
||||
let touched = false
|
||||
|
||||
const routeChanged = event => {
|
||||
if (!event.detail.startsWith("/")) {
|
||||
url = "/" + event.detail
|
||||
screenUrl = "/" + event.detail
|
||||
}
|
||||
url = sanitizeUrl(url)
|
||||
|
||||
if (routeExists(url, roleId)) {
|
||||
touched = true
|
||||
screenUrl = sanitizeUrl(screenUrl)
|
||||
if (routeExists(screenUrl)) {
|
||||
routeError = "This URL is already taken for this access role"
|
||||
} else {
|
||||
routeError = ""
|
||||
routeError = null
|
||||
}
|
||||
}
|
||||
|
||||
const routeExists = (url, roleId) => {
|
||||
return $allScreens.some(
|
||||
const routeExists = url => {
|
||||
const roleId = get(selectedAccessRole) || "BASIC"
|
||||
return get(allScreens).some(
|
||||
screen =>
|
||||
screen.routing.route.toLowerCase() === url.toLowerCase() &&
|
||||
screen.routing.roleId === roleId
|
||||
)
|
||||
}
|
||||
|
||||
onDestroy(() => {
|
||||
screenName = ""
|
||||
url = ""
|
||||
})
|
||||
const confirmScreenDetails = async () => {
|
||||
await onConfirm({
|
||||
screenName,
|
||||
screenUrl,
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<ModalContent
|
||||
size="M"
|
||||
title={"Enter details"}
|
||||
confirmText={"Continue"}
|
||||
onCancel={() => chooseModal(0)}
|
||||
onConfirm={() => save()}
|
||||
{confirmText}
|
||||
onConfirm={confirmScreenDetails}
|
||||
{onCancel}
|
||||
cancelText={"Back"}
|
||||
disabled={!screenName || !url || routeError}
|
||||
disabled={!screenName || !screenUrl || routeError || !touched}
|
||||
>
|
||||
<Input label="Name" bind:value={screenName} />
|
||||
<Input
|
||||
label="URL"
|
||||
error={routeError}
|
||||
bind:value={url}
|
||||
bind:value={screenUrl}
|
||||
on:change={routeChanged}
|
||||
/>
|
||||
<div slot="footer">
|
||||
|
|
|
@ -3,141 +3,133 @@
|
|||
import NewScreenModal from "components/design/NavigationPanel/NewScreenModal.svelte"
|
||||
import sanitizeUrl from "builderStore/store/screenTemplates/utils/sanitizeUrl"
|
||||
import { Modal, notifications } from "@budibase/bbui"
|
||||
import { store, selectedAccessRole, allScreens } from "builderStore"
|
||||
import { store, selectedAccessRole } from "builderStore"
|
||||
import analytics, { Events } from "analytics"
|
||||
import { get } from "svelte/store"
|
||||
|
||||
let newScreenModal
|
||||
let navigationSelectionModal
|
||||
let screenDetailsModal
|
||||
let screenName = ""
|
||||
let url = ""
|
||||
let selectedScreens = []
|
||||
let pendingScreen
|
||||
let showProgressCircle = false
|
||||
let routeError
|
||||
let createdScreens = []
|
||||
|
||||
$: roleId = $selectedAccessRole || "BASIC"
|
||||
// Modal refs
|
||||
let newScreenModal
|
||||
let screenDetailsModal
|
||||
|
||||
const createScreens = async () => {
|
||||
for (let screen of selectedScreens) {
|
||||
let test = screen.create()
|
||||
createdScreens.push(test)
|
||||
analytics.captureEvent(Events.SCREEN.CREATED, {
|
||||
template: screen.id || screen.name,
|
||||
})
|
||||
}
|
||||
}
|
||||
// External handler to show the screen wizard
|
||||
export const showModal = () => {
|
||||
newScreenModal.show()
|
||||
|
||||
const save = async () => {
|
||||
showProgressCircle = true
|
||||
try {
|
||||
await createScreens()
|
||||
for (let screen of createdScreens) {
|
||||
await saveScreens(screen)
|
||||
}
|
||||
await store.actions.routing.fetch()
|
||||
selectedScreens = []
|
||||
createdScreens = []
|
||||
screenName = ""
|
||||
url = ""
|
||||
} catch (error) {
|
||||
notifications.error("Error creating screens")
|
||||
}
|
||||
// Reset state when showing modal again
|
||||
pendingScreen = null
|
||||
showProgressCircle = false
|
||||
}
|
||||
|
||||
const saveScreens = async draftScreen => {
|
||||
let existingScreenCount = $store.screens.filter(
|
||||
s => s.props._instanceName == draftScreen.props._instanceName
|
||||
).length
|
||||
if (existingScreenCount > 0) {
|
||||
let oldUrlArr = draftScreen.routing.route.split("/")
|
||||
oldUrlArr[1] = `${oldUrlArr[1]}-${existingScreenCount + 1}`
|
||||
draftScreen.routing.route = oldUrlArr.join("/")
|
||||
// Creates an array of screens, checking and sanitising their URLs
|
||||
const createScreens = async screens => {
|
||||
if (!screens?.length) {
|
||||
return
|
||||
}
|
||||
showProgressCircle = true
|
||||
|
||||
let route = url ? sanitizeUrl(`${url}`) : draftScreen.routing.route
|
||||
if (draftScreen) {
|
||||
if (!route) {
|
||||
routeError = "URL is required"
|
||||
} else {
|
||||
if (routeExists(route, roleId)) {
|
||||
routeError = "This URL is already taken for this access role"
|
||||
} else {
|
||||
routeError = ""
|
||||
try {
|
||||
for (let screen of screens) {
|
||||
// Check we aren't clashing with an existing URL
|
||||
if (hasExistingUrl(screen.routing.route)) {
|
||||
let suffix = 2
|
||||
let candidateUrl = makeCandidateUrl(screen, suffix)
|
||||
while (hasExistingUrl(candidateUrl)) {
|
||||
candidateUrl = makeCandidateUrl(screen, ++suffix)
|
||||
}
|
||||
screen.routing.route = candidateUrl
|
||||
}
|
||||
}
|
||||
|
||||
if (routeError) return false
|
||||
// Sanitise URL
|
||||
screen.routing.route = sanitizeUrl(screen.routing.route)
|
||||
|
||||
if (screenName) {
|
||||
draftScreen.props._instanceName = screenName
|
||||
}
|
||||
// Use the currently selected role
|
||||
screen.routing.roleId = get(selectedAccessRole) || "BASIC"
|
||||
|
||||
draftScreen.routing.route = route
|
||||
draftScreen.routing.roleId = roleId
|
||||
// Create the screen
|
||||
await store.actions.screens.save(screen)
|
||||
|
||||
await store.actions.screens.save(draftScreen)
|
||||
if (draftScreen.props._instanceName.endsWith("List")) {
|
||||
try {
|
||||
// Analytics
|
||||
if (screen.template) {
|
||||
analytics.captureEvent(Events.SCREEN.CREATED, {
|
||||
template: screen.template,
|
||||
})
|
||||
}
|
||||
|
||||
// Add link in layout for list screens
|
||||
if (screen.props._instanceName.endsWith("List")) {
|
||||
await store.actions.components.links.save(
|
||||
draftScreen.routing.route,
|
||||
draftScreen.routing.route.split("/")[1]
|
||||
screen.routing.route,
|
||||
screen.routing.route.split("/")[1]
|
||||
)
|
||||
} catch (error) {
|
||||
notifications.error("Error creating link to screen")
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
notifications.error("Error creating screens")
|
||||
}
|
||||
|
||||
showProgressCircle = false
|
||||
}
|
||||
|
||||
// Checks if any screens exist in the store with the given route and
|
||||
// currently selected role
|
||||
const hasExistingUrl = url => {
|
||||
const roleId = get(selectedAccessRole) || "BASIC"
|
||||
const screens = get(store).screens.filter(s => s.routing.roleId === roleId)
|
||||
return !!screens.find(s => s.routing?.route === url)
|
||||
}
|
||||
|
||||
// Constructs a candidate URL for a new screen, suffixing the base of the
|
||||
// screen's URL with a given suffix.
|
||||
// e.g. "/sales/:id" => "/sales-1/:id"
|
||||
const makeCandidateUrl = (screen, suffix) => {
|
||||
let url = screen.routing?.route || ""
|
||||
if (url.startsWith("/")) {
|
||||
url = url.slice(1)
|
||||
}
|
||||
if (!url.includes("/")) {
|
||||
return `/${url}-${suffix}`
|
||||
} else {
|
||||
const split = url.split("/")
|
||||
return `/${split[0]}-${suffix}/${split.slice(1).join("/")}`
|
||||
}
|
||||
}
|
||||
|
||||
const routeExists = (route, roleId) => {
|
||||
return $allScreens.some(
|
||||
screen =>
|
||||
screen.routing.route.toLowerCase() === route.toLowerCase() &&
|
||||
screen.routing.roleId === roleId
|
||||
)
|
||||
}
|
||||
|
||||
export const showModal = () => {
|
||||
newScreenModal.show()
|
||||
}
|
||||
|
||||
const setScreens = evt => {
|
||||
selectedScreens = evt.detail.screens
|
||||
}
|
||||
|
||||
const chooseModal = index => {
|
||||
/*
|
||||
0 = newScreenModal
|
||||
1 = screenDetailsModal
|
||||
2 = navigationSelectionModal
|
||||
*/
|
||||
if (index === 0) {
|
||||
newScreenModal.show()
|
||||
} else if (index === 1) {
|
||||
// Handler for NewScreenModal
|
||||
const confirmScreenSelection = async templates => {
|
||||
// Handle template selection
|
||||
if (templates?.length > 1) {
|
||||
// Autoscreens, so create immediately
|
||||
const screens = templates.map(template => template.create())
|
||||
await createScreens(screens)
|
||||
} else {
|
||||
// Empty screen, so proceed to the next modal
|
||||
pendingScreen = templates[0].create()
|
||||
screenDetailsModal.show()
|
||||
} else if (index === 2) {
|
||||
navigationSelectionModal.show()
|
||||
}
|
||||
}
|
||||
|
||||
// Handler for ScreenDetailsModal
|
||||
const confirmScreenDetails = async ({ screenName, screenUrl }) => {
|
||||
if (!pendingScreen) {
|
||||
return
|
||||
}
|
||||
pendingScreen.props._instanceName = screenName
|
||||
pendingScreen.routing.route = screenUrl
|
||||
await createScreens([pendingScreen])
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal bind:this={newScreenModal}>
|
||||
<NewScreenModal
|
||||
on:save={setScreens}
|
||||
{showProgressCircle}
|
||||
{save}
|
||||
{chooseModal}
|
||||
/>
|
||||
<NewScreenModal onConfirm={confirmScreenSelection} {showProgressCircle} />
|
||||
</Modal>
|
||||
|
||||
<Modal bind:this={screenDetailsModal}>
|
||||
<ScreenDetailsModal
|
||||
bind:screenName
|
||||
bind:url
|
||||
{showProgressCircle}
|
||||
{save}
|
||||
{chooseModal}
|
||||
onConfirm={confirmScreenDetails}
|
||||
onCancel={() => newScreenModal.show()}
|
||||
/>
|
||||
</Modal>
|
||||
|
|
|
@ -33,7 +33,7 @@
|
|||
const customSections = settings.filter(setting => setting.section)
|
||||
return [
|
||||
{
|
||||
name: "General",
|
||||
name: componentDefinition?.name || "General",
|
||||
info: componentDefinition?.info,
|
||||
settings: generalSettings,
|
||||
},
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
export let parameters
|
||||
|
||||
$: components = findAllMatchingComponents($currentAsset.props, component =>
|
||||
$: components = findAllMatchingComponents($currentAsset?.props, component =>
|
||||
component._component.endsWith("s3upload")
|
||||
)
|
||||
</script>
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
const dispatch = createEventDispatcher()
|
||||
const getValue = component => `{{ literal ${makePropSafe(component._id)} }}`
|
||||
|
||||
$: path = findComponentPath($currentAsset.props, $store.selectedComponentId)
|
||||
$: path = findComponentPath($currentAsset?.props, $store.selectedComponentId)
|
||||
$: providers = path.filter(c => c._component?.endsWith("/dataprovider"))
|
||||
|
||||
// Set initial value to closest data provider
|
||||
|
|
|
@ -18,9 +18,7 @@
|
|||
let tempValue = value || []
|
||||
|
||||
$: dataSource = getDatasourceForProvider($currentAsset, componentInstance)
|
||||
$: schema = getSchemaForDatasource($currentAsset, dataSource, {
|
||||
searchableSchema: true,
|
||||
})?.schema
|
||||
$: schema = getSchemaForDatasource($currentAsset, dataSource)?.schema
|
||||
$: schemaFields = Object.values(schema || {})
|
||||
|
||||
const saveFilter = async () => {
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
export let type
|
||||
|
||||
$: form = findClosestMatchingComponent(
|
||||
$currentAsset.props,
|
||||
$currentAsset?.props,
|
||||
componentInstance._id,
|
||||
component => component._component === "@budibase/standard-components/form"
|
||||
)
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
|
||||
const resetFormFields = async () => {
|
||||
const form = findClosestMatchingComponent(
|
||||
$currentAsset.props,
|
||||
$currentAsset?.props,
|
||||
componentInstance._id,
|
||||
component => component._component.endsWith("/form")
|
||||
)
|
||||
|
|
|
@ -1,171 +0,0 @@
|
|||
<script>
|
||||
import analytics from "analytics"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import { fade, fly } from "svelte/transition"
|
||||
import {
|
||||
ActionButton,
|
||||
ClearButton,
|
||||
RadioGroup,
|
||||
TextArea,
|
||||
ButtonGroup,
|
||||
Button,
|
||||
Heading,
|
||||
Detail,
|
||||
Divider,
|
||||
Layout,
|
||||
notifications,
|
||||
} from "@budibase/bbui"
|
||||
import { auth } from "stores/portal"
|
||||
|
||||
let step = 0
|
||||
let ratings = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
|
||||
let options = [
|
||||
"Importing / managing data",
|
||||
"Designing",
|
||||
"Automations",
|
||||
"Managing users / groups",
|
||||
"Deployment / hosting",
|
||||
"Documentation",
|
||||
]
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
// Data to send off
|
||||
let rating
|
||||
let improvements = ""
|
||||
let comment = ""
|
||||
|
||||
function selectNumber(n) {
|
||||
rating = n
|
||||
step = 1
|
||||
}
|
||||
|
||||
function submitFeedback() {
|
||||
analytics.submitFeedback({
|
||||
rating,
|
||||
improvements,
|
||||
comment,
|
||||
})
|
||||
try {
|
||||
auth.updateSelf({
|
||||
flags: {
|
||||
feedbackSubmitted: true,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
notifications.error("Error updating user")
|
||||
}
|
||||
dispatch("complete")
|
||||
}
|
||||
|
||||
function cancelFeedback() {
|
||||
try {
|
||||
auth.updateSelf({
|
||||
flags: {
|
||||
feedbackSubmitted: true,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
notifications.error("Error updating user")
|
||||
}
|
||||
dispatch("complete")
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="position"
|
||||
in:fade={{ duration: 200 }}
|
||||
out:fade|local={{ duration: 200 }}
|
||||
>
|
||||
<div
|
||||
class="feedback-frame"
|
||||
in:fly={{ y: 30, duration: 200 }}
|
||||
out:fly|local={{ y: 30, duration: 200 }}
|
||||
>
|
||||
<div class="close">
|
||||
<ClearButton on:click={cancelFeedback} />
|
||||
</div>
|
||||
<Layout gap="XS">
|
||||
{#if step === 0}
|
||||
<Heading size="XS"
|
||||
>How likely are you to recommend Budibase to a colleague?</Heading
|
||||
>
|
||||
<Divider />
|
||||
<div class="ratings">
|
||||
{#each ratings as number}
|
||||
<ActionButton
|
||||
size="L"
|
||||
emphasized
|
||||
selected={number === rating}
|
||||
on:click={() => selectNumber(number)}
|
||||
>
|
||||
{number}
|
||||
</ActionButton>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="footer">
|
||||
<Detail size="S">NOT LIKELY</Detail>
|
||||
<Detail size="S">EXTREMELY LIKELY</Detail>
|
||||
</div>
|
||||
{:else if step === 1}
|
||||
<Heading size="XS">What could be improved most in Budibase?</Heading>
|
||||
<Divider />
|
||||
<RadioGroup bind:value={improvements} {options} />
|
||||
<div class="footer">
|
||||
<Detail size="S">STEP 2 OF 3</Detail>
|
||||
<ButtonGroup>
|
||||
<Button secondary on:click={() => (step -= 1)}>Previous</Button>
|
||||
<Button primary on:click={() => (step += 1)}>Next</Button>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
{:else}
|
||||
<Heading size="XS">How can we improve your experience?</Heading>
|
||||
<Divider />
|
||||
<TextArea bind:value={comment} placeholder="Add comments" />
|
||||
<div class="footer">
|
||||
<Detail size="S">STEP 3 OF 3</Detail>
|
||||
<ButtonGroup>
|
||||
<Button secondary on:click={() => (step -= 1)}>Previous</Button>
|
||||
<Button cta on:click={submitFeedback}>Complete</Button>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
{/if}
|
||||
</Layout>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.feedback-frame :global(textarea) {
|
||||
min-height: 180px !important;
|
||||
}
|
||||
|
||||
.position {
|
||||
position: absolute;
|
||||
right: var(--spacing-l);
|
||||
bottom: calc(5 * var(--spacing-xl));
|
||||
}
|
||||
.feedback-frame {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
min-width: 510px;
|
||||
background: var(--background);
|
||||
border-radius: var(--spectrum-global-dimension-size-50);
|
||||
border: 2px solid var(--spectrum-global-color-blue-400);
|
||||
padding: var(--spacing-xl);
|
||||
}
|
||||
.ratings {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.close {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
}
|
||||
.footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
|
@ -33,8 +33,7 @@
|
|||
let parameters
|
||||
let data = []
|
||||
let saveId
|
||||
const transformerDocs =
|
||||
"https://docs.budibase.com/building-apps/data/transformers"
|
||||
const transformerDocs = "https://docs.budibase.com/docs/transformers"
|
||||
|
||||
$: datasource = $datasources.list.find(ds => ds._id === query.datasourceId)
|
||||
$: query.schema = fieldsToSchema(fields)
|
||||
|
|
|
@ -178,6 +178,7 @@ export const IntegrationTypes = {
|
|||
ORACLE: "ORACLE",
|
||||
INTERNAL: "INTERNAL",
|
||||
GOOGLE_SHEETS: "GOOGLE_SHEETS",
|
||||
FIREBASE: "FIREBASE",
|
||||
}
|
||||
|
||||
export const IntegrationNames = {
|
||||
|
@ -195,6 +196,7 @@ export const IntegrationNames = {
|
|||
[IntegrationTypes.ORACLE]: "Oracle",
|
||||
[IntegrationTypes.INTERNAL]: "Internal",
|
||||
[IntegrationTypes.GOOGLE_SHEETS]: "Google Sheets",
|
||||
[IntegrationTypes.FIREBASE]: "Firebase",
|
||||
}
|
||||
|
||||
export const SchemaTypeOptions = [
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
import DeployModal from "components/deploy/DeployModal.svelte"
|
||||
import RevertModal from "components/deploy/RevertModal.svelte"
|
||||
import VersionModal from "components/deploy/VersionModal.svelte"
|
||||
import NPSFeedbackForm from "components/feedback/NPSFeedbackForm.svelte"
|
||||
import { API } from "api"
|
||||
import { auth, admin } from "stores/portal"
|
||||
import { isActive, goto, layout, redirect } from "@roxi/routify"
|
||||
|
@ -21,15 +20,11 @@
|
|||
|
||||
// Sync once when you load the app
|
||||
let hasSynced = false
|
||||
let userShouldPostFeedback = false
|
||||
$: selected = capitalise(
|
||||
$layout.children.find(layout => $isActive(layout.path))?.title ?? "data"
|
||||
)
|
||||
|
||||
function previewApp() {
|
||||
if (!$auth?.user?.flags?.feedbackSubmitted) {
|
||||
userShouldPostFeedback = true
|
||||
}
|
||||
window.open(`/${application}`)
|
||||
}
|
||||
|
||||
|
@ -126,10 +121,6 @@
|
|||
<p>Something went wrong: {error.message}</p>
|
||||
{/await}
|
||||
|
||||
{#if userShouldPostFeedback}
|
||||
<NPSFeedbackForm on:complete={() => (userShouldPostFeedback = false)} />
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.loading {
|
||||
min-height: 100%;
|
||||
|
|
|
@ -23,10 +23,8 @@
|
|||
<div class="nav">
|
||||
<Tabs {selected} on:select={selectFirstDatasource}>
|
||||
<Tab title="Sources">
|
||||
<div class="tab-content-padding">
|
||||
<DatasourceNavigator />
|
||||
<CreateDatasourceModal bind:modal />
|
||||
</div>
|
||||
<DatasourceNavigator />
|
||||
<CreateDatasourceModal bind:modal />
|
||||
</Tab>
|
||||
</Tabs>
|
||||
<div
|
||||
|
@ -63,10 +61,6 @@
|
|||
display: contents;
|
||||
}
|
||||
|
||||
.tab-content-padding {
|
||||
padding: 0 var(--spacing-xl);
|
||||
}
|
||||
|
||||
.nav {
|
||||
overflow-y: auto;
|
||||
background: var(--background);
|
||||
|
|
|
@ -415,9 +415,7 @@
|
|||
<Banner
|
||||
extraButtonText="Learn more"
|
||||
extraButtonAction={() =>
|
||||
window.open(
|
||||
"https://docs.budibase.com/building-apps/data/transformers"
|
||||
)}
|
||||
window.open("https://docs.budibase.com/docs/transformers")}
|
||||
on:change={() => updateFlag("queryTransformerBanner", true)}
|
||||
>
|
||||
Add a JavaScript function to transform the query result.
|
||||
|
|
|
@ -135,7 +135,7 @@
|
|||
if (asset?._id) {
|
||||
url += `/${asset._id}`
|
||||
if (componentId) {
|
||||
const componentPath = findComponentPath(asset.props, componentId)
|
||||
const componentPath = findComponentPath(asset?.props, componentId)
|
||||
const componentURL = componentPath
|
||||
.slice(1)
|
||||
.map(comp => comp._id)
|
||||
|
@ -244,8 +244,6 @@
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-l);
|
||||
padding: 0 0 60px 0;
|
||||
overflow-y: auto;
|
||||
border-right: var(--border-light);
|
||||
}
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@budibase/cli",
|
||||
"version": "1.0.91-alpha.0",
|
||||
"version": "1.0.91-alpha.16",
|
||||
"description": "Budibase CLI, for developers, self hosting and migrations.",
|
||||
"main": "src/index.js",
|
||||
"bin": {
|
||||
|
|
|
@ -35,7 +35,7 @@ async function downloadFiles() {
|
|||
|
||||
async function checkDockerConfigured() {
|
||||
const error =
|
||||
"docker/docker-compose has not been installed, please follow instructions at: https://docs.budibase.com/self-hosting/hosting-methods/docker-compose#installing-docker"
|
||||
"docker/docker-compose has not been installed, please follow instructions at: https://docs.budibase.com/docs/docker-compose"
|
||||
const docker = await lookpath("docker")
|
||||
const compose = await lookpath("docker-compose")
|
||||
if (!docker || !compose) {
|
||||
|
|
|
@ -3125,7 +3125,8 @@
|
|||
"label": "Table Columns",
|
||||
"key": "tableColumns",
|
||||
"dependsOn": "dataSource",
|
||||
"placeholder": "All columns"
|
||||
"placeholder": "All columns",
|
||||
"nested": true
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@budibase/client",
|
||||
"version": "1.0.91-alpha.0",
|
||||
"version": "1.0.91-alpha.16",
|
||||
"license": "MPL-2.0",
|
||||
"module": "dist/budibase-client.js",
|
||||
"main": "dist/budibase-client.js",
|
||||
|
@ -19,9 +19,9 @@
|
|||
"dev:builder": "rollup -cw"
|
||||
},
|
||||
"dependencies": {
|
||||
"@budibase/bbui": "^1.0.91-alpha.0",
|
||||
"@budibase/frontend-core": "^1.0.91-alpha.0",
|
||||
"@budibase/string-templates": "^1.0.91-alpha.0",
|
||||
"@budibase/bbui": "^1.0.91-alpha.16",
|
||||
"@budibase/frontend-core": "^1.0.91-alpha.16",
|
||||
"@budibase/string-templates": "^1.0.91-alpha.16",
|
||||
"@spectrum-css/button": "^3.0.3",
|
||||
"@spectrum-css/card": "^3.0.3",
|
||||
"@spectrum-css/divider": "^1.0.3",
|
||||
|
|
|
@ -226,4 +226,13 @@
|
|||
border: 1px solid var(--spectrum-global-color-gray-300);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* Print styles */
|
||||
@media print {
|
||||
#spectrum-root,
|
||||
#clip-root,
|
||||
#app-root {
|
||||
overflow: visible !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -427,4 +427,20 @@
|
|||
height: var(--height);
|
||||
z-index: 998;
|
||||
}
|
||||
|
||||
/* Print styles */
|
||||
@media print {
|
||||
.layout,
|
||||
.main-wrapper {
|
||||
overflow: visible !important;
|
||||
}
|
||||
.nav-wrapper {
|
||||
display: none !important;
|
||||
}
|
||||
.layout {
|
||||
flex-direction: column !important;
|
||||
justify-content: flex-start !important;
|
||||
align-items: stretch !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -130,6 +130,7 @@
|
|||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.button-container {
|
||||
|
|
|
@ -36,4 +36,13 @@
|
|||
div :global(.apexcharts-datalabel) {
|
||||
fill: var(--spectrum-global-color-gray-800);
|
||||
}
|
||||
div :global(.apexcharts-tooltip) {
|
||||
background-color: var(--spectrum-global-color-gray-200) !important;
|
||||
border-color: var(--spectrum-global-color-gray-300) !important;
|
||||
box-shadow: 2px 2px 6px -4px rgba(0, 0, 0, 0.1) !important;
|
||||
}
|
||||
div :global(.apexcharts-tooltip-title) {
|
||||
background-color: var(--spectrum-global-color-gray-100) !important;
|
||||
border-color: var(--spectrum-global-color-gray-300) !important;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -18,16 +18,53 @@
|
|||
export let palette
|
||||
export let horizontal
|
||||
|
||||
$: options = setUpChart(dataProvider)
|
||||
$: options = setUpChart(
|
||||
title,
|
||||
dataProvider,
|
||||
labelColumn,
|
||||
valueColumns,
|
||||
xAxisLabel,
|
||||
yAxisLabel,
|
||||
height,
|
||||
width,
|
||||
dataLabels,
|
||||
animate,
|
||||
legend,
|
||||
stacked,
|
||||
yAxisUnits,
|
||||
palette,
|
||||
horizontal
|
||||
)
|
||||
|
||||
const setUpChart = provider => {
|
||||
const setUpChart = (
|
||||
title,
|
||||
dataProvider,
|
||||
labelColumn,
|
||||
valueColumns,
|
||||
xAxisLabel,
|
||||
yAxisLabel,
|
||||
height,
|
||||
width,
|
||||
dataLabels,
|
||||
animate,
|
||||
legend,
|
||||
stacked,
|
||||
yAxisUnits,
|
||||
palette,
|
||||
horizontal
|
||||
) => {
|
||||
console.log("new chart")
|
||||
const allCols = [labelColumn, ...(valueColumns || [null])]
|
||||
if (!provider || !provider.rows?.length || allCols.find(x => x == null)) {
|
||||
if (
|
||||
!dataProvider ||
|
||||
!dataProvider.rows?.length ||
|
||||
allCols.find(x => x == null)
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Fetch data
|
||||
const { schema, rows } = provider
|
||||
const { schema, rows } = dataProvider
|
||||
const reducer = row => (valid, column) => valid && row[column] != null
|
||||
const hasAllColumns = row => allCols.reduce(reducer(row), true)
|
||||
const data = rows.filter(row => hasAllColumns(row)).slice(0, 100)
|
||||
|
|
|
@ -16,17 +16,48 @@
|
|||
export let animate
|
||||
export let yAxisUnits
|
||||
|
||||
$: options = setUpChart(dataProvider)
|
||||
$: options = setUpChart(
|
||||
title,
|
||||
dataProvider,
|
||||
dateColumn,
|
||||
openColumn,
|
||||
highColumn,
|
||||
lowColumn,
|
||||
closeColumn,
|
||||
xAxisLabel,
|
||||
yAxisLabel,
|
||||
height,
|
||||
width,
|
||||
animate,
|
||||
yAxisUnits
|
||||
)
|
||||
|
||||
// Fetch data on mount
|
||||
const setUpChart = provider => {
|
||||
const setUpChart = (
|
||||
title,
|
||||
dataProvider,
|
||||
dateColumn,
|
||||
openColumn,
|
||||
highColumn,
|
||||
lowColumn,
|
||||
closeColumn,
|
||||
xAxisLabel,
|
||||
yAxisLabel,
|
||||
height,
|
||||
width,
|
||||
animate,
|
||||
yAxisUnits
|
||||
) => {
|
||||
const allCols = [dateColumn, openColumn, highColumn, lowColumn, closeColumn]
|
||||
if (!provider || !provider.rows?.length || allCols.find(x => x == null)) {
|
||||
if (
|
||||
!dataProvider ||
|
||||
!dataProvider.rows?.length ||
|
||||
allCols.find(x => x == null)
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Fetch data
|
||||
const { schema, rows } = provider
|
||||
const { schema, rows } = dataProvider
|
||||
const reducer = row => (valid, column) => valid && row[column] != null
|
||||
const hasAllColumns = row => allCols.reduce(reducer(row), true)
|
||||
const data = rows.filter(row => hasAllColumns(row))
|
||||
|
|
|
@ -23,17 +23,56 @@
|
|||
export let stacked
|
||||
export let gradient
|
||||
|
||||
$: options = setUpChart(dataProvider)
|
||||
$: options = setUpChart(
|
||||
title,
|
||||
dataProvider,
|
||||
labelColumn,
|
||||
valueColumns,
|
||||
xAxisLabel,
|
||||
yAxisLabel,
|
||||
height,
|
||||
width,
|
||||
animate,
|
||||
dataLabels,
|
||||
curve,
|
||||
legend,
|
||||
yAxisUnits,
|
||||
palette,
|
||||
area,
|
||||
stacked,
|
||||
gradient
|
||||
)
|
||||
|
||||
// Fetch data on mount
|
||||
const setUpChart = provider => {
|
||||
const setUpChart = (
|
||||
title,
|
||||
dataProvider,
|
||||
labelColumn,
|
||||
valueColumns,
|
||||
xAxisLabel,
|
||||
yAxisLabel,
|
||||
height,
|
||||
width,
|
||||
animate,
|
||||
dataLabels,
|
||||
curve,
|
||||
legend,
|
||||
yAxisUnits,
|
||||
palette,
|
||||
area,
|
||||
stacked,
|
||||
gradient
|
||||
) => {
|
||||
const allCols = [labelColumn, ...(valueColumns || [null])]
|
||||
if (!provider || !provider.rows?.length || allCols.find(x => x == null)) {
|
||||
if (
|
||||
!dataProvider ||
|
||||
!dataProvider.rows?.length ||
|
||||
allCols.find(x => x == null)
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Fetch, filter and sort data
|
||||
const { schema, rows } = provider
|
||||
const { schema, rows } = dataProvider
|
||||
const reducer = row => (valid, column) => valid && row[column] != null
|
||||
const hasAllColumns = row => allCols.reduce(reducer(row), true)
|
||||
const data = rows.filter(row => hasAllColumns(row))
|
||||
|
|
|
@ -14,16 +14,44 @@
|
|||
export let donut
|
||||
export let palette
|
||||
|
||||
$: options = setUpChart(dataProvider)
|
||||
$: options = setUpChart(
|
||||
title,
|
||||
dataProvider,
|
||||
labelColumn,
|
||||
valueColumn,
|
||||
height,
|
||||
width,
|
||||
dataLabels,
|
||||
animate,
|
||||
legend,
|
||||
donut,
|
||||
palette
|
||||
)
|
||||
|
||||
// Fetch data on mount
|
||||
const setUpChart = provider => {
|
||||
if (!provider || !provider.rows?.length || !labelColumn || !valueColumn) {
|
||||
const setUpChart = (
|
||||
title,
|
||||
dataProvider,
|
||||
labelColumn,
|
||||
valueColumn,
|
||||
height,
|
||||
width,
|
||||
dataLabels,
|
||||
animate,
|
||||
legend,
|
||||
donut,
|
||||
palette
|
||||
) => {
|
||||
if (
|
||||
!dataProvider ||
|
||||
!dataProvider.rows?.length ||
|
||||
!labelColumn ||
|
||||
!valueColumn
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Fetch, filter and sort data
|
||||
const { schema, rows } = provider
|
||||
const { schema, rows } = dataProvider
|
||||
const data = rows
|
||||
.filter(row => row[labelColumn] != null && row[valueColumn] != null)
|
||||
.slice(0, 100)
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
Button,
|
||||
Combobox,
|
||||
DatePicker,
|
||||
DrawerContent,
|
||||
Icon,
|
||||
Input,
|
||||
Layout,
|
||||
|
@ -12,10 +11,12 @@
|
|||
} from "@budibase/bbui"
|
||||
import { generate } from "shortid"
|
||||
import { LuceneUtils, Constants } from "@budibase/frontend-core"
|
||||
import { getContext } from "svelte"
|
||||
|
||||
export let schemaFields
|
||||
export let filters = []
|
||||
|
||||
const context = getContext("context")
|
||||
const BannedTypes = ["link", "attachment", "json"]
|
||||
|
||||
$: fieldOptions = (schemaFields ?? [])
|
||||
|
@ -89,55 +90,55 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<DrawerContent>
|
||||
<div class="container">
|
||||
<Layout noPadding>
|
||||
<Body size="S">
|
||||
{#if !filters?.length}
|
||||
Add your first filter expression.
|
||||
{:else}
|
||||
Results are filtered to only those which match all of the following
|
||||
constraints.
|
||||
{/if}
|
||||
</Body>
|
||||
{#if filters?.length}
|
||||
<div class="fields">
|
||||
{#each filters as filter, idx}
|
||||
<Select
|
||||
bind:value={filter.field}
|
||||
options={fieldOptions}
|
||||
on:change={e => onFieldChange(filter, e.detail)}
|
||||
placeholder="Column"
|
||||
<div class="container" class:mobile={$context.device.mobile}>
|
||||
<Layout noPadding>
|
||||
<Body size="S">
|
||||
{#if !filters?.length}
|
||||
Add your first filter expression.
|
||||
{:else}
|
||||
Results are filtered to only those which match all of the following
|
||||
constraints.
|
||||
{/if}
|
||||
</Body>
|
||||
{#if filters?.length}
|
||||
<div class="fields">
|
||||
{#each filters as filter, idx}
|
||||
<Select
|
||||
bind:value={filter.field}
|
||||
options={fieldOptions}
|
||||
on:change={e => onFieldChange(filter, e.detail)}
|
||||
placeholder="Column"
|
||||
/>
|
||||
<Select
|
||||
disabled={!filter.field}
|
||||
options={LuceneUtils.getValidOperatorsForType(filter.type)}
|
||||
bind:value={filter.operator}
|
||||
on:change={e => onOperatorChange(filter, e.detail)}
|
||||
placeholder={null}
|
||||
/>
|
||||
{#if ["string", "longform", "number", "formula"].includes(filter.type)}
|
||||
<Input disabled={filter.noValue} bind:value={filter.value} />
|
||||
{:else if ["options", "array"].includes(filter.type)}
|
||||
<Combobox
|
||||
disabled={filter.noValue}
|
||||
options={getFieldOptions(filter.field)}
|
||||
bind:value={filter.value}
|
||||
/>
|
||||
<Select
|
||||
disabled={!filter.field}
|
||||
options={LuceneUtils.getValidOperatorsForType(filter.type)}
|
||||
bind:value={filter.operator}
|
||||
on:change={e => onOperatorChange(filter, e.detail)}
|
||||
placeholder={null}
|
||||
{:else if filter.type === "boolean"}
|
||||
<Combobox
|
||||
disabled={filter.noValue}
|
||||
options={[
|
||||
{ label: "True", value: "true" },
|
||||
{ label: "False", value: "false" },
|
||||
]}
|
||||
bind:value={filter.value}
|
||||
/>
|
||||
{#if ["string", "longform", "number", "formula"].includes(filter.type)}
|
||||
<Input disabled={filter.noValue} bind:value={filter.value} />
|
||||
{:else if ["options", "array"].includes(filter.type)}
|
||||
<Combobox
|
||||
disabled={filter.noValue}
|
||||
options={getFieldOptions(filter.field)}
|
||||
bind:value={filter.value}
|
||||
/>
|
||||
{:else if filter.type === "boolean"}
|
||||
<Combobox
|
||||
disabled={filter.noValue}
|
||||
options={[
|
||||
{ label: "True", value: "true" },
|
||||
{ label: "False", value: "false" },
|
||||
]}
|
||||
bind:value={filter.value}
|
||||
/>
|
||||
{:else if filter.type === "datetime"}
|
||||
<DatePicker disabled={filter.noValue} bind:value={filter.value} />
|
||||
{:else}
|
||||
<Input disabled />
|
||||
{/if}
|
||||
{:else if filter.type === "datetime"}
|
||||
<DatePicker disabled={filter.noValue} bind:value={filter.value} />
|
||||
{:else}
|
||||
<Input disabled />
|
||||
{/if}
|
||||
<div class="controls">
|
||||
<Icon
|
||||
name="Duplicate"
|
||||
hoverable
|
||||
|
@ -150,17 +151,17 @@
|
|||
size="S"
|
||||
on:click={() => removeFilter(filter.id)}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
<div>
|
||||
<Button icon="AddCircle" size="M" secondary on:click={addFilter}>
|
||||
Add filter
|
||||
</Button>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</Layout>
|
||||
</div>
|
||||
</DrawerContent>
|
||||
{/if}
|
||||
<div>
|
||||
<Button icon="AddCircle" size="M" secondary on:click={addFilter}>
|
||||
Add filter
|
||||
</Button>
|
||||
</div>
|
||||
</Layout>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.container {
|
||||
|
@ -175,4 +176,19 @@
|
|||
align-items: center;
|
||||
grid-template-columns: 1fr 120px 1fr auto auto;
|
||||
}
|
||||
.controls {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
.container.mobile .fields {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.container.mobile .controls {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
padding: var(--spacing-s) 0;
|
||||
gap: var(--spacing-s);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
let table
|
||||
|
||||
$: fetchSchema(dataSource)
|
||||
$: fetchTable(dataSource)
|
||||
|
||||
// Returns the closes data context which isn't a built in context
|
||||
const getInitialValues = (type, dataSource, context) => {
|
||||
|
@ -74,6 +75,16 @@
|
|||
}
|
||||
}
|
||||
|
||||
const fetchTable = async dataSource => {
|
||||
if (dataSource?.tableId) {
|
||||
try {
|
||||
table = await API.fetchTableDefinition(dataSource.tableId)
|
||||
} catch (error) {
|
||||
table = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$: initialValues = getInitialValues(actionType, dataSource, $context)
|
||||
$: resetKey = Helpers.hashString(
|
||||
JSON.stringify(initialValues) + JSON.stringify(schema)
|
||||
|
|
|
@ -157,13 +157,9 @@
|
|||
const { fieldState } = get(existingField)
|
||||
fieldId = fieldState.fieldId
|
||||
|
||||
// Use new default value if default value changed,
|
||||
// otherwise use the current value if possible
|
||||
if (defaultValue !== fieldState.defaultValue) {
|
||||
initialValue = defaultValue
|
||||
} else {
|
||||
initialValue = fieldState.value ?? initialValue
|
||||
}
|
||||
// Determine the initial value for this field, reusing the current
|
||||
// value if one exists
|
||||
initialValue = fieldState.value ?? initialValue
|
||||
|
||||
// If this field has already been registered and we previously had an
|
||||
// error set, then re-run the validator to see if we can unset it
|
||||
|
|
|
@ -146,6 +146,15 @@
|
|||
<div class="divider" />
|
||||
{/if}
|
||||
{/each}
|
||||
<SettingsButton
|
||||
icon="Duplicate"
|
||||
on:click={() => {
|
||||
builderStore.actions.duplicateComponent(
|
||||
$builderStore.selectedComponent._id
|
||||
)
|
||||
}}
|
||||
title="Duplicate component"
|
||||
/>
|
||||
<SettingsButton
|
||||
icon="Delete"
|
||||
on:click={() => {
|
||||
|
@ -153,6 +162,7 @@
|
|||
$builderStore.selectedComponent._id
|
||||
)
|
||||
}}
|
||||
title="Delete component"
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
|
@ -6,12 +6,26 @@ const createAuthStore = () => {
|
|||
|
||||
// Fetches the user object if someone is logged in and has reloaded the page
|
||||
const fetchUser = async () => {
|
||||
let globalSelf = null
|
||||
let appSelf = null
|
||||
|
||||
// First try and get the global user, to see if we are logged in at all
|
||||
try {
|
||||
const user = await API.fetchSelf()
|
||||
store.set(user)
|
||||
globalSelf = await API.fetchBuilderSelf()
|
||||
} catch (error) {
|
||||
store.set(null)
|
||||
return
|
||||
}
|
||||
|
||||
// Then try and get the user for this app to provide via context
|
||||
try {
|
||||
appSelf = await API.fetchSelf()
|
||||
} catch (error) {
|
||||
// Swallow
|
||||
}
|
||||
|
||||
// Use the app self if present, otherwise fallback to the global self
|
||||
store.set(appSelf || globalSelf || null)
|
||||
}
|
||||
|
||||
const logOut = async () => {
|
||||
|
|
|
@ -62,6 +62,9 @@ const createBuilderStore = () => {
|
|||
deleteComponent: id => {
|
||||
dispatchEvent("delete-component", { id })
|
||||
},
|
||||
duplicateComponent: id => {
|
||||
dispatchEvent("duplicate-component", { id })
|
||||
},
|
||||
notifyLoaded: () => {
|
||||
dispatchEvent("preview-loaded")
|
||||
},
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "@budibase/frontend-core",
|
||||
"version": "1.0.91-alpha.0",
|
||||
"version": "1.0.91-alpha.16",
|
||||
"description": "Budibase frontend core libraries used in builder and client",
|
||||
"author": "Budibase",
|
||||
"license": "MPL-2.0",
|
||||
"svelte": "src/index.js",
|
||||
"dependencies": {
|
||||
"@budibase/bbui": "^1.0.91-alpha.0",
|
||||
"@budibase/bbui": "^1.0.91-alpha.16",
|
||||
"lodash": "^4.17.21",
|
||||
"svelte": "^3.46.2"
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ export const buildAnalyticsEndpoints = API => ({
|
|||
*/
|
||||
pingEndUser: async () => {
|
||||
return await API.post({
|
||||
url: `/api/analytics/ping`,
|
||||
url: `/api/bbtel/ping`,
|
||||
})
|
||||
},
|
||||
|
||||
|
@ -13,7 +13,7 @@ export const buildAnalyticsEndpoints = API => ({
|
|||
*/
|
||||
getAnalyticsStatus: async () => {
|
||||
return await API.get({
|
||||
url: "/api/analytics",
|
||||
url: "/api/bbtel",
|
||||
})
|
||||
},
|
||||
})
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "@budibase/server",
|
||||
"email": "hi@budibase.com",
|
||||
"version": "1.0.91-alpha.0",
|
||||
"version": "1.0.91-alpha.16",
|
||||
"description": "Budibase Web Server",
|
||||
"main": "src/index.ts",
|
||||
"repository": {
|
||||
|
@ -71,12 +71,13 @@
|
|||
"license": "GPL-3.0",
|
||||
"dependencies": {
|
||||
"@apidevtools/swagger-parser": "^10.0.3",
|
||||
"@budibase/backend-core": "^1.0.91-alpha.0",
|
||||
"@budibase/client": "^1.0.91-alpha.0",
|
||||
"@budibase/string-templates": "^1.0.91-alpha.0",
|
||||
"@budibase/backend-core": "^1.0.91-alpha.16",
|
||||
"@budibase/client": "^1.0.91-alpha.16",
|
||||
"@budibase/string-templates": "^1.0.91-alpha.16",
|
||||
"@bull-board/api": "^3.7.0",
|
||||
"@bull-board/koa": "^3.7.0",
|
||||
"@elastic/elasticsearch": "7.10.0",
|
||||
"@google-cloud/firestore": "^5.0.2",
|
||||
"@koa/router": "8.0.0",
|
||||
"@sendgrid/mail": "7.1.1",
|
||||
"@sentry/node": "^6.0.0",
|
||||
|
@ -148,6 +149,7 @@
|
|||
"@types/jest": "^26.0.23",
|
||||
"@types/koa": "^2.13.3",
|
||||
"@types/koa-router": "^7.4.2",
|
||||
"@types/lodash": "4.14.180",
|
||||
"@types/node": "^15.12.4",
|
||||
"@types/oracledb": "^5.2.1",
|
||||
"@types/redis": "^4.0.11",
|
||||
|
@ -157,6 +159,7 @@
|
|||
"copyfiles": "^2.4.1",
|
||||
"docker-compose": "^0.23.6",
|
||||
"eslint": "^6.8.0",
|
||||
"is-wsl": "^2.2.0",
|
||||
"jest": "^27.0.5",
|
||||
"jest-openapi": "^0.14.2",
|
||||
"nodemon": "^2.0.4",
|
||||
|
|
|
@ -2,10 +2,11 @@
|
|||
const compose = require("docker-compose")
|
||||
const path = require("path")
|
||||
const fs = require("fs")
|
||||
const isWsl = require("is-wsl")
|
||||
const { processStringSync } = require("@budibase/string-templates")
|
||||
|
||||
function isLinux() {
|
||||
return process.platform !== "darwin" && process.platform !== "win32"
|
||||
return !isWsl && process.platform !== "darwin" && process.platform !== "win32"
|
||||
}
|
||||
|
||||
// This script wraps docker-compose allowing you to manage your dev infrastructure with simple commands.
|
||||
|
|
|
@ -52,16 +52,14 @@ export async function read(ctx: any, next: any) {
|
|||
}
|
||||
|
||||
export async function update(ctx: any, next: any) {
|
||||
ctx.request.body = await addRev(fixRow(ctx.request.body, ctx.params.tableId))
|
||||
ctx.request.body = await addRev(fixRow(ctx.request.body, ctx.params))
|
||||
await rowController.save(ctx)
|
||||
await next()
|
||||
}
|
||||
|
||||
export async function destroy(ctx: any, next: any) {
|
||||
// set the body as expected, with the _id and _rev fields
|
||||
ctx.request.body = await addRev(
|
||||
fixRow({ _id: ctx.params.rowId }, ctx.params.tableId)
|
||||
)
|
||||
ctx.request.body = await addRev(fixRow({ _id: ctx.params.rowId }, ctx.params))
|
||||
await rowController.destroy(ctx)
|
||||
// destroy controller doesn't currently return the row as the body, need to adjust this
|
||||
// in the public API to be correct
|
||||
|
|
|
@ -2,6 +2,7 @@ import { queryValidation } from "../validation"
|
|||
import { generateQueryID } from "../../../../db/utils"
|
||||
import { ImportInfo, ImportSource } from "./sources/base"
|
||||
import { OpenAPI2 } from "./sources/openapi2"
|
||||
import { OpenAPI3 } from "./sources/openapi3"
|
||||
import { Query } from "./../../../../definitions/common"
|
||||
import { Curl } from "./sources/curl"
|
||||
// @ts-ignore
|
||||
|
@ -18,7 +19,7 @@ export class RestImporter {
|
|||
|
||||
constructor(data: string) {
|
||||
this.data = data
|
||||
this.sources = [new OpenAPI2(), new Curl()]
|
||||
this.sources = [new OpenAPI2(), new OpenAPI3(), new Curl()]
|
||||
}
|
||||
|
||||
init = async () => {
|
||||
|
|
|
@ -23,7 +23,7 @@ export abstract class ImportSource {
|
|||
name: string,
|
||||
method: string,
|
||||
path: string,
|
||||
url: URL,
|
||||
url: URL | string | undefined,
|
||||
queryString: string,
|
||||
headers: object = {},
|
||||
parameters: QueryParameter[] = [],
|
||||
|
@ -34,7 +34,17 @@ export abstract class ImportSource {
|
|||
const transformer = "return data"
|
||||
const schema = {}
|
||||
path = this.processPath(path)
|
||||
path = `${url.origin}/${path}`
|
||||
if (url) {
|
||||
if (typeof url === "string") {
|
||||
path = `${url}/${path}`
|
||||
} else {
|
||||
let href = url.href
|
||||
if (href.endsWith("/")) {
|
||||
href = href.slice(0, -1)
|
||||
}
|
||||
path = `${href}/${path}`
|
||||
}
|
||||
}
|
||||
queryString = this.processQuery(queryString)
|
||||
const requestBody = JSON.stringify(body, null, 2)
|
||||
|
||||
|
|
|
@ -74,7 +74,7 @@ export class Curl extends ImportSource {
|
|||
getQueries = async (datasourceId: string): Promise<Query[]> => {
|
||||
const url = this.getUrl()
|
||||
const name = url.pathname
|
||||
const path = url.pathname
|
||||
const path = url.origin + url.pathname
|
||||
const method = this.curl.method
|
||||
const queryString = url.search
|
||||
const headers = this.curl.headers
|
||||
|
@ -90,7 +90,7 @@ export class Curl extends ImportSource {
|
|||
name,
|
||||
method,
|
||||
path,
|
||||
url,
|
||||
undefined,
|
||||
queryString,
|
||||
headers,
|
||||
[],
|
||||
|
|
|
@ -0,0 +1,205 @@
|
|||
import { ImportInfo } from "./base"
|
||||
import { Query, QueryParameter } from "../../../../../definitions/datasource"
|
||||
import { OpenAPIV3 } from "openapi-types"
|
||||
import { OpenAPISource } from "./base/openapi"
|
||||
import { URL } from "url"
|
||||
|
||||
const parameterNotRef = (
|
||||
param: OpenAPIV3.ParameterObject | OpenAPIV3.ReferenceObject
|
||||
): param is OpenAPIV3.ParameterObject => {
|
||||
// all refs are deferenced by parser library
|
||||
return true
|
||||
}
|
||||
|
||||
const requestBodyNotRef = (
|
||||
param: OpenAPIV3.RequestBodyObject | OpenAPIV3.ReferenceObject | undefined
|
||||
): param is OpenAPIV3.RequestBodyObject => {
|
||||
// all refs are deferenced by parser library
|
||||
return param !== undefined
|
||||
}
|
||||
|
||||
const schemaNotRef = (
|
||||
param: OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject | undefined
|
||||
): param is OpenAPIV3.SchemaObject => {
|
||||
// all refs are deferenced by parser library
|
||||
return param !== undefined
|
||||
}
|
||||
|
||||
const isOpenAPI3 = (document: any): document is OpenAPIV3.Document => {
|
||||
return document.openapi.includes("3.0")
|
||||
}
|
||||
|
||||
const methods: string[] = Object.values(OpenAPIV3.HttpMethods)
|
||||
|
||||
const isOperation = (
|
||||
key: string,
|
||||
pathItem: any
|
||||
): pathItem is OpenAPIV3.OperationObject => {
|
||||
return methods.includes(key)
|
||||
}
|
||||
|
||||
const isParameter = (
|
||||
key: string,
|
||||
pathItem: any
|
||||
): pathItem is OpenAPIV3.ParameterObject => {
|
||||
return !isOperation(key, pathItem)
|
||||
}
|
||||
|
||||
const getRequestBody = (operation: OpenAPIV3.OperationObject) => {
|
||||
if (requestBodyNotRef(operation.requestBody)) {
|
||||
const request: OpenAPIV3.RequestBodyObject = operation.requestBody
|
||||
const supportedMimeTypes = getMimeTypes(operation)
|
||||
if (supportedMimeTypes.length > 0) {
|
||||
const mimeType = supportedMimeTypes[0]
|
||||
|
||||
// try get example from request
|
||||
const content = request.content[mimeType]
|
||||
if (content.example) {
|
||||
return content.example
|
||||
}
|
||||
|
||||
// try get example from schema
|
||||
if (schemaNotRef(content.schema)) {
|
||||
const schema = content.schema
|
||||
if (schema.example) {
|
||||
return schema.example
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
const getMimeTypes = (operation: OpenAPIV3.OperationObject): string[] => {
|
||||
if (requestBodyNotRef(operation.requestBody)) {
|
||||
const request: OpenAPIV3.RequestBodyObject = operation.requestBody
|
||||
return Object.keys(request.content)
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
/**
|
||||
* OpenAPI Version 3.0
|
||||
* https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.0.md
|
||||
*/
|
||||
export class OpenAPI3 extends OpenAPISource {
|
||||
document!: OpenAPIV3.Document
|
||||
|
||||
isSupported = async (data: string): Promise<boolean> => {
|
||||
try {
|
||||
const document: any = await this.parseData(data)
|
||||
if (isOpenAPI3(document)) {
|
||||
this.document = document
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
} catch (err) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
getInfo = async (): Promise<ImportInfo> => {
|
||||
const name = this.document.info.title || "OpenAPI Import"
|
||||
return {
|
||||
name,
|
||||
}
|
||||
}
|
||||
|
||||
getQueries = async (datasourceId: string): Promise<Query[]> => {
|
||||
let url: string | URL | undefined
|
||||
if (this.document.servers?.length) {
|
||||
url = this.document.servers[0].url
|
||||
try {
|
||||
url = new URL(url)
|
||||
} catch (err) {
|
||||
// unable to construct url, e.g. with variables
|
||||
// proceed with string form of url
|
||||
}
|
||||
}
|
||||
|
||||
const queries: Query[] = []
|
||||
|
||||
for (let [path, pathItemObject] of Object.entries(this.document.paths)) {
|
||||
// parameters that apply to every operation in the path
|
||||
let pathParams: OpenAPIV3.ParameterObject[] = []
|
||||
|
||||
// pathItemObject can be undefined
|
||||
if (!pathItemObject) {
|
||||
continue
|
||||
}
|
||||
|
||||
for (let [key, opOrParams] of Object.entries(pathItemObject)) {
|
||||
if (isParameter(key, opOrParams)) {
|
||||
const pathParameters = opOrParams as OpenAPIV3.ParameterObject[]
|
||||
pathParams.push(...pathParameters)
|
||||
continue
|
||||
}
|
||||
// can not be a parameter, must be an operation
|
||||
const operation = opOrParams as OpenAPIV3.OperationObject
|
||||
|
||||
const methodName = key
|
||||
const name = operation.operationId || path
|
||||
let queryString = ""
|
||||
const headers: any = {}
|
||||
let requestBody = getRequestBody(operation)
|
||||
const parameters: QueryParameter[] = []
|
||||
const mimeTypes = getMimeTypes(operation)
|
||||
|
||||
if (mimeTypes.length > 0) {
|
||||
headers["Content-Type"] = mimeTypes[0]
|
||||
}
|
||||
|
||||
// combine the path parameters with the operation parameters
|
||||
const operationParams = operation.parameters || []
|
||||
const allParams = [...pathParams, ...operationParams]
|
||||
|
||||
for (let param of allParams) {
|
||||
if (parameterNotRef(param)) {
|
||||
switch (param.in) {
|
||||
case "query":
|
||||
let prefix = ""
|
||||
if (queryString) {
|
||||
prefix = "&"
|
||||
}
|
||||
queryString = `${queryString}${prefix}${param.name}={{${param.name}}}`
|
||||
break
|
||||
case "header":
|
||||
headers[param.name] = `{{${param.name}}}`
|
||||
break
|
||||
case "path":
|
||||
// do nothing: param is already in the path
|
||||
break
|
||||
case "formData":
|
||||
// future enhancement
|
||||
break
|
||||
}
|
||||
|
||||
// add the parameter if it can be bound in our config
|
||||
if (["query", "header", "path"].includes(param.in)) {
|
||||
parameters.push({
|
||||
name: param.name,
|
||||
default: "",
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const query = this.constructQuery(
|
||||
datasourceId,
|
||||
name,
|
||||
methodName,
|
||||
path,
|
||||
url,
|
||||
queryString,
|
||||
headers,
|
||||
parameters,
|
||||
requestBody
|
||||
)
|
||||
queries.push(query)
|
||||
}
|
||||
}
|
||||
|
||||
return queries
|
||||
}
|
||||
}
|
|
@ -0,0 +1,253 @@
|
|||
{
|
||||
"openapi": "3.0.2",
|
||||
"info": {
|
||||
"description": "A basic swagger file",
|
||||
"version": "1.0.0",
|
||||
"title": "CRUD"
|
||||
},
|
||||
"servers": [
|
||||
{
|
||||
"url": "http://example.com"
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
{
|
||||
"name": "entity"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"/entities": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"entity"
|
||||
],
|
||||
"operationId": "createEntity",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/CreateEntity"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "successful operation",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Entity"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"get": {
|
||||
"tags": [
|
||||
"entity"
|
||||
],
|
||||
"operationId": "getEntities",
|
||||
"parameters": [
|
||||
{
|
||||
"$ref": "#/components/parameters/PageParameter"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/parameters/SizeParameter"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "successful operation",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Entities"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/entities/{entityId}": {
|
||||
"parameters": [
|
||||
{
|
||||
"$ref": "#/components/parameters/EntityIdParameter"
|
||||
}
|
||||
],
|
||||
"get": {
|
||||
"tags": [
|
||||
"entity"
|
||||
],
|
||||
"operationId": "getEntity",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "successful operation",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Entity"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"put": {
|
||||
"tags": [
|
||||
"entity"
|
||||
],
|
||||
"operationId": "updateEntity",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Entity"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "successful operation",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Entity"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"patch": {
|
||||
"tags": [
|
||||
"entity"
|
||||
],
|
||||
"operationId": "patchEntity",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Entity"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "successful operation",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Entity"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"tags": [
|
||||
"entity"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"$ref": "#/components/parameters/APIKeyParameter"
|
||||
}
|
||||
],
|
||||
"operationId": "deleteEntity",
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "successful operation"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"parameters": {
|
||||
"EntityIdParameter": {
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"name": "entityId",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
"PageParameter": {
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
},
|
||||
"name": "page",
|
||||
"in": "query",
|
||||
"required": false
|
||||
},
|
||||
"SizeParameter": {
|
||||
"schema": {
|
||||
"type": "integer",
|
||||
"format": "int32"
|
||||
},
|
||||
"name": "size",
|
||||
"in": "query",
|
||||
"required": false
|
||||
},
|
||||
"APIKeyParameter": {
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": "x-api-key",
|
||||
"in": "header",
|
||||
"required": false
|
||||
}
|
||||
},
|
||||
"schemas": {
|
||||
"CreateEntity": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"example": {
|
||||
"name": "name",
|
||||
"type": "type"
|
||||
}
|
||||
},
|
||||
"Entity": {
|
||||
"allOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/CreateEntity"
|
||||
}
|
||||
],
|
||||
"example": {
|
||||
"id": 1,
|
||||
"name": "name",
|
||||
"type": "type"
|
||||
}
|
||||
},
|
||||
"Entities" : {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/Entity"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,153 @@
|
|||
---
|
||||
openapi: 3.0.2
|
||||
info:
|
||||
description: A basic swagger file
|
||||
version: 1.0.0
|
||||
title: CRUD
|
||||
servers:
|
||||
- url: http://example.com
|
||||
tags:
|
||||
- name: entity
|
||||
paths:
|
||||
"/entities":
|
||||
post:
|
||||
tags:
|
||||
- entity
|
||||
operationId: createEntity
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
"$ref": "#/components/schemas/CreateEntity"
|
||||
responses:
|
||||
'200':
|
||||
description: successful operation
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
"$ref": "#/components/schemas/Entity"
|
||||
get:
|
||||
tags:
|
||||
- entity
|
||||
operationId: getEntities
|
||||
parameters:
|
||||
- "$ref": "#/components/parameters/PageParameter"
|
||||
- "$ref": "#/components/parameters/SizeParameter"
|
||||
responses:
|
||||
'200':
|
||||
description: successful operation
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
"$ref": "#/components/schemas/Entities"
|
||||
"/entities/{entityId}":
|
||||
parameters:
|
||||
- "$ref": "#/components/parameters/EntityIdParameter"
|
||||
get:
|
||||
tags:
|
||||
- entity
|
||||
operationId: getEntity
|
||||
responses:
|
||||
'200':
|
||||
description: successful operation
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
"$ref": "#/components/schemas/Entity"
|
||||
put:
|
||||
tags:
|
||||
- entity
|
||||
operationId: updateEntity
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
"$ref": "#/components/schemas/Entity"
|
||||
responses:
|
||||
'200':
|
||||
description: successful operation
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
"$ref": "#/components/schemas/Entity"
|
||||
patch:
|
||||
tags:
|
||||
- entity
|
||||
operationId: patchEntity
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
"$ref": "#/components/schemas/Entity"
|
||||
responses:
|
||||
'200':
|
||||
description: successful operation
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
"$ref": "#/components/schemas/Entity"
|
||||
delete:
|
||||
tags:
|
||||
- entity
|
||||
parameters:
|
||||
- "$ref": "#/components/parameters/APIKeyParameter"
|
||||
operationId: deleteEntity
|
||||
responses:
|
||||
'204':
|
||||
description: successful operation
|
||||
components:
|
||||
parameters:
|
||||
EntityIdParameter:
|
||||
schema:
|
||||
type: integer
|
||||
format: int64
|
||||
name: entityId
|
||||
in: path
|
||||
required: true
|
||||
PageParameter:
|
||||
schema:
|
||||
type: integer
|
||||
format: int32
|
||||
name: page
|
||||
in: query
|
||||
required: false
|
||||
SizeParameter:
|
||||
schema:
|
||||
type: integer
|
||||
format: int32
|
||||
name: size
|
||||
in: query
|
||||
required: false
|
||||
APIKeyParameter:
|
||||
schema:
|
||||
type: string
|
||||
name: x-api-key
|
||||
in: header
|
||||
required: false
|
||||
schemas:
|
||||
CreateEntity:
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
type:
|
||||
type: string
|
||||
example:
|
||||
name: name
|
||||
type: type
|
||||
Entity:
|
||||
allOf:
|
||||
- type: object
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
format: int64
|
||||
- "$ref": "#/components/schemas/CreateEntity"
|
||||
example:
|
||||
id: 1
|
||||
name: name
|
||||
type: type
|
||||
Entities:
|
||||
type: array
|
||||
items:
|
||||
"$ref": "#/components/schemas/Entity"
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,804 @@
|
|||
---
|
||||
openapi: 3.0.2
|
||||
info:
|
||||
title: Swagger Petstore - OpenAPI 3.0
|
||||
description: |-
|
||||
This is a sample Pet Store Server based on the OpenAPI 3.0 specification. You can find out more about
|
||||
Swagger at [http://swagger.io](http://swagger.io). In the third iteration of the pet store, we've switched to the design first approach!
|
||||
You can now help us improve the API whether it's by making changes to the definition itself or to the code.
|
||||
That way, with time, we can improve the API in general, and expose some of the new features in OAS3.
|
||||
|
||||
Some useful links:
|
||||
- [The Pet Store repository](https://github.com/swagger-api/swagger-petstore)
|
||||
- [The source API definition for the Pet Store](https://github.com/swagger-api/swagger-petstore/blob/master/src/main/resources/openapi.yaml)
|
||||
termsOfService: http://swagger.io/terms/
|
||||
contact:
|
||||
email: apiteam@swagger.io
|
||||
license:
|
||||
name: Apache 2.0
|
||||
url: http://www.apache.org/licenses/LICENSE-2.0.html
|
||||
version: 1.0.11
|
||||
externalDocs:
|
||||
description: Find out more about Swagger
|
||||
url: http://swagger.io
|
||||
servers:
|
||||
- url: "/api/v3"
|
||||
tags:
|
||||
- name: pet
|
||||
description: Everything about your Pets
|
||||
externalDocs:
|
||||
description: Find out more
|
||||
url: http://swagger.io
|
||||
- name: store
|
||||
description: Access to Petstore orders
|
||||
externalDocs:
|
||||
description: Find out more about our store
|
||||
url: http://swagger.io
|
||||
- name: user
|
||||
description: Operations about user
|
||||
paths:
|
||||
"/pet":
|
||||
put:
|
||||
tags:
|
||||
- pet
|
||||
summary: Update an existing pet
|
||||
description: Update an existing pet by Id
|
||||
operationId: updatePet
|
||||
requestBody:
|
||||
description: Update an existent pet in the store
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
"$ref": "#/components/schemas/Pet"
|
||||
application/xml:
|
||||
schema:
|
||||
"$ref": "#/components/schemas/Pet"
|
||||
application/x-www-form-urlencoded:
|
||||
schema:
|
||||
"$ref": "#/components/schemas/Pet"
|
||||
required: true
|
||||
responses:
|
||||
'200':
|
||||
description: Successful operation
|
||||
content:
|
||||
application/xml:
|
||||
schema:
|
||||
"$ref": "#/components/schemas/Pet"
|
||||
application/json:
|
||||
schema:
|
||||
"$ref": "#/components/schemas/Pet"
|
||||
'400':
|
||||
description: Invalid ID supplied
|
||||
'404':
|
||||
description: Pet not found
|
||||
'405':
|
||||
description: Validation exception
|
||||
security:
|
||||
- petstore_auth:
|
||||
- write:pets
|
||||
- read:pets
|
||||
post:
|
||||
tags:
|
||||
- pet
|
||||
summary: Add a new pet to the store
|
||||
description: Add a new pet to the store
|
||||
operationId: addPet
|
||||
requestBody:
|
||||
description: Create a new pet in the store
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
"$ref": "#/components/schemas/Pet"
|
||||
application/xml:
|
||||
schema:
|
||||
"$ref": "#/components/schemas/Pet"
|
||||
application/x-www-form-urlencoded:
|
||||
schema:
|
||||
"$ref": "#/components/schemas/Pet"
|
||||
required: true
|
||||
responses:
|
||||
'200':
|
||||
description: Successful operation
|
||||
content:
|
||||
application/xml:
|
||||
schema:
|
||||
"$ref": "#/components/schemas/Pet"
|
||||
application/json:
|
||||
schema:
|
||||
"$ref": "#/components/schemas/Pet"
|
||||
'405':
|
||||
description: Invalid input
|
||||
security:
|
||||
- petstore_auth:
|
||||
- write:pets
|
||||
- read:pets
|
||||
"/pet/findByStatus":
|
||||
get:
|
||||
tags:
|
||||
- pet
|
||||
summary: Finds Pets by status
|
||||
description: Multiple status values can be provided with comma separated strings
|
||||
operationId: findPetsByStatus
|
||||
parameters:
|
||||
- name: status
|
||||
in: query
|
||||
description: Status values that need to be considered for filter
|
||||
required: false
|
||||
explode: true
|
||||
schema:
|
||||
type: string
|
||||
default: available
|
||||
enum:
|
||||
- available
|
||||
- pending
|
||||
- sold
|
||||
responses:
|
||||
'200':
|
||||
description: successful operation
|
||||
content:
|
||||
application/xml:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
"$ref": "#/components/schemas/Pet"
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
"$ref": "#/components/schemas/Pet"
|
||||
'400':
|
||||
description: Invalid status value
|
||||
security:
|
||||
- petstore_auth:
|
||||
- write:pets
|
||||
- read:pets
|
||||
"/pet/findByTags":
|
||||
get:
|
||||
tags:
|
||||
- pet
|
||||
summary: Finds Pets by tags
|
||||
description: Multiple tags can be provided with comma separated strings. Use
|
||||
tag1, tag2, tag3 for testing.
|
||||
operationId: findPetsByTags
|
||||
parameters:
|
||||
- name: tags
|
||||
in: query
|
||||
description: Tags to filter by
|
||||
required: false
|
||||
explode: true
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: successful operation
|
||||
content:
|
||||
application/xml:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
"$ref": "#/components/schemas/Pet"
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
"$ref": "#/components/schemas/Pet"
|
||||
'400':
|
||||
description: Invalid tag value
|
||||
security:
|
||||
- petstore_auth:
|
||||
- write:pets
|
||||
- read:pets
|
||||
"/pet/{petId}":
|
||||
get:
|
||||
tags:
|
||||
- pet
|
||||
summary: Find pet by ID
|
||||
description: Returns a single pet
|
||||
operationId: getPetById
|
||||
parameters:
|
||||
- name: petId
|
||||
in: path
|
||||
description: ID of pet to return
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
format: int64
|
||||
responses:
|
||||
'200':
|
||||
description: successful operation
|
||||
content:
|
||||
application/xml:
|
||||
schema:
|
||||
"$ref": "#/components/schemas/Pet"
|
||||
application/json:
|
||||
schema:
|
||||
"$ref": "#/components/schemas/Pet"
|
||||
'400':
|
||||
description: Invalid ID supplied
|
||||
'404':
|
||||
description: Pet not found
|
||||
security:
|
||||
- api_key: []
|
||||
- petstore_auth:
|
||||
- write:pets
|
||||
- read:pets
|
||||
post:
|
||||
tags:
|
||||
- pet
|
||||
summary: Updates a pet in the store with form data
|
||||
description: ''
|
||||
operationId: updatePetWithForm
|
||||
parameters:
|
||||
- name: petId
|
||||
in: path
|
||||
description: ID of pet that needs to be updated
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
format: int64
|
||||
- name: name
|
||||
in: query
|
||||
description: Name of pet that needs to be updated
|
||||
schema:
|
||||
type: string
|
||||
- name: status
|
||||
in: query
|
||||
description: Status of pet that needs to be updated
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'405':
|
||||
description: Invalid input
|
||||
security:
|
||||
- petstore_auth:
|
||||
- write:pets
|
||||
- read:pets
|
||||
delete:
|
||||
tags:
|
||||
- pet
|
||||
summary: Deletes a pet
|
||||
description: ''
|
||||
operationId: deletePet
|
||||
parameters:
|
||||
- name: api_key
|
||||
in: header
|
||||
description: ''
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
- name: petId
|
||||
in: path
|
||||
description: Pet id to delete
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
format: int64
|
||||
responses:
|
||||
'400':
|
||||
description: Invalid pet value
|
||||
security:
|
||||
- petstore_auth:
|
||||
- write:pets
|
||||
- read:pets
|
||||
"/pet/{petId}/uploadImage":
|
||||
post:
|
||||
tags:
|
||||
- pet
|
||||
summary: uploads an image
|
||||
description: ''
|
||||
operationId: uploadFile
|
||||
parameters:
|
||||
- name: petId
|
||||
in: path
|
||||
description: ID of pet to update
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
format: int64
|
||||
- name: additionalMetadata
|
||||
in: query
|
||||
description: Additional Metadata
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
requestBody:
|
||||
content:
|
||||
application/octet-stream:
|
||||
schema:
|
||||
type: string
|
||||
format: binary
|
||||
responses:
|
||||
'200':
|
||||
description: successful operation
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
"$ref": "#/components/schemas/ApiResponse"
|
||||
security:
|
||||
- petstore_auth:
|
||||
- write:pets
|
||||
- read:pets
|
||||
"/store/inventory":
|
||||
get:
|
||||
tags:
|
||||
- store
|
||||
summary: Returns pet inventories by status
|
||||
description: Returns a map of status codes to quantities
|
||||
operationId: getInventory
|
||||
responses:
|
||||
'200':
|
||||
description: successful operation
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
additionalProperties:
|
||||
type: integer
|
||||
format: int32
|
||||
security:
|
||||
- api_key: []
|
||||
"/store/order":
|
||||
post:
|
||||
tags:
|
||||
- store
|
||||
summary: Place an order for a pet
|
||||
description: Place a new order in the store
|
||||
operationId: placeOrder
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
"$ref": "#/components/schemas/Order"
|
||||
application/xml:
|
||||
schema:
|
||||
"$ref": "#/components/schemas/Order"
|
||||
application/x-www-form-urlencoded:
|
||||
schema:
|
||||
"$ref": "#/components/schemas/Order"
|
||||
responses:
|
||||
'200':
|
||||
description: successful operation
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
"$ref": "#/components/schemas/Order"
|
||||
'405':
|
||||
description: Invalid input
|
||||
"/store/order/{orderId}":
|
||||
get:
|
||||
tags:
|
||||
- store
|
||||
summary: Find purchase order by ID
|
||||
description: For valid response try integer IDs with value <= 5 or > 10. Other
|
||||
values will generate exceptions.
|
||||
operationId: getOrderById
|
||||
parameters:
|
||||
- name: orderId
|
||||
in: path
|
||||
description: ID of order that needs to be fetched
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
format: int64
|
||||
responses:
|
||||
'200':
|
||||
description: successful operation
|
||||
content:
|
||||
application/xml:
|
||||
schema:
|
||||
"$ref": "#/components/schemas/Order"
|
||||
application/json:
|
||||
schema:
|
||||
"$ref": "#/components/schemas/Order"
|
||||
'400':
|
||||
description: Invalid ID supplied
|
||||
'404':
|
||||
description: Order not found
|
||||
delete:
|
||||
tags:
|
||||
- store
|
||||
summary: Delete purchase order by ID
|
||||
description: For valid response try integer IDs with value < 1000. Anything
|
||||
above 1000 or nonintegers will generate API errors
|
||||
operationId: deleteOrder
|
||||
parameters:
|
||||
- name: orderId
|
||||
in: path
|
||||
description: ID of the order that needs to be deleted
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
format: int64
|
||||
responses:
|
||||
'400':
|
||||
description: Invalid ID supplied
|
||||
'404':
|
||||
description: Order not found
|
||||
"/user":
|
||||
post:
|
||||
tags:
|
||||
- user
|
||||
summary: Create user
|
||||
description: This can only be done by the logged in user.
|
||||
operationId: createUser
|
||||
requestBody:
|
||||
description: Created user object
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
"$ref": "#/components/schemas/User"
|
||||
application/xml:
|
||||
schema:
|
||||
"$ref": "#/components/schemas/User"
|
||||
application/x-www-form-urlencoded:
|
||||
schema:
|
||||
"$ref": "#/components/schemas/User"
|
||||
responses:
|
||||
default:
|
||||
description: successful operation
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
"$ref": "#/components/schemas/User"
|
||||
application/xml:
|
||||
schema:
|
||||
"$ref": "#/components/schemas/User"
|
||||
"/user/createWithList":
|
||||
post:
|
||||
tags:
|
||||
- user
|
||||
summary: Creates list of users with given input array
|
||||
description: Creates list of users with given input array
|
||||
operationId: createUsersWithListInput
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
"$ref": "#/components/schemas/User"
|
||||
responses:
|
||||
'200':
|
||||
description: Successful operation
|
||||
content:
|
||||
application/xml:
|
||||
schema:
|
||||
"$ref": "#/components/schemas/User"
|
||||
application/json:
|
||||
schema:
|
||||
"$ref": "#/components/schemas/User"
|
||||
default:
|
||||
description: successful operation
|
||||
"/user/login":
|
||||
get:
|
||||
tags:
|
||||
- user
|
||||
summary: Logs user into the system
|
||||
description: ''
|
||||
operationId: loginUser
|
||||
parameters:
|
||||
- name: username
|
||||
in: query
|
||||
description: The user name for login
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
- name: password
|
||||
in: query
|
||||
description: The password for login in clear text
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: successful operation
|
||||
headers:
|
||||
X-Rate-Limit:
|
||||
description: calls per hour allowed by the user
|
||||
schema:
|
||||
type: integer
|
||||
format: int32
|
||||
X-Expires-After:
|
||||
description: date in UTC when token expires
|
||||
schema:
|
||||
type: string
|
||||
format: date-time
|
||||
content:
|
||||
application/xml:
|
||||
schema:
|
||||
type: string
|
||||
application/json:
|
||||
schema:
|
||||
type: string
|
||||
'400':
|
||||
description: Invalid username/password supplied
|
||||
"/user/logout":
|
||||
get:
|
||||
tags:
|
||||
- user
|
||||
summary: Logs out current logged in user session
|
||||
description: ''
|
||||
operationId: logoutUser
|
||||
parameters: []
|
||||
responses:
|
||||
default:
|
||||
description: successful operation
|
||||
"/user/{username}":
|
||||
get:
|
||||
tags:
|
||||
- user
|
||||
summary: Get user by user name
|
||||
description: ''
|
||||
operationId: getUserByName
|
||||
parameters:
|
||||
- name: username
|
||||
in: path
|
||||
description: 'The name that needs to be fetched. Use user1 for testing. '
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: successful operation
|
||||
content:
|
||||
application/xml:
|
||||
schema:
|
||||
"$ref": "#/components/schemas/User"
|
||||
application/json:
|
||||
schema:
|
||||
"$ref": "#/components/schemas/User"
|
||||
'400':
|
||||
description: Invalid username supplied
|
||||
'404':
|
||||
description: User not found
|
||||
put:
|
||||
tags:
|
||||
- user
|
||||
summary: Update user
|
||||
description: This can only be done by the logged in user.
|
||||
operationId: updateUser
|
||||
parameters:
|
||||
- name: username
|
||||
in: path
|
||||
description: name that need to be deleted
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
requestBody:
|
||||
description: Update an existent user in the store
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
"$ref": "#/components/schemas/User"
|
||||
application/xml:
|
||||
schema:
|
||||
"$ref": "#/components/schemas/User"
|
||||
application/x-www-form-urlencoded:
|
||||
schema:
|
||||
"$ref": "#/components/schemas/User"
|
||||
responses:
|
||||
default:
|
||||
description: successful operation
|
||||
delete:
|
||||
tags:
|
||||
- user
|
||||
summary: Delete user
|
||||
description: This can only be done by the logged in user.
|
||||
operationId: deleteUser
|
||||
parameters:
|
||||
- name: username
|
||||
in: path
|
||||
description: The name that needs to be deleted
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'400':
|
||||
description: Invalid username supplied
|
||||
'404':
|
||||
description: User not found
|
||||
components:
|
||||
schemas:
|
||||
Order:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
format: int64
|
||||
example: 10
|
||||
petId:
|
||||
type: integer
|
||||
format: int64
|
||||
example: 198772
|
||||
quantity:
|
||||
type: integer
|
||||
format: int32
|
||||
example: 7
|
||||
shipDate:
|
||||
type: string
|
||||
format: date-time
|
||||
status:
|
||||
type: string
|
||||
description: Order Status
|
||||
example: approved
|
||||
enum:
|
||||
- placed
|
||||
- approved
|
||||
- delivered
|
||||
complete:
|
||||
type: boolean
|
||||
xml:
|
||||
name: order
|
||||
Customer:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
format: int64
|
||||
example: 100000
|
||||
username:
|
||||
type: string
|
||||
example: fehguy
|
||||
address:
|
||||
type: array
|
||||
xml:
|
||||
name: addresses
|
||||
wrapped: true
|
||||
items:
|
||||
"$ref": "#/components/schemas/Address"
|
||||
xml:
|
||||
name: customer
|
||||
Address:
|
||||
type: object
|
||||
properties:
|
||||
street:
|
||||
type: string
|
||||
example: 437 Lytton
|
||||
city:
|
||||
type: string
|
||||
example: Palo Alto
|
||||
state:
|
||||
type: string
|
||||
example: CA
|
||||
zip:
|
||||
type: string
|
||||
example: '94301'
|
||||
xml:
|
||||
name: address
|
||||
Category:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
format: int64
|
||||
example: 1
|
||||
name:
|
||||
type: string
|
||||
example: Dogs
|
||||
xml:
|
||||
name: category
|
||||
User:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
format: int64
|
||||
example: 10
|
||||
username:
|
||||
type: string
|
||||
example: theUser
|
||||
firstName:
|
||||
type: string
|
||||
example: John
|
||||
lastName:
|
||||
type: string
|
||||
example: James
|
||||
email:
|
||||
type: string
|
||||
example: john@email.com
|
||||
password:
|
||||
type: string
|
||||
example: '12345'
|
||||
phone:
|
||||
type: string
|
||||
example: '12345'
|
||||
userStatus:
|
||||
type: integer
|
||||
description: User Status
|
||||
format: int32
|
||||
example: 1
|
||||
xml:
|
||||
name: user
|
||||
Tag:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
format: int64
|
||||
name:
|
||||
type: string
|
||||
xml:
|
||||
name: tag
|
||||
Pet:
|
||||
required:
|
||||
- name
|
||||
- photoUrls
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
format: int64
|
||||
example: 10
|
||||
name:
|
||||
type: string
|
||||
example: doggie
|
||||
category:
|
||||
"$ref": "#/components/schemas/Category"
|
||||
photoUrls:
|
||||
type: array
|
||||
xml:
|
||||
wrapped: true
|
||||
items:
|
||||
type: string
|
||||
xml:
|
||||
name: photoUrl
|
||||
tags:
|
||||
type: array
|
||||
xml:
|
||||
wrapped: true
|
||||
items:
|
||||
"$ref": "#/components/schemas/Tag"
|
||||
status:
|
||||
type: string
|
||||
description: pet status in the store
|
||||
enum:
|
||||
- available
|
||||
- pending
|
||||
- sold
|
||||
xml:
|
||||
name: pet
|
||||
ApiResponse:
|
||||
type: object
|
||||
properties:
|
||||
code:
|
||||
type: integer
|
||||
format: int32
|
||||
type:
|
||||
type: string
|
||||
message:
|
||||
type: string
|
||||
xml:
|
||||
name: "##default"
|
||||
requestBodies:
|
||||
Pet:
|
||||
description: Pet object that needs to be added to the store
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
"$ref": "#/components/schemas/Pet"
|
||||
application/xml:
|
||||
schema:
|
||||
"$ref": "#/components/schemas/Pet"
|
||||
UserArray:
|
||||
description: List of user object
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
"$ref": "#/components/schemas/User"
|
||||
securitySchemes:
|
||||
petstore_auth:
|
||||
type: oauth2
|
||||
flows:
|
||||
implicit:
|
||||
authorizationUrl: https://petstore3.swagger.io/oauth/authorize
|
||||
scopes:
|
||||
write:pets: modify pets in your account
|
||||
read:pets: read your pets
|
||||
api_key:
|
||||
type: apiKey
|
||||
name: api_key
|
||||
in: header
|
|
@ -0,0 +1,240 @@
|
|||
const fs = require("fs")
|
||||
const path = require("path")
|
||||
|
||||
const { OpenAPI3 } = require("../../openapi3")
|
||||
|
||||
const getData = (file, extension) => {
|
||||
return fs.readFileSync(
|
||||
path.join(__dirname, `./data/${file}/${file}.${extension}`),
|
||||
"utf8"
|
||||
)
|
||||
}
|
||||
|
||||
describe("OpenAPI3 Import", () => {
|
||||
let openapi3
|
||||
|
||||
beforeEach(() => {
|
||||
openapi3 = new OpenAPI3()
|
||||
})
|
||||
|
||||
it("validates unsupported data", async () => {
|
||||
let data
|
||||
let supported
|
||||
|
||||
// non json / yaml
|
||||
data = "curl http://example.com"
|
||||
supported = await openapi3.isSupported(data)
|
||||
expect(supported).toBe(false)
|
||||
|
||||
// Empty
|
||||
data = ""
|
||||
supported = await openapi3.isSupported(data)
|
||||
expect(supported).toBe(false)
|
||||
})
|
||||
|
||||
const runTests = async (filename, test, assertions) => {
|
||||
for (let extension of ["json", "yaml"]) {
|
||||
await test(filename, extension, assertions)
|
||||
}
|
||||
}
|
||||
|
||||
const testImportInfo = async (file, extension) => {
|
||||
await openapi3.isSupported(getData(file, extension))
|
||||
const info = await openapi3.getInfo()
|
||||
expect(info.name).toBe("Swagger Petstore - OpenAPI 3.0")
|
||||
}
|
||||
|
||||
it("returns import info", async () => {
|
||||
await runTests("petstore", testImportInfo)
|
||||
})
|
||||
|
||||
describe("Returns queries", () => {
|
||||
const indexQueries = queries => {
|
||||
return queries.reduce((acc, query) => {
|
||||
acc[query.name] = query
|
||||
return acc
|
||||
}, {})
|
||||
}
|
||||
|
||||
const getQueries = async (file, extension) => {
|
||||
await openapi3.isSupported(getData(file, extension))
|
||||
const queries = await openapi3.getQueries()
|
||||
expect(queries.length).toBe(6)
|
||||
return indexQueries(queries)
|
||||
}
|
||||
|
||||
const testVerb = async (file, extension, assertions) => {
|
||||
const queries = await getQueries(file, extension)
|
||||
for (let [operationId, method] of Object.entries(assertions)) {
|
||||
expect(queries[operationId].queryVerb).toBe(method)
|
||||
}
|
||||
}
|
||||
|
||||
it("populates verb", async () => {
|
||||
const assertions = {
|
||||
createEntity: "create",
|
||||
getEntities: "read",
|
||||
getEntity: "read",
|
||||
updateEntity: "update",
|
||||
patchEntity: "patch",
|
||||
deleteEntity: "delete",
|
||||
}
|
||||
await runTests("crud", testVerb, assertions)
|
||||
})
|
||||
|
||||
const testPath = async (file, extension, assertions) => {
|
||||
const queries = await getQueries(file, extension)
|
||||
for (let [operationId, urlPath] of Object.entries(assertions)) {
|
||||
expect(queries[operationId].fields.path).toBe(urlPath)
|
||||
}
|
||||
}
|
||||
|
||||
it("populates path", async () => {
|
||||
const assertions = {
|
||||
createEntity: "http://example.com/entities",
|
||||
getEntities: "http://example.com/entities",
|
||||
getEntity: "http://example.com/entities/{{entityId}}",
|
||||
updateEntity: "http://example.com/entities/{{entityId}}",
|
||||
patchEntity: "http://example.com/entities/{{entityId}}",
|
||||
deleteEntity: "http://example.com/entities/{{entityId}}",
|
||||
}
|
||||
await runTests("crud", testPath, assertions)
|
||||
})
|
||||
|
||||
const testHeaders = async (file, extension, assertions) => {
|
||||
const queries = await getQueries(file, extension)
|
||||
for (let [operationId, headers] of Object.entries(assertions)) {
|
||||
expect(queries[operationId].fields.headers).toStrictEqual(headers)
|
||||
}
|
||||
}
|
||||
|
||||
const contentTypeHeader = {
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
it("populates headers", async () => {
|
||||
const assertions = {
|
||||
createEntity: {
|
||||
...contentTypeHeader,
|
||||
},
|
||||
getEntities: {},
|
||||
getEntity: {},
|
||||
updateEntity: {
|
||||
...contentTypeHeader,
|
||||
},
|
||||
patchEntity: {
|
||||
...contentTypeHeader,
|
||||
},
|
||||
deleteEntity: {
|
||||
"x-api-key": "{{x-api-key}}",
|
||||
},
|
||||
}
|
||||
|
||||
await runTests("crud", testHeaders, assertions)
|
||||
})
|
||||
|
||||
const testQuery = async (file, extension, assertions) => {
|
||||
const queries = await getQueries(file, extension)
|
||||
for (let [operationId, queryString] of Object.entries(assertions)) {
|
||||
expect(queries[operationId].fields.queryString).toStrictEqual(
|
||||
queryString
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
it("populates query", async () => {
|
||||
const assertions = {
|
||||
createEntity: "",
|
||||
getEntities: "page={{page}}&size={{size}}",
|
||||
getEntity: "",
|
||||
updateEntity: "",
|
||||
patchEntity: "",
|
||||
deleteEntity: "",
|
||||
}
|
||||
await runTests("crud", testQuery, assertions)
|
||||
})
|
||||
|
||||
const testParameters = async (file, extension, assertions) => {
|
||||
const queries = await getQueries(file, extension)
|
||||
for (let [operationId, parameters] of Object.entries(assertions)) {
|
||||
expect(queries[operationId].parameters).toStrictEqual(parameters)
|
||||
}
|
||||
}
|
||||
|
||||
it("populates parameters", async () => {
|
||||
const assertions = {
|
||||
createEntity: [],
|
||||
getEntities: [
|
||||
{
|
||||
name: "page",
|
||||
default: "",
|
||||
},
|
||||
{
|
||||
name: "size",
|
||||
default: "",
|
||||
},
|
||||
],
|
||||
getEntity: [
|
||||
{
|
||||
name: "entityId",
|
||||
default: "",
|
||||
},
|
||||
],
|
||||
updateEntity: [
|
||||
{
|
||||
name: "entityId",
|
||||
default: "",
|
||||
},
|
||||
],
|
||||
patchEntity: [
|
||||
{
|
||||
name: "entityId",
|
||||
default: "",
|
||||
},
|
||||
],
|
||||
deleteEntity: [
|
||||
{
|
||||
name: "entityId",
|
||||
default: "",
|
||||
},
|
||||
{
|
||||
name: "x-api-key",
|
||||
default: "",
|
||||
},
|
||||
],
|
||||
}
|
||||
await runTests("crud", testParameters, assertions)
|
||||
})
|
||||
|
||||
const testBody = async (file, extension, assertions) => {
|
||||
const queries = await getQueries(file, extension)
|
||||
for (let [operationId, body] of Object.entries(assertions)) {
|
||||
expect(queries[operationId].fields.requestBody).toStrictEqual(
|
||||
JSON.stringify(body, null, 2)
|
||||
)
|
||||
}
|
||||
}
|
||||
it("populates body", async () => {
|
||||
const assertions = {
|
||||
createEntity: {
|
||||
name: "name",
|
||||
type: "type",
|
||||
},
|
||||
getEntities: undefined,
|
||||
getEntity: undefined,
|
||||
updateEntity: {
|
||||
id: 1,
|
||||
name: "name",
|
||||
type: "type",
|
||||
},
|
||||
patchEntity: {
|
||||
id: 1,
|
||||
name: "name",
|
||||
type: "type",
|
||||
},
|
||||
deleteEntity: undefined,
|
||||
}
|
||||
await runTests("crud", testBody, assertions)
|
||||
})
|
||||
})
|
||||
})
|
|
@ -23,14 +23,27 @@ const oapi2CrudYaml = getData("openapi2/data/crud/crud.json")
|
|||
const oapi2PetstoreJson = getData("openapi2/data/petstore/petstore.json")
|
||||
const oapi2PetstoreYaml = getData("openapi2/data/petstore/petstore.json")
|
||||
|
||||
// openapi3
|
||||
const oapi3CrudJson = getData("openapi3/data/crud/crud.json")
|
||||
const oapi3CrudYaml = getData("openapi3/data/crud/crud.json")
|
||||
const oapi3PetstoreJson = getData("openapi3/data/petstore/petstore.json")
|
||||
const oapi3PetstoreYaml = getData("openapi3/data/petstore/petstore.json")
|
||||
|
||||
// curl
|
||||
const curl = getData("curl/data/post.txt")
|
||||
|
||||
const datasets = {
|
||||
// openapi2 (swagger)
|
||||
oapi2CrudJson,
|
||||
oapi2CrudYaml,
|
||||
oapi2PetstoreJson,
|
||||
oapi2PetstoreYaml,
|
||||
// openapi3
|
||||
oapi3CrudJson,
|
||||
oapi3CrudYaml,
|
||||
oapi3PetstoreJson,
|
||||
oapi3PetstoreYaml,
|
||||
// curl
|
||||
curl
|
||||
}
|
||||
|
||||
|
@ -56,6 +69,7 @@ describe("Rest Importer", () => {
|
|||
|
||||
it("gets info", async () => {
|
||||
const assertions = {
|
||||
// openapi2 (swagger)
|
||||
"oapi2CrudJson" : {
|
||||
name: "CRUD",
|
||||
},
|
||||
|
@ -68,6 +82,20 @@ describe("Rest Importer", () => {
|
|||
"oapi2PetstoreYaml" :{
|
||||
name: "Swagger Petstore",
|
||||
},
|
||||
// openapi3
|
||||
"oapi3CrudJson" : {
|
||||
name: "CRUD",
|
||||
},
|
||||
"oapi3CrudYaml" : {
|
||||
name: "CRUD",
|
||||
},
|
||||
"oapi3PetstoreJson" : {
|
||||
name: "Swagger Petstore - OpenAPI 3.0",
|
||||
},
|
||||
"oapi3PetstoreYaml" :{
|
||||
name: "Swagger Petstore - OpenAPI 3.0",
|
||||
},
|
||||
// curl
|
||||
"curl": {
|
||||
name: "example.com",
|
||||
}
|
||||
|
@ -89,6 +117,7 @@ describe("Rest Importer", () => {
|
|||
// simple sanity assertions that the whole dataset
|
||||
// makes it through the importer
|
||||
const assertions = {
|
||||
// openapi2 (swagger)
|
||||
"oapi2CrudJson" : {
|
||||
count: 6,
|
||||
},
|
||||
|
@ -101,6 +130,20 @@ describe("Rest Importer", () => {
|
|||
"oapi2PetstoreYaml" :{
|
||||
count: 20,
|
||||
},
|
||||
// openapi3
|
||||
"oapi3CrudJson" : {
|
||||
count: 6,
|
||||
},
|
||||
"oapi3CrudYaml" :{
|
||||
count: 6,
|
||||
},
|
||||
"oapi3PetstoreJson" : {
|
||||
count: 19,
|
||||
},
|
||||
"oapi3PetstoreYaml" :{
|
||||
count: 19,
|
||||
},
|
||||
// curl
|
||||
"curl": {
|
||||
count: 1
|
||||
}
|
||||
|
|
|
@ -85,7 +85,11 @@ exports.patch = async ctx => {
|
|||
const isUserTable = tableId === InternalTables.USER_METADATA
|
||||
let oldRow
|
||||
try {
|
||||
oldRow = await db.get(inputs._id)
|
||||
let dbTable = await db.get(tableId)
|
||||
oldRow = await outputProcessing(
|
||||
dbTable,
|
||||
await findRow(ctx, tableId, inputs._id)
|
||||
)
|
||||
} catch (err) {
|
||||
if (isUserTable) {
|
||||
// don't include the rev, it'll be the global rev
|
||||
|
|
|
@ -4,7 +4,7 @@ const controller = require("../controllers/analytics")
|
|||
const router = Router()
|
||||
|
||||
router
|
||||
.get("/api/analytics", controller.isEnabled)
|
||||
.post("/api/analytics/ping", controller.endUserPing)
|
||||
.get("/api/bbtel", controller.isEnabled)
|
||||
.post("/api/bbtel/ping", controller.endUserPing)
|
||||
|
||||
module.exports = router
|
||||
|
|
|
@ -11,10 +11,10 @@ describe("run misc tests", () => {
|
|||
await config.init()
|
||||
})
|
||||
|
||||
describe("/analytics", () => {
|
||||
describe("/bbtel", () => {
|
||||
it("check if analytics enabled", async () => {
|
||||
const res = await request
|
||||
.get(`/api/analytics`)
|
||||
.get(`/api/bbtel`)
|
||||
.set(config.defaultHeaders())
|
||||
.expect("Content-Type", /json/)
|
||||
.expect(200)
|
||||
|
|
|
@ -48,6 +48,7 @@ export enum SourceNames {
|
|||
REST = "REST",
|
||||
ORACLE = "ORACLE",
|
||||
GOOGLE_SHEETS = "GOOGLE_SHEETS",
|
||||
FIREBASE = "FIREBASE",
|
||||
}
|
||||
|
||||
export enum IncludeRelationships {
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -27,11 +27,8 @@ function parse(input: any) {
|
|||
if (typeof input !== "string") {
|
||||
return input
|
||||
}
|
||||
if (input === MAX_ISO_DATE) {
|
||||
return new Date(8640000000000000)
|
||||
}
|
||||
if (input === MIN_ISO_DATE) {
|
||||
return new Date(-8640000000000000)
|
||||
if (input === MAX_ISO_DATE || input === MIN_ISO_DATE) {
|
||||
return null
|
||||
}
|
||||
if (isIsoDateString(input)) {
|
||||
return new Date(input)
|
||||
|
@ -130,11 +127,19 @@ class InternalBuilder {
|
|||
}
|
||||
if (filters.range) {
|
||||
iterate(filters.range, (key, value) => {
|
||||
if (!value.high || !value.low) {
|
||||
return
|
||||
if (value.low && value.high) {
|
||||
// Use a between operator if we have 2 valid range values
|
||||
const fnc = allOr ? "orWhereBetween" : "whereBetween"
|
||||
query = query[fnc](key, [value.low, value.high])
|
||||
} else if (value.low) {
|
||||
// Use just a single greater than operator if we only have a low
|
||||
const fnc = allOr ? "orWhere" : "where"
|
||||
query = query[fnc](key, ">", value.low)
|
||||
} else if (value.high) {
|
||||
// Use just a single less than operator if we only have a high
|
||||
const fnc = allOr ? "orWhere" : "where"
|
||||
query = query[fnc](key, "<", value.high)
|
||||
}
|
||||
const fnc = allOr ? "orWhereBetween" : "whereBetween"
|
||||
query = query[fnc](key, [value.low, value.high])
|
||||
})
|
||||
}
|
||||
if (filters.equal) {
|
||||
|
|
|
@ -0,0 +1,205 @@
|
|||
import {
|
||||
DatasourceFieldTypes,
|
||||
Integration,
|
||||
QueryTypes,
|
||||
} from "../definitions/datasource"
|
||||
import { IntegrationBase } from "./base/IntegrationBase"
|
||||
import { Firestore, WhereFilterOp } from "@google-cloud/firestore"
|
||||
|
||||
module Firebase {
|
||||
interface FirebaseConfig {
|
||||
email: string
|
||||
privateKey: string
|
||||
projectId: string
|
||||
serviceAccount?: string
|
||||
}
|
||||
|
||||
const SCHEMA: Integration = {
|
||||
docs: "https://firebase.google.com/docs/firestore/quickstart",
|
||||
friendlyName: "Firestore",
|
||||
description:
|
||||
"Cloud Firestore is a flexible, scalable database for mobile, web, and server development from Firebase and Google Cloud.",
|
||||
datasource: {
|
||||
email: {
|
||||
type: DatasourceFieldTypes.STRING,
|
||||
required: true,
|
||||
},
|
||||
privateKey: {
|
||||
type: DatasourceFieldTypes.STRING,
|
||||
required: true,
|
||||
},
|
||||
projectId: {
|
||||
type: DatasourceFieldTypes.STRING,
|
||||
required: true,
|
||||
},
|
||||
serviceAccount: {
|
||||
type: DatasourceFieldTypes.JSON,
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
query: {
|
||||
create: {
|
||||
type: QueryTypes.JSON,
|
||||
},
|
||||
read: {
|
||||
type: QueryTypes.JSON,
|
||||
},
|
||||
update: {
|
||||
type: QueryTypes.JSON,
|
||||
},
|
||||
delete: {
|
||||
type: QueryTypes.JSON,
|
||||
},
|
||||
},
|
||||
extra: {
|
||||
collection: {
|
||||
displayName: "Collection",
|
||||
type: DatasourceFieldTypes.STRING,
|
||||
required: true,
|
||||
},
|
||||
filterField: {
|
||||
displayName: "Filter field",
|
||||
type: DatasourceFieldTypes.STRING,
|
||||
required: false,
|
||||
},
|
||||
filter: {
|
||||
displayName: "Filter comparison",
|
||||
type: DatasourceFieldTypes.LIST,
|
||||
required: false,
|
||||
data: {
|
||||
read: [
|
||||
"==",
|
||||
"<",
|
||||
"<=",
|
||||
"==",
|
||||
"!=",
|
||||
">=",
|
||||
">",
|
||||
"array-contains",
|
||||
"in",
|
||||
"not-in",
|
||||
"array-contains-any",
|
||||
],
|
||||
},
|
||||
},
|
||||
filterValue: {
|
||||
displayName: "Filter value",
|
||||
type: DatasourceFieldTypes.STRING,
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
class FirebaseIntegration implements IntegrationBase {
|
||||
private config: FirebaseConfig
|
||||
private db: Firestore
|
||||
|
||||
constructor(config: FirebaseConfig) {
|
||||
this.config = config
|
||||
if (config.serviceAccount) {
|
||||
const serviceAccount = JSON.parse(config.serviceAccount)
|
||||
this.db = new Firestore({
|
||||
projectId: serviceAccount.project_id,
|
||||
credentials: {
|
||||
client_email: serviceAccount.client_email,
|
||||
private_key: serviceAccount.private_key,
|
||||
},
|
||||
})
|
||||
} else {
|
||||
this.db = new Firestore({
|
||||
projectId: config.projectId,
|
||||
credentials: {
|
||||
client_email: config.email,
|
||||
private_key: config.privateKey,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async create(query: { json: object; extra: { [key: string]: string } }) {
|
||||
try {
|
||||
const documentReference = this.db
|
||||
.collection(query.extra.collection)
|
||||
.doc()
|
||||
await documentReference.set({ ...query.json, id: documentReference.id })
|
||||
const snapshot = await documentReference.get()
|
||||
return snapshot.data()
|
||||
} catch (err) {
|
||||
console.error("Error writing to Firestore", err)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
async read(query: { json: object; extra: { [key: string]: string } }) {
|
||||
try {
|
||||
let snapshot
|
||||
const collectionRef = this.db.collection(query.extra.collection)
|
||||
if (
|
||||
query.extra.filterField &&
|
||||
query.extra.filter &&
|
||||
query.extra.filterValue
|
||||
) {
|
||||
snapshot = await collectionRef
|
||||
.where(
|
||||
query.extra.filterField,
|
||||
query.extra.filter as WhereFilterOp,
|
||||
query.extra.filterValue
|
||||
)
|
||||
.get()
|
||||
} else {
|
||||
snapshot = await collectionRef.get()
|
||||
}
|
||||
const result: any[] = []
|
||||
snapshot.forEach(doc => result.push(doc.data()))
|
||||
|
||||
return result
|
||||
} catch (err) {
|
||||
console.error("Error querying Firestore", err)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
async update(query: {
|
||||
json: Record<string, any>
|
||||
extra: { [key: string]: string }
|
||||
}) {
|
||||
try {
|
||||
await this.db
|
||||
.collection(query.extra.collection)
|
||||
.doc(query.json.id)
|
||||
.update(query.json)
|
||||
|
||||
return (
|
||||
await this.db
|
||||
.collection(query.extra.collection)
|
||||
.doc(query.json.id)
|
||||
.get()
|
||||
).data()
|
||||
} catch (err) {
|
||||
console.error("Error writing to firebase", err)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
async delete(query: {
|
||||
json: { id: string }
|
||||
extra: { [key: string]: string }
|
||||
}) {
|
||||
try {
|
||||
await this.db
|
||||
.collection(query.extra.collection)
|
||||
.doc(query.json.id)
|
||||
.delete()
|
||||
return true
|
||||
} catch (err) {
|
||||
console.error("Error writing to mongodb", err)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
schema: SCHEMA,
|
||||
integration: FirebaseIntegration,
|
||||
}
|
||||
}
|
|
@ -10,6 +10,7 @@ const mysql = require("./mysql")
|
|||
const arangodb = require("./arangodb")
|
||||
const rest = require("./rest")
|
||||
const googlesheets = require("./googlesheets")
|
||||
const firebase = require("./firebase")
|
||||
const { SourceNames } = require("../definitions/datasource")
|
||||
const environment = require("../environment")
|
||||
|
||||
|
@ -25,6 +26,7 @@ const DEFINITIONS = {
|
|||
[SourceNames.MYSQL]: mysql.schema,
|
||||
[SourceNames.ARANGODB]: arangodb.schema,
|
||||
[SourceNames.REST]: rest.schema,
|
||||
[SourceNames.FIREBASE]: firebase.schema,
|
||||
}
|
||||
|
||||
const INTEGRATIONS = {
|
||||
|
@ -39,6 +41,7 @@ const INTEGRATIONS = {
|
|||
[SourceNames.MYSQL]: mysql.integration,
|
||||
[SourceNames.ARANGODB]: arangodb.integration,
|
||||
[SourceNames.REST]: rest.integration,
|
||||
[SourceNames.FIREBASE]: firebase.integration,
|
||||
}
|
||||
|
||||
// optionally add oracle integration if the oracle binary can be installed
|
||||
|
|
|
@ -11,6 +11,7 @@ import {
|
|||
PaginationValues,
|
||||
} from "../definitions/datasource"
|
||||
import { IntegrationBase } from "./base/IntegrationBase"
|
||||
import { get } from "lodash"
|
||||
|
||||
const BodyTypes = {
|
||||
NONE: "none",
|
||||
|
@ -163,7 +164,7 @@ module RestModule {
|
|||
// Check if a pagination cursor exists in the response
|
||||
let nextCursor = null
|
||||
if (pagination?.responseParam) {
|
||||
nextCursor = data?.[pagination.responseParam]
|
||||
nextCursor = get(data, pagination.responseParam)
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
|
@ -187,4 +187,55 @@ describe("SQL query builder", () => {
|
|||
sql: `select * from (select * from \`${TABLE_NAME}\` limit ?) as \`${TABLE_NAME}\``
|
||||
})
|
||||
})
|
||||
|
||||
it("should use greater than when only low range specified", () => {
|
||||
const date = new Date()
|
||||
const query = sql._query(generateReadJson({
|
||||
filters: {
|
||||
range: {
|
||||
property: {
|
||||
low: date,
|
||||
}
|
||||
}
|
||||
}
|
||||
}))
|
||||
expect(query).toEqual({
|
||||
bindings: [date, limit],
|
||||
sql: `select * from (select * from "${TABLE_NAME}" where "${TABLE_NAME}"."property" > $1 limit $2) as "${TABLE_NAME}"`
|
||||
})
|
||||
})
|
||||
|
||||
it("should use less than when only high range specified", () => {
|
||||
const date = new Date()
|
||||
const query = sql._query(generateReadJson({
|
||||
filters: {
|
||||
range: {
|
||||
property: {
|
||||
high: date,
|
||||
}
|
||||
}
|
||||
}
|
||||
}))
|
||||
expect(query).toEqual({
|
||||
bindings: [date, limit],
|
||||
sql: `select * from (select * from "${TABLE_NAME}" where "${TABLE_NAME}"."property" < $1 limit $2) as "${TABLE_NAME}"`
|
||||
})
|
||||
})
|
||||
|
||||
it("should use greater than when only low range specified", () => {
|
||||
const date = new Date()
|
||||
const query = sql._query(generateReadJson({
|
||||
filters: {
|
||||
range: {
|
||||
property: {
|
||||
low: date,
|
||||
}
|
||||
}
|
||||
}
|
||||
}))
|
||||
expect(query).toEqual({
|
||||
bindings: [date, limit],
|
||||
sql: `select * from (select * from "${TABLE_NAME}" where "${TABLE_NAME}"."property" > $1 limit $2) as "${TABLE_NAME}"`
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue