forked from tsai/budibase
183 changed files with 4816 additions and 1892 deletions
@ -1,4 +1,5 @@ |
|||||
module.exports = { |
module.exports = { |
||||
...require("./src/db/utils"), |
...require("./src/db/utils"), |
||||
...require("./src/db/constants"), |
...require("./src/db/constants"), |
||||
|
...require("./src/db/views"), |
||||
} |
} |
||||
|
|||||
@ -0,0 +1 @@ |
|||||
|
module.exports = require("./src/migrations") |
||||
@ -0,0 +1,78 @@ |
|||||
|
const { Headers } = require("../constants") |
||||
|
const { buildMatcherRegex, matches } = require("./matchers") |
||||
|
|
||||
|
/** |
||||
|
* GET, HEAD and OPTIONS methods are considered safe operations |
||||
|
* |
||||
|
* POST, PUT, PATCH, and DELETE methods, being state changing verbs, |
||||
|
* should have a CSRF token attached to the request |
||||
|
*/ |
||||
|
const EXCLUDED_METHODS = ["GET", "HEAD", "OPTIONS"] |
||||
|
|
||||
|
/** |
||||
|
* There are only three content type values that can be used in cross domain requests. |
||||
|
* If any other value is used, e.g. application/json, the browser will first make a OPTIONS |
||||
|
* request which will be protected by CORS. |
||||
|
*/ |
||||
|
const INCLUDED_CONTENT_TYPES = [ |
||||
|
"application/x-www-form-urlencoded", |
||||
|
"multipart/form-data", |
||||
|
"text/plain", |
||||
|
] |
||||
|
|
||||
|
/** |
||||
|
* Validate the CSRF token generated aganst the user session. |
||||
|
* Compare the token with the x-csrf-token header. |
||||
|
* |
||||
|
* If the token is not found within the request or the value provided |
||||
|
* does not match the value within the user session, the request is rejected. |
||||
|
* |
||||
|
* CSRF protection provided using the 'Synchronizer Token Pattern' |
||||
|
* https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#synchronizer-token-pattern
|
||||
|
* |
||||
|
*/ |
||||
|
module.exports = (opts = { noCsrfPatterns: [] }) => { |
||||
|
const noCsrfOptions = buildMatcherRegex(opts.noCsrfPatterns) |
||||
|
return async (ctx, next) => { |
||||
|
// don't apply for excluded paths
|
||||
|
const found = matches(ctx, noCsrfOptions) |
||||
|
if (found) { |
||||
|
return next() |
||||
|
} |
||||
|
|
||||
|
// don't apply for the excluded http methods
|
||||
|
if (EXCLUDED_METHODS.indexOf(ctx.method) !== -1) { |
||||
|
return next() |
||||
|
} |
||||
|
|
||||
|
// don't apply when the content type isn't supported
|
||||
|
let contentType = ctx.get("content-type") |
||||
|
? ctx.get("content-type").toLowerCase() |
||||
|
: "" |
||||
|
if ( |
||||
|
!INCLUDED_CONTENT_TYPES.filter(type => contentType.includes(type)).length |
||||
|
) { |
||||
|
return next() |
||||
|
} |
||||
|
|
||||
|
// don't apply csrf when the internal api key has been used
|
||||
|
if (ctx.internal) { |
||||
|
return next() |
||||
|
} |
||||
|
|
||||
|
// apply csrf when there is a token in the session (new logins)
|
||||
|
// in future there should be a hard requirement that the token is present
|
||||
|
const userToken = ctx.user.csrfToken |
||||
|
if (!userToken) { |
||||
|
return next() |
||||
|
} |
||||
|
|
||||
|
// reject if no token in request or mismatch
|
||||
|
const requestToken = ctx.get(Headers.CSRF_TOKEN) |
||||
|
if (!requestToken || requestToken !== userToken) { |
||||
|
ctx.throw(403, "Invalid CSRF token") |
||||
|
} |
||||
|
|
||||
|
return next() |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,14 @@ |
|||||
|
const env = require("../environment") |
||||
|
const { Headers } = require("../constants") |
||||
|
|
||||
|
/** |
||||
|
* API Key only endpoint. |
||||
|
*/ |
||||
|
module.exports = async (ctx, next) => { |
||||
|
const apiKey = ctx.request.headers[Headers.API_KEY] |
||||
|
if (apiKey !== env.INTERNAL_API_KEY) { |
||||
|
ctx.throw(403, "Unauthorized") |
||||
|
} |
||||
|
|
||||
|
return next() |
||||
|
} |
||||
@ -0,0 +1,76 @@ |
|||||
|
const { getScopedConfig } = require("../../../db/utils") |
||||
|
const { getGlobalDB } = require("../../../tenancy") |
||||
|
const google = require("../google") |
||||
|
const { Configs, Cookies } = require("../../../constants") |
||||
|
const { clearCookie, getCookie } = require("../../../utils") |
||||
|
const { getDB } = require("../../../db") |
||||
|
|
||||
|
async function preAuth(passport, ctx, next) { |
||||
|
const db = getGlobalDB() |
||||
|
// get the relevant config
|
||||
|
const config = await getScopedConfig(db, { |
||||
|
type: Configs.GOOGLE, |
||||
|
workspace: ctx.query.workspace, |
||||
|
}) |
||||
|
const publicConfig = await getScopedConfig(db, { |
||||
|
type: Configs.SETTINGS, |
||||
|
}) |
||||
|
let callbackUrl = `${publicConfig.platformUrl}/api/global/auth/datasource/google/callback` |
||||
|
const strategy = await google.strategyFactory(config, callbackUrl) |
||||
|
|
||||
|
if (!ctx.query.appId || !ctx.query.datasourceId) { |
||||
|
ctx.throw(400, "appId and datasourceId query params not present.") |
||||
|
} |
||||
|
|
||||
|
return passport.authenticate(strategy, { |
||||
|
scope: ["profile", "email", "https://www.googleapis.com/auth/spreadsheets"], |
||||
|
accessType: "offline", |
||||
|
prompt: "consent", |
||||
|
})(ctx, next) |
||||
|
} |
||||
|
|
||||
|
async function postAuth(passport, ctx, next) { |
||||
|
const db = getGlobalDB() |
||||
|
|
||||
|
const config = await getScopedConfig(db, { |
||||
|
type: Configs.GOOGLE, |
||||
|
workspace: ctx.query.workspace, |
||||
|
}) |
||||
|
|
||||
|
const publicConfig = await getScopedConfig(db, { |
||||
|
type: Configs.SETTINGS, |
||||
|
}) |
||||
|
|
||||
|
let callbackUrl = `${publicConfig.platformUrl}/api/global/auth/datasource/google/callback` |
||||
|
const strategy = await google.strategyFactory( |
||||
|
config, |
||||
|
callbackUrl, |
||||
|
(accessToken, refreshToken, profile, done) => { |
||||
|
clearCookie(ctx, Cookies.DatasourceAuth) |
||||
|
done(null, { accessToken, refreshToken }) |
||||
|
} |
||||
|
) |
||||
|
|
||||
|
const authStateCookie = getCookie(ctx, Cookies.DatasourceAuth) |
||||
|
|
||||
|
return passport.authenticate( |
||||
|
strategy, |
||||
|
{ successRedirect: "/", failureRedirect: "/error" }, |
||||
|
async (err, tokens) => { |
||||
|
// update the DB for the datasource with all the user info
|
||||
|
const db = getDB(authStateCookie.appId) |
||||
|
const datasource = await db.get(authStateCookie.datasourceId) |
||||
|
if (!datasource.config) { |
||||
|
datasource.config = {} |
||||
|
} |
||||
|
datasource.config.auth = { type: "google", ...tokens } |
||||
|
await db.put(datasource) |
||||
|
ctx.redirect( |
||||
|
`/builder/app/${authStateCookie.appId}/data/datasource/${authStateCookie.datasourceId}` |
||||
|
) |
||||
|
} |
||||
|
)(ctx, next) |
||||
|
} |
||||
|
|
||||
|
exports.preAuth = preAuth |
||||
|
exports.postAuth = postAuth |
||||
@ -1,73 +1,20 @@ |
|||||
<script> |
<script> |
||||
import "@spectrum-css/fieldlabel/dist/index-vars.css" |
import "@spectrum-css/fieldlabel/dist/index-vars.css" |
||||
import Tooltip from "../Tooltip/Tooltip.svelte" |
import TooltipWrapper from "../Tooltip/TooltipWrapper.svelte" |
||||
import Icon from "../Icon/Icon.svelte" |
|
||||
|
|
||||
export let size = "M" |
export let size = "M" |
||||
export let tooltip = "" |
export let tooltip = "" |
||||
export let showTooltip = false |
|
||||
</script> |
</script> |
||||
|
|
||||
{#if tooltip} |
<TooltipWrapper {tooltip} {size}> |
||||
<div class="container"> |
|
||||
<label |
|
||||
for="" |
|
||||
class={`spectrum-FieldLabel spectrum-FieldLabel--size${size}`} |
|
||||
> |
|
||||
<slot /> |
|
||||
</label> |
|
||||
<div class="icon-container"> |
|
||||
<div |
|
||||
class="icon" |
|
||||
class:icon-small={size === "M" || size === "S"} |
|
||||
on:mouseover={() => (showTooltip = true)} |
|
||||
on:mouseleave={() => (showTooltip = false)} |
|
||||
> |
|
||||
<Icon name="InfoOutline" size="S" disabled={true} /> |
|
||||
</div> |
|
||||
{#if showTooltip} |
|
||||
<div class="tooltip"> |
|
||||
<Tooltip textWrapping={true} direction={"bottom"} text={tooltip} /> |
|
||||
</div> |
|
||||
{/if} |
|
||||
</div> |
|
||||
</div> |
|
||||
{:else} |
|
||||
<label for="" class={`spectrum-FieldLabel spectrum-FieldLabel--size${size}`}> |
<label for="" class={`spectrum-FieldLabel spectrum-FieldLabel--size${size}`}> |
||||
<slot /> |
<slot /> |
||||
</label> |
</label> |
||||
{/if} |
</TooltipWrapper> |
||||
|
|
||||
<style> |
<style> |
||||
label { |
label { |
||||
padding: 0; |
padding: 0; |
||||
white-space: nowrap; |
white-space: nowrap; |
||||
} |
} |
||||
.container { |
|
||||
display: flex; |
|
||||
align-items: center; |
|
||||
} |
|
||||
.icon-container { |
|
||||
position: relative; |
|
||||
display: flex; |
|
||||
justify-content: center; |
|
||||
margin-top: 1px; |
|
||||
margin-left: 5px; |
|
||||
margin-right: 5px; |
|
||||
} |
|
||||
.tooltip { |
|
||||
position: absolute; |
|
||||
display: flex; |
|
||||
justify-content: center; |
|
||||
top: 15px; |
|
||||
z-index: 1; |
|
||||
width: 160px; |
|
||||
} |
|
||||
.icon { |
|
||||
transform: scale(0.75); |
|
||||
} |
|
||||
.icon-small { |
|
||||
margin-top: -2px; |
|
||||
margin-bottom: -5px; |
|
||||
} |
|
||||
</style> |
</style> |
||||
|
|||||
@ -0,0 +1,60 @@ |
|||||
|
<script> |
||||
|
import Tooltip from "./Tooltip.svelte" |
||||
|
import Icon from "../Icon/Icon.svelte" |
||||
|
|
||||
|
export let tooltip = "" |
||||
|
export let size = "M" |
||||
|
|
||||
|
let showTooltip = false |
||||
|
</script> |
||||
|
|
||||
|
<div class:container={!!tooltip}> |
||||
|
<slot /> |
||||
|
{#if tooltip} |
||||
|
<div class="icon-container"> |
||||
|
<div |
||||
|
class="icon" |
||||
|
class:icon-small={size === "M" || size === "S"} |
||||
|
on:mouseover={() => (showTooltip = true)} |
||||
|
on:mouseleave={() => (showTooltip = false)} |
||||
|
> |
||||
|
<Icon name="InfoOutline" size="S" disabled={true} /> |
||||
|
</div> |
||||
|
{#if showTooltip} |
||||
|
<div class="tooltip"> |
||||
|
<Tooltip textWrapping={true} direction={"bottom"} text={tooltip} /> |
||||
|
</div> |
||||
|
{/if} |
||||
|
</div> |
||||
|
{/if} |
||||
|
</div> |
||||
|
|
||||
|
<style> |
||||
|
.container { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
} |
||||
|
.icon-container { |
||||
|
position: relative; |
||||
|
display: flex; |
||||
|
justify-content: center; |
||||
|
margin-top: 1px; |
||||
|
margin-left: 5px; |
||||
|
margin-right: 5px; |
||||
|
} |
||||
|
.tooltip { |
||||
|
position: absolute; |
||||
|
display: flex; |
||||
|
justify-content: center; |
||||
|
top: 15px; |
||||
|
z-index: 1; |
||||
|
width: 160px; |
||||
|
} |
||||
|
.icon { |
||||
|
transform: scale(0.75); |
||||
|
} |
||||
|
.icon-small { |
||||
|
margin-top: -2px; |
||||
|
margin-bottom: -5px; |
||||
|
} |
||||
|
</style> |
||||
@ -1,34 +0,0 @@ |
|||||
import { writable } from "svelte/store" |
|
||||
import api, { get } from "../api" |
|
||||
|
|
||||
const INITIAL_HOSTING_UI_STATE = { |
|
||||
appUrl: "", |
|
||||
deployedApps: {}, |
|
||||
deployedAppNames: [], |
|
||||
deployedAppUrls: [], |
|
||||
} |
|
||||
|
|
||||
export const getHostingStore = () => { |
|
||||
const store = writable({ ...INITIAL_HOSTING_UI_STATE }) |
|
||||
store.actions = { |
|
||||
fetch: async () => { |
|
||||
const response = await api.get("/api/hosting/urls") |
|
||||
const urls = await response.json() |
|
||||
store.update(state => { |
|
||||
state.appUrl = urls.app |
|
||||
return state |
|
||||
}) |
|
||||
}, |
|
||||
fetchDeployedApps: async () => { |
|
||||
let deployments = await (await get("/api/hosting/apps")).json() |
|
||||
store.update(state => { |
|
||||
state.deployedApps = deployments |
|
||||
state.deployedAppNames = Object.values(deployments).map(app => app.name) |
|
||||
state.deployedAppUrls = Object.values(deployments).map(app => app.url) |
|
||||
return state |
|
||||
}) |
|
||||
return deployments |
|
||||
}, |
|
||||
} |
|
||||
return store |
|
||||
} |
|
||||
@ -0,0 +1,47 @@ |
|||||
|
<script> |
||||
|
import { ActionButton } from "@budibase/bbui" |
||||
|
import GoogleLogo from "assets/google-logo.png" |
||||
|
import { store } from "builderStore" |
||||
|
import { auth } from "stores/portal" |
||||
|
|
||||
|
export let preAuthStep |
||||
|
export let datasource |
||||
|
|
||||
|
$: tenantId = $auth.tenantId |
||||
|
</script> |
||||
|
|
||||
|
<ActionButton |
||||
|
on:click={async () => { |
||||
|
let ds = datasource |
||||
|
if (!ds) { |
||||
|
ds = await preAuthStep() |
||||
|
} |
||||
|
window.open( |
||||
|
`/api/global/auth/${tenantId}/datasource/google?datasourceId=${datasource._id}&appId=${$store.appId}`, |
||||
|
"_blank" |
||||
|
) |
||||
|
}} |
||||
|
> |
||||
|
<div class="inner"> |
||||
|
<img src={GoogleLogo} alt="google icon" /> |
||||
|
<p>Sign in with Google</p> |
||||
|
</div> |
||||
|
</ActionButton> |
||||
|
|
||||
|
<style> |
||||
|
.inner { |
||||
|
display: flex; |
||||
|
flex-direction: row; |
||||
|
align-items: center; |
||||
|
justify-content: center; |
||||
|
padding-top: var(--spacing-xs); |
||||
|
padding-bottom: var(--spacing-xs); |
||||
|
} |
||||
|
.inner img { |
||||
|
width: 18px; |
||||
|
margin: 3px 10px 3px 3px; |
||||
|
} |
||||
|
.inner p { |
||||
|
margin: 0; |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,184 @@ |
|||||
|
<script> |
||||
|
export let width = "100" |
||||
|
export let height = "100" |
||||
|
</script> |
||||
|
|
||||
|
<svg |
||||
|
{width} |
||||
|
{height} |
||||
|
version="1.0" |
||||
|
xmlns="http://www.w3.org/2000/svg" |
||||
|
viewBox="0 0 50 80" |
||||
|
preserveAspectRatio="xMidYMid meet" |
||||
|
> |
||||
|
<defs> |
||||
|
<path |
||||
|
d="M29.5833333,0 L4.4375,0 C1.996875,0 0,1.996875 0,4.4375 L0,60.6458333 C0,63.0864583 1.996875,65.0833333 4.4375,65.0833333 L42.8958333,65.0833333 C45.3364583,65.0833333 47.3333333,63.0864583 47.3333333,60.6458333 L47.3333333,17.75 L29.5833333,0 Z" |
||||
|
id="path-1" |
||||
|
/> |
||||
|
<path |
||||
|
d="M29.5833333,0 L4.4375,0 C1.996875,0 0,1.996875 0,4.4375 L0,60.6458333 C0,63.0864583 1.996875,65.0833333 4.4375,65.0833333 L42.8958333,65.0833333 C45.3364583,65.0833333 47.3333333,63.0864583 47.3333333,60.6458333 L47.3333333,17.75 L29.5833333,0 Z" |
||||
|
id="path-3" |
||||
|
/> |
||||
|
<path |
||||
|
d="M29.5833333,0 L4.4375,0 C1.996875,0 0,1.996875 0,4.4375 L0,60.6458333 C0,63.0864583 1.996875,65.0833333 4.4375,65.0833333 L42.8958333,65.0833333 C45.3364583,65.0833333 47.3333333,63.0864583 47.3333333,60.6458333 L47.3333333,17.75 L29.5833333,0 Z" |
||||
|
id="path-5" |
||||
|
/> |
||||
|
<linearGradient |
||||
|
x1="50.0053945%" |
||||
|
y1="8.58610612%" |
||||
|
x2="50.0053945%" |
||||
|
y2="100.013939%" |
||||
|
id="linearGradient-7" |
||||
|
> |
||||
|
<stop stop-color="#263238" stop-opacity="0.2" offset="0%" /> |
||||
|
<stop stop-color="#263238" stop-opacity="0.02" offset="100%" /> |
||||
|
</linearGradient> |
||||
|
<path |
||||
|
d="M29.5833333,0 L4.4375,0 C1.996875,0 0,1.996875 0,4.4375 L0,60.6458333 C0,63.0864583 1.996875,65.0833333 4.4375,65.0833333 L42.8958333,65.0833333 C45.3364583,65.0833333 47.3333333,63.0864583 47.3333333,60.6458333 L47.3333333,17.75 L29.5833333,0 Z" |
||||
|
id="path-8" |
||||
|
/> |
||||
|
<path |
||||
|
d="M29.5833333,0 L4.4375,0 C1.996875,0 0,1.996875 0,4.4375 L0,60.6458333 C0,63.0864583 1.996875,65.0833333 4.4375,65.0833333 L42.8958333,65.0833333 C45.3364583,65.0833333 47.3333333,63.0864583 47.3333333,60.6458333 L47.3333333,17.75 L29.5833333,0 Z" |
||||
|
id="path-10" |
||||
|
/> |
||||
|
<path |
||||
|
d="M29.5833333,0 L4.4375,0 C1.996875,0 0,1.996875 0,4.4375 L0,60.6458333 C0,63.0864583 1.996875,65.0833333 4.4375,65.0833333 L42.8958333,65.0833333 C45.3364583,65.0833333 47.3333333,63.0864583 47.3333333,60.6458333 L47.3333333,17.75 L29.5833333,0 Z" |
||||
|
id="path-12" |
||||
|
/> |
||||
|
<path |
||||
|
d="M29.5833333,0 L4.4375,0 C1.996875,0 0,1.996875 0,4.4375 L0,60.6458333 C0,63.0864583 1.996875,65.0833333 4.4375,65.0833333 L42.8958333,65.0833333 C45.3364583,65.0833333 47.3333333,63.0864583 47.3333333,60.6458333 L47.3333333,17.75 L29.5833333,0 Z" |
||||
|
id="path-14" |
||||
|
/> |
||||
|
<radialGradient |
||||
|
cx="3.16804688%" |
||||
|
cy="2.71744318%" |
||||
|
fx="3.16804688%" |
||||
|
fy="2.71744318%" |
||||
|
r="161.248516%" |
||||
|
gradientTransform="translate(0.031680,0.027174),scale(1.000000,0.727273),translate(-0.031680,-0.027174)" |
||||
|
id="radialGradient-16" |
||||
|
> |
||||
|
<stop stop-color="#FFFFFF" stop-opacity="0.1" offset="0%" /> |
||||
|
<stop stop-color="#FFFFFF" stop-opacity="0" offset="100%" /> |
||||
|
</radialGradient> |
||||
|
</defs> |
||||
|
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"> |
||||
|
<g |
||||
|
id="Consumer-Apps-Sheets-Large-VD-R8-" |
||||
|
transform="translate(-451.000000, -451.000000)" |
||||
|
> |
||||
|
<g id="Hero" transform="translate(0.000000, 63.000000)"> |
||||
|
<g id="Personal" transform="translate(277.000000, 299.000000)"> |
||||
|
<g id="Sheets-icon" transform="translate(174.833333, 89.958333)"> |
||||
|
<g id="Group"> |
||||
|
<g id="Clipped"> |
||||
|
<mask id="mask-2" fill="white"> |
||||
|
<use xlink:href="#path-1" /> |
||||
|
</mask> |
||||
|
<g id="SVGID_1_" /> |
||||
|
<path |
||||
|
d="M29.5833333,0 L4.4375,0 C1.996875,0 0,1.996875 0,4.4375 L0,60.6458333 C0,63.0864583 1.996875,65.0833333 4.4375,65.0833333 L42.8958333,65.0833333 C45.3364583,65.0833333 47.3333333,63.0864583 47.3333333,60.6458333 L47.3333333,17.75 L36.9791667,10.3541667 L29.5833333,0 Z" |
||||
|
id="Path" |
||||
|
fill="#0F9D58" |
||||
|
fill-rule="nonzero" |
||||
|
mask="url(#mask-2)" |
||||
|
/> |
||||
|
</g> |
||||
|
<g id="Clipped"> |
||||
|
<mask id="mask-4" fill="white"> |
||||
|
<use xlink:href="#path-3" /> |
||||
|
</mask> |
||||
|
<g id="SVGID_1_" /> |
||||
|
<path |
||||
|
d="M11.8333333,31.8020833 L11.8333333,53.25 L35.5,53.25 L35.5,31.8020833 L11.8333333,31.8020833 Z M22.1875,50.2916667 L14.7916667,50.2916667 L14.7916667,46.59375 L22.1875,46.59375 L22.1875,50.2916667 Z M22.1875,44.375 L14.7916667,44.375 L14.7916667,40.6770833 L22.1875,40.6770833 L22.1875,44.375 Z M22.1875,38.4583333 L14.7916667,38.4583333 L14.7916667,34.7604167 L22.1875,34.7604167 L22.1875,38.4583333 Z M32.5416667,50.2916667 L25.1458333,50.2916667 L25.1458333,46.59375 L32.5416667,46.59375 L32.5416667,50.2916667 Z M32.5416667,44.375 L25.1458333,44.375 L25.1458333,40.6770833 L32.5416667,40.6770833 L32.5416667,44.375 Z M32.5416667,38.4583333 L25.1458333,38.4583333 L25.1458333,34.7604167 L32.5416667,34.7604167 L32.5416667,38.4583333 Z" |
||||
|
id="Shape" |
||||
|
fill="#F1F1F1" |
||||
|
fill-rule="nonzero" |
||||
|
mask="url(#mask-4)" |
||||
|
/> |
||||
|
</g> |
||||
|
<g id="Clipped"> |
||||
|
<mask id="mask-6" fill="white"> |
||||
|
<use xlink:href="#path-5" /> |
||||
|
</mask> |
||||
|
<g id="SVGID_1_" /> |
||||
|
<polygon |
||||
|
id="Path" |
||||
|
fill="url(#linearGradient-7)" |
||||
|
fill-rule="nonzero" |
||||
|
mask="url(#mask-6)" |
||||
|
points="30.8813021 16.4520313 47.3333333 32.9003646 47.3333333 17.75" |
||||
|
/> |
||||
|
</g> |
||||
|
<g id="Clipped"> |
||||
|
<mask id="mask-9" fill="white"> |
||||
|
<use xlink:href="#path-8" /> |
||||
|
</mask> |
||||
|
<g id="SVGID_1_" /> |
||||
|
<g id="Group" mask="url(#mask-9)"> |
||||
|
<g transform="translate(26.625000, -2.958333)"> |
||||
|
<path |
||||
|
d="M2.95833333,2.95833333 L2.95833333,16.2708333 C2.95833333,18.7225521 4.94411458,20.7083333 7.39583333,20.7083333 L20.7083333,20.7083333 L2.95833333,2.95833333 Z" |
||||
|
id="Path" |
||||
|
fill="#87CEAC" |
||||
|
fill-rule="nonzero" |
||||
|
/> |
||||
|
</g> |
||||
|
</g> |
||||
|
</g> |
||||
|
<g id="Clipped"> |
||||
|
<mask id="mask-11" fill="white"> |
||||
|
<use xlink:href="#path-10" /> |
||||
|
</mask> |
||||
|
<g id="SVGID_1_" /> |
||||
|
<path |
||||
|
d="M4.4375,0 C1.996875,0 0,1.996875 0,4.4375 L0,4.80729167 C0,2.36666667 1.996875,0.369791667 4.4375,0.369791667 L29.5833333,0.369791667 L29.5833333,0 L4.4375,0 Z" |
||||
|
id="Path" |
||||
|
fill-opacity="0.2" |
||||
|
fill="#FFFFFF" |
||||
|
fill-rule="nonzero" |
||||
|
mask="url(#mask-11)" |
||||
|
/> |
||||
|
</g> |
||||
|
<g id="Clipped"> |
||||
|
<mask id="mask-13" fill="white"> |
||||
|
<use xlink:href="#path-12" /> |
||||
|
</mask> |
||||
|
<g id="SVGID_1_" /> |
||||
|
<path |
||||
|
d="M42.8958333,64.7135417 L4.4375,64.7135417 C1.996875,64.7135417 0,62.7166667 0,60.2760417 L0,60.6458333 C0,63.0864583 1.996875,65.0833333 4.4375,65.0833333 L42.8958333,65.0833333 C45.3364583,65.0833333 47.3333333,63.0864583 47.3333333,60.6458333 L47.3333333,60.2760417 C47.3333333,62.7166667 45.3364583,64.7135417 42.8958333,64.7135417 Z" |
||||
|
id="Path" |
||||
|
fill-opacity="0.2" |
||||
|
fill="#263238" |
||||
|
fill-rule="nonzero" |
||||
|
mask="url(#mask-13)" |
||||
|
/> |
||||
|
</g> |
||||
|
<g id="Clipped"> |
||||
|
<mask id="mask-15" fill="white"> |
||||
|
<use xlink:href="#path-14" /> |
||||
|
</mask> |
||||
|
<g id="SVGID_1_" /> |
||||
|
<path |
||||
|
d="M34.0208333,17.75 C31.5691146,17.75 29.5833333,15.7642188 29.5833333,13.3125 L29.5833333,13.6822917 C29.5833333,16.1340104 31.5691146,18.1197917 34.0208333,18.1197917 L47.3333333,18.1197917 L47.3333333,17.75 L34.0208333,17.75 Z" |
||||
|
id="Path" |
||||
|
fill-opacity="0.1" |
||||
|
fill="#263238" |
||||
|
fill-rule="nonzero" |
||||
|
mask="url(#mask-15)" |
||||
|
/> |
||||
|
</g> |
||||
|
</g> |
||||
|
<path |
||||
|
d="M29.5833333,0 L4.4375,0 C1.996875,0 0,1.996875 0,4.4375 L0,60.6458333 C0,63.0864583 1.996875,65.0833333 4.4375,65.0833333 L42.8958333,65.0833333 C45.3364583,65.0833333 47.3333333,63.0864583 47.3333333,60.6458333 L47.3333333,17.75 L29.5833333,0 Z" |
||||
|
id="Path" |
||||
|
fill="url(#radialGradient-16)" |
||||
|
fill-rule="nonzero" |
||||
|
/> |
||||
|
</g> |
||||
|
</g> |
||||
|
</g> |
||||
|
</g> |
||||
|
</g> |
||||
|
</svg> |
||||
@ -0,0 +1,29 @@ |
|||||
|
<script> |
||||
|
import { ModalContent, Body, Layout } from "@budibase/bbui" |
||||
|
import { IntegrationNames } from "constants/backend" |
||||
|
import cloneDeep from "lodash/cloneDeepWith" |
||||
|
import GoogleButton from "../_components/GoogleButton.svelte" |
||||
|
import { saveDatasource as save } from "builderStore/datasource" |
||||
|
|
||||
|
export let integration |
||||
|
export let modal |
||||
|
|
||||
|
// kill the reference so the input isn't saved |
||||
|
let datasource = cloneDeep(integration) |
||||
|
</script> |
||||
|
|
||||
|
<ModalContent |
||||
|
title={`Connect to ${IntegrationNames[datasource.type]}`} |
||||
|
onCancel={() => modal.show()} |
||||
|
cancelText="Back" |
||||
|
size="L" |
||||
|
> |
||||
|
<Layout noPadding> |
||||
|
<Body size="XS" |
||||
|
>Authenticate with your google account to use the {IntegrationNames[ |
||||
|
datasource.type |
||||
|
]} integration.</Body |
||||
|
> |
||||
|
</Layout> |
||||
|
<GoogleButton preAuthStep={() => save(datasource, true)} /> |
||||
|
</ModalContent> |
||||
@ -0,0 +1,33 @@ |
|||||
|
<script> |
||||
|
import { Select, Label } from "@budibase/bbui" |
||||
|
import { currentAsset } from "builderStore" |
||||
|
import { findAllMatchingComponents } from "builderStore/componentUtils" |
||||
|
|
||||
|
export let parameters |
||||
|
|
||||
|
$: components = findAllMatchingComponents($currentAsset.props, component => |
||||
|
component._component.endsWith("s3upload") |
||||
|
) |
||||
|
</script> |
||||
|
|
||||
|
<div class="root"> |
||||
|
<Label small>S3 Upload Component</Label> |
||||
|
<Select |
||||
|
bind:value={parameters.componentId} |
||||
|
options={components} |
||||
|
getOptionLabel={x => x._instanceName} |
||||
|
getOptionValue={x => x._id} |
||||
|
/> |
||||
|
</div> |
||||
|
|
||||
|
<style> |
||||
|
.root { |
||||
|
display: grid; |
||||
|
column-gap: var(--spacing-l); |
||||
|
row-gap: var(--spacing-s); |
||||
|
grid-template-columns: 120px 1fr; |
||||
|
align-items: center; |
||||
|
max-width: 400px; |
||||
|
margin: 0 auto; |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,15 @@ |
|||||
|
<script> |
||||
|
import { Select } from "@budibase/bbui" |
||||
|
import { datasources } from "stores/backend" |
||||
|
|
||||
|
export let value = null |
||||
|
|
||||
|
$: dataSources = $datasources.list |
||||
|
.filter(ds => ds.source === "S3" && !ds.config?.endpoint) |
||||
|
.map(ds => ({ |
||||
|
label: ds.name, |
||||
|
value: ds._id, |
||||
|
})) |
||||
|
</script> |
||||
|
|
||||
|
<Select options={dataSources} {value} on:change /> |
||||
@ -1,120 +1,75 @@ |
|||||
<script> |
<script> |
||||
import { writable, get as svelteGet } from "svelte/store" |
import { writable, get as svelteGet } from "svelte/store" |
||||
import { |
import { notifications, Input, ModalContent, Body } from "@budibase/bbui" |
||||
notifications, |
|
||||
Input, |
|
||||
Modal, |
|
||||
ModalContent, |
|
||||
Body, |
|
||||
} from "@budibase/bbui" |
|
||||
import { hostingStore } from "builderStore" |
|
||||
import { apps } from "stores/portal" |
import { apps } from "stores/portal" |
||||
import { string, object } from "yup" |
|
||||
import { onMount } from "svelte" |
import { onMount } from "svelte" |
||||
import { capitalise } from "helpers" |
import { createValidationStore } from "helpers/validation/yup" |
||||
import { APP_NAME_REGEX } from "constants" |
import * as appValidation from "helpers/validation/yup/app" |
||||
|
|
||||
const values = writable({ name: null }) |
|
||||
const errors = writable({}) |
|
||||
const touched = writable({}) |
|
||||
const validator = { |
|
||||
name: string() |
|
||||
.trim() |
|
||||
.required("Your application must have a name") |
|
||||
.matches( |
|
||||
APP_NAME_REGEX, |
|
||||
"App name must be letters, numbers and spaces only" |
|
||||
), |
|
||||
} |
|
||||
|
|
||||
export let app |
export let app |
||||
|
|
||||
let modal |
const values = writable({ name: "", url: null }) |
||||
let valid = false |
const validation = createValidationStore() |
||||
let dirty = false |
$: validation.check($values) |
||||
$: checkValidity($values, validator) |
|
||||
$: { |
|
||||
// prevent validation by setting name to undefined without an app |
|
||||
if (app) { |
|
||||
$values.name = app?.name |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
onMount(async () => { |
onMount(async () => { |
||||
await hostingStore.actions.fetchDeployedApps() |
$values.name = app.name |
||||
const existingAppNames = svelteGet(hostingStore).deployedAppNames |
$values.url = app.url |
||||
validator.name = string() |
setupValidation() |
||||
.trim() |
|
||||
.required("Your application must have a name") |
|
||||
.matches( |
|
||||
APP_NAME_REGEX, |
|
||||
"App name must be letters, numbers and spaces only" |
|
||||
) |
|
||||
.test( |
|
||||
"non-existing-app-name", |
|
||||
"Another app with the same name already exists", |
|
||||
value => { |
|
||||
return !existingAppNames.some( |
|
||||
appName => dirty && appName.toLowerCase() === value.toLowerCase() |
|
||||
) |
|
||||
} |
|
||||
) |
|
||||
}) |
}) |
||||
|
|
||||
const checkValidity = async (values, validator) => { |
const setupValidation = async () => { |
||||
const obj = object().shape(validator) |
const applications = svelteGet(apps) |
||||
Object.keys(validator).forEach(key => ($errors[key] = null)) |
appValidation.name(validation, { apps: applications, currentApp: app }) |
||||
try { |
appValidation.url(validation, { apps: applications, currentApp: app }) |
||||
await obj.validate(values, { abortEarly: false }) |
// init validation |
||||
} catch (validationErrors) { |
validation.check($values) |
||||
validationErrors.inner.forEach(error => { |
|
||||
$errors[error.path] = capitalise(error.message) |
|
||||
}) |
|
||||
} |
|
||||
valid = await obj.isValid(values) |
|
||||
} |
} |
||||
|
|
||||
async function updateApp() { |
async function updateApp() { |
||||
try { |
try { |
||||
// Update App |
// Update App |
||||
await apps.update(app.instance._id, { name: $values.name.trim() }) |
const body = { |
||||
hide() |
name: $values.name.trim(), |
||||
|
} |
||||
|
if ($values.url) { |
||||
|
body.url = $values.url.trim() |
||||
|
} |
||||
|
await apps.update(app.instance._id, body) |
||||
} catch (error) { |
} catch (error) { |
||||
console.error(error) |
console.error(error) |
||||
notifications.error(error) |
notifications.error(error) |
||||
} |
} |
||||
} |
} |
||||
|
|
||||
export const show = () => { |
// auto add slash to url |
||||
modal.show() |
$: { |
||||
} |
if ($values.url && !$values.url.startsWith("/")) { |
||||
export const hide = () => { |
$values.url = `/${$values.url}` |
||||
modal.hide() |
} |
||||
} |
|
||||
|
|
||||
const onCancel = () => { |
|
||||
hide() |
|
||||
} |
|
||||
|
|
||||
const onShow = () => { |
|
||||
dirty = false |
|
||||
} |
} |
||||
</script> |
</script> |
||||
|
|
||||
<Modal bind:this={modal} on:hide={onCancel} on:show={onShow}> |
<ModalContent |
||||
<ModalContent |
title={"Edit app"} |
||||
title={"Edit app"} |
confirmText={"Save"} |
||||
confirmText={"Save"} |
onConfirm={updateApp} |
||||
onConfirm={updateApp} |
disabled={!$validation.valid} |
||||
disabled={!(valid && dirty)} |
> |
||||
> |
<Body size="S">Update the name of your app.</Body> |
||||
<Body size="S">Update the name of your app.</Body> |
<Input |
||||
<Input |
bind:value={$values.name} |
||||
bind:value={$values.name} |
error={$validation.touched.name && $validation.errors.name} |
||||
error={$touched.name && $errors.name} |
on:blur={() => ($validation.touched.name = true)} |
||||
on:blur={() => ($touched.name = true)} |
label="Name" |
||||
on:change={() => (dirty = true)} |
/> |
||||
label="Name" |
<Input |
||||
/> |
bind:value={$values.url} |
||||
</ModalContent> |
error={$validation.touched.url && $validation.errors.url} |
||||
</Modal> |
on:blur={() => ($validation.touched.url = true)} |
||||
|
label="URL" |
||||
|
placeholder={$values.name |
||||
|
? "/" + encodeURIComponent($values.name).toLowerCase() |
||||
|
: "/"} |
||||
|
/> |
||||
|
</ModalContent> |
||||
|
|||||
@ -0,0 +1,83 @@ |
|||||
|
import { string, mixed } from "yup" |
||||
|
import { APP_NAME_REGEX, APP_URL_REGEX } from "constants" |
||||
|
|
||||
|
export const name = (validation, { apps, currentApp } = { apps: [] }) => { |
||||
|
validation.addValidator( |
||||
|
"name", |
||||
|
string() |
||||
|
.trim() |
||||
|
.required("Your application must have a name") |
||||
|
.matches( |
||||
|
APP_NAME_REGEX, |
||||
|
"App name must be letters, numbers and spaces only" |
||||
|
) |
||||
|
.test( |
||||
|
"non-existing-app-name", |
||||
|
"Another app with the same name already exists", |
||||
|
value => { |
||||
|
if (!value) { |
||||
|
// exit early, above validator will fail
|
||||
|
return true |
||||
|
} |
||||
|
if (currentApp) { |
||||
|
// filter out the current app if present
|
||||
|
apps = apps.filter(app => app.appId !== currentApp.appId) |
||||
|
} |
||||
|
return !apps |
||||
|
.map(app => app.name) |
||||
|
.some(appName => appName.toLowerCase() === value.toLowerCase()) |
||||
|
} |
||||
|
) |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
export const url = (validation, { apps, currentApp } = { apps: [] }) => { |
||||
|
validation.addValidator( |
||||
|
"url", |
||||
|
string() |
||||
|
.nullable() |
||||
|
.matches(APP_URL_REGEX, "App URL must not contain spaces") |
||||
|
.test( |
||||
|
"non-existing-app-url", |
||||
|
"Another app with the same URL already exists", |
||||
|
value => { |
||||
|
// url is nullable
|
||||
|
if (!value) { |
||||
|
return true |
||||
|
} |
||||
|
if (currentApp) { |
||||
|
// filter out the current app if present
|
||||
|
apps = apps.filter(app => app.appId !== currentApp.appId) |
||||
|
} |
||||
|
return !apps |
||||
|
.map(app => app.url) |
||||
|
.some(appUrl => appUrl?.toLowerCase() === value.toLowerCase()) |
||||
|
} |
||||
|
) |
||||
|
.test("valid-url", "Not a valid URL", value => { |
||||
|
// url is nullable
|
||||
|
if (!value) { |
||||
|
return true |
||||
|
} |
||||
|
// make it clear that this is a url path and cannot be a full url
|
||||
|
return ( |
||||
|
value.startsWith("/") && |
||||
|
!value.includes("http") && |
||||
|
!value.includes("www") && |
||||
|
!value.includes(".") && |
||||
|
value.length > 1 // just '/' is not valid
|
||||
|
) |
||||
|
}) |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
export const file = (validation, { template } = {}) => { |
||||
|
const templateToUse = |
||||
|
template && Object.keys(template).length === 0 ? null : template |
||||
|
validation.addValidator( |
||||
|
"file", |
||||
|
templateToUse?.fromFile |
||||
|
? mixed().required("Please choose a file to import") |
||||
|
: null |
||||
|
) |
||||
|
} |
||||
@ -0,0 +1,66 @@ |
|||||
|
import { capitalise } from "helpers" |
||||
|
import { object } from "yup" |
||||
|
import { writable, get } from "svelte/store" |
||||
|
import { notifications } from "@budibase/bbui" |
||||
|
|
||||
|
export const createValidationStore = () => { |
||||
|
const DEFAULT = { |
||||
|
errors: {}, |
||||
|
touched: {}, |
||||
|
valid: false, |
||||
|
} |
||||
|
|
||||
|
const validator = {} |
||||
|
const validation = writable(DEFAULT) |
||||
|
|
||||
|
const addValidator = (propertyName, propertyValidator) => { |
||||
|
if (!propertyValidator || !propertyName) { |
||||
|
return |
||||
|
} |
||||
|
validator[propertyName] = propertyValidator |
||||
|
} |
||||
|
|
||||
|
const check = async values => { |
||||
|
const obj = object().shape(validator) |
||||
|
// clear the previous errors
|
||||
|
const properties = Object.keys(validator) |
||||
|
properties.forEach(property => (get(validation).errors[property] = null)) |
||||
|
|
||||
|
let validationError = false |
||||
|
try { |
||||
|
await obj.validate(values, { abortEarly: false }) |
||||
|
} catch (error) { |
||||
|
if (!error.inner) { |
||||
|
notifications.error("Unexpected validation error", error) |
||||
|
validationError = true |
||||
|
} else { |
||||
|
error.inner.forEach(err => { |
||||
|
validation.update(store => { |
||||
|
store.errors[err.path] = capitalise(err.message) |
||||
|
return store |
||||
|
}) |
||||
|
}) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
let valid |
||||
|
if (properties.length && !validationError) { |
||||
|
valid = await obj.isValid(values) |
||||
|
} else { |
||||
|
// don't say valid until validators have been loaded
|
||||
|
valid = false |
||||
|
} |
||||
|
|
||||
|
validation.update(store => { |
||||
|
store.valid = valid |
||||
|
return store |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
return { |
||||
|
subscribe: validation.subscribe, |
||||
|
set: validation.set, |
||||
|
check, |
||||
|
addValidator, |
||||
|
} |
||||
|
} |
||||
File diff suppressed because it is too large
@ -0,0 +1,143 @@ |
|||||
|
<script> |
||||
|
import Field from "./Field.svelte" |
||||
|
import { CoreDropzone, ProgressCircle } from "@budibase/bbui" |
||||
|
import { getContext, onMount, onDestroy } from "svelte" |
||||
|
|
||||
|
export let datasourceId |
||||
|
export let bucket |
||||
|
export let key |
||||
|
export let field |
||||
|
export let label |
||||
|
export let disabled = false |
||||
|
export let validation |
||||
|
|
||||
|
let fieldState |
||||
|
let fieldApi |
||||
|
|
||||
|
const { API, notificationStore, uploadStore } = getContext("sdk") |
||||
|
const component = getContext("component") |
||||
|
|
||||
|
// 5GB cap per item sent via S3 REST API |
||||
|
const MaxFileSize = 1000000000 * 5 |
||||
|
|
||||
|
// Actual file data to upload |
||||
|
let data |
||||
|
let loading = false |
||||
|
|
||||
|
const handleFileTooLarge = () => { |
||||
|
notificationStore.actions.warning( |
||||
|
"Files cannot exceed 5GB. Please try again with a smaller file." |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
// Process the file input and return a serializable structure expected by |
||||
|
// the dropzone component to display the file |
||||
|
const processFiles = async fileList => { |
||||
|
return await new Promise(resolve => { |
||||
|
if (!fileList?.length) { |
||||
|
return [] |
||||
|
} |
||||
|
|
||||
|
// Don't read in non-image files |
||||
|
data = fileList[0] |
||||
|
if (!data.type?.startsWith("image")) { |
||||
|
resolve([ |
||||
|
{ |
||||
|
name: data.name, |
||||
|
type: data.type, |
||||
|
}, |
||||
|
]) |
||||
|
} |
||||
|
|
||||
|
// Read image files and display as preview |
||||
|
const reader = new FileReader() |
||||
|
reader.addEventListener( |
||||
|
"load", |
||||
|
() => { |
||||
|
resolve([ |
||||
|
{ |
||||
|
url: reader.result, |
||||
|
name: data.name, |
||||
|
type: data.type, |
||||
|
}, |
||||
|
]) |
||||
|
}, |
||||
|
false |
||||
|
) |
||||
|
reader.readAsDataURL(fileList[0]) |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
const upload = async () => { |
||||
|
loading = true |
||||
|
try { |
||||
|
const res = await API.externalUpload(datasourceId, bucket, key, data) |
||||
|
notificationStore.actions.success("File uploaded successfully") |
||||
|
loading = false |
||||
|
return res |
||||
|
} catch (error) { |
||||
|
notificationStore.actions.error(`Error uploading file: ${error}`) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
onMount(() => { |
||||
|
uploadStore.actions.registerFileUpload($component.id, upload) |
||||
|
}) |
||||
|
|
||||
|
onDestroy(() => { |
||||
|
uploadStore.actions.unregisterFileUpload($component.id) |
||||
|
}) |
||||
|
</script> |
||||
|
|
||||
|
<Field |
||||
|
{label} |
||||
|
{field} |
||||
|
{disabled} |
||||
|
{validation} |
||||
|
type="s3upload" |
||||
|
bind:fieldState |
||||
|
bind:fieldApi |
||||
|
defaultValue={[]} |
||||
|
> |
||||
|
<div class="content"> |
||||
|
{#if fieldState} |
||||
|
<CoreDropzone |
||||
|
value={fieldState.value} |
||||
|
disabled={loading || fieldState.disabled} |
||||
|
error={fieldState.error} |
||||
|
on:change={e => { |
||||
|
fieldApi.setValue(e.detail) |
||||
|
}} |
||||
|
{processFiles} |
||||
|
{handleFileTooLarge} |
||||
|
maximum={1} |
||||
|
fileSizeLimit={MaxFileSize} |
||||
|
/> |
||||
|
{/if} |
||||
|
{#if loading} |
||||
|
<div class="overlay" /> |
||||
|
<div class="loading"> |
||||
|
<ProgressCircle /> |
||||
|
</div> |
||||
|
{/if} |
||||
|
</div> |
||||
|
</Field> |
||||
|
|
||||
|
<style> |
||||
|
.content { |
||||
|
position: relative; |
||||
|
} |
||||
|
.overlay, |
||||
|
.loading { |
||||
|
position: absolute; |
||||
|
top: 0; |
||||
|
height: 100%; |
||||
|
width: 100%; |
||||
|
display: grid; |
||||
|
place-items: center; |
||||
|
} |
||||
|
.overlay { |
||||
|
background-color: var(--spectrum-global-color-gray-50); |
||||
|
opacity: 0.5; |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,42 @@ |
|||||
|
import { writable, get } from "svelte/store" |
||||
|
|
||||
|
export const createUploadStore = () => { |
||||
|
const store = writable([]) |
||||
|
|
||||
|
// Registers a new file upload component
|
||||
|
const registerFileUpload = (componentId, callback) => { |
||||
|
if (!componentId || !callback) { |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
store.update(state => { |
||||
|
state.push({ |
||||
|
componentId, |
||||
|
callback, |
||||
|
}) |
||||
|
return state |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
// Unregisters a file upload component
|
||||
|
const unregisterFileUpload = componentId => { |
||||
|
store.update(state => state.filter(c => c.componentId !== componentId)) |
||||
|
} |
||||
|
|
||||
|
// Processes a file upload for a given component ID
|
||||
|
const processFileUpload = async componentId => { |
||||
|
if (!componentId) { |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
const component = get(store).find(c => c.componentId === componentId) |
||||
|
return await component?.callback() |
||||
|
} |
||||
|
|
||||
|
return { |
||||
|
subscribe: store.subscribe, |
||||
|
actions: { registerFileUpload, unregisterFileUpload, processFileUpload }, |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export const uploadStore = createUploadStore() |
||||
File diff suppressed because it is too large
@ -1,5 +1,5 @@ |
|||||
{ |
{ |
||||
"watch": ["src", "../auth"], |
"watch": ["src", "../backend-core"], |
||||
"ext": "js,ts,json", |
"ext": "js,ts,json", |
||||
"ignore": ["src/**/*.spec.ts", "src/**/*.spec.js"], |
"ignore": ["src/**/*.spec.ts", "src/**/*.spec.js"], |
||||
"exec": "ts-node src/index.ts" |
"exec": "ts-node src/index.ts" |
||||
|
|||||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue