merge master

This commit is contained in:
Gerard Burns 2023-11-24 11:53:32 +00:00
commit fdcfd1be02
387 changed files with 4295 additions and 2025 deletions

View File

@ -19,6 +19,7 @@
"bundle.js" "bundle.js"
], ],
"extends": ["eslint:recommended"], "extends": ["eslint:recommended"],
"plugins": ["import", "eslint-plugin-local-rules"],
"overrides": [ "overrides": [
{ {
"files": ["**/*.svelte"], "files": ["**/*.svelte"],
@ -30,7 +31,6 @@
"sourceType": "module", "sourceType": "module",
"allowImportExportEverywhere": true "allowImportExportEverywhere": true
} }
}, },
{ {
"files": ["**/*.ts"], "files": ["**/*.ts"],
@ -42,13 +42,25 @@
"no-case-declarations": "off", "no-case-declarations": "off",
"no-useless-escape": "off", "no-useless-escape": "off",
"no-undef": "off", "no-undef": "off",
"no-prototype-builtins": "off" "no-prototype-builtins": "off",
"local-rules/no-budibase-imports": "error"
} }
} }
], ],
"rules": { "rules": {
"no-self-assign": "off", "no-self-assign": "off",
"no-unused-vars": ["error", { "varsIgnorePattern": "^_", "argsIgnorePattern": "^_", "destructuredArrayIgnorePattern": "^_" }] "no-unused-vars": [
"error",
{
"varsIgnorePattern": "^_",
"argsIgnorePattern": "^_",
"destructuredArrayIgnorePattern": "^_"
}
],
"import/no-relative-packages": "error",
"import/export": "error",
"import/no-duplicates": "error",
"import/newline-after-import": "error"
}, },
"globals": { "globals": {
"GeolocationPositionError": true "GeolocationPositionError": true

View File

@ -12,6 +12,13 @@ on:
- master - master
pull_request: pull_request:
workflow_dispatch: workflow_dispatch:
workflow_call:
inputs:
run_as_oss:
type: boolean
required: false
description: Force running checks as if it was an OSS contributor
default: false
env: env:
BRANCH: ${{ github.event.pull_request.head.ref }} BRANCH: ${{ github.event.pull_request.head.ref }}
@ -19,50 +26,41 @@ env:
PERSONAL_ACCESS_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }} PERSONAL_ACCESS_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
NX_BASE_BRANCH: origin/${{ github.base_ref }} NX_BASE_BRANCH: origin/${{ github.base_ref }}
USE_NX_AFFECTED: ${{ github.event_name == 'pull_request' }} USE_NX_AFFECTED: ${{ github.event_name == 'pull_request' }}
IS_OSS_CONTRIBUTOR: ${{ inputs.run_as_oss == true || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != 'Budibase/budibase') }}
jobs: jobs:
lint: lint:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout repo and submodules - name: Checkout repo
uses: actions/checkout@v3 uses: actions/checkout@v3
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'Budibase/budibase'
with: with:
submodules: true submodules: ${{ env.IS_OSS_CONTRIBUTOR == 'false' }}
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }} token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
- name: Checkout repo only
uses: actions/checkout@v3
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != 'Budibase/budibase'
- name: Use Node.js 18.x - name: Use Node.js 18.x
uses: actions/setup-node@v3 uses: actions/setup-node@v3
with: with:
node-version: 18.x node-version: 18.x
cache: "yarn" cache: yarn
- run: yarn --frozen-lockfile - run: yarn --frozen-lockfile
- run: yarn lint - run: yarn lint
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout repo and submodules - name: Checkout repo
uses: actions/checkout@v3 uses: actions/checkout@v3
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'Budibase/budibase'
with: with:
submodules: true submodules: ${{ env.IS_OSS_CONTRIBUTOR == 'false' }}
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }} token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
fetch-depth: 0 fetch-depth: 0
- name: Checkout repo only
uses: actions/checkout@v3
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != 'Budibase/budibase'
with:
fetch-depth: 0
- name: Use Node.js 18.x - name: Use Node.js 18.x
uses: actions/setup-node@v3 uses: actions/setup-node@v3
with: with:
node-version: 18.x node-version: 18.x
cache: "yarn" cache: yarn
- run: yarn --frozen-lockfile - run: yarn --frozen-lockfile
# Run build all the projects # Run build all the projects
@ -81,24 +79,18 @@ jobs:
test-libraries: test-libraries:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout repo and submodules - name: Checkout repo
uses: actions/checkout@v3 uses: actions/checkout@v3
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'Budibase/budibase'
with: with:
submodules: true submodules: ${{ env.IS_OSS_CONTRIBUTOR == 'false' }}
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }} token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
fetch-depth: 0 fetch-depth: 0
- name: Checkout repo only
uses: actions/checkout@v3
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != 'Budibase/budibase'
with:
fetch-depth: 0
- name: Use Node.js 18.x - name: Use Node.js 18.x
uses: actions/setup-node@v3 uses: actions/setup-node@v3
with: with:
node-version: 18.x node-version: 18.x
cache: "yarn" cache: yarn
- run: yarn --frozen-lockfile - run: yarn --frozen-lockfile
- name: Test - name: Test
run: | run: |
@ -116,24 +108,18 @@ jobs:
test-worker: test-worker:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout repo and submodules - name: Checkout repo
uses: actions/checkout@v3 uses: actions/checkout@v3
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'Budibase/budibase'
with: with:
submodules: true submodules: ${{ env.IS_OSS_CONTRIBUTOR == 'false' }}
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }} token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
fetch-depth: 0 fetch-depth: 0
- name: Checkout repo only
uses: actions/checkout@v3
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != 'Budibase/budibase'
with:
fetch-depth: 0
- name: Use Node.js 18.x - name: Use Node.js 18.x
uses: actions/setup-node@v3 uses: actions/setup-node@v3
with: with:
node-version: 18.x node-version: 18.x
cache: "yarn" cache: yarn
- run: yarn --frozen-lockfile - run: yarn --frozen-lockfile
- name: Test worker - name: Test worker
run: | run: |
@ -152,24 +138,18 @@ jobs:
test-server: test-server:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout repo and submodules - name: Checkout repo
uses: actions/checkout@v3 uses: actions/checkout@v3
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'Budibase/budibase'
with: with:
submodules: true submodules: ${{ env.IS_OSS_CONTRIBUTOR == 'false' }}
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }} token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
fetch-depth: 0 fetch-depth: 0
- name: Checkout repo only
uses: actions/checkout@v3
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != 'Budibase/budibase'
with:
fetch-depth: 0
- name: Use Node.js 18.x - name: Use Node.js 18.x
uses: actions/setup-node@v3 uses: actions/setup-node@v3
with: with:
node-version: 18.x node-version: 18.x
cache: "yarn" cache: yarn
- run: yarn --frozen-lockfile - run: yarn --frozen-lockfile
- name: Test server - name: Test server
run: | run: |
@ -200,7 +180,7 @@ jobs:
uses: actions/setup-node@v3 uses: actions/setup-node@v3
with: with:
node-version: 18.x node-version: 18.x
cache: "yarn" cache: yarn
- run: yarn --frozen-lockfile - run: yarn --frozen-lockfile
- name: Test - name: Test
run: | run: |
@ -213,24 +193,23 @@ jobs:
integration-test: integration-test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout repo and submodules - name: Checkout repo
uses: actions/checkout@v3 uses: actions/checkout@v3
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'Budibase/budibase'
with: with:
submodules: true submodules: ${{ env.IS_OSS_CONTRIBUTOR == 'false' }}
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }} token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
- name: Checkout repo only
uses: actions/checkout@v3
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != 'Budibase/budibase'
- name: Use Node.js 18.x - name: Use Node.js 18.x
uses: actions/setup-node@v3 uses: actions/setup-node@v3
with: with:
node-version: 18.x node-version: 18.x
cache: "yarn" cache: yarn
- run: yarn --frozen-lockfile - run: yarn --frozen-lockfile
- name: Build packages - name: Build packages
run: yarn build --scope @budibase/server --scope @budibase/worker run: yarn build --scope @budibase/server --scope @budibase/worker
- name: Build backend-core for OSS contributor (required for pro)
if: ${{ env.IS_OSS_CONTRIBUTOR == 'true' }}
run: yarn build --scope @budibase/backend-core
- name: Run tests - name: Run tests
run: | run: |
cd qa-core cd qa-core

View File

@ -0,0 +1,35 @@
name: OSS contributor checks
on:
workflow_dispatch:
schedule:
- cron: "0 8,16 * * 1-5" # on weekdays at 8am and 4pm
jobs:
run-checks:
name: Publish server and worker docker images
uses: ./.github/workflows/budibase_ci.yml
with:
run_as_oss: true
secrets: inherit
notify-error:
needs: ["run-checks"]
if: ${{ failure() }}
name: Notify error
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set commit SHA
id: set_sha
run: echo "::set-output name=sha::$(git rev-parse --short ${{ github.sha }})"
- name: Notify error
uses: tsickert/discord-webhook@v5.3.0
with:
webhook-url: ${{ secrets.OSS_CHECKS_WEBHOOK_URL }}
embed-title: 🚨 OSS checks failed in master
embed-url: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
embed-description: |
Git sha: `${{ steps.set_sha.outputs.sha }}`

View File

@ -7,6 +7,7 @@ on:
jobs: jobs:
release: release:
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == 'Budibase/budibase'
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3

View File

@ -1,13 +1,11 @@
node_modules node_modules
dist dist
*.spec.js
packages/builder/src/components/design/AppPreview/CurrentItemPreview.svelte
packages/server/builder packages/server/builder
packages/server/coverage packages/server/coverage
packages/worker/coverage
packages/backend-core/coverage
packages/server/client packages/server/client
packages/server/src/definitions/openapi.ts packages/server/src/definitions/openapi.ts
packages/worker/coverage
packages/backend-core/coverage
packages/builder/.routify packages/builder/.routify
packages/sdk/sdk packages/sdk/sdk
packages/pro/coverage packages/pro/coverage

View File

@ -46,11 +46,9 @@ spec:
image: minio/minio image: minio/minio
imagePullPolicy: "" imagePullPolicy: ""
livenessProbe: livenessProbe:
exec: httpGet:
command: path: /minio/health/live
- curl port: 9000
- -f
- http://localhost:9000/minio/health/live
failureThreshold: 3 failureThreshold: 3
periodSeconds: 30 periodSeconds: 30
timeoutSeconds: 20 timeoutSeconds: 20

View File

@ -0,0 +1,21 @@
module.exports = {
"no-budibase-imports": {
create: function (context) {
return {
ImportDeclaration(node) {
const importPath = node.source.value
if (
/^@budibase\/[^/]+\/.*$/.test(importPath) &&
importPath !== "@budibase/backend-core/tests"
) {
context.report({
node,
message: `Importing from @budibase is not allowed, except for @budibase/backend-core/tests.`,
})
}
},
}
},
},
}

View File

@ -1,24 +0,0 @@
#!/bin/bash
echo ${TARGETBUILD} > /buildtarget.txt
if [[ "${TARGETBUILD}" = "aas" ]]; then
# Azure AppService uses /home for persistent data & SSH on port 2222
DATA_DIR="${DATA_DIR:-/home}"
WEBSITES_ENABLE_APP_SERVICE_STORAGE=true
mkdir -p $DATA_DIR/{search,minio,couch}
mkdir -p $DATA_DIR/couch/{dbs,views}
chown -R couchdb:couchdb $DATA_DIR/couch/
apt update
apt-get install -y openssh-server
echo "root:Docker!" | chpasswd
mkdir -p /tmp
chmod +x /tmp/ssh_setup.sh \
&& (sleep 1;/tmp/ssh_setup.sh 2>&1 > /dev/null)
cp /etc/sshd_config /etc/ssh/sshd_config
/etc/init.d/ssh restart
sed -i "s#DATA_DIR#/home#g" /opt/clouseau/clouseau.ini
sed -i "s#DATA_DIR#/home#g" /opt/couchdb/etc/local.ini
else
sed -i "s#DATA_DIR#/data#g" /opt/clouseau/clouseau.ini
sed -i "s#DATA_DIR#/data#g" /opt/couchdb/etc/local.ini
fi

View File

@ -25,7 +25,7 @@ if [[ $(curl -s -w "%{http_code}\n" http://localhost:4002/health -o /dev/null) -
healthy=false healthy=false
fi fi
if [[ $(curl -s -w "%{http_code}\n" http://localhost:5984/ -o /dev/null) -ne 200 ]]; then if [[ $(curl -s -w "%{http_code}\n" http://localhost:5984/_up -o /dev/null) -ne 200 ]]; then
echo 'ERROR: CouchDB is not running'; echo 'ERROR: CouchDB is not running';
healthy=false healthy=false
fi fi

View File

@ -1,5 +1,5 @@
{ {
"version": "2.13.9", "version": "2.13.15",
"npmClient": "yarn", "npmClient": "yarn",
"packages": [ "packages": [
"packages/*" "packages/*"

View File

@ -2,11 +2,17 @@
"name": "root", "name": "root",
"private": true, "private": true,
"devDependencies": { "devDependencies": {
"@babel/core": "^7.22.5",
"@babel/eslint-parser": "^7.22.5",
"@babel/preset-env": "^7.22.5",
"@esbuild-plugins/tsconfig-paths": "^0.1.2", "@esbuild-plugins/tsconfig-paths": "^0.1.2",
"@typescript-eslint/parser": "6.7.2", "@typescript-eslint/parser": "6.7.2",
"esbuild": "^0.18.17", "esbuild": "^0.18.17",
"esbuild-node-externals": "^1.8.0", "esbuild-node-externals": "^1.8.0",
"eslint": "^8.44.0", "eslint": "^8.44.0",
"eslint-plugin-import": "^2.29.0",
"eslint-plugin-local-rules": "^2.0.0",
"eslint-plugin-svelte": "^2.32.2",
"husky": "^8.0.3", "husky": "^8.0.3",
"kill-port": "^1.6.1", "kill-port": "^1.6.1",
"lerna": "7.1.1", "lerna": "7.1.1",
@ -17,12 +23,8 @@
"prettier": "2.8.8", "prettier": "2.8.8",
"prettier-plugin-svelte": "^2.3.0", "prettier-plugin-svelte": "^2.3.0",
"svelte": "3.49.0", "svelte": "3.49.0",
"typescript": "5.2.2", "svelte-eslint-parser": "^0.32.0",
"@babel/core": "^7.22.5", "typescript": "5.2.2"
"@babel/eslint-parser": "^7.22.5",
"@babel/preset-env": "^7.22.5",
"eslint-plugin-svelte": "^2.32.2",
"svelte-eslint-parser": "^0.32.0"
}, },
"scripts": { "scripts": {
"preinstall": "node scripts/syncProPackage.js", "preinstall": "node scripts/syncProPackage.js",

View File

@ -1,5 +1,6 @@
const _passport = require("koa-passport") const _passport = require("koa-passport")
const LocalStrategy = require("passport-local").Strategy const LocalStrategy = require("passport-local").Strategy
import { getGlobalDB } from "../context" import { getGlobalDB } from "../context"
import { Cookie } from "../constants" import { Cookie } from "../constants"
import { getSessionsForUser, invalidateSessions } from "../security/sessions" import { getSessionsForUser, invalidateSessions } from "../security/sessions"
@ -26,6 +27,7 @@ import { clearCookie, getCookie } from "../utils"
import { ssoSaveUserNoOp } from "../middleware/passport/sso/sso" import { ssoSaveUserNoOp } from "../middleware/passport/sso/sso"
const refresh = require("passport-oauth2-refresh") const refresh = require("passport-oauth2-refresh")
export { export {
auditLog, auditLog,
authError, authError,

View File

@ -1,6 +1,6 @@
const BaseCache = require("./base") import BaseCache from "./base"
const GENERIC = new BaseCache.default() const GENERIC = new BaseCache()
export enum CacheKey { export enum CacheKey {
CHECKLIST = "checklist", CHECKLIST = "checklist",
@ -19,6 +19,7 @@ export enum TTL {
} }
function performExport(funcName: string) { function performExport(funcName: string) {
// @ts-ignore
return (...args: any) => GENERIC[funcName](...args) return (...args: any) => GENERIC[funcName](...args)
} }

View File

@ -2,4 +2,6 @@ export * as generic from "./generic"
export * as user from "./user" export * as user from "./user"
export * as app from "./appMetadata" export * as app from "./appMetadata"
export * as writethrough from "./writethrough" export * as writethrough from "./writethrough"
export * as invite from "./invite"
export * as passwordReset from "./passwordReset"
export * from "./generic" export * from "./generic"

View File

@ -0,0 +1,86 @@
import * as utils from "../utils"
import { Duration, DurationType } from "../utils"
import env from "../environment"
import { getTenantId } from "../context"
import * as redis from "../redis/init"
const TTL_SECONDS = Duration.fromDays(7).toSeconds()
interface Invite {
email: string
info: any
}
interface InviteWithCode extends Invite {
code: string
}
/**
* Given an invite code and invite body, allow the update an existing/valid invite in redis
* @param code The invite code for an invite in redis
* @param value The body of the updated user invitation
*/
export async function updateCode(code: string, value: Invite) {
const client = await redis.getInviteClient()
await client.store(code, value, TTL_SECONDS)
}
/**
* Generates an invitation code and writes it to redis - which can later be checked for user creation.
* @param email the email address which the code is being sent to (for use later).
* @param info Information to be carried along with the invitation.
* @return returns the code that was stored to redis.
*/
export async function createCode(email: string, info: any): Promise<string> {
const code = utils.newid()
const client = await redis.getInviteClient()
await client.store(code, { email, info }, TTL_SECONDS)
return code
}
/**
* Checks that the provided invite code is valid - will return the email address of user that was invited.
* @param code the invite code that was provided as part of the link.
* @return If the code is valid then an email address will be returned.
*/
export async function getCode(code: string): Promise<Invite> {
const client = await redis.getInviteClient()
const value = (await client.get(code)) as Invite | undefined
if (!value) {
throw "Invitation is not valid or has expired, please request a new one."
}
return value
}
export async function deleteCode(code: string) {
const client = await redis.getInviteClient()
await client.delete(code)
}
/**
Get all currently available user invitations for the current tenant.
**/
export async function getInviteCodes(): Promise<InviteWithCode[]> {
const client = await redis.getInviteClient()
const invites: { key: string; value: Invite }[] = await client.scan()
const results: InviteWithCode[] = invites.map(invite => {
return {
...invite.value,
code: invite.key,
}
})
if (!env.MULTI_TENANCY) {
return results
}
const tenantId = getTenantId()
return results.filter(invite => tenantId === invite.info.tenantId)
}
export async function getExistingInvites(
emails: string[]
): Promise<InviteWithCode[]> {
return (await getInviteCodes()).filter(invite =>
emails.includes(invite.email)
)
}

View File

@ -0,0 +1,38 @@
import * as redis from "../redis/init"
import * as utils from "../utils"
import { Duration, DurationType } from "../utils"
const TTL_SECONDS = Duration.fromHours(1).toSeconds()
interface PasswordReset {
userId: string
info: any
}
/**
* Given a user ID this will store a code (that is returned) for an hour in redis.
* The user can then return this code for resetting their password (through their reset link).
* @param userId the ID of the user which is to be reset.
* @param info Info about the user/the reset process.
* @return returns the code that was stored to redis.
*/
export async function createCode(userId: string, info: any): Promise<string> {
const code = utils.newid()
const client = await redis.getPasswordResetClient()
await client.store(code, { userId, info }, TTL_SECONDS)
return code
}
/**
* Given a reset code this will lookup to redis, check if the code is valid.
* @param code The code provided via the email link.
* @return returns the user ID if it is found
*/
export async function getCode(code: string): Promise<PasswordReset> {
const client = await redis.getPasswordResetClient()
const value = (await client.get(code)) as PasswordReset | undefined
if (!value) {
throw "Provided information is not valid, cannot reset password - please try again."
}
return value
}

View File

@ -17,7 +17,6 @@ import { DocumentType, SEPARATOR } from "../constants"
import { CacheKey, TTL, withCache } from "../cache" import { CacheKey, TTL, withCache } from "../cache"
import * as context from "../context" import * as context from "../context"
import env from "../environment" import env from "../environment"
import environment from "../environment"
// UTILS // UTILS
@ -181,10 +180,10 @@ export async function getGoogleDatasourceConfig(): Promise<
} }
export function getDefaultGoogleConfig(): GoogleInnerConfig | undefined { export function getDefaultGoogleConfig(): GoogleInnerConfig | undefined {
if (environment.GOOGLE_CLIENT_ID && environment.GOOGLE_CLIENT_SECRET) { if (env.GOOGLE_CLIENT_ID && env.GOOGLE_CLIENT_SECRET) {
return { return {
clientID: environment.GOOGLE_CLIENT_ID!, clientID: env.GOOGLE_CLIENT_ID!,
clientSecret: environment.GOOGLE_CLIENT_SECRET!, clientSecret: env.GOOGLE_CLIENT_SECRET!,
activated: true, activated: true,
} }
} }

View File

@ -1,4 +1,5 @@
import { prefixed, DocumentType } from "@budibase/types" import { prefixed, DocumentType } from "@budibase/types"
export { export {
SEPARATOR, SEPARATOR,
UNICODE_MAX, UNICODE_MAX,

View File

@ -5,7 +5,6 @@ const { getDB } = require("../db")
describe("db", () => { describe("db", () => {
describe("getDB", () => { describe("getDB", () => {
it("returns a db", async () => { it("returns a db", async () => {
const dbName = structures.db.id() const dbName = structures.db.id()
const db = getDB(dbName) const db = getDB(dbName)
expect(db).toBeDefined() expect(db).toBeDefined()

View File

@ -6,6 +6,7 @@ import { AppState, DeletedApp, getAppMetadata } from "../cache/appMetadata"
import { isDevApp, isDevAppID, getProdAppID } from "../docIds/conversions" import { isDevApp, isDevAppID, getProdAppID } from "../docIds/conversions"
import { App, Database } from "@budibase/types" import { App, Database } from "@budibase/types"
import { getStartEndKeyURL } from "../docIds" import { getStartEndKeyURL } from "../docIds"
export * from "../docIds" export * from "../docIds"
/** /**

View File

@ -1,5 +1,6 @@
import { APP_DEV_PREFIX, APP_PREFIX } from "../constants" import { APP_DEV_PREFIX, APP_PREFIX } from "../constants"
import { App } from "@budibase/types" import { App } from "@budibase/types"
const NO_APP_ERROR = "No app provided" const NO_APP_ERROR = "No app provided"
export function isDevAppID(appId?: string) { export function isDevAppID(appId?: string) {

View File

@ -1,2 +1,3 @@
import PosthogProcessor from "./PosthogProcessor" import PosthogProcessor from "./PosthogProcessor"
export default PosthogProcessor export default PosthogProcessor

View File

@ -1,7 +1,9 @@
import { testEnv } from "../../../../../tests/extra" import { testEnv } from "../../../../../tests/extra"
import PosthogProcessor from "../PosthogProcessor" import PosthogProcessor from "../PosthogProcessor"
import { Event, IdentityType, Hosting } from "@budibase/types" import { Event, IdentityType, Hosting } from "@budibase/types"
const tk = require("timekeeper") const tk = require("timekeeper")
import * as cache from "../../../../cache/generic" import * as cache from "../../../../cache/generic"
import { CacheKey } from "../../../../cache/generic" import { CacheKey } from "../../../../cache/generic"
import * as context from "../../../../context" import * as context from "../../../../context"

View File

@ -1,5 +1,6 @@
import env from "../environment" import env from "../environment"
import * as context from "../context" import * as context from "../context"
export * from "./installation" export * from "./installation"
/** /**

View File

@ -32,11 +32,13 @@ export * as blacklist from "./blacklist"
export * as docUpdates from "./docUpdates" export * as docUpdates from "./docUpdates"
export * from "./utils/Duration" export * from "./utils/Duration"
export { SearchParams } from "./db" export { SearchParams } from "./db"
export * as docIds from "./docIds"
// Add context to tenancy for backwards compatibility // Add context to tenancy for backwards compatibility
// only do this for external usages to prevent internal // only do this for external usages to prevent internal
// circular dependencies // circular dependencies
import * as context from "./context" import * as context from "./context"
import * as _tenancy from "./tenancy" import * as _tenancy from "./tenancy"
export const tenancy = { export const tenancy = {
..._tenancy, ..._tenancy,
...context, ...context,
@ -50,6 +52,7 @@ export * from "./constants"
// expose package init function // expose package init function
import * as db from "./db" import * as db from "./db"
export const init = (opts: any = {}) => { export const init = (opts: any = {}) => {
db.init(opts.db) db.init(opts.db)
} }

View File

@ -1,7 +1,6 @@
import { newid } from "./utils" import { newid } from "./utils"
import * as events from "./events" import * as events from "./events"
import { StaticDatabases } from "./db" import { StaticDatabases, doWithDB } from "./db"
import { doWithDB } from "./db"
import { Installation, IdentityType, Database } from "@budibase/types" import { Installation, IdentityType, Database } from "@budibase/types"
import * as context from "./context" import * as context from "./context"
import semver from "semver" import semver from "semver"

View File

@ -1,4 +1,5 @@
import { Header } from "../../constants" import { Header } from "../../constants"
const correlator = require("correlation-id") const correlator = require("correlation-id")
export const setHeader = (headers: any) => { export const setHeader = (headers: any) => {

View File

@ -1,5 +1,6 @@
import { Header } from "../../constants" import { Header } from "../../constants"
import { v4 as uuid } from "uuid" import { v4 as uuid } from "uuid"
const correlator = require("correlation-id") const correlator = require("correlation-id")
const correlation = (ctx: any, next: any) => { const correlation = (ctx: any, next: any) => {

View File

@ -1,9 +1,12 @@
import env from "../../environment" import env from "../../environment"
import { logger } from "./logger" import { logger } from "./logger"
import { IncomingMessage } from "http" import { IncomingMessage } from "http"
const pino = require("koa-pino-logger") const pino = require("koa-pino-logger")
import { Options } from "pino-http" import { Options } from "pino-http"
import { Ctx } from "@budibase/types" import { Ctx } from "@budibase/types"
const correlator = require("correlation-id") const correlator = require("correlation-id")
export function pinoSettings(): Options { export function pinoSettings(): Options {

View File

@ -2,6 +2,7 @@ export * as local from "./passport/local"
export * as google from "./passport/sso/google" export * as google from "./passport/sso/google"
export * as oidc from "./passport/sso/oidc" export * as oidc from "./passport/sso/oidc"
import * as datasourceGoogle from "./passport/datasource/google" import * as datasourceGoogle from "./passport/datasource/google"
export const datasource = { export const datasource = {
google: datasourceGoogle, google: datasourceGoogle,
} }

View File

@ -8,6 +8,7 @@ import {
SaveSSOUserFunction, SaveSSOUserFunction,
GoogleInnerConfig, GoogleInnerConfig,
} from "@budibase/types" } from "@budibase/types"
const GoogleStrategy = require("passport-google-oauth").OAuth2Strategy const GoogleStrategy = require("passport-google-oauth").OAuth2Strategy
export function buildVerifyFn(saveUserFn: SaveSSOUserFunction) { export function buildVerifyFn(saveUserFn: SaveSSOUserFunction) {

View File

@ -6,6 +6,7 @@ const mockStrategy = require("passport-google-oauth").OAuth2Strategy
jest.mock("../sso") jest.mock("../sso")
import * as _sso from "../sso" import * as _sso from "../sso"
const sso = jest.mocked(_sso) const sso = jest.mocked(_sso)
const mockSaveUserFn = jest.fn() const mockSaveUserFn = jest.fn()

View File

@ -11,6 +11,7 @@ const mockSaveUser = jest.fn()
jest.mock("../../../../users") jest.mock("../../../../users")
import * as _users from "../../../../users" import * as _users from "../../../../users"
const users = jest.mocked(_users) const users = jest.mocked(_users)
const getErrorMessage = () => { const getErrorMessage = () => {

View File

@ -5,6 +5,7 @@ import { structures } from "../../../tests"
import { ContextUser, ServiceType } from "@budibase/types" import { ContextUser, ServiceType } from "@budibase/types"
import { doInAppContext } from "../../context" import { doInAppContext } from "../../context"
import env from "../../environment" import env from "../../environment"
env._set("SERVICE_TYPE", ServiceType.APPS) env._set("SERVICE_TYPE", ServiceType.APPS)
const appId = "app_aaa" const appId = "app_aaa"

View File

@ -1,4 +1,5 @@
const sanitize = require("sanitize-s3-objectkey") const sanitize = require("sanitize-s3-objectkey")
import AWS from "aws-sdk" import AWS from "aws-sdk"
import stream, { Readable } from "stream" import stream, { Readable } from "stream"
import fetch from "node-fetch" import fetch from "node-fetch"

View File

@ -7,15 +7,19 @@ let userClient: Client,
cacheClient: Client, cacheClient: Client,
writethroughClient: Client, writethroughClient: Client,
lockClient: Client, lockClient: Client,
socketClient: Client socketClient: Client,
inviteClient: Client,
passwordResetClient: Client
async function init() { export async function init() {
userClient = await new Client(utils.Databases.USER_CACHE).init() userClient = await new Client(utils.Databases.USER_CACHE).init()
sessionClient = await new Client(utils.Databases.SESSIONS).init() sessionClient = await new Client(utils.Databases.SESSIONS).init()
appClient = await new Client(utils.Databases.APP_METADATA).init() appClient = await new Client(utils.Databases.APP_METADATA).init()
cacheClient = await new Client(utils.Databases.GENERIC_CACHE).init() cacheClient = await new Client(utils.Databases.GENERIC_CACHE).init()
lockClient = await new Client(utils.Databases.LOCKS).init() lockClient = await new Client(utils.Databases.LOCKS).init()
writethroughClient = await new Client(utils.Databases.WRITE_THROUGH).init() writethroughClient = await new Client(utils.Databases.WRITE_THROUGH).init()
inviteClient = await new Client(utils.Databases.INVITATIONS).init()
passwordResetClient = await new Client(utils.Databases.PW_RESETS).init()
socketClient = await new Client( socketClient = await new Client(
utils.Databases.SOCKET_IO, utils.Databases.SOCKET_IO,
utils.SelectableDatabase.SOCKET_IO utils.SelectableDatabase.SOCKET_IO
@ -29,6 +33,8 @@ export async function shutdown() {
if (cacheClient) await cacheClient.finish() if (cacheClient) await cacheClient.finish()
if (writethroughClient) await writethroughClient.finish() if (writethroughClient) await writethroughClient.finish()
if (lockClient) await lockClient.finish() if (lockClient) await lockClient.finish()
if (inviteClient) await inviteClient.finish()
if (passwordResetClient) await passwordResetClient.finish()
if (socketClient) await socketClient.finish() if (socketClient) await socketClient.finish()
} }
@ -84,3 +90,17 @@ export async function getSocketClient() {
} }
return socketClient return socketClient
} }
export async function getInviteClient() {
if (!inviteClient) {
await init()
}
return inviteClient
}
export async function getPasswordResetClient() {
if (!passwordResetClient) {
await init()
}
return passwordResetClient
}

View File

@ -3,6 +3,7 @@ import { getLockClient } from "./init"
import { LockOptions, LockType } from "@budibase/types" import { LockOptions, LockType } from "@budibase/types"
import * as context from "../context" import * as context from "../context"
import env from "../environment" import env from "../environment"
import { logWarn } from "../logging"
async function getClient( async function getClient(
type: LockType, type: LockType,
@ -116,7 +117,7 @@ export async function doWithLock<T>(
const result = await task() const result = await task()
return { executed: true, result } return { executed: true, result }
} catch (e: any) { } catch (e: any) {
console.warn("lock error") logWarn(`lock type: ${opts.type} error`, e)
// lock limit exceeded // lock limit exceeded
if (e.name === "LockError") { if (e.name === "LockError") {
if (opts.type === LockType.TRY_ONCE) { if (opts.type === LockType.TRY_ONCE) {
@ -124,11 +125,9 @@ export async function doWithLock<T>(
// due to retry count (0) exceeded // due to retry count (0) exceeded
return { executed: false } return { executed: false }
} else { } else {
console.error(e)
throw e throw e
} }
} else { } else {
console.error(e)
throw e throw e
} }
} finally { } finally {

View File

@ -75,10 +75,12 @@ export function getRedisConnectionDetails() {
} }
const [host, port] = url.split(":") const [host, port] = url.split(":")
const portNumber = parseInt(port)
return { return {
host, host,
password, password,
port: parseInt(port), // assume default port for redis if invalid found
port: isNaN(portNumber) ? 6379 : portNumber,
} }
} }

View File

@ -1,7 +1,12 @@
import { BuiltinPermissionID, PermissionLevel } from "./permissions" import { BuiltinPermissionID, PermissionLevel } from "./permissions"
import { prefixRoleID, getRoleParams, DocumentType, SEPARATOR } from "../db" import {
prefixRoleID,
getRoleParams,
DocumentType,
SEPARATOR,
doWithDB,
} from "../db"
import { getAppDB } from "../context" import { getAppDB } from "../context"
import { doWithDB } from "../db"
import { Screen, Role as RoleDoc } from "@budibase/types" import { Screen, Role as RoleDoc } from "@budibase/types"
import cloneDeep from "lodash/fp/cloneDeep" import cloneDeep from "lodash/fp/cloneDeep"

View File

@ -1,6 +1,7 @@
const redis = require("../redis/init") const redis = require("../redis/init")
const { v4: uuidv4 } = require("uuid") const { v4: uuidv4 } = require("uuid")
const { logWarn } = require("../logging") const { logWarn } = require("../logging")
import env from "../environment" import env from "../environment"
import { import {
Session, Session,

View File

@ -1,9 +1,8 @@
import env from "../environment" import env from "../environment"
import * as eventHelpers from "./events" import * as eventHelpers from "./events"
import * as accounts from "../accounts"
import * as accountSdk from "../accounts" import * as accountSdk from "../accounts"
import * as cache from "../cache" import * as cache from "../cache"
import { getGlobalDB, getIdentity, getTenantId } from "../context" import { doInTenant, getGlobalDB, getIdentity, getTenantId } from "../context"
import * as dbUtils from "../db" import * as dbUtils from "../db"
import { EmailUnavailableError, HTTPError } from "../errors" import { EmailUnavailableError, HTTPError } from "../errors"
import * as platform from "../platform" import * as platform from "../platform"
@ -11,12 +10,10 @@ import * as sessions from "../security/sessions"
import * as usersCore from "./users" import * as usersCore from "./users"
import { import {
Account, Account,
AllDocsResponse,
BulkUserCreated, BulkUserCreated,
BulkUserDeleted, BulkUserDeleted,
isSSOAccount, isSSOAccount,
isSSOUser, isSSOUser,
RowResponse,
SaveUserOpts, SaveUserOpts,
User, User,
UserStatus, UserStatus,
@ -303,7 +300,7 @@ export class UserDB {
static async bulkCreate( static async bulkCreate(
newUsersRequested: User[], newUsersRequested: User[],
groups: string[] groups?: string[]
): Promise<BulkUserCreated> { ): Promise<BulkUserCreated> {
const tenantId = getTenantId() const tenantId = getTenantId()
@ -328,7 +325,7 @@ export class UserDB {
}) })
continue continue
} }
newUser.userGroups = groups newUser.userGroups = groups || []
newUsers.push(newUser) newUsers.push(newUser)
if (isCreator(newUser)) { if (isCreator(newUser)) {
newCreators.push(newUser) newCreators.push(newUser)
@ -467,7 +464,7 @@ export class UserDB {
if (!env.SELF_HOSTED && !env.DISABLE_ACCOUNT_PORTAL) { if (!env.SELF_HOSTED && !env.DISABLE_ACCOUNT_PORTAL) {
// root account holder can't be deleted from inside budibase // root account holder can't be deleted from inside budibase
const email = dbUser.email const email = dbUser.email
const account = await accounts.getAccount(email) const account = await accountSdk.getAccount(email)
if (account) { if (account) {
if (dbUser.userId === getIdentity()!._id) { if (dbUser.userId === getIdentity()!._id) {
throw new HTTPError('Please visit "Account" to delete this user', 400) throw new HTTPError('Please visit "Account" to delete this user', 400)
@ -488,6 +485,37 @@ export class UserDB {
await sessions.invalidateSessions(userId, { reason: "deletion" }) await sessions.invalidateSessions(userId, { reason: "deletion" })
} }
static async createAdminUser(
email: string,
password: string,
tenantId: string,
opts?: { ssoId?: string; hashPassword?: boolean; requirePassword?: boolean }
) {
const user: User = {
email: email,
password: password,
createdAt: Date.now(),
roles: {},
builder: {
global: true,
},
admin: {
global: true,
},
tenantId,
}
if (opts?.ssoId) {
user.ssoId = opts.ssoId
}
// always bust checklist beforehand, if an error occurs but can proceed, don't get
// stuck in a cycle
await cache.bustCache(cache.CacheKey.CHECKLIST)
return await UserDB.save(user, {
hashPassword: opts?.hashPassword,
requirePassword: opts?.requirePassword,
})
}
static async getGroups(groupIds: string[]) { static async getGroups(groupIds: string[]) {
return await this.groups.getBulk(groupIds) return await this.groups.getBulk(groupIds)
} }

View File

@ -6,6 +6,7 @@ import {
} from "@budibase/types" } from "@budibase/types"
import * as dbUtils from "../db" import * as dbUtils from "../db"
import { ViewName } from "../constants" import { ViewName } from "../constants"
import { getExistingInvites } from "../cache/invite"
/** /**
* Apply a system-wide search on emails: * Apply a system-wide search on emails:
@ -26,6 +27,9 @@ export async function searchExistingEmails(emails: string[]) {
const existingAccounts = await getExistingAccounts(emails) const existingAccounts = await getExistingAccounts(emails)
matchedEmails.push(...existingAccounts.map(account => account.email)) matchedEmails.push(...existingAccounts.map(account => account.email))
const invitedEmails = await getExistingInvites(emails)
matchedEmails.push(...invitedEmails.map(invite => invite.email))
return [...new Set(matchedEmails.map(email => email.toLowerCase()))] return [...new Set(matchedEmails.map(email => email.toLowerCase()))]
} }

View File

@ -43,7 +43,7 @@ function removeUserPassword(users: User | User[]) {
return users return users
} }
export const isSupportedUserSearch = (query: SearchQuery) => { export function isSupportedUserSearch(query: SearchQuery) {
const allowed = [ const allowed = [
{ op: SearchQueryOperators.STRING, key: "email" }, { op: SearchQueryOperators.STRING, key: "email" },
{ op: SearchQueryOperators.EQUAL, key: "_id" }, { op: SearchQueryOperators.EQUAL, key: "_id" },
@ -68,10 +68,10 @@ export const isSupportedUserSearch = (query: SearchQuery) => {
return true return true
} }
export const bulkGetGlobalUsersById = async ( export async function bulkGetGlobalUsersById(
userIds: string[], userIds: string[],
opts?: GetOpts opts?: GetOpts
) => { ) {
const db = getGlobalDB() const db = getGlobalDB()
let users = ( let users = (
await db.allDocs({ await db.allDocs({
@ -85,7 +85,7 @@ export const bulkGetGlobalUsersById = async (
return users return users
} }
export const getAllUserIds = async () => { export async function getAllUserIds() {
const db = getGlobalDB() const db = getGlobalDB()
const startKey = `${DocumentType.USER}${SEPARATOR}` const startKey = `${DocumentType.USER}${SEPARATOR}`
const response = await db.allDocs({ const response = await db.allDocs({
@ -95,7 +95,7 @@ export const getAllUserIds = async () => {
return response.rows.map(row => row.id) return response.rows.map(row => row.id)
} }
export const bulkUpdateGlobalUsers = async (users: User[]) => { export async function bulkUpdateGlobalUsers(users: User[]) {
const db = getGlobalDB() const db = getGlobalDB()
return (await db.bulkDocs(users)) as BulkDocsResponse return (await db.bulkDocs(users)) as BulkDocsResponse
} }
@ -113,10 +113,10 @@ export async function getById(id: string, opts?: GetOpts): Promise<User> {
* Given an email address this will use a view to search through * Given an email address this will use a view to search through
* all the users to find one with this email address. * all the users to find one with this email address.
*/ */
export const getGlobalUserByEmail = async ( export async function getGlobalUserByEmail(
email: String, email: String,
opts?: GetOpts opts?: GetOpts
): Promise<User | undefined> => { ): Promise<User | undefined> {
if (email == null) { if (email == null) {
throw "Must supply an email address to view" throw "Must supply an email address to view"
} }
@ -139,11 +139,23 @@ export const getGlobalUserByEmail = async (
return user return user
} }
export const searchGlobalUsersByApp = async ( export async function doesUserExist(email: string) {
try {
const user = await getGlobalUserByEmail(email)
if (Array.isArray(user) || user != null) {
return true
}
} catch (err) {
return false
}
return false
}
export async function searchGlobalUsersByApp(
appId: any, appId: any,
opts: DatabaseQueryOpts, opts: DatabaseQueryOpts,
getOpts?: GetOpts getOpts?: GetOpts
) => { ) {
if (typeof appId !== "string") { if (typeof appId !== "string") {
throw new Error("Must provide a string based app ID") throw new Error("Must provide a string based app ID")
} }
@ -167,10 +179,10 @@ export const searchGlobalUsersByApp = async (
Return any user who potentially has access to the application Return any user who potentially has access to the application
Admins, developers and app users with the explicitly role. Admins, developers and app users with the explicitly role.
*/ */
export const searchGlobalUsersByAppAccess = async ( export async function searchGlobalUsersByAppAccess(
appId: any, appId: any,
opts?: { limit?: number } opts?: { limit?: number }
) => { ) {
const roleSelector = `roles.${appId}` const roleSelector = `roles.${appId}`
let orQuery: any[] = [ let orQuery: any[] = [
@ -205,7 +217,7 @@ export const searchGlobalUsersByAppAccess = async (
return resp.rows return resp.rows
} }
export const getGlobalUserByAppPage = (appId: string, user: User) => { export function getGlobalUserByAppPage(appId: string, user: User) {
if (!user) { if (!user) {
return return
} }
@ -215,11 +227,11 @@ export const getGlobalUserByAppPage = (appId: string, user: User) => {
/** /**
* Performs a starts with search on the global email view. * Performs a starts with search on the global email view.
*/ */
export const searchGlobalUsersByEmail = async ( export async function searchGlobalUsersByEmail(
email: string | unknown, email: string | unknown,
opts: any, opts: any,
getOpts?: GetOpts getOpts?: GetOpts
) => { ) {
if (typeof email !== "string") { if (typeof email !== "string") {
throw new Error("Must provide a string to search by") throw new Error("Must provide a string to search by")
} }
@ -242,12 +254,12 @@ export const searchGlobalUsersByEmail = async (
} }
const PAGE_LIMIT = 8 const PAGE_LIMIT = 8
export const paginatedUsers = async ({ export async function paginatedUsers({
bookmark, bookmark,
query, query,
appId, appId,
limit, limit,
}: SearchUsersRequest = {}) => { }: SearchUsersRequest = {}) {
const db = getGlobalDB() const db = getGlobalDB()
const pageSize = limit ?? PAGE_LIMIT const pageSize = limit ?? PAGE_LIMIT
const pageLimit = pageSize + 1 const pageLimit = pageSize + 1

View File

@ -28,6 +28,9 @@ export class Duration {
toMs: () => { toMs: () => {
return Duration.convert(from, DurationType.MILLISECONDS, duration) return Duration.convert(from, DurationType.MILLISECONDS, duration)
}, },
toSeconds: () => {
return Duration.convert(from, DurationType.SECONDS, duration)
},
} }
} }

View File

@ -1,4 +1,5 @@
import env from "../environment" import env from "../environment"
export * from "../docIds/newid" export * from "../docIds/newid"
const bcrypt = env.JS_BCRYPT ? require("bcryptjs") : require("bcrypt") const bcrypt = env.JS_BCRYPT ? require("bcryptjs") : require("bcrypt")

View File

@ -11,6 +11,7 @@ import {
TenantResolutionStrategy, TenantResolutionStrategy,
} from "@budibase/types" } from "@budibase/types"
import type { SetOption } from "cookies" import type { SetOption } from "cookies"
const jwt = require("jsonwebtoken") const jwt = require("jsonwebtoken")
const APP_PREFIX = DocumentType.APP + SEPARATOR const APP_PREFIX = DocumentType.APP + SEPARATOR

View File

@ -1,5 +1,5 @@
const _ = require('lodash/fp') const _ = require("lodash/fp")
const {structures} = require("../../../tests") const { structures } = require("../../../tests")
jest.mock("../../../src/context") jest.mock("../../../src/context")
jest.mock("../../../src/db") jest.mock("../../../src/db")
@ -7,10 +7,9 @@ jest.mock("../../../src/db")
const context = require("../../../src/context") const context = require("../../../src/context")
const db = require("../../../src/db") const db = require("../../../src/db")
const {getCreatorCount} = require('../../../src/users/users') const { getCreatorCount } = require("../../../src/users/users")
describe("Users", () => { describe("Users", () => {
let getGlobalDBMock let getGlobalDBMock
let getGlobalUserParamsMock let getGlobalUserParamsMock
let paginationMock let paginationMock
@ -26,26 +25,26 @@ describe("Users", () => {
it("Retrieves the number of creators", async () => { it("Retrieves the number of creators", async () => {
const getUsers = (offset, limit, creators = false) => { const getUsers = (offset, limit, creators = false) => {
const range = _.range(offset, limit) const range = _.range(offset, limit)
const opts = creators ? {builder: {global: true}} : undefined const opts = creators ? { builder: { global: true } } : undefined
return range.map(() => structures.users.user(opts)) return range.map(() => structures.users.user(opts))
} }
const page1Data = getUsers(0, 8) const page1Data = getUsers(0, 8)
const page2Data = getUsers(8, 12, true) const page2Data = getUsers(8, 12, true)
getGlobalDBMock.mockImplementation(() => ({ getGlobalDBMock.mockImplementation(() => ({
name : "fake-db", name: "fake-db",
allDocs: () => ({ allDocs: () => ({
rows: [...page1Data, ...page2Data] rows: [...page1Data, ...page2Data],
}) }),
})) }))
paginationMock.mockImplementationOnce(() => ({ paginationMock.mockImplementationOnce(() => ({
data: page1Data, data: page1Data,
hasNextPage: true, hasNextPage: true,
nextPage: "1" nextPage: "1",
})) }))
paginationMock.mockImplementation(() => ({ paginationMock.mockImplementation(() => ({
data: page2Data, data: page2Data,
hasNextPage: false, hasNextPage: false,
nextPage: undefined nextPage: undefined,
})) }))
const creatorsCount = await getCreatorCount() const creatorsCount = await getCreatorCount()
expect(creatorsCount).toBe(4) expect(creatorsCount).toBe(4)

View File

@ -1,3 +1,4 @@
jest.mock("../../../../src/logging/alerts") jest.mock("../../../../src/logging/alerts")
import * as _alerts from "../../../../src/logging/alerts" import * as _alerts from "../../../../src/logging/alerts"
export const alerts = jest.mocked(_alerts) export const alerts = jest.mocked(_alerts)

View File

@ -1,5 +1,6 @@
jest.mock("../../../../src/accounts") jest.mock("../../../../src/accounts")
import * as _accounts from "../../../../src/accounts" import * as _accounts from "../../../../src/accounts"
export const accounts = jest.mocked(_accounts) export const accounts = jest.mocked(_accounts)
export * as date from "./date" export * as date from "./date"

View File

@ -1,2 +1,3 @@
import Chance from "./Chance" import Chance from "./Chance"
export const generator = new Chance() export const generator = new Chance()

View File

@ -12,7 +12,7 @@ import { generator } from "./generator"
import { tenant } from "." import { tenant } from "."
export const newEmail = () => { export const newEmail = () => {
return `${uuid()}@test.com` return `${uuid()}@example.com`
} }
export const user = (userProps?: Partial<Omit<User, "userId">>): User => { export const user = (userProps?: Partial<Omit<User, "userId">>): User => {

View File

@ -9,6 +9,7 @@ mocks.fetch.enable()
// mock all dates to 2020-01-01T00:00:00.000Z // mock all dates to 2020-01-01T00:00:00.000Z
// use tk.reset() to use real dates in individual tests // use tk.reset() to use real dates in individual tests
import tk from "timekeeper" import tk from "timekeeper"
tk.freeze(mocks.date.MOCK_DATE) tk.freeze(mocks.date.MOCK_DATE)
if (!process.env.DEBUG) { if (!process.env.DEBUG) {

View File

@ -1,5 +1,6 @@
<script> <script>
import "@spectrum-css/actiongroup/dist/index-vars.css" import "@spectrum-css/actiongroup/dist/index-vars.css"
export let vertical = false export let vertical = false
export let justified = false export let justified = false
export let quiet = false export let quiet = false

View File

@ -1,5 +1,6 @@
<script> <script>
import "@spectrum-css/avatar/dist/index-vars.css" import "@spectrum-css/avatar/dist/index-vars.css"
let sizes = new Map([ let sizes = new Map([
["XXS", "--spectrum-alias-avatar-size-50"], ["XXS", "--spectrum-alias-avatar-size-50"],
["XS", "--spectrum-alias-avatar-size-75"], ["XS", "--spectrum-alias-avatar-size-75"],

View File

@ -1,5 +1,6 @@
<script> <script>
import "@spectrum-css/buttongroup/dist/index-vars.css" import "@spectrum-css/buttongroup/dist/index-vars.css"
export let vertical = false export let vertical = false
export let gap = "" export let gap = ""

View File

@ -1,5 +1,6 @@
<script> <script>
import "@spectrum-css/divider/dist/index-vars.css" import "@spectrum-css/divider/dist/index-vars.css"
export let size = "M" export let size = "M"
export let vertical = false export let vertical = false

View File

@ -3,8 +3,7 @@
import Button from "../Button/Button.svelte" import Button from "../Button/Button.svelte"
import Body from "../Typography/Body.svelte" import Body from "../Typography/Body.svelte"
import Heading from "../Typography/Heading.svelte" import Heading from "../Typography/Heading.svelte"
import { setContext } from "svelte" import { setContext, createEventDispatcher } from "svelte"
import { createEventDispatcher } from "svelte"
import { generate } from "shortid" import { generate } from "shortid"
export let title export let title

View File

@ -10,6 +10,7 @@
export let disabled = false export let disabled = false
export let error = null export let error = null
export let size = "M" export let size = "M"
export let helpText = null
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
const onChange = e => { const onChange = e => {
@ -18,6 +19,6 @@
} }
</script> </script>
<Field {label} {labelPosition} {error}> <Field {helpText} {label} {labelPosition} {error}>
<Checkbox {error} {disabled} {text} {value} {size} on:change={onChange} /> <Checkbox {error} {disabled} {text} {value} {size} on:change={onChange} />
</Field> </Field>

View File

@ -11,6 +11,7 @@
export let error = null export let error = null
export let placeholder = "Choose an option or type" export let placeholder = "Choose an option or type"
export let options = [] export let options = []
export let helpText = null
export let getOptionLabel = option => extractProperty(option, "label") export let getOptionLabel = option => extractProperty(option, "label")
export let getOptionValue = option => extractProperty(option, "value") export let getOptionValue = option => extractProperty(option, "value")
@ -27,7 +28,7 @@
} }
</script> </script>
<Field {label} {labelPosition} {error}> <Field {helpText} {label} {labelPosition} {error}>
<Combobox <Combobox
{error} {error}
{disabled} {disabled}

View File

@ -4,7 +4,6 @@
import { createEventDispatcher } from "svelte" import { createEventDispatcher } from "svelte"
export let value = false export let value = false
export let error = null
export let id = null export let id = null
export let text = null export let text = null
export let disabled = false export let disabled = false
@ -22,7 +21,6 @@
<label <label
class="spectrum-Checkbox spectrum-Checkbox--emphasized {sizeClass}" class="spectrum-Checkbox spectrum-Checkbox--emphasized {sizeClass}"
class:is-invalid={!!error}
class:checked={value} class:checked={value}
class:is-indeterminate={indeterminate} class:is-indeterminate={indeterminate}
class:readonly class:readonly

View File

@ -6,7 +6,6 @@
export let direction = "vertical" export let direction = "vertical"
export let value = [] export let value = []
export let options = [] export let options = []
export let error = null
export let disabled = false export let disabled = false
export let readonly = false export let readonly = false
export let getOptionLabel = option => option export let getOptionLabel = option => option
@ -34,7 +33,6 @@
<div <div
title={getOptionLabel(option)} title={getOptionLabel(option)}
class="spectrum-Checkbox spectrum-FieldGroup-item" class="spectrum-Checkbox spectrum-FieldGroup-item"
class:is-invalid={!!error}
class:readonly class:readonly
> >
<label <label

View File

@ -10,7 +10,6 @@
export let placeholder = "Choose an option or type" export let placeholder = "Choose an option or type"
export let disabled = false export let disabled = false
export let readonly = false export let readonly = false
export let error = null
export let options = [] export let options = []
export let getOptionLabel = option => option export let getOptionLabel = option => option
export let getOptionValue = option => option export let getOptionValue = option => option
@ -39,12 +38,10 @@
<div <div
class="spectrum-InputGroup" class="spectrum-InputGroup"
class:is-focused={open || focus} class:is-focused={open || focus}
class:is-invalid={!!error}
class:is-disabled={disabled} class:is-disabled={disabled}
> >
<div <div
class="spectrum-Textfield spectrum-InputGroup-textfield" class="spectrum-Textfield spectrum-InputGroup-textfield"
class:is-invalid={!!error}
class:is-disabled={disabled} class:is-disabled={disabled}
class:is-focused={open || focus} class:is-focused={open || focus}
> >

View File

@ -10,7 +10,6 @@
export let id = null export let id = null
export let disabled = false export let disabled = false
export let readonly = false export let readonly = false
export let error = null
export let enableTime = true export let enableTime = true
export let value = null export let value = null
export let placeholder = null export let placeholder = null
@ -188,7 +187,6 @@
<div <div
id={flatpickrId} id={flatpickrId}
class:is-disabled={disabled || readonly} class:is-disabled={disabled || readonly}
class:is-invalid={!!error}
class="flatpickr spectrum-InputGroup spectrum-Datepicker" class="flatpickr spectrum-InputGroup spectrum-Datepicker"
class:is-focused={open} class:is-focused={open}
aria-readonly="false" aria-readonly="false"
@ -199,17 +197,7 @@
on:click={flatpickr?.open} on:click={flatpickr?.open}
class="spectrum-Textfield spectrum-InputGroup-textfield" class="spectrum-Textfield spectrum-InputGroup-textfield"
class:is-disabled={disabled} class:is-disabled={disabled}
class:is-invalid={!!error}
> >
{#if !!error}
<svg
class="spectrum-Icon spectrum-Icon--sizeM spectrum-Textfield-validationIcon"
focusable="false"
aria-hidden="true"
>
<use xlink:href="#spectrum-icon-18-Alert" />
</svg>
{/if}
<input <input
{disabled} {disabled}
{readonly} {readonly}
@ -227,7 +215,6 @@
class="spectrum-Picker spectrum-Picker--sizeM spectrum-InputGroup-button" class="spectrum-Picker spectrum-Picker--sizeM spectrum-InputGroup-button"
tabindex="-1" tabindex="-1"
class:is-disabled={disabled} class:is-disabled={disabled}
class:is-invalid={!!error}
on:click={flatpickr?.open} on:click={flatpickr?.open}
> >
<svg <svg

View File

@ -22,7 +22,6 @@
export let handleFileTooLarge = null export let handleFileTooLarge = null
export let handleTooManyFiles = null export let handleTooManyFiles = null
export let gallery = true export let gallery = true
export let error = null
export let fileTags = [] export let fileTags = []
export let maximum = null export let maximum = null
export let extensions = "*" export let extensions = "*"
@ -222,7 +221,6 @@
{#if showDropzone} {#if showDropzone}
<div <div
class="spectrum-Dropzone" class="spectrum-Dropzone"
class:is-invalid={!!error}
class:disabled class:disabled
role="region" role="region"
tabindex="0" tabindex="0"
@ -351,9 +349,6 @@
.spectrum-Dropzone { .spectrum-Dropzone {
user-select: none; user-select: none;
} }
.spectrum-Dropzone.is-invalid {
border-color: var(--spectrum-global-color-red-400);
}
input[type="file"] { input[type="file"] {
display: none; display: none;
} }

View File

@ -14,7 +14,6 @@
export let disabled = false export let disabled = false
export let readonly = false export let readonly = false
export let updateOnChange = true export let updateOnChange = true
export let error = null
export let options = [] export let options = []
export let getOptionLabel = option => extractProperty(option, "label") export let getOptionLabel = option => extractProperty(option, "label")
export let getOptionValue = option => extractProperty(option, "value") export let getOptionValue = option => extractProperty(option, "value")
@ -111,27 +110,12 @@
} }
</script> </script>
<div <div class="spectrum-InputGroup" class:is-disabled={disabled}>
class="spectrum-InputGroup"
class:is-invalid={!!error}
class:is-disabled={disabled}
>
<div <div
class="spectrum-Textfield spectrum-InputGroup-textfield" class="spectrum-Textfield spectrum-InputGroup-textfield"
class:is-invalid={!!error}
class:is-disabled={disabled} class:is-disabled={disabled}
class:is-focused={focus} class:is-focused={focus}
> >
{#if error}
<svg
class="spectrum-Icon spectrum-Icon--sizeM spectrum-Textfield-validationIcon"
focusable="false"
aria-hidden="true"
>
<use xlink:href="#spectrum-icon-18-Alert" />
</svg>
{/if}
<input <input
{id} {id}
on:click on:click

View File

@ -6,7 +6,6 @@
export let id = null export let id = null
export let placeholder = null export let placeholder = null
export let disabled = false export let disabled = false
export let error = null
export let options = [] export let options = []
export let getOptionLabel = option => option export let getOptionLabel = option => option
export let getOptionValue = option => option export let getOptionValue = option => option
@ -84,7 +83,6 @@
<Picker <Picker
on:loadMore on:loadMore
{id} {id}
{error}
{disabled} {disabled}
{readonly} {readonly}
{fieldText} {fieldText}

View File

@ -14,7 +14,6 @@
export let id = null export let id = null
export let disabled = false export let disabled = false
export let error = null
export let fieldText = "" export let fieldText = ""
export let fieldIcon = "" export let fieldIcon = ""
export let fieldColour = "" export let fieldColour = ""
@ -113,7 +112,6 @@
class="spectrum-Picker spectrum-Picker--sizeM" class="spectrum-Picker spectrum-Picker--sizeM"
class:spectrum-Picker--quiet={quiet} class:spectrum-Picker--quiet={quiet}
{disabled} {disabled}
class:is-invalid={!!error}
class:is-open={open} class:is-open={open}
aria-haspopup="listbox" aria-haspopup="listbox"
on:click={onClick} on:click={onClick}
@ -142,16 +140,6 @@
> >
{fieldText} {fieldText}
</span> </span>
{#if error}
<svg
class="spectrum-Icon spectrum-Icon--sizeM spectrum-Picker-validationIcon"
focusable="false"
aria-hidden="true"
aria-label="Folder"
>
<use xlink:href="#spectrum-icon-18-Alert" />
</svg>
{/if}
<svg <svg
class="spectrum-Icon spectrum-UIIcon-ChevronDown100 spectrum-Picker-menuIcon" class="spectrum-Icon spectrum-UIIcon-ChevronDown100 spectrum-Picker-menuIcon"
focusable="false" focusable="false"

View File

@ -16,7 +16,6 @@
export let id = null export let id = null
export let placeholder = "Choose an option or type" export let placeholder = "Choose an option or type"
export let disabled = false export let disabled = false
export let error = null
export let secondaryOptions = [] export let secondaryOptions = []
export let primaryOptions = [] export let primaryOptions = []
export let secondaryFieldText = "" export let secondaryFieldText = ""
@ -105,14 +104,9 @@
} }
</script> </script>
<div <div class="spectrum-InputGroup" class:is-disabled={disabled}>
class="spectrum-InputGroup"
class:is-invalid={!!error}
class:is-disabled={disabled}
>
<div <div
class="spectrum-Textfield spectrum-InputGroup-textfield" class="spectrum-Textfield spectrum-InputGroup-textfield"
class:is-invalid={!!error}
class:is-disabled={disabled} class:is-disabled={disabled}
class:is-focused={focus} class:is-focused={focus}
class:is-full-width={!secondaryOptions.length} class:is-full-width={!secondaryOptions.length}

View File

@ -6,7 +6,6 @@
export let direction = "vertical" export let direction = "vertical"
export let value = null export let value = null
export let options = [] export let options = []
export let error = null
export let disabled = false export let disabled = false
export let readonly = false export let readonly = false
export let getOptionLabel = option => option export let getOptionLabel = option => option
@ -40,7 +39,6 @@
<div <div
title={getOptionTitle(option)} title={getOptionTitle(option)}
class="spectrum-Radio spectrum-FieldGroup-item spectrum-Radio--emphasized" class="spectrum-Radio spectrum-FieldGroup-item spectrum-Radio--emphasized"
class:is-invalid={!!error}
class:readonly class:readonly
> >
<input <input

View File

@ -5,14 +5,13 @@
export let placeholder = null export let placeholder = null
export let disabled = false export let disabled = false
export let readonly = false export let readonly = false
export let error = null
export let height = null export let height = null
export let id = null export let id = null
export let fullScreenOffset = null export let fullScreenOffset = null
export let easyMDEOptions = null export let easyMDEOptions = null
</script> </script>
<div class:error> <div>
<MarkdownEditor <MarkdownEditor
{value} {value}
{placeholder} {placeholder}
@ -27,18 +26,4 @@
</div> </div>
<style> <style>
.error :global(.EasyMDEContainer .editor-toolbar) {
border-top-color: var(--spectrum-semantic-negative-color-default);
border-left-color: var(--spectrum-semantic-negative-color-default);
border-right-color: var(--spectrum-semantic-negative-color-default);
}
.error :global(.EasyMDEContainer .CodeMirror) {
border-bottom-color: var(--spectrum-semantic-negative-color-default);
border-left-color: var(--spectrum-semantic-negative-color-default);
border-right-color: var(--spectrum-semantic-negative-color-default);
}
.error :global(.EasyMDEContainer .editor-preview-side) {
border-bottom-color: var(--spectrum-semantic-negative-color-default);
border-right-color: var(--spectrum-semantic-negative-color-default);
}
</style> </style>

View File

@ -6,7 +6,6 @@
export let id = null export let id = null
export let placeholder = "Choose an option" export let placeholder = "Choose an option"
export let disabled = false export let disabled = false
export let error = null
export let options = [] export let options = []
export let getOptionLabel = option => option export let getOptionLabel = option => option
export let getOptionValue = option => option export let getOptionValue = option => option
@ -71,7 +70,6 @@
on:loadMore on:loadMore
{quiet} {quiet}
{id} {id}
{error}
{disabled} {disabled}
{readonly} {readonly}
{fieldText} {fieldText}

View File

@ -7,7 +7,6 @@
export let value = null export let value = null
export let placeholder = null export let placeholder = null
export let disabled = false export let disabled = false
export let error = null
export let id = null export let id = null
export let readonly = false export let readonly = false
export let updateOnChange = true export let updateOnChange = true
@ -98,20 +97,9 @@
<div <div
class="spectrum-Stepper" class="spectrum-Stepper"
class:spectrum-Stepper--quiet={quiet} class:spectrum-Stepper--quiet={quiet}
class:is-invalid={!!error}
class:is-disabled={disabled} class:is-disabled={disabled}
class:is-focused={focus} class:is-focused={focus}
> >
{#if error}
<svg
class="spectrum-Icon spectrum-Icon--sizeM spectrum-Textfield-validationIcon"
focusable="false"
aria-hidden="true"
>
<use xlink:href="#spectrum-icon-18-Alert" />
</svg>
{/if}
<div class="spectrum-Textfield spectrum-Stepper-textfield"> <div class="spectrum-Textfield spectrum-Stepper-textfield">
<input <input
{disabled} {disabled}

View File

@ -6,7 +6,6 @@
export let placeholder = null export let placeholder = null
export let disabled = false export let disabled = false
export let readonly = false export let readonly = false
export let error = null
export let id = null export let id = null
export let height = null export let height = null
export let minHeight = null export let minHeight = null
@ -41,20 +40,9 @@
<div <div
style={`${heightString}${minHeightString}`} style={`${heightString}${minHeightString}`}
class="spectrum-Textfield spectrum-Textfield--multiline" class="spectrum-Textfield spectrum-Textfield--multiline"
class:is-invalid={!!error}
class:is-disabled={disabled} class:is-disabled={disabled}
class:is-focused={focus} class:is-focused={focus}
> >
{#if error}
<svg
class="spectrum-Icon spectrum-Icon--sizeM
spectrum-Textfield-validationIcon"
focusable="false"
aria-hidden="true"
>
<use xlink:href="#spectrum-icon-18-Alert" />
</svg>
{/if}
<!-- prettier-ignore --> <!-- prettier-ignore -->
<textarea <textarea
bind:this={textarea} bind:this={textarea}

View File

@ -6,7 +6,6 @@
export let placeholder = null export let placeholder = null
export let type = "text" export let type = "text"
export let disabled = false export let disabled = false
export let error = null
export let id = null export let id = null
export let readonly = false export let readonly = false
export let updateOnChange = true export let updateOnChange = true
@ -79,19 +78,9 @@
<div <div
class="spectrum-Textfield" class="spectrum-Textfield"
class:spectrum-Textfield--quiet={quiet} class:spectrum-Textfield--quiet={quiet}
class:is-invalid={!!error}
class:is-disabled={disabled} class:is-disabled={disabled}
class:is-focused={focus} class:is-focused={focus}
> >
{#if error}
<svg
class="spectrum-Icon spectrum-Icon--sizeM spectrum-Textfield-validationIcon"
focusable="false"
aria-hidden="true"
>
<use xlink:href="#spectrum-icon-18-Alert" />
</svg>
{/if}
<input <input
bind:this={field} bind:this={field}
{disabled} {disabled}

View File

@ -16,6 +16,7 @@
export let appendTo = undefined export let appendTo = undefined
export let ignoreTimezones = false export let ignoreTimezones = false
export let range = false export let range = false
export let helpText = null
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
const onChange = e => { const onChange = e => {
@ -30,7 +31,7 @@
} }
</script> </script>
<Field {label} {labelPosition} {error}> <Field {helpText} {label} {labelPosition} {error}>
<DatePicker <DatePicker
{error} {error}
{disabled} {disabled}

View File

@ -17,6 +17,7 @@
export let fileTags = [] export let fileTags = []
export let maximum = undefined export let maximum = undefined
export let compact = false export let compact = false
export let helpText = null
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
const onChange = e => { const onChange = e => {
@ -25,7 +26,7 @@
} }
</script> </script>
<Field {label} {labelPosition} {error}> <Field {helpText} {label} {labelPosition} {error}>
<CoreDropzone <CoreDropzone
{error} {error}
{disabled} {disabled}

View File

@ -16,6 +16,7 @@
export let autofocus export let autofocus
export let variables export let variables
export let showModal export let showModal
export let helpText = null
export let environmentVariablesEnabled export let environmentVariablesEnabled
export let handleUpgradePanel export let handleUpgradePanel
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
@ -25,7 +26,7 @@
} }
</script> </script>
<Field {label} {labelPosition} {error}> <Field {helpText} {label} {labelPosition} {error}>
<EnvDropdown <EnvDropdown
{updateOnChange} {updateOnChange}
{error} {error}

View File

@ -1,11 +1,13 @@
<script> <script>
import "@spectrum-css/fieldlabel/dist/index-vars.css" import "@spectrum-css/fieldlabel/dist/index-vars.css"
import FieldLabel from "./FieldLabel.svelte" import FieldLabel from "./FieldLabel.svelte"
import Icon from "../Icon/Icon.svelte"
export let id = null export let id = null
export let label = null export let label = null
export let labelPosition = "above" export let labelPosition = "above"
export let error = null export let error = null
export let helpText = null
export let tooltip = "" export let tooltip = ""
</script> </script>
@ -17,6 +19,10 @@
<slot /> <slot />
{#if error} {#if error}
<div class="error">{error}</div> <div class="error">{error}</div>
{:else if helpText}
<div class="helpText">
<Icon name="HelpOutline" /> <span>{helpText}</span>
</div>
{/if} {/if}
</div> </div>
</div> </div>
@ -39,4 +45,21 @@
font-size: var(--spectrum-global-dimension-font-size-75); font-size: var(--spectrum-global-dimension-font-size-75);
margin-top: var(--spectrum-global-dimension-size-75); margin-top: var(--spectrum-global-dimension-size-75);
} }
.helpText {
display: flex;
margin-top: var(--spectrum-global-dimension-size-75);
align-items: center;
}
.helpText :global(svg) {
width: 14px;
color: var(--grey-5);
margin-right: 6px;
}
.helpText span {
color: var(--grey-7);
font-size: var(--spectrum-global-dimension-font-size-75);
}
</style> </style>

View File

@ -14,6 +14,7 @@
export let title = null export let title = null
export let value = null export let value = null
export let tooltip = null export let tooltip = null
export let helpText = null
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
const onChange = e => { const onChange = e => {
@ -22,7 +23,7 @@
} }
</script> </script>
<Field {label} {labelPosition} {error} {tooltip}> <Field {helpText} {label} {labelPosition} {error} {tooltip}>
<CoreFile <CoreFile
{error} {error}
{disabled} {disabled}

View File

@ -15,6 +15,7 @@
export let quiet = false export let quiet = false
export let autofocus export let autofocus
export let autocomplete export let autocomplete
export let helpText = null
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
const onChange = e => { const onChange = e => {
@ -23,7 +24,7 @@
} }
</script> </script>
<Field {label} {labelPosition} {error}> <Field {helpText} {label} {labelPosition} {error}>
<TextField <TextField
{updateOnChange} {updateOnChange}
{error} {error}

View File

@ -15,6 +15,7 @@
export let updateOnChange = true export let updateOnChange = true
export let quiet = false export let quiet = false
export let autofocus export let autofocus
export let helpText = null
export let options = [] export let options = []
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
@ -29,7 +30,7 @@
} }
</script> </script>
<Field {label} {labelPosition} {error}> <Field {helpText} {label} {labelPosition} {error}>
<InputDropdown <InputDropdown
{updateOnChange} {updateOnChange}
{error} {error}

View File

@ -18,6 +18,7 @@
export let autocomplete = false export let autocomplete = false
export let searchTerm = null export let searchTerm = null
export let customPopoverHeight export let customPopoverHeight
export let helpText = null
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
const onChange = e => { const onChange = e => {
@ -26,7 +27,7 @@
} }
</script> </script>
<Field {label} {labelPosition} {error}> <Field {helpText} {label} {labelPosition} {error}>
<Multiselect <Multiselect
{error} {error}
{disabled} {disabled}

View File

@ -26,6 +26,7 @@
export let secondaryOptions = [] export let secondaryOptions = []
export let searchTerm export let searchTerm
export let showClearIcon = true export let showClearIcon = true
export let helpText = null
let primaryLabel let primaryLabel
let secondaryLabel let secondaryLabel
@ -93,7 +94,7 @@
} }
</script> </script>
<Field {label} {labelPosition} {error}> <Field {helpText} {label} {labelPosition} {error}>
<PickerDropdown <PickerDropdown
{searchTerm} {searchTerm}
{autocomplete} {autocomplete}

View File

@ -13,6 +13,7 @@
export let getOptionLabel = option => extractProperty(option, "label") export let getOptionLabel = option => extractProperty(option, "label")
export let getOptionValue = option => extractProperty(option, "value") export let getOptionValue = option => extractProperty(option, "value")
export let getOptionTitle = option => extractProperty(option, "label") export let getOptionTitle = option => extractProperty(option, "label")
export let helpText = null
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
const onChange = e => { const onChange = e => {
@ -27,7 +28,7 @@
} }
</script> </script>
<Field {label} {labelPosition} {error}> <Field {helpText} {label} {labelPosition} {error}>
<RadioGroup <RadioGroup
{error} {error}
{disabled} {disabled}

View File

@ -13,6 +13,7 @@
export let id = null export let id = null
export let fullScreenOffset = null export let fullScreenOffset = null
export let easyMDEOptions = null export let easyMDEOptions = null
export let helpText = null
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
const onChange = e => { const onChange = e => {
@ -21,7 +22,7 @@
} }
</script> </script>
<Field {label} {labelPosition} {error}> <Field {helpText} {label} {labelPosition} {error}>
<RichTextField <RichTextField
{error} {error}
{disabled} {disabled}

View File

@ -11,6 +11,7 @@
export let updateOnChange = true export let updateOnChange = true
export let quiet = false export let quiet = false
export let inputRef export let inputRef
export let helpText = null
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
const onChange = e => { const onChange = e => {
@ -19,7 +20,7 @@
} }
</script> </script>
<Field {label} {labelPosition}> <Field {helpText} {label} {labelPosition}>
<Search <Search
{updateOnChange} {updateOnChange}
{disabled} {disabled}

View File

@ -26,6 +26,7 @@
export let align export let align
export let footer = null export let footer = null
export let tag = null export let tag = null
export let helpText = null
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
const onChange = e => { const onChange = e => {
value = e.detail value = e.detail
@ -40,7 +41,7 @@
} }
</script> </script>
<Field {label} {labelPosition} {error} {tooltip}> <Field {helpText} {label} {labelPosition} {error} {tooltip}>
<Select <Select
{quiet} {quiet}
{error} {error}

View File

@ -11,6 +11,7 @@
export let step = 1 export let step = 1
export let disabled = false export let disabled = false
export let error = null export let error = null
export let helpText = null
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
const onChange = e => { const onChange = e => {
@ -19,6 +20,6 @@
} }
</script> </script>
<Field {label} {labelPosition} {error}> <Field {helpText} {label} {labelPosition} {error}>
<Slider {disabled} {value} {min} {max} {step} on:change={onChange} /> <Slider {disabled} {value} {min} {max} {step} on:change={onChange} />
</Field> </Field>

View File

@ -15,6 +15,7 @@
export let min = null export let min = null
export let max = null export let max = null
export let step = 1 export let step = 1
export let helpText = null
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
const onChange = e => { const onChange = e => {
@ -23,7 +24,7 @@
} }
</script> </script>
<Field {label} {labelPosition} {error}> <Field {helpText} {label} {labelPosition} {error}>
<Stepper <Stepper
{updateOnChange} {updateOnChange}
{error} {error}

View File

@ -12,6 +12,7 @@
export let getCaretPosition = null export let getCaretPosition = null
export let height = null export let height = null
export let minHeight = null export let minHeight = null
export let helpText = null
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
const onChange = e => { const onChange = e => {
@ -20,7 +21,7 @@
} }
</script> </script>
<Field {label} {labelPosition} {error}> <Field {helpText} {label} {labelPosition} {error}>
<TextArea <TextArea
bind:getCaretPosition bind:getCaretPosition
{error} {error}

View File

@ -9,6 +9,7 @@
export let text = null export let text = null
export let disabled = false export let disabled = false
export let error = null export let error = null
export let helpText = null
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
const onChange = e => { const onChange = e => {
@ -17,6 +18,6 @@
} }
</script> </script>
<Field {label} {labelPosition} {error}> <Field {helpText} {label} {labelPosition} {error}>
<Switch {error} {disabled} {text} {value} on:change={onChange} /> <Switch {error} {disabled} {text} {value} on:change={onChange} />
</Field> </Field>

View File

@ -16,10 +16,9 @@
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
const onClick = e => { const onClick = () => {
if (!disabled) { if (!disabled) {
dispatch("click") dispatch("click")
e.stopPropagation()
} }
} }
</script> </script>

View File

@ -1,5 +1,6 @@
<script> <script>
import Input from "../Form/Input.svelte" import Input from "../Form/Input.svelte"
let value = "" let value = ""
</script> </script>

View File

@ -4,6 +4,7 @@
import Icon from "../Icon/Icon.svelte" import Icon from "../Icon/Icon.svelte"
import Popover from "../Popover/Popover.svelte" import Popover from "../Popover/Popover.svelte"
import { onMount } from "svelte" import { onMount } from "svelte"
const flipDurationMs = 150 const flipDurationMs = 150
export let constraints export let constraints

View File

@ -1,11 +1,10 @@
<script> <script>
import "@spectrum-css/popover/dist/index-vars.css" import "@spectrum-css/popover/dist/index-vars.css"
import Portal from "svelte-portal" import Portal from "svelte-portal"
import { createEventDispatcher } from "svelte" import { createEventDispatcher, getContext } from "svelte"
import positionDropdown from "../Actions/position_dropdown" import positionDropdown from "../Actions/position_dropdown"
import clickOutside from "../Actions/click_outside" import clickOutside from "../Actions/click_outside"
import { fly } from "svelte/transition" import { fly } from "svelte/transition"
import { getContext } from "svelte"
import Context from "../context" import Context from "../context"
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()

View File

@ -1,7 +1,9 @@
<script> <script>
import { getContext } from "svelte" import { getContext } from "svelte"
const multilevel = getContext("sidenav-type") const multilevel = getContext("sidenav-type")
import Badge from "../Badge/Badge.svelte" import Badge from "../Badge/Badge.svelte"
export let href = "" export let href = ""
export let external = false export let external = false
export let heading = "" export let heading = ""

View File

@ -1,6 +1,7 @@
<script> <script>
import { setContext } from "svelte" import { setContext } from "svelte"
import "@spectrum-css/sidenav/dist/index-vars.css" import "@spectrum-css/sidenav/dist/index-vars.css"
export let multilevel = false export let multilevel = false
setContext("sidenav-type", multilevel) setContext("sidenav-type", multilevel)
</script> </script>

View File

@ -1,6 +1,7 @@
<script> <script>
import "@spectrum-css/label/dist/index-vars.css" import "@spectrum-css/label/dist/index-vars.css"
import Badge from "../Badge/Badge.svelte" import Badge from "../Badge/Badge.svelte"
export let value export let value
const displayLimit = 5 const displayLimit = 5

View File

@ -1,6 +1,7 @@
<script> <script>
import { getContext, onMount, createEventDispatcher } from "svelte" import { getContext, onMount, createEventDispatcher } from "svelte"
import Portal from "svelte-portal" import Portal from "svelte-portal"
export let title export let title
export let icon = "" export let icon = ""
export let id export let id

View File

@ -1,4 +1,5 @@
import { helpers } from "@budibase/shared-core" import { helpers } from "@budibase/shared-core"
export const deepGet = helpers.deepGet export const deepGet = helpers.deepGet
/** /**

Some files were not shown because too many files have changed in this diff Show More