Merge branch 'develop' of github.com:Budibase/budibase into feature/query-variables
This commit is contained in:
commit
85858ff6b1
|
@ -12,6 +12,12 @@ jobs:
|
|||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 14.x
|
||||
- run: yarn
|
||||
- run: yarn bootstrap
|
||||
|
||||
- name: 'Get Previous tag'
|
||||
id: previoustag
|
||||
uses: "WyriHaximus/github-action-get-previous-tag@v1"
|
||||
|
|
|
@ -11,8 +11,8 @@ sources:
|
|||
- https://github.com/Budibase/budibase
|
||||
- https://budibase.com
|
||||
type: application
|
||||
version: 0.2.6
|
||||
appVersion: 1.0.10
|
||||
version: 1.0.0
|
||||
appVersion: 1.0.20
|
||||
dependencies:
|
||||
- name: couchdb
|
||||
version: 3.3.4
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"version": "1.0.19-alpha.2",
|
||||
"version": "1.0.22",
|
||||
"npmClient": "yarn",
|
||||
"packages": [
|
||||
"packages/*"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@budibase/auth",
|
||||
"version": "1.0.19-alpha.2",
|
||||
"version": "1.0.22",
|
||||
"description": "Authentication middlewares for budibase builder and apps",
|
||||
"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.19-alpha.2",
|
||||
"version": "1.0.22",
|
||||
"license": "MPL-2.0",
|
||||
"svelte": "src/index.js",
|
||||
"module": "dist/bbui.es.js",
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
export let disabled = false
|
||||
export let id = null
|
||||
export let updateOnChange = true
|
||||
|
||||
export let quiet = false
|
||||
const dispatch = createEventDispatcher()
|
||||
let focus = false
|
||||
|
||||
|
@ -41,6 +41,7 @@
|
|||
<div class="spectrum-Search" class:is-disabled={disabled}>
|
||||
<div
|
||||
class="spectrum-Textfield"
|
||||
class:spectrum-Textfield--quiet={quiet}
|
||||
class:is-focused={focus}
|
||||
class:is-disabled={disabled}
|
||||
>
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
export let placeholder = null
|
||||
export let disabled = false
|
||||
export let updateOnChange = true
|
||||
export let quiet = false
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const onChange = e => {
|
||||
|
@ -23,6 +24,7 @@
|
|||
{disabled}
|
||||
{value}
|
||||
{placeholder}
|
||||
{quiet}
|
||||
on:change={onChange}
|
||||
on:click
|
||||
on:input
|
||||
|
|
|
@ -10,7 +10,7 @@ it("should rename an unpublished application", () => {
|
|||
cy.get(".home-logo").click()
|
||||
renameApp(appRename)
|
||||
cy.searchForApplication(appRename)
|
||||
cy.get(".appGrid").find(".wrapper").should("have.length", 1)
|
||||
cy.get(".appTable").find(".title").should("have.length", 1)
|
||||
cy.deleteApp(appRename)
|
||||
})
|
||||
|
||||
|
@ -29,7 +29,7 @@ xit("Should rename a published application", () => {
|
|||
cy.get(".home-logo").click()
|
||||
renameApp(appRename, true)
|
||||
cy.searchForApplication(appRename)
|
||||
cy.get(".appGrid").find(".wrapper").should("have.length", 1)
|
||||
cy.get(".appTable").find(".title").should("have.length", 1)
|
||||
})
|
||||
|
||||
it("Should try to rename an application to have no name", () => {
|
||||
|
@ -38,7 +38,7 @@ it("Should try to rename an application to have no name", () => {
|
|||
// Close modal and confirm name has not been changed
|
||||
cy.get(".spectrum-Dialog-grid").contains("Cancel").click()
|
||||
cy.searchForApplication("Cypress Tests")
|
||||
cy.get(".appGrid").find(".wrapper").should("have.length", 1)
|
||||
cy.get(".appTable").find(".title").should("have.length", 1)
|
||||
})
|
||||
|
||||
xit("Should create two applications with the same name", () => {
|
||||
|
@ -64,7 +64,7 @@ it("should validate application names", () => {
|
|||
cy.get(".home-logo").click()
|
||||
renameApp(numberName)
|
||||
cy.searchForApplication(numberName)
|
||||
cy.get(".appGrid").find(".wrapper").should("have.length", 1)
|
||||
cy.get(".appTable").find(".title").should("have.length", 1)
|
||||
renameApp(specialCharName)
|
||||
cy.get(".error").should("have.text", "App name must be letters, numbers and spaces only")
|
||||
})
|
||||
|
@ -74,14 +74,14 @@ it("should validate application names", () => {
|
|||
.its("body")
|
||||
.then(val => {
|
||||
if (val.length > 0) {
|
||||
cy.get(".title > :nth-child(3) > .spectrum-Icon").click()
|
||||
cy.get(".appTable > :nth-child(5) > :nth-child(2) > .spectrum-Icon").click()
|
||||
// Check for when an app is published
|
||||
if (published == true){
|
||||
// Should not have Edit as option, will unpublish app
|
||||
cy.should("not.have.value", "Edit")
|
||||
cy.get(".spectrum-Menu").contains("Unpublish").click()
|
||||
cy.get(".spectrum-Dialog-grid").contains("Unpublish app").click()
|
||||
cy.get(".title > :nth-child(3) > .spectrum-Icon").click()
|
||||
cy.get(".appTable > :nth-child(5) > :nth-child(2) > .spectrum-Icon").click()
|
||||
}
|
||||
cy.contains("Edit").click()
|
||||
cy.get(".spectrum-Modal")
|
||||
|
|
|
@ -50,7 +50,9 @@ Cypress.Commands.add("deleteApp", appName => {
|
|||
.its("body")
|
||||
.then(val => {
|
||||
if (val.length > 0) {
|
||||
cy.get(".title > :nth-child(3) > .spectrum-Icon").click()
|
||||
cy.get(
|
||||
".appTable > :nth-child(5) > :nth-child(2) > .spectrum-Icon"
|
||||
).click()
|
||||
cy.contains("Delete").click()
|
||||
cy.get(".spectrum-Modal").within(() => {
|
||||
cy.get("input").type(appName)
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@budibase/builder",
|
||||
"version": "1.0.19-alpha.2",
|
||||
"version": "1.0.22",
|
||||
"license": "GPL-3.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
@ -65,10 +65,10 @@
|
|||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@budibase/bbui": "^1.0.19-alpha.2",
|
||||
"@budibase/client": "^1.0.19-alpha.2",
|
||||
"@budibase/bbui": "^1.0.22",
|
||||
"@budibase/client": "^1.0.22",
|
||||
"@budibase/colorpicker": "1.1.2",
|
||||
"@budibase/string-templates": "^1.0.19-alpha.2",
|
||||
"@budibase/string-templates": "^1.0.22",
|
||||
"@sentry/browser": "5.19.1",
|
||||
"@spectrum-css/page": "^3.0.1",
|
||||
"@spectrum-css/vars": "^3.0.1",
|
||||
|
|
|
@ -8,6 +8,9 @@
|
|||
name: "javascript",
|
||||
json: true,
|
||||
},
|
||||
XML: {
|
||||
name: "xml",
|
||||
},
|
||||
SQL: {
|
||||
name: "sql",
|
||||
},
|
||||
|
@ -40,11 +43,12 @@
|
|||
let editor
|
||||
|
||||
// Keep editor up to date with value
|
||||
$: editor?.setOption("mode", mode)
|
||||
$: editor?.setValue(value || "")
|
||||
|
||||
// Creates an instance of a code mirror editor
|
||||
async function createEditor(mode, value) {
|
||||
if (!CodeMirror || !textarea || editor) {
|
||||
if (!CodeMirror || !textarea) {
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ import "codemirror/lib/codemirror.css"
|
|||
// Modes
|
||||
import "codemirror/mode/javascript/javascript"
|
||||
import "codemirror/mode/sql/sql"
|
||||
import "codemirror/mode/xml/xml"
|
||||
import "codemirror/mode/css/css"
|
||||
import "codemirror/mode/handlebars/handlebars"
|
||||
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
<script>
|
||||
import { gradient } from "actions"
|
||||
import {
|
||||
Heading,
|
||||
Button,
|
||||
|
@ -18,16 +17,21 @@
|
|||
export let deleteApp
|
||||
export let unpublishApp
|
||||
export let releaseLock
|
||||
export let editIcon
|
||||
</script>
|
||||
|
||||
<div class="title">
|
||||
<div class="preview" use:gradient={{ seed: app.name }} />
|
||||
<div style="display: flex;">
|
||||
<div style="color: {app.icon?.color || ''}">
|
||||
<Icon size="XL" name={app.icon?.name || "Apps"} />
|
||||
</div>
|
||||
<div class="name" on:click={() => editApp(app)}>
|
||||
<Heading size="XS">
|
||||
{app.name}
|
||||
</Heading>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="desktop">
|
||||
{#if app.updatedAt}
|
||||
{processStringSync("Updated {{ duration time 'millisecond' }} ago", {
|
||||
|
@ -62,6 +66,7 @@
|
|||
disabled={app.lockedOther}
|
||||
on:click={() => editApp(app)}
|
||||
size="S"
|
||||
quiet
|
||||
secondary>Open</Button
|
||||
>
|
||||
<ActionMenu align="right">
|
||||
|
@ -86,15 +91,11 @@
|
|||
<MenuItem on:click={() => updateApp(app)} icon="Edit">Edit</MenuItem>
|
||||
<MenuItem on:click={() => deleteApp(app)} icon="Delete">Delete</MenuItem>
|
||||
{/if}
|
||||
<MenuItem on:click={() => editIcon(app)} icon="Brush">Edit Icon</MenuItem>
|
||||
</ActionMenu>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.preview {
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
border-radius: var(--border-radius-s);
|
||||
}
|
||||
.name {
|
||||
text-decoration: none;
|
||||
overflow: hidden;
|
||||
|
@ -103,6 +104,7 @@
|
|||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
margin-left: calc(1.5 * var(--spacing-xl));
|
||||
}
|
||||
.title :global(h1:hover) {
|
||||
color: var(--spectrum-global-color-blue-600);
|
||||
|
|
|
@ -0,0 +1,127 @@
|
|||
<script>
|
||||
import { ModalContent, Modal, Icon, ColorPicker, Label } from "@budibase/bbui"
|
||||
import { apps } from "stores/portal"
|
||||
|
||||
export let app
|
||||
let modal
|
||||
$: selectedIcon = app?.icon?.name
|
||||
$: selectedColor = app?.icon?.color
|
||||
|
||||
let iconsList = [
|
||||
"Actions",
|
||||
"ConversionFunnel",
|
||||
"App",
|
||||
"Briefcase",
|
||||
"Money",
|
||||
"ShoppingCart",
|
||||
"Form",
|
||||
"Help",
|
||||
"Monitoring",
|
||||
"Sandbox",
|
||||
"Project",
|
||||
"Organisations",
|
||||
"Magnify",
|
||||
"Launch",
|
||||
"Car",
|
||||
"Camera",
|
||||
"Bug",
|
||||
"Channel",
|
||||
"Calculator",
|
||||
"Calendar",
|
||||
"GraphDonut",
|
||||
"GraphBarHorizontal",
|
||||
"Demographic",
|
||||
"Apps",
|
||||
]
|
||||
export const show = () => {
|
||||
modal.show()
|
||||
}
|
||||
export const hide = () => {
|
||||
modal.hide()
|
||||
}
|
||||
|
||||
const onCancel = () => {
|
||||
selectedIcon = ""
|
||||
selectedColor = ""
|
||||
hide()
|
||||
}
|
||||
|
||||
const changeColor = val => {
|
||||
selectedColor = val
|
||||
}
|
||||
|
||||
const save = async () => {
|
||||
await apps.update(app.instance._id, {
|
||||
icon: {
|
||||
name: selectedIcon,
|
||||
color: selectedColor,
|
||||
},
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal bind:this={modal} on:hide={onCancel}>
|
||||
<ModalContent
|
||||
title={"Edit Icon"}
|
||||
confirmText={"Save"}
|
||||
onConfirm={() => save()}
|
||||
>
|
||||
<div class="scrollable-icons">
|
||||
<div class="title-spacing">
|
||||
<Label>Select an Icon</Label>
|
||||
</div>
|
||||
<div class="grid">
|
||||
{#each iconsList as item}
|
||||
<div
|
||||
class="icon-item"
|
||||
style="color: {item === selectedIcon ? selectedColor : ''}"
|
||||
on:click={() => (selectedIcon = item)}
|
||||
>
|
||||
<Icon name={item} />
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
<div class="color-selection">
|
||||
<div>
|
||||
<Label>Select a Color</Label>
|
||||
</div>
|
||||
<div class="color-selection-item">
|
||||
<ColorPicker
|
||||
bind:value={selectedColor}
|
||||
on:change={e => changeColor(e.detail)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
|
||||
<style>
|
||||
.scrollable-icons {
|
||||
overflow-y: auto;
|
||||
height: 230px;
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-gap: 20px;
|
||||
grid-template-columns: 1fr 1fr 1fr 1fr 1fr;
|
||||
}
|
||||
|
||||
.color-selection {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.color-selection-item {
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.title-spacing {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.icon-item {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
|
@ -1,13 +1,7 @@
|
|||
<script>
|
||||
import { writable, get as svelteGet } from "svelte/store"
|
||||
import {
|
||||
notifications,
|
||||
Input,
|
||||
ModalContent,
|
||||
Dropzone,
|
||||
Body,
|
||||
Checkbox,
|
||||
} from "@budibase/bbui"
|
||||
|
||||
import { notifications, Input, ModalContent, Dropzone } from "@budibase/bbui"
|
||||
import { store, automationStore, hostingStore } from "builderStore"
|
||||
import { admin, auth } from "stores/portal"
|
||||
import { string, mixed, object } from "yup"
|
||||
|
@ -147,16 +141,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
function getModalTitle() {
|
||||
let title = "Create App"
|
||||
if (template.fromFile) {
|
||||
title = "Import App"
|
||||
} else if (template.key) {
|
||||
title = "Create app from template"
|
||||
}
|
||||
return title
|
||||
}
|
||||
|
||||
async function onCancel() {
|
||||
template = null
|
||||
await auth.setInitInfo({})
|
||||
|
@ -187,7 +171,7 @@
|
|||
</ModalContent>
|
||||
{:else}
|
||||
<ModalContent
|
||||
title={getModalTitle()}
|
||||
title={"Name your app"}
|
||||
confirmText={template?.fromFile ? "Import app" : "Create app"}
|
||||
onConfirm={createNewApp}
|
||||
onCancel={inline ? onCancel : null}
|
||||
|
@ -207,16 +191,14 @@
|
|||
}}
|
||||
/>
|
||||
{/if}
|
||||
<Body size="S">
|
||||
Give your new app a name, and choose which groups have access (paid plans
|
||||
only).
|
||||
</Body>
|
||||
<Input
|
||||
bind:value={$values.name}
|
||||
error={$touched.name && $errors.name}
|
||||
on:blur={() => ($touched.name = true)}
|
||||
label="Name"
|
||||
placeholder={$auth.user.firstName
|
||||
? `${$auth.user.firstName}'s app`
|
||||
: "My app"}
|
||||
/>
|
||||
<Checkbox label="Group access" disabled value={true} text="All users" />
|
||||
</ModalContent>
|
||||
{/if}
|
||||
|
|
|
@ -1,46 +1,10 @@
|
|||
<script>
|
||||
import { Heading, Layout, Icon, Body } from "@budibase/bbui"
|
||||
import Spinner from "components/common/Spinner.svelte"
|
||||
import api from "builderStore/api"
|
||||
import { Heading, Layout, Icon } from "@budibase/bbui"
|
||||
|
||||
export let onSelect
|
||||
|
||||
async function fetchTemplates() {
|
||||
const response = await api.get("/api/templates?type=app")
|
||||
return await response.json()
|
||||
}
|
||||
|
||||
let templatesPromise = fetchTemplates()
|
||||
</script>
|
||||
|
||||
<Layout gap="XS" noPadding>
|
||||
{#await templatesPromise}
|
||||
<div class="spinner-container">
|
||||
<Spinner size="30" />
|
||||
</div>
|
||||
{:then templates}
|
||||
{#if templates?.length > 0}
|
||||
<Body size="M">Select a template below, or start from scratch.</Body>
|
||||
{:else}
|
||||
<Body size="M">Start your app from scratch below.</Body>
|
||||
{/if}
|
||||
<div class="templates">
|
||||
{#each templates as template}
|
||||
<div class="template" on:click={() => onSelect(template)}>
|
||||
<div
|
||||
class="background-icon"
|
||||
style={`background: ${template.background};`}
|
||||
>
|
||||
<Icon name={template.icon} />
|
||||
</div>
|
||||
<Heading size="XS">{template.name}</Heading>
|
||||
<p class="detail">{template?.category?.toUpperCase()}</p>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{:catch err}
|
||||
<h1 style="color:red">{err}</h1>
|
||||
{/await}
|
||||
<div class="template start-from-scratch" on:click={() => onSelect(null)}>
|
||||
<div
|
||||
class="background-icon"
|
||||
|
@ -67,15 +31,6 @@
|
|||
</Layout>
|
||||
|
||||
<style>
|
||||
.templates {
|
||||
display: grid;
|
||||
width: 100%;
|
||||
grid-gap: var(--spacing-m);
|
||||
grid-template-columns: 1fr;
|
||||
justify-content: start;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.background-icon {
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
|
|
|
@ -77,7 +77,7 @@
|
|||
async function updateApp() {
|
||||
try {
|
||||
// Update App
|
||||
await apps.update(app.instance._id, $values.name.trim())
|
||||
await apps.update(app.instance._id, { name: $values.name.trim() })
|
||||
hide()
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
|
|
|
@ -200,6 +200,7 @@ export const RawRestBodyTypes = {
|
|||
ENCODED: "encoded",
|
||||
JSON: "json",
|
||||
TEXT: "text",
|
||||
XML: "xml",
|
||||
}
|
||||
|
||||
export const RestBodyTypes = [
|
||||
|
@ -207,5 +208,6 @@ export const RestBodyTypes = [
|
|||
{ name: "form-data", value: "form" },
|
||||
{ name: "x-www-form-encoded", value: "encoded" },
|
||||
{ name: "raw (JSON)", value: "json" },
|
||||
{ name: "raw (XML)", value: "xml" },
|
||||
{ name: "raw (Text)", value: "text" },
|
||||
]
|
||||
|
|
|
@ -7,7 +7,11 @@
|
|||
} from "components/common/CodeMirrorEditor.svelte"
|
||||
|
||||
const objectTypes = [RawRestBodyTypes.FORM, RawRestBodyTypes.ENCODED]
|
||||
const textTypes = [RawRestBodyTypes.JSON, RawRestBodyTypes.TEXT]
|
||||
const textTypes = [
|
||||
RawRestBodyTypes.JSON,
|
||||
RawRestBodyTypes.XML,
|
||||
RawRestBodyTypes.TEXT,
|
||||
]
|
||||
|
||||
export let query
|
||||
export let bodyType
|
||||
|
@ -25,6 +29,18 @@
|
|||
query.fields.requestBody = ""
|
||||
}
|
||||
}
|
||||
|
||||
function editorMode(type) {
|
||||
switch (type) {
|
||||
case RawRestBodyTypes.JSON:
|
||||
return EditorModes.JSON
|
||||
case RawRestBodyTypes.XML:
|
||||
return EditorModes.XML
|
||||
default:
|
||||
case RawRestBodyTypes.TEXT:
|
||||
return EditorModes.Text
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="margin">
|
||||
|
@ -41,9 +57,7 @@
|
|||
{:else if textTypes.includes(bodyType)}
|
||||
<CodeMirrorEditor
|
||||
height={200}
|
||||
mode={bodyType === RawRestBodyTypes.JSON
|
||||
? EditorModes.JSON
|
||||
: EditorModes.Text}
|
||||
mode={editorMode(bodyType)}
|
||||
value={query.fields.requestBody}
|
||||
resize="vertical"
|
||||
on:change={e => (query.fields.requestBody = e.detail)}
|
||||
|
|
|
@ -48,7 +48,7 @@
|
|||
let breakQs = {},
|
||||
bindings = {}
|
||||
let url = ""
|
||||
let saveId
|
||||
let saveId, isGet
|
||||
let response, schema, enabledHeaders
|
||||
let datasourceType, integrationInfo, queryConfig, responseSuccess
|
||||
let authConfigId
|
||||
|
@ -60,6 +60,7 @@
|
|||
$: url = buildUrl(url, breakQs)
|
||||
$: checkQueryName(url)
|
||||
$: responseSuccess = response?.info?.code >= 200 && response?.info?.code < 400
|
||||
$: isGet = query?.queryVerb === "read"
|
||||
$: authConfigs = buildAuthConfigs(datasource)
|
||||
$: schemaReadOnly = !responseSuccess
|
||||
$: variablesReadOnly = !responseSuccess
|
||||
|
@ -327,7 +328,7 @@
|
|||
<Tab title="Body">
|
||||
<RadioGroup
|
||||
bind:value={query.fields.bodyType}
|
||||
options={bodyTypes}
|
||||
options={isGet ? [bodyTypes[0]] : bodyTypes}
|
||||
direction="horizontal"
|
||||
getOptionLabel={option => option.name}
|
||||
getOptionValue={option => option.value}
|
||||
|
|
|
@ -2,33 +2,33 @@
|
|||
import {
|
||||
Heading,
|
||||
Layout,
|
||||
Detail,
|
||||
Button,
|
||||
ActionButton,
|
||||
ActionGroup,
|
||||
ButtonGroup,
|
||||
Input,
|
||||
Select,
|
||||
Modal,
|
||||
Page,
|
||||
notifications,
|
||||
Body,
|
||||
Search,
|
||||
} from "@budibase/bbui"
|
||||
import Spinner from "components/common/Spinner.svelte"
|
||||
import CreateAppModal from "components/start/CreateAppModal.svelte"
|
||||
import UpdateAppModal from "components/start/UpdateAppModal.svelte"
|
||||
import ChooseIconModal from "components/start/ChooseIconModal.svelte"
|
||||
|
||||
import { store, automationStore } from "builderStore"
|
||||
import api, { del, post, get } from "builderStore/api"
|
||||
import { onMount } from "svelte"
|
||||
import { apps, auth, admin } from "stores/portal"
|
||||
import { apps, auth, admin, templates } from "stores/portal"
|
||||
import download from "downloadjs"
|
||||
import { goto } from "@roxi/routify"
|
||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||
import AppCard from "components/start/AppCard.svelte"
|
||||
import AppRow from "components/start/AppRow.svelte"
|
||||
import { AppStatus } from "constants"
|
||||
import analytics, { Events } from "analytics"
|
||||
|
||||
let layout = "grid"
|
||||
let sortBy = "name"
|
||||
let template
|
||||
let selectedApp
|
||||
|
@ -36,13 +36,13 @@
|
|||
let updatingModal
|
||||
let deletionModal
|
||||
let unpublishModal
|
||||
let iconModal
|
||||
let creatingApp = false
|
||||
let loaded = false
|
||||
let searchTerm = ""
|
||||
let cloud = $admin.cloud
|
||||
let appName = ""
|
||||
let creatingFromTemplate = false
|
||||
|
||||
$: enrichedApps = enrichApps($apps, $auth.user, sortBy)
|
||||
$: filteredApps = enrichedApps.filter(app =>
|
||||
app?.name?.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
|
@ -172,6 +172,11 @@
|
|||
$goto(`../../app/${app.devId}`)
|
||||
}
|
||||
|
||||
const editIcon = app => {
|
||||
selectedApp = app
|
||||
iconModal.show()
|
||||
}
|
||||
|
||||
const exportApp = app => {
|
||||
const id = app.deployed ? app.prodId : app.devId
|
||||
const appName = encodeURIComponent(app.name)
|
||||
|
@ -262,6 +267,7 @@
|
|||
|
||||
onMount(async () => {
|
||||
await apps.load()
|
||||
await templates.load()
|
||||
// if the portal is loaded from an external URL with a template param
|
||||
const initInfo = await auth.getInitInfo()
|
||||
if (initInfo?.init_template) {
|
||||
|
@ -274,21 +280,66 @@
|
|||
</script>
|
||||
|
||||
<Page wide>
|
||||
{#if loaded && enrichedApps.length}
|
||||
<Layout noPadding>
|
||||
<div class="title">
|
||||
<Heading>Apps</Heading>
|
||||
<Heading size="S">Welcome to Budibase</Heading>
|
||||
|
||||
<ButtonGroup>
|
||||
{#if cloud}
|
||||
<Button secondary on:click={initiateAppsExport}>Export apps</Button>
|
||||
{/if}
|
||||
<Button secondary on:click={initiateAppImport}>Import app</Button>
|
||||
<Button cta on:click={initiateAppCreation}>Create app</Button>
|
||||
<Button icon="Import" quiet secondary on:click={initiateAppImport}
|
||||
>Import app</Button
|
||||
>
|
||||
<Button icon="Add" cta on:click={initiateAppCreation}>Create app</Button
|
||||
>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
|
||||
<div class="title-text">
|
||||
<Body size="S">Manage your apps and get a head start with templates</Body>
|
||||
</div>
|
||||
<Detail>Quick Start Templates</Detail>
|
||||
<div class="grid">
|
||||
{#each $templates as item}
|
||||
<div
|
||||
on:click={() => {
|
||||
template = item
|
||||
creationModal.show()
|
||||
creatingApp = true
|
||||
}}
|
||||
class="template-card"
|
||||
>
|
||||
<div class="card-body">
|
||||
<div style="color: {item.background}" class="iconAlign">
|
||||
<svg
|
||||
width="26px"
|
||||
height="26px"
|
||||
class="spectrum-Icon"
|
||||
style="color:{item.background};"
|
||||
focusable="false"
|
||||
>
|
||||
<use xlink:href="#spectrum-icon-18-{item.icon}" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="iconAlign">
|
||||
<Body weight="900" size="S">{item.name}</Body>
|
||||
<div style="font-size: 10px;">
|
||||
<Body size="S">{item.category.toUpperCase()}</Body>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{#if loaded && enrichedApps.length}
|
||||
<div class="title">
|
||||
<Detail>My Apps</Detail>
|
||||
</div>
|
||||
<div class="filter">
|
||||
<div class="select">
|
||||
<Select
|
||||
quiet
|
||||
autoWidth
|
||||
bind:value={sortBy}
|
||||
placeholder={null}
|
||||
|
@ -299,35 +350,18 @@
|
|||
]}
|
||||
/>
|
||||
<div class="desktop-search">
|
||||
<Search placeholder="Search" bind:value={searchTerm} />
|
||||
<Search quiet placeholder="Search" bind:value={searchTerm} />
|
||||
</div>
|
||||
</div>
|
||||
<ActionGroup>
|
||||
<ActionButton
|
||||
on:click={() => (layout = "grid")}
|
||||
selected={layout === "grid"}
|
||||
quiet
|
||||
icon="ClassicGridView"
|
||||
/>
|
||||
<ActionButton
|
||||
on:click={() => (layout = "table")}
|
||||
selected={layout === "table"}
|
||||
quiet
|
||||
icon="ViewRow"
|
||||
/>
|
||||
</ActionGroup>
|
||||
</div>
|
||||
<div class="mobile-search">
|
||||
<Search placeholder="Search" bind:value={searchTerm} />
|
||||
</div>
|
||||
<div
|
||||
class:appGrid={layout === "grid"}
|
||||
class:appTable={layout === "table"}
|
||||
>
|
||||
<div class="appTable">
|
||||
{#each filteredApps as app (app.appId)}
|
||||
<svelte:component
|
||||
this={layout === "grid" ? AppCard : AppRow}
|
||||
<AppRow
|
||||
{releaseLock}
|
||||
{editIcon}
|
||||
{app}
|
||||
{unpublishApp}
|
||||
{viewApp}
|
||||
|
@ -338,7 +372,6 @@
|
|||
/>
|
||||
{/each}
|
||||
</div>
|
||||
</Layout>
|
||||
{/if}
|
||||
{#if !enrichedApps.length && !creatingApp && loaded}
|
||||
<div class="empty-wrapper">
|
||||
|
@ -353,7 +386,9 @@
|
|||
<Spinner size="10" />
|
||||
</div>
|
||||
{/if}
|
||||
</Layout>
|
||||
</Page>
|
||||
|
||||
<Modal
|
||||
bind:this={creationModal}
|
||||
padding={false}
|
||||
|
@ -389,6 +424,7 @@
|
|||
</ConfirmDialog>
|
||||
|
||||
<UpdateAppModal app={selectedApp} bind:this={updatingModal} />
|
||||
<ChooseIconModal app={selectedApp} bind:this={iconModal} />
|
||||
|
||||
<style>
|
||||
.title,
|
||||
|
@ -397,7 +433,7 @@
|
|||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 560px) {
|
||||
|
@ -405,12 +441,48 @@
|
|||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
.iconAlign {
|
||||
padding: 0 0 0 var(--spacing-m);
|
||||
display: inline-block;
|
||||
}
|
||||
.template-card {
|
||||
height: 80px;
|
||||
width: 270px;
|
||||
border-radius: var(--border-radius-s);
|
||||
margin-bottom: var(--spacing-m);
|
||||
border: 1px solid var(--spectrum-global-color-gray-300);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.title-text {
|
||||
margin-top: calc(var(--spacing-xl) * -1);
|
||||
}
|
||||
|
||||
.card-body {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-gap: 5px;
|
||||
grid-template-columns: repeat(auto-fill, minmax(270px, 1fr));
|
||||
}
|
||||
|
||||
@media (min-width: 200px) {
|
||||
}
|
||||
|
||||
.select {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-gap: 10px;
|
||||
grid-template-columns: auto auto;
|
||||
grid-gap: 30px;
|
||||
}
|
||||
.filter :global(.spectrum-ActionGroup) {
|
||||
flex-wrap: nowrap;
|
||||
|
@ -419,11 +491,6 @@
|
|||
display: none;
|
||||
}
|
||||
|
||||
.appGrid {
|
||||
display: grid;
|
||||
grid-gap: 50px;
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
}
|
||||
.appTable {
|
||||
display: grid;
|
||||
grid-template-rows: auto;
|
||||
|
@ -464,4 +531,8 @@
|
|||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.template-card:hover {
|
||||
background: var(--spectrum-alias-background-color-tertiary);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -65,16 +65,17 @@ export function createAppStore() {
|
|||
}
|
||||
}
|
||||
|
||||
async function update(appId, name) {
|
||||
const response = await api.put(`/api/applications/${appId}`, { name })
|
||||
async function update(appId, value) {
|
||||
console.log({ value })
|
||||
const response = await api.put(`/api/applications/${appId}`, { ...value })
|
||||
if (response.status === 200) {
|
||||
store.update(state => {
|
||||
const updatedAppIndex = state.findIndex(
|
||||
app => app.instance._id === appId
|
||||
)
|
||||
if (updatedAppIndex !== -1) {
|
||||
const updatedApp = state[updatedAppIndex]
|
||||
updatedApp.name = name
|
||||
let updatedApp = state[updatedAppIndex]
|
||||
updatedApp = { ...updatedApp, ...value }
|
||||
state.apps = state.splice(updatedAppIndex, 1, updatedApp)
|
||||
}
|
||||
return state
|
||||
|
|
|
@ -5,3 +5,4 @@ export { apps } from "./apps"
|
|||
export { email } from "./email"
|
||||
export { auth } from "./auth"
|
||||
export { oidc } from "./oidc"
|
||||
export { templates } from "./templates"
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
import { writable } from "svelte/store"
|
||||
import api from "builderStore/api"
|
||||
|
||||
export function templatesStore() {
|
||||
const { subscribe, set } = writable([])
|
||||
|
||||
async function load() {
|
||||
const response = await api.get("/api/templates?type=app")
|
||||
const json = await response.json()
|
||||
set(json)
|
||||
}
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
load,
|
||||
}
|
||||
}
|
||||
|
||||
export const templates = templatesStore()
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@budibase/cli",
|
||||
"version": "1.0.19-alpha.2",
|
||||
"version": "1.0.22",
|
||||
"description": "Budibase CLI, for developers, self hosting and migrations.",
|
||||
"main": "src/index.js",
|
||||
"bin": {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@budibase/client",
|
||||
"version": "1.0.19-alpha.2",
|
||||
"version": "1.0.22",
|
||||
"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.19-alpha.2",
|
||||
"@budibase/bbui": "^1.0.22",
|
||||
"@budibase/standard-components": "^0.9.139",
|
||||
"@budibase/string-templates": "^1.0.19-alpha.2",
|
||||
"@budibase/string-templates": "^1.0.22",
|
||||
"regexparam": "^1.3.0",
|
||||
"shortid": "^2.2.15",
|
||||
"svelte-spa-router": "^3.0.5"
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
<script>
|
||||
import { setContext, getContext } from "svelte"
|
||||
import { setContext, getContext, onMount } from "svelte"
|
||||
import Router, { querystring } from "svelte-spa-router"
|
||||
import { routeStore } from "stores"
|
||||
import { routeStore, stateStore } from "stores"
|
||||
import Screen from "./Screen.svelte"
|
||||
import { get } from "svelte/store"
|
||||
|
||||
const { styleable } = getContext("sdk")
|
||||
const component = getContext("component")
|
||||
|
@ -17,15 +18,17 @@
|
|||
}
|
||||
|
||||
// Keep query params up to date
|
||||
$: {
|
||||
$: routeStore.actions.setQueryParams(parseQueryString($querystring))
|
||||
|
||||
const parseQueryString = query => {
|
||||
let queryParams = {}
|
||||
if ($querystring) {
|
||||
const urlSearchParams = new URLSearchParams($querystring)
|
||||
if (query) {
|
||||
const urlSearchParams = new URLSearchParams(query)
|
||||
for (const [key, value] of urlSearchParams) {
|
||||
queryParams[key] = value
|
||||
}
|
||||
}
|
||||
routeStore.actions.setQueryParams(queryParams)
|
||||
return queryParams
|
||||
}
|
||||
|
||||
const getRouterConfig = routes => {
|
||||
|
@ -42,6 +45,19 @@
|
|||
const onRouteLoading = ({ detail }) => {
|
||||
routeStore.actions.setActiveRoute(detail.route)
|
||||
}
|
||||
|
||||
// Initialise state store from query string on initial load
|
||||
onMount(() => {
|
||||
const queryParams = parseQueryString(get(querystring))
|
||||
if (queryParams.state) {
|
||||
try {
|
||||
const state = JSON.parse(atob(queryParams.state))
|
||||
stateStore.actions.initialise(state)
|
||||
} catch (error) {
|
||||
// Swallow error and do nothing
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
{#key config.id}
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
dataSourceStore,
|
||||
notificationStore,
|
||||
routeStore,
|
||||
stateStore,
|
||||
} from "stores"
|
||||
import { Modal, ModalContent, ActionButton } from "@budibase/bbui"
|
||||
import { onDestroy } from "svelte"
|
||||
|
@ -12,12 +13,13 @@
|
|||
NOTIFICATION: "notification",
|
||||
CLOSE_SCREEN_MODAL: "close-screen-modal",
|
||||
INVALIDATE_DATASOURCE: "invalidate-datasource",
|
||||
UPDATE_STATE: "update-state",
|
||||
}
|
||||
|
||||
let iframe
|
||||
let listenersAttached = false
|
||||
|
||||
const invalidateDataSource = event => {
|
||||
const proxyInvalidation = event => {
|
||||
const { dataSourceId } = event.detail
|
||||
dataSourceStore.actions.invalidateDataSource(dataSourceId)
|
||||
}
|
||||
|
@ -27,14 +29,28 @@
|
|||
notificationStore.actions.send(message, type, icon)
|
||||
}
|
||||
|
||||
const proxyStateUpdate = event => {
|
||||
const { type, key, value, persist } = event.detail
|
||||
if (type === "set") {
|
||||
stateStore.actions.setValue(key, value, persist)
|
||||
} else if (type === "delete") {
|
||||
stateStore.actions.deleteValue(key)
|
||||
}
|
||||
}
|
||||
|
||||
function receiveMessage(message) {
|
||||
const handlers = {
|
||||
[MessageTypes.NOTIFICATION]: () => {
|
||||
proxyNotification(message.data)
|
||||
},
|
||||
[MessageTypes.CLOSE_SCREEN_MODAL]: peekStore.actions.hidePeek,
|
||||
[MessageTypes.CLOSE_SCREEN_MODAL]: () => {
|
||||
peekStore.actions.hidePeek()
|
||||
},
|
||||
[MessageTypes.INVALIDATE_DATASOURCE]: () => {
|
||||
invalidateDataSource(message.data)
|
||||
proxyInvalidation(message.data)
|
||||
},
|
||||
[MessageTypes.UPDATE_STATE]: () => {
|
||||
proxyStateUpdate(message.data)
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { writable, get } from "svelte/store"
|
||||
import { fetchTableDefinition } from "../api"
|
||||
import { FieldTypes } from "../constants"
|
||||
import { routeStore } from "./routes"
|
||||
|
||||
export const createDataSourceStore = () => {
|
||||
const store = writable([])
|
||||
|
@ -60,10 +61,12 @@ export const createDataSourceStore = () => {
|
|||
|
||||
// Emit this as a window event, so parent screens which are iframing us in
|
||||
// can also invalidate the same datasource
|
||||
if (get(routeStore).queryParams?.peek) {
|
||||
window.parent.postMessage({
|
||||
type: "close-screen-modal",
|
||||
type: "invalidate-datasource",
|
||||
detail: { dataSourceId },
|
||||
})
|
||||
}
|
||||
|
||||
let invalidations = [dataSourceId]
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { writable } from "svelte/store"
|
||||
import { writable, get } from "svelte/store"
|
||||
import { stateStore } from "./state.js"
|
||||
|
||||
const initialState = {
|
||||
showPeek: false,
|
||||
|
@ -14,7 +15,10 @@ const createPeekStore = () => {
|
|||
let href = url
|
||||
let external = !url.startsWith("/")
|
||||
if (!external) {
|
||||
href = `${window.location.href.split("#")[0]}#${url}?peek=true`
|
||||
const state = get(stateStore)
|
||||
const serialised = encodeURIComponent(btoa(JSON.stringify(state)))
|
||||
const query = `peek=true&state=${serialised}`
|
||||
href = `${window.location.href.split("#")[0]}#${url}?${query}`
|
||||
}
|
||||
store.set({
|
||||
showPeek: true,
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { writable, get, derived } from "svelte/store"
|
||||
import { localStorageStore } from "builder/src/builderStore/store/localStorage"
|
||||
import { appStore } from "./app"
|
||||
|
||||
const createStateStore = () => {
|
||||
const localStorageKey = `${get(appStore).appId}.state`
|
||||
const appId = window["##BUDIBASE_APP_ID##"] || "app"
|
||||
const localStorageKey = `${appId}.state`
|
||||
const persistentStore = localStorageStore(localStorageKey, {})
|
||||
|
||||
// Initialise the temp store to mirror the persistent store
|
||||
|
@ -34,6 +34,9 @@ const createStateStore = () => {
|
|||
})
|
||||
}
|
||||
|
||||
// Initialises the temporary state store with a certain value
|
||||
const initialise = tempStore.set
|
||||
|
||||
// Derive the combination of both persisted and non persisted stores
|
||||
const store = derived(
|
||||
[tempStore, persistentStore],
|
||||
|
@ -47,7 +50,7 @@ const createStateStore = () => {
|
|||
|
||||
return {
|
||||
subscribe: store.subscribe,
|
||||
actions: { setValue, deleteValue },
|
||||
actions: { setValue, deleteValue, initialise },
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -116,6 +116,15 @@ const updateStateHandler = action => {
|
|||
} else if (type === "delete") {
|
||||
stateStore.actions.deleteValue(key)
|
||||
}
|
||||
|
||||
// Emit this as an event so that parent windows which are iframing us in
|
||||
// can also update their state
|
||||
if (get(routeStore).queryParams?.peek) {
|
||||
window.parent.postMessage({
|
||||
type: "update-state",
|
||||
detail: { type, key, value, persist },
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const handlerMap = {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "@budibase/server",
|
||||
"email": "hi@budibase.com",
|
||||
"version": "1.0.19-alpha.2",
|
||||
"version": "1.0.22",
|
||||
"description": "Budibase Web Server",
|
||||
"main": "src/index.ts",
|
||||
"repository": {
|
||||
|
@ -70,9 +70,9 @@
|
|||
"license": "GPL-3.0",
|
||||
"dependencies": {
|
||||
"@apidevtools/swagger-parser": "^10.0.3",
|
||||
"@budibase/auth": "^1.0.19-alpha.2",
|
||||
"@budibase/client": "^1.0.19-alpha.2",
|
||||
"@budibase/string-templates": "^1.0.19-alpha.2",
|
||||
"@budibase/auth": "^1.0.22",
|
||||
"@budibase/client": "^1.0.22",
|
||||
"@budibase/string-templates": "^1.0.22",
|
||||
"@bull-board/api": "^3.7.0",
|
||||
"@bull-board/koa": "^3.7.0",
|
||||
"@elastic/elasticsearch": "7.10.0",
|
||||
|
@ -90,6 +90,7 @@
|
|||
"dotenv": "8.2.0",
|
||||
"download": "8.0.0",
|
||||
"fix-path": "3.0.0",
|
||||
"form-data": "^4.0.0",
|
||||
"fs-extra": "8.1.0",
|
||||
"jimp": "0.16.1",
|
||||
"joi": "17.2.1",
|
||||
|
@ -126,6 +127,7 @@
|
|||
"validate.js": "0.13.1",
|
||||
"vm2": "^3.9.3",
|
||||
"worker-farm": "^1.7.0",
|
||||
"xml2js": "^0.4.23",
|
||||
"yargs": "13.2.4",
|
||||
"zlib": "1.0.5"
|
||||
},
|
||||
|
|
|
@ -5,11 +5,15 @@ const { getAutomationParams, generateAutomationID } = require("../../db/utils")
|
|||
const {
|
||||
checkForWebhooks,
|
||||
updateTestHistory,
|
||||
removeDeprecated,
|
||||
} = require("../../automations/utils")
|
||||
const { deleteEntityMetadata } = require("../../utilities")
|
||||
const { MetadataTypes } = require("../../constants")
|
||||
const { setTestFlag, clearTestFlag } = require("../../utilities/redis")
|
||||
|
||||
const ACTION_DEFS = removeDeprecated(actions.ACTION_DEFINITIONS)
|
||||
const TRIGGER_DEFS = removeDeprecated(triggers.TRIGGER_DEFINITIONS)
|
||||
|
||||
/*************************
|
||||
* *
|
||||
* BUILDER FUNCTIONS *
|
||||
|
@ -155,17 +159,17 @@ exports.destroy = async function (ctx) {
|
|||
}
|
||||
|
||||
exports.getActionList = async function (ctx) {
|
||||
ctx.body = actions.ACTION_DEFINITIONS
|
||||
ctx.body = ACTION_DEFS
|
||||
}
|
||||
|
||||
exports.getTriggerList = async function (ctx) {
|
||||
ctx.body = triggers.TRIGGER_DEFINITIONS
|
||||
ctx.body = TRIGGER_DEFS
|
||||
}
|
||||
|
||||
module.exports.getDefinitionList = async function (ctx) {
|
||||
ctx.body = {
|
||||
trigger: triggers.TRIGGER_DEFINITIONS,
|
||||
action: actions.ACTION_DEFINITIONS,
|
||||
trigger: TRIGGER_DEFS,
|
||||
action: ACTION_DEFS,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -13,12 +13,11 @@ const RequestType = {
|
|||
const BODY_REQUESTS = [RequestType.POST, RequestType.PUT, RequestType.PATCH]
|
||||
|
||||
/**
|
||||
* Note, there is some functionality in this that is not currently exposed as it
|
||||
* is complex and maybe better to be opinionated here.
|
||||
* GET/DELETE requests cannot handle body elements so they will not be sent if configured.
|
||||
* NOTE: this functionality is deprecated - it no longer should be used.
|
||||
*/
|
||||
|
||||
exports.definition = {
|
||||
deprecated: true,
|
||||
name: "Outgoing webhook",
|
||||
tagline: "Send a {{inputs.requestMethod}} request",
|
||||
icon: "Send",
|
||||
|
|
|
@ -7,6 +7,7 @@ const newid = require("../db/newid")
|
|||
const { updateEntityMetadata } = require("../utilities")
|
||||
const { MetadataTypes } = require("../constants")
|
||||
const { getDeployedAppID } = require("@budibase/auth/db")
|
||||
const { cloneDeep } = require("lodash/fp")
|
||||
|
||||
const WH_STEP_ID = definitions.WEBHOOK.stepId
|
||||
const CRON_STEP_ID = definitions.CRON.stepId
|
||||
|
@ -42,6 +43,16 @@ exports.updateTestHistory = async (appId, automation, history) => {
|
|||
)
|
||||
}
|
||||
|
||||
exports.removeDeprecated = definitions => {
|
||||
const base = cloneDeep(definitions)
|
||||
for (let key of Object.keys(base)) {
|
||||
if (base[key].deprecated) {
|
||||
delete base[key]
|
||||
}
|
||||
}
|
||||
return base
|
||||
}
|
||||
|
||||
// end the repetition and the job itself
|
||||
exports.disableAllCrons = async appId => {
|
||||
const promises = []
|
||||
|
|
|
@ -6,13 +6,14 @@ import {
|
|||
RestQueryFields as RestQuery,
|
||||
AuthType,
|
||||
BasicAuthConfig,
|
||||
BearerAuthConfig
|
||||
BearerAuthConfig,
|
||||
} from "../definitions/datasource"
|
||||
import { IntegrationBase } from "./base/IntegrationBase"
|
||||
|
||||
const BodyTypes = {
|
||||
NONE: "none",
|
||||
FORM_DATA: "form",
|
||||
XML: "xml",
|
||||
ENCODED: "encoded",
|
||||
JSON: "json",
|
||||
TEXT: "text",
|
||||
|
@ -45,6 +46,9 @@ module RestModule {
|
|||
const fetch = require("node-fetch")
|
||||
const { formatBytes } = require("../utilities")
|
||||
const { performance } = require("perf_hooks")
|
||||
const FormData = require("form-data")
|
||||
const { URLSearchParams } = require("url")
|
||||
const { parseStringPromise: xmlParser, Builder: XmlBuilder } = require("xml2js")
|
||||
|
||||
const SCHEMA: Integration = {
|
||||
docs: "https://github.com/node-fetch/node-fetch",
|
||||
|
@ -110,15 +114,38 @@ module RestModule {
|
|||
|
||||
async parseResponse(response: any) {
|
||||
let data, raw, headers
|
||||
const contentType = response.headers.get("content-type")
|
||||
if (contentType && contentType.indexOf("application/json") !== -1) {
|
||||
const contentType = response.headers.get("content-type") || ""
|
||||
try {
|
||||
if (contentType.includes("application/json")) {
|
||||
data = await response.json()
|
||||
raw = JSON.stringify(data)
|
||||
} else if (
|
||||
contentType.includes("text/xml") ||
|
||||
contentType.includes("application/xml")
|
||||
) {
|
||||
const rawXml = await response.text()
|
||||
data =
|
||||
(await xmlParser(rawXml, {
|
||||
explicitArray: false,
|
||||
trim: true,
|
||||
explicitRoot: false,
|
||||
})) || {}
|
||||
// there is only one structure, its an array, return the array so it appears as rows
|
||||
const keys = Object.keys(data)
|
||||
if (keys.length === 1 && Array.isArray(data[keys[0]])) {
|
||||
data = data[keys[0]]
|
||||
}
|
||||
raw = rawXml
|
||||
} else {
|
||||
data = await response.text()
|
||||
raw = data
|
||||
}
|
||||
const size = formatBytes(response.headers.get("content-length") || Buffer.byteLength(raw, "utf8"))
|
||||
} catch (err) {
|
||||
throw "Failed to parse response body."
|
||||
}
|
||||
const size = formatBytes(
|
||||
response.headers.get("content-length") || Buffer.byteLength(raw, "utf8")
|
||||
)
|
||||
const time = `${Math.round(performance.now() - this.startTimeMs)}ms`
|
||||
headers = response.headers.raw()
|
||||
for (let [key, value] of Object.entries(headers)) {
|
||||
|
@ -150,6 +177,58 @@ module RestModule {
|
|||
return complete
|
||||
}
|
||||
|
||||
addBody(bodyType: string, body: string | any, input: any) {
|
||||
let error, object, string
|
||||
try {
|
||||
string = typeof body !== "string" ? JSON.stringify(body) : body
|
||||
object = typeof body === "object" ? body : JSON.parse(body)
|
||||
} catch (err) {
|
||||
error = err
|
||||
}
|
||||
if (!input.headers) {
|
||||
input.headers = {}
|
||||
}
|
||||
switch (bodyType) {
|
||||
case BodyTypes.NONE:
|
||||
break
|
||||
case BodyTypes.TEXT:
|
||||
// content type defaults to plaintext
|
||||
input.body = string
|
||||
break
|
||||
case BodyTypes.ENCODED:
|
||||
const params = new URLSearchParams()
|
||||
for (let [key, value] of Object.entries(object)) {
|
||||
params.append(key, value)
|
||||
}
|
||||
input.body = params
|
||||
break
|
||||
case BodyTypes.FORM_DATA:
|
||||
const form = new FormData()
|
||||
for (let [key, value] of Object.entries(object)) {
|
||||
form.append(key, value)
|
||||
}
|
||||
input.body = form
|
||||
break
|
||||
case BodyTypes.XML:
|
||||
if (object != null) {
|
||||
string = (new XmlBuilder()).buildObject(object)
|
||||
}
|
||||
input.body = string
|
||||
input.headers["Content-Type"] = "application/xml"
|
||||
break
|
||||
default:
|
||||
case BodyTypes.JSON:
|
||||
// if JSON error, throw it
|
||||
if (error) {
|
||||
throw "Invalid JSON for request body"
|
||||
}
|
||||
input.body = string
|
||||
input.headers["Content-Type"] = "application/json"
|
||||
break
|
||||
}
|
||||
return input
|
||||
}
|
||||
|
||||
getAuthHeaders(authConfigId: string): { [key: string]: any } {
|
||||
let headers: any = {}
|
||||
|
||||
|
@ -180,7 +259,16 @@ module RestModule {
|
|||
}
|
||||
|
||||
async _req(query: RestQuery) {
|
||||
const { path = "", queryString = "", headers = {}, method = "GET", disabledHeaders, bodyType, requestBody, authConfigId } = query
|
||||
const {
|
||||
path = "",
|
||||
queryString = "",
|
||||
headers = {},
|
||||
method = "GET",
|
||||
disabledHeaders,
|
||||
bodyType,
|
||||
requestBody,
|
||||
authConfigId,
|
||||
} = query
|
||||
const authHeaders = this.getAuthHeaders(authConfigId)
|
||||
|
||||
this.headers = {
|
||||
|
@ -197,18 +285,9 @@ module RestModule {
|
|||
}
|
||||
}
|
||||
|
||||
let json
|
||||
if (bodyType === BodyTypes.JSON && requestBody) {
|
||||
try {
|
||||
json = JSON.parse(requestBody)
|
||||
} catch (err) {
|
||||
throw "Invalid JSON for request body"
|
||||
}
|
||||
}
|
||||
|
||||
const input: any = { method, headers: this.headers }
|
||||
if (json && typeof json === "object" && Object.keys(json).length > 0) {
|
||||
input.body = JSON.stringify(json)
|
||||
let input: any = { method, headers: this.headers }
|
||||
if (requestBody) {
|
||||
input = this.addBody(bodyType, requestBody, input)
|
||||
}
|
||||
|
||||
this.startTimeMs = performance.now()
|
||||
|
|
|
@ -14,6 +14,11 @@ const fetch = require("node-fetch")
|
|||
const RestIntegration = require("../rest")
|
||||
const { AuthType } = require("../rest")
|
||||
|
||||
const HEADERS = {
|
||||
"Accept": "application/json",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
|
||||
class TestConfiguration {
|
||||
constructor(config = {}) {
|
||||
this.integration = new RestIntegration.integration(config)
|
||||
|
@ -35,9 +40,7 @@ describe("REST Integration", () => {
|
|||
const query = {
|
||||
path: "api",
|
||||
queryString: "test=1",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
},
|
||||
headers: HEADERS,
|
||||
bodyType: "json",
|
||||
requestBody: JSON.stringify({
|
||||
name: "test",
|
||||
|
@ -47,9 +50,7 @@ describe("REST Integration", () => {
|
|||
expect(fetch).toHaveBeenCalledWith(`${BASE_URL}/api?test=1`, {
|
||||
method: "POST",
|
||||
body: '{"name":"test"}',
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
},
|
||||
headers: HEADERS,
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -86,9 +87,7 @@ describe("REST Integration", () => {
|
|||
expect(fetch).toHaveBeenCalledWith(`${BASE_URL}/api?test=1`, {
|
||||
method: "PUT",
|
||||
body: '{"name":"test"}',
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
},
|
||||
headers: HEADERS,
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -107,13 +106,98 @@ describe("REST Integration", () => {
|
|||
const response = await config.integration.delete(query)
|
||||
expect(fetch).toHaveBeenCalledWith(`${BASE_URL}/api?test=1`, {
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
},
|
||||
headers: HEADERS,
|
||||
body: '{"name":"test"}',
|
||||
})
|
||||
})
|
||||
|
||||
describe("request body", () => {
|
||||
const input = { a: 1, b: 2 }
|
||||
|
||||
it("should allow no body", () => {
|
||||
const output = config.integration.addBody("none", null, {})
|
||||
expect(output.body).toBeUndefined()
|
||||
expect(Object.keys(output.headers).length).toEqual(0)
|
||||
})
|
||||
|
||||
it("should allow text body", () => {
|
||||
const output = config.integration.addBody("text", "hello world", {})
|
||||
expect(output.body).toEqual("hello world")
|
||||
// gets added by fetch
|
||||
expect(Object.keys(output.headers).length).toEqual(0)
|
||||
})
|
||||
|
||||
it("should allow form data", () => {
|
||||
const FormData = require("form-data")
|
||||
const output = config.integration.addBody("form", input, {})
|
||||
expect(output.body instanceof FormData).toEqual(true)
|
||||
expect(output.body._valueLength).toEqual(2)
|
||||
// gets added by fetch
|
||||
expect(Object.keys(output.headers).length).toEqual(0)
|
||||
})
|
||||
|
||||
it("should allow encoded form data", () => {
|
||||
const { URLSearchParams } = require("url")
|
||||
const output = config.integration.addBody("encoded", input, {})
|
||||
expect(output.body instanceof URLSearchParams).toEqual(true)
|
||||
expect(output.body.toString()).toEqual("a=1&b=2")
|
||||
// gets added by fetch
|
||||
expect(Object.keys(output.headers).length).toEqual(0)
|
||||
})
|
||||
|
||||
it("should allow JSON", () => {
|
||||
const output = config.integration.addBody("json", input, {})
|
||||
expect(output.body).toEqual(JSON.stringify(input))
|
||||
expect(output.headers["Content-Type"]).toEqual("application/json")
|
||||
})
|
||||
|
||||
it("should allow XML", () => {
|
||||
const output = config.integration.addBody("xml", input, {})
|
||||
expect(output.body.includes("<a>1</a>")).toEqual(true)
|
||||
expect(output.body.includes("<b>2</b>")).toEqual(true)
|
||||
expect(output.headers["Content-Type"]).toEqual("application/xml")
|
||||
})
|
||||
})
|
||||
|
||||
describe("response", () => {
|
||||
function buildInput(json, text, header) {
|
||||
return {
|
||||
status: 200,
|
||||
json: json ? async () => json : undefined,
|
||||
text: text ? async () => text : undefined,
|
||||
headers: { get: key => key === "content-length" ? 100 : header, raw: () => ({ "content-type": header }) }
|
||||
}
|
||||
}
|
||||
|
||||
it("should be able to parse JSON response", async () => {
|
||||
const input = buildInput({a: 1}, null, "application/json")
|
||||
const output = await config.integration.parseResponse(input)
|
||||
expect(output.data).toEqual({a: 1})
|
||||
expect(output.info.code).toEqual(200)
|
||||
expect(output.info.size).toEqual("100B")
|
||||
expect(output.extra.raw).toEqual(JSON.stringify({a: 1}))
|
||||
expect(output.extra.headers["content-type"]).toEqual("application/json")
|
||||
})
|
||||
|
||||
it("should be able to parse text response", async () => {
|
||||
const text = "hello world"
|
||||
const input = buildInput(null, text, "text/plain")
|
||||
const output = await config.integration.parseResponse(input)
|
||||
expect(output.data).toEqual(text)
|
||||
expect(output.extra.raw).toEqual(text)
|
||||
expect(output.extra.headers["content-type"]).toEqual("text/plain")
|
||||
})
|
||||
|
||||
it("should be able to parse XML response", async () => {
|
||||
const text = "<root><a>1</a><b>2</b></root>"
|
||||
const input = buildInput(null, text, "application/xml")
|
||||
const output = await config.integration.parseResponse(input)
|
||||
expect(output.data).toEqual({a: "1", b: "2"})
|
||||
expect(output.extra.raw).toEqual(text)
|
||||
expect(output.extra.headers["content-type"]).toEqual("application/xml")
|
||||
})
|
||||
})
|
||||
|
||||
describe("authentication", () => {
|
||||
const basicAuth = {
|
||||
_id: "c59c14bd1898a43baa08da68959b24686",
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
// when thread starts, make sure it is recorded
|
||||
require("./utils").threadSetup()
|
||||
const env = require("../environment")
|
||||
env.setInThread()
|
||||
const actions = require("../automations/actions")
|
||||
const automationUtils = require("../automations/automationUtils")
|
||||
const AutomationEmitter = require("../events/AutomationEmitter")
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
// when thread starts, make sure it is recorded
|
||||
const env = require("../environment")
|
||||
env.setInThread()
|
||||
require("./utils").threadSetup()
|
||||
const ScriptRunner = require("../utilities/scriptRunner")
|
||||
const { integrations } = require("../integrations")
|
||||
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
const env = require("../environment")
|
||||
const CouchDB = require("../db")
|
||||
const { init } = require("@budibase/auth")
|
||||
|
||||
exports.threadSetup = () => {
|
||||
// don't run this if not threading
|
||||
if (env.isTest() || env.DISABLE_THREADING) {
|
||||
return
|
||||
}
|
||||
// when thread starts, make sure it is recorded
|
||||
env.setInThread()
|
||||
init(CouchDB)
|
||||
}
|
|
@ -983,10 +983,10 @@
|
|||
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
|
||||
integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==
|
||||
|
||||
"@budibase/auth@^1.0.18":
|
||||
version "1.0.19"
|
||||
resolved "https://registry.yarnpkg.com/@budibase/auth/-/auth-1.0.19.tgz#b5a8ad51170443d2136d244f51cfe7dbcc0db116"
|
||||
integrity sha512-6H1K80KX8RUseLXD307tKRc+b0B7/b2SZmAYYGq5qrUSdUotydZaZ90pt5pXVdE754duxyc8DlrwmRfri5xu+A==
|
||||
"@budibase/auth@^1.0.19-alpha.1":
|
||||
version "1.0.22"
|
||||
resolved "https://registry.yarnpkg.com/@budibase/auth/-/auth-1.0.22.tgz#a93ea2fea46e00138ad3fa129c9ea19b056654e2"
|
||||
integrity sha512-eHCNEzGl6HxYlMpfRTXBokq2ALTK5f+CDSgJmGaL/jfPc2NlzCI5NoigZUkSrdwDiYZnnWLfDDR4dArYyLlFuA==
|
||||
dependencies:
|
||||
"@techpass/passport-openidconnect" "^0.3.0"
|
||||
aws-sdk "^2.901.0"
|
||||
|
@ -1056,10 +1056,10 @@
|
|||
svelte-flatpickr "^3.2.3"
|
||||
svelte-portal "^1.0.0"
|
||||
|
||||
"@budibase/bbui@^1.0.19":
|
||||
version "1.0.19"
|
||||
resolved "https://registry.yarnpkg.com/@budibase/bbui/-/bbui-1.0.19.tgz#d79c99e8c0adcf24d9b83f00a15eb262ad73a7e2"
|
||||
integrity sha512-GhsyqkDjHMvU1MCr7oXKIZi6NOhmkunJ6eAoob8obCLDm+LXC/1Q8ymSuJicctQpDpraaFS7zqQ6vYY9v7kpiQ==
|
||||
"@budibase/bbui@^1.0.22":
|
||||
version "1.0.22"
|
||||
resolved "https://registry.yarnpkg.com/@budibase/bbui/-/bbui-1.0.22.tgz#ac3bd3a8699bd0be84aac3c5dff9d093e5b08462"
|
||||
integrity sha512-8/5rXEOwkr0OcQD1fn5GpmI3d5dS1cIJBAODjTVtlZrTdacwlz5W2j3zIh+CBG0X7zhVxEze3zs2b1vDNTvK6A==
|
||||
dependencies:
|
||||
"@adobe/spectrum-css-workflow-icons" "^1.2.1"
|
||||
"@spectrum-css/actionbutton" "^1.0.1"
|
||||
|
@ -1106,14 +1106,14 @@
|
|||
svelte-flatpickr "^3.2.3"
|
||||
svelte-portal "^1.0.0"
|
||||
|
||||
"@budibase/client@^1.0.18":
|
||||
version "1.0.19"
|
||||
resolved "https://registry.yarnpkg.com/@budibase/client/-/client-1.0.19.tgz#50ba2ad91ac2fd57c51306b80fbab24a26ba1403"
|
||||
integrity sha512-8vAsD7VkLfq9ZrD+QPXGUcj/2D3vGO++IPr0zIKGNVG5FlOLFceQ9b7itExSFWutyVAjK/e/yq56tugnf0S+Fg==
|
||||
"@budibase/client@^1.0.19-alpha.1":
|
||||
version "1.0.22"
|
||||
resolved "https://registry.yarnpkg.com/@budibase/client/-/client-1.0.22.tgz#80d6c3fb2b57a050199dde4a4b3e82b221601c25"
|
||||
integrity sha512-Cpao7l2lIWyJZJs8+zq1wFnQGaWRTDiRG+HkkjvqQZDkZexlo89zWPkY56NBbMT1qAXd6K3zAdRNNKVCBCtOaA==
|
||||
dependencies:
|
||||
"@budibase/bbui" "^1.0.19"
|
||||
"@budibase/bbui" "^1.0.22"
|
||||
"@budibase/standard-components" "^0.9.139"
|
||||
"@budibase/string-templates" "^1.0.19"
|
||||
"@budibase/string-templates" "^1.0.22"
|
||||
regexparam "^1.3.0"
|
||||
shortid "^2.2.15"
|
||||
svelte-spa-router "^3.0.5"
|
||||
|
@ -1163,10 +1163,10 @@
|
|||
svelte-apexcharts "^1.0.2"
|
||||
svelte-flatpickr "^3.1.0"
|
||||
|
||||
"@budibase/string-templates@^1.0.18", "@budibase/string-templates@^1.0.19":
|
||||
version "1.0.19"
|
||||
resolved "https://registry.yarnpkg.com/@budibase/string-templates/-/string-templates-1.0.19.tgz#4b476dfc5d317e56d84a24dffd34715cc74c7b37"
|
||||
integrity sha512-MmSHF2HK3JS3goyNr3mUQi3azt5vSWlmSGlYFyw473jplRVYkmI8wXrP8gVy9mNJ4vksn3bkgFPI8Hi9RoNSbA==
|
||||
"@budibase/string-templates@^1.0.19-alpha.1", "@budibase/string-templates@^1.0.22":
|
||||
version "1.0.22"
|
||||
resolved "https://registry.yarnpkg.com/@budibase/string-templates/-/string-templates-1.0.22.tgz#b795c61e53d541c0aa346a90d04b50dcca6ae117"
|
||||
integrity sha512-1ZhxzL75kVhP44fJlCWwqmGIPjZol1eB/xi3O11xJPYQ7lfzeJcGUpksvlgbLgBlw+MKkgppK7gEoMP247E0Qw==
|
||||
dependencies:
|
||||
"@budibase/handlebars-helpers" "^0.11.7"
|
||||
dayjs "^1.10.4"
|
||||
|
@ -5829,6 +5829,15 @@ form-data@^3.0.0:
|
|||
combined-stream "^1.0.8"
|
||||
mime-types "^2.1.12"
|
||||
|
||||
form-data@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452"
|
||||
integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==
|
||||
dependencies:
|
||||
asynckit "^0.4.0"
|
||||
combined-stream "^1.0.8"
|
||||
mime-types "^2.1.12"
|
||||
|
||||
formidable@^1.1.1, formidable@^1.2.0:
|
||||
version "1.2.6"
|
||||
resolved "https://registry.yarnpkg.com/formidable/-/formidable-1.2.6.tgz#d2a51d60162bbc9b4a055d8457a7c75315d1a168"
|
||||
|
@ -13081,7 +13090,7 @@ xml2js@0.4.19:
|
|||
sax ">=0.6.0"
|
||||
xmlbuilder "~9.0.1"
|
||||
|
||||
xml2js@^0.4.19, xml2js@^0.4.5:
|
||||
xml2js@^0.4.19, xml2js@^0.4.23, xml2js@^0.4.5:
|
||||
version "0.4.23"
|
||||
resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.23.tgz#a0c69516752421eb2ac758ee4d4ccf58843eac66"
|
||||
integrity sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@budibase/string-templates",
|
||||
"version": "1.0.19-alpha.2",
|
||||
"version": "1.0.22",
|
||||
"description": "Handlebars wrapper for Budibase templating.",
|
||||
"main": "src/index.cjs",
|
||||
"module": "dist/bundle.mjs",
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "@budibase/worker",
|
||||
"email": "hi@budibase.com",
|
||||
"version": "1.0.19-alpha.2",
|
||||
"version": "1.0.22",
|
||||
"description": "Budibase background service",
|
||||
"main": "src/index.js",
|
||||
"repository": {
|
||||
|
@ -29,8 +29,8 @@
|
|||
"author": "Budibase",
|
||||
"license": "GPL-3.0",
|
||||
"dependencies": {
|
||||
"@budibase/auth": "^1.0.19-alpha.2",
|
||||
"@budibase/string-templates": "^1.0.19-alpha.2",
|
||||
"@budibase/auth": "^1.0.22",
|
||||
"@budibase/string-templates": "^1.0.22",
|
||||
"@koa/router": "^8.0.0",
|
||||
"@sentry/node": "^6.0.0",
|
||||
"@techpass/passport-openidconnect": "^0.3.0",
|
||||
|
|
Loading…
Reference in New Issue