Merge branch 'master' into settings-enhancements

This commit is contained in:
Andrew Kingston 2023-11-22 12:26:19 +00:00 committed by GitHub
commit a58af2b7e4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
312 changed files with 2098 additions and 1109 deletions

View File

@ -57,7 +57,10 @@
"destructuredArrayIgnorePattern": "^_" "destructuredArrayIgnorePattern": "^_"
} }
], ],
"import/no-relative-packages": "error" "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

@ -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

@ -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.10", "version": "2.13.14",
"npmClient": "yarn", "npmClient": "yarn",
"packages": [ "packages": [
"packages/*" "packages/*"

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

@ -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

@ -38,6 +38,7 @@ export * as docIds from "./docIds"
// 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,

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

@ -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,
@ -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

@ -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

@ -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

@ -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
@ -78,19 +77,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
/** /**

View File

@ -4,11 +4,10 @@ import { getTemporalStore } from "./store/temporal"
import { getThemeStore } from "./store/theme" import { getThemeStore } from "./store/theme"
import { getUserStore } from "./store/users" import { getUserStore } from "./store/users"
import { getDeploymentStore } from "./store/deployments" import { getDeploymentStore } from "./store/deployments"
import { derived, writable } from "svelte/store" import { derived, writable, get } from "svelte/store"
import { findComponent, findComponentPath } from "./componentUtils" import { findComponent, findComponentPath } from "./componentUtils"
import { RoleUtils } from "@budibase/frontend-core" import { RoleUtils } from "@budibase/frontend-core"
import { createHistoryStore } from "builderStore/store/history" import { createHistoryStore } from "builderStore/store/history"
import { get } from "svelte/store"
export const store = getFrontendStore() export const store = getFrontendStore()
export const automationStore = getAutomationStore() export const automationStore = getAutomationStore()

View File

@ -7,11 +7,9 @@ import {
} from "builderStore" } from "builderStore"
import { datasources, tables } from "stores/backend" import { datasources, tables } from "stores/backend"
import { get } from "svelte/store" import { get } from "svelte/store"
import { auth } from "stores/portal" import { auth, apps } from "stores/portal"
import { SocketEvent, BuilderSocketEvent } from "@budibase/shared-core" import { SocketEvent, BuilderSocketEvent, helpers } from "@budibase/shared-core"
import { apps } from "stores/portal"
import { notifications } from "@budibase/bbui" import { notifications } from "@budibase/bbui"
import { helpers } from "@budibase/shared-core"
export const createBuilderWebsocket = appId => { export const createBuilderWebsocket = appId => {
const socket = createWebsocket("/socket/builder") const socket = createWebsocket("/socket/builder")

View File

@ -1,5 +1,9 @@
<script> <script>
import { automationStore, selectedAutomation } from "builderStore" import {
automationStore,
selectedAutomation,
automationHistoryStore,
} from "builderStore"
import ConfirmDialog from "components/common/ConfirmDialog.svelte" import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import FlowItem from "./FlowItem.svelte" import FlowItem from "./FlowItem.svelte"
import TestDataModal from "./TestDataModal.svelte" import TestDataModal from "./TestDataModal.svelte"
@ -8,7 +12,6 @@
import { Icon, notifications, Modal } from "@budibase/bbui" import { Icon, notifications, Modal } from "@budibase/bbui"
import { ActionStepID } from "constants/backend/automations" import { ActionStepID } from "constants/backend/automations"
import UndoRedoControl from "components/common/UndoRedoControl.svelte" import UndoRedoControl from "components/common/UndoRedoControl.svelte"
import { automationHistoryStore } from "builderStore"
export let automation export let automation

View File

@ -1,7 +1,7 @@
<script> <script>
import { automationStore } from "builderStore" import { automationStore } from "builderStore"
import { notifications } from "@budibase/bbui"
import { import {
notifications,
Input, Input,
InlineAlert, InlineAlert,
ModalContent, ModalContent,

View File

@ -1,7 +1,12 @@
<script> <script>
import { automationStore } from "builderStore" import { automationStore } from "builderStore"
import { notifications } from "@budibase/bbui" import {
import { Icon, Input, ModalContent, Modal } from "@budibase/bbui" notifications,
Icon,
Input,
ModalContent,
Modal,
} from "@budibase/bbui"
export let automation export let automation
export let onCancel = undefined export let onCancel = undefined

View File

@ -38,12 +38,11 @@
EditorModes, EditorModes,
} from "components/common/CodeEditor" } from "components/common/CodeEditor"
import FilterDrawer from "components/design/settings/controls/FilterEditor/FilterDrawer.svelte" import FilterDrawer from "components/design/settings/controls/FilterEditor/FilterDrawer.svelte"
import { LuceneUtils } from "@budibase/frontend-core" import { LuceneUtils, Utils } from "@budibase/frontend-core"
import { import {
getSchemaForDatasourcePlus, getSchemaForDatasourcePlus,
getEnvironmentBindings, getEnvironmentBindings,
} from "builderStore/dataBinding" } from "builderStore/dataBinding"
import { Utils } from "@budibase/frontend-core"
import { TriggerStepID, ActionStepID } from "constants/backend/automations" import { TriggerStepID, ActionStepID } from "constants/backend/automations"
import { onMount } from "svelte" import { onMount } from "svelte"
import { cloneDeep } from "lodash/fp" import { cloneDeep } from "lodash/fp"

View File

@ -2,6 +2,7 @@
import { Button, Select, Input, Label } from "@budibase/bbui" import { Button, Select, Input, Label } from "@budibase/bbui"
import { onMount, createEventDispatcher } from "svelte" import { onMount, createEventDispatcher } from "svelte"
import { flags } from "stores/backend" import { flags } from "stores/backend"
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
export let value export let value

View File

@ -1,8 +1,7 @@
<script> <script>
import { Icon, notifications } from "@budibase/bbui" import { Icon, notifications, ModalContent } from "@budibase/bbui"
import { automationStore, selectedAutomation } from "builderStore" import { automationStore, selectedAutomation } from "builderStore"
import WebhookDisplay from "./WebhookDisplay.svelte" import WebhookDisplay from "./WebhookDisplay.svelte"
import { ModalContent } from "@budibase/bbui"
import { onMount, onDestroy } from "svelte" import { onMount, onDestroy } from "svelte"
const POLL_RATE_MS = 2500 const POLL_RATE_MS = 2500

View File

@ -1,11 +1,15 @@
<script> <script>
import { createEventDispatcher } from "svelte" import { createEventDispatcher } from "svelte"
import { tables } from "stores/backend" import { tables, roles } from "stores/backend"
import { roles } from "stores/backend" import {
import { notifications } from "@budibase/bbui" notifications,
keepOpen,
ModalContent,
Select,
Link,
} from "@budibase/bbui"
import RowFieldControl from "../RowFieldControl.svelte" import RowFieldControl from "../RowFieldControl.svelte"
import { API } from "api" import { API } from "api"
import { keepOpen, ModalContent, Select, Link } from "@budibase/bbui"
import ErrorsBox from "components/common/ErrorsBox.svelte" import ErrorsBox from "components/common/ErrorsBox.svelte"
import { goto } from "@roxi/routify" import { goto } from "@roxi/routify"

View File

@ -1,8 +1,14 @@
<script> <script>
import { keepOpen, ModalContent, Select, Input, Button } from "@budibase/bbui" import {
keepOpen,
ModalContent,
Select,
Input,
Button,
notifications,
} from "@budibase/bbui"
import { onMount } from "svelte" import { onMount } from "svelte"
import { API } from "api" import { API } from "api"
import { notifications } from "@budibase/bbui"
import ErrorsBox from "components/common/ErrorsBox.svelte" import ErrorsBox from "components/common/ErrorsBox.svelte"
import { roles } from "stores/backend" import { roles } from "stores/backend"

View File

@ -8,7 +8,8 @@
} from "@budibase/bbui" } from "@budibase/bbui"
import download from "downloadjs" import download from "downloadjs"
import { API } from "api" import { API } from "api"
import { Constants, LuceneUtils } from "@budibase/frontend-core" import { LuceneUtils } from "@budibase/frontend-core"
import { utils } from "@budibase/shared-core"
import { ROW_EXPORT_FORMATS } from "constants/backend" import { ROW_EXPORT_FORMATS } from "constants/backend"
export let view export let view
@ -32,6 +33,8 @@
}, },
] ]
$: appliedFilters = filters?.filter(filter => !filter.onEmptyFilter)
$: options = FORMATS.filter(format => { $: options = FORMATS.filter(format => {
if (formats && !formats.includes(format.key)) { if (formats && !formats.includes(format.key)) {
return false return false
@ -46,23 +49,20 @@
exportFormat = Array.isArray(options) ? options[0]?.key : [] exportFormat = Array.isArray(options) ? options[0]?.key : []
} }
$: luceneFilter = LuceneUtils.buildLuceneQuery(filters) $: luceneFilter = LuceneUtils.buildLuceneQuery(appliedFilters)
$: exportOpDisplay = buildExportOpDisplay(sorting, filterDisplay, filters) $: exportOpDisplay = buildExportOpDisplay(
sorting,
filterDisplay,
appliedFilters
)
const buildFilterLookup = () => { filterLookup = utils.filterValueToLabel()
return Object.keys(Constants.OperatorOptions).reduce((acc, key) => {
const op = Constants.OperatorOptions[key]
acc[op.value] = op.label
return acc
}, {})
}
filterLookup = buildFilterLookup()
const filterDisplay = () => { const filterDisplay = () => {
if (!filters) { if (!appliedFilters) {
return [] return []
} }
return filters.map(filter => { return appliedFilters.map(filter => {
let newFieldName = filter.field + "" let newFieldName = filter.field + ""
const parts = newFieldName.split(":") const parts = newFieldName.split(":")
parts.shift() parts.shift()
@ -77,7 +77,7 @@
const buildExportOpDisplay = (sorting, filterDisplay) => { const buildExportOpDisplay = (sorting, filterDisplay) => {
let filterDisplayConfig = filterDisplay() let filterDisplayConfig = filterDisplay()
if (sorting) { if (sorting?.sortColumn) {
filterDisplayConfig = [ filterDisplayConfig = [
...filterDisplayConfig, ...filterDisplayConfig,
{ {
@ -132,7 +132,7 @@
format: exportFormat, format: exportFormat,
}) })
downloadWithBlob(data, `export.${exportFormat}`) downloadWithBlob(data, `export.${exportFormat}`)
} else if (filters || sorting) { } else if (appliedFilters || sorting) {
let response let response
try { try {
response = await API.exportRows({ response = await API.exportRows({
@ -163,29 +163,33 @@
title="Export Data" title="Export Data"
confirmText="Export" confirmText="Export"
onConfirm={exportRows} onConfirm={exportRows}
size={filters?.length || sorting ? "M" : "S"} size={appliedFilters?.length || sorting ? "M" : "S"}
> >
{#if selectedRows?.length} {#if selectedRows?.length}
<Body size="S"> <Body size="S">
<strong>{selectedRows?.length}</strong> <span data-testid="exporting-n-rows">
{`row${selectedRows?.length > 1 ? "s" : ""} will be exported`} <strong>{selectedRows?.length}</strong>
{`row${selectedRows?.length > 1 ? "s" : ""} will be exported`}
</span>
</Body> </Body>
{:else if filters || (sorting?.sortOrder && sorting?.sortColumn)} {:else if appliedFilters?.length || (sorting?.sortOrder && sorting?.sortColumn)}
<Body size="S"> <Body size="S">
{#if !filters} {#if !appliedFilters}
Exporting <strong>all</strong> rows <span data-testid="exporting-rows">
Exporting <strong>all</strong> rows
</span>
{:else} {:else}
Filters applied <span data-testid="filters-applied">Filters applied</span>
{/if} {/if}
</Body> </Body>
<div class="table-wrap"> <div class="table-wrap" data-testid="export-config-table">
<Table <Table
schema={displaySchema} schema={displaySchema}
data={exportOpDisplay} data={exportOpDisplay}
{filters} {appliedFilters}
loading={false} loading={false}
rowCount={filters?.length + 1} rowCount={appliedFilters?.length + 1}
disableSorting={true} disableSorting={true}
allowSelectRows={false} allowSelectRows={false}
allowEditRows={false} allowEditRows={false}
@ -196,18 +200,21 @@
</div> </div>
{:else} {:else}
<Body size="S"> <Body size="S">
Exporting <strong>all</strong> rows <span data-testid="export-all-rows">
Exporting <strong>all</strong> rows
</span>
</Body> </Body>
{/if} {/if}
<span data-testid="format-select">
<Select <Select
label="Format" label="Format"
bind:value={exportFormat} bind:value={exportFormat}
{options} {options}
placeholder={null} placeholder={null}
getOptionLabel={x => x.name} getOptionLabel={x => x.name}
getOptionValue={x => x.key} getOptionValue={x => x.key}
/> />
</span>
</ModalContent> </ModalContent>
<style> <style>

View File

@ -0,0 +1,240 @@
import { it, expect, describe, vi } from "vitest"
import { render, screen } from "@testing-library/svelte"
import "@testing-library/jest-dom"
import ExportModal from "./ExportModal.svelte"
import { utils } from "@budibase/shared-core"
const labelLookup = utils.filterValueToLabel()
const rowText = filter => {
let readableField = filter.field.split(":")[1]
let rowLabel = labelLookup[filter.operator]
let value = Array.isArray(filter.value)
? JSON.stringify(filter.value)
: filter.value
return `${readableField}${rowLabel}${value}`.trim()
}
const defaultFilters = [
{
onEmptyFilter: "all",
},
]
vi.mock("svelte", async () => {
return {
getContext: () => {
return {
hide: vi.fn(),
cancel: vi.fn(),
}
},
createEventDispatcher: vi.fn(),
onDestroy: vi.fn(),
}
})
vi.mock("api", async () => {
return {
API: {
exportView: vi.fn(),
exportRows: vi.fn(),
},
}
})
describe("Export Modal", () => {
it("show default messaging with no export config specified", () => {
render(ExportModal, {
props: {},
})
expect(screen.getByTestId("export-all-rows")).toBeVisible()
expect(screen.getByTestId("export-all-rows")).toHaveTextContent(
"Exporting all rows"
)
expect(screen.queryByTestId("export-config-table")).toBe(null)
})
it("indicate that a filter is being applied to the export", () => {
const propsCfg = {
filters: [
{
id: "MOQkMx9p9",
field: "1:Cost",
operator: "rangeHigh",
value: "100",
valueType: "Value",
type: "number",
noValue: false,
},
...defaultFilters,
],
}
render(ExportModal, {
props: propsCfg,
})
expect(screen.getByTestId("filters-applied")).toBeVisible()
expect(screen.getByTestId("filters-applied").textContent).toBe(
"Filters applied"
)
const ele = screen.queryByTestId("export-config-table")
expect(ele).toBeVisible()
const rows = ele.getElementsByClassName("spectrum-Table-row")
expect(rows.length).toBe(1)
let rowTextContent = rowText(propsCfg.filters[0])
//"CostLess than or equal to100"
expect(rows[0].textContent?.trim()).toEqual(rowTextContent)
})
it("Show only selected row messaging if rows are supplied", () => {
const propsCfg = {
filters: [
{
id: "MOQkMx9p9",
field: "1:Cost",
operator: "rangeHigh",
value: "100",
valueType: "Value",
type: "number",
noValue: false,
},
...defaultFilters,
],
sorting: {
sortColumn: "Cost",
sortOrder: "descending",
},
selectedRows: [
{
_id: "ro_ta_bb_expenses_57d5f6fe1b6640d8bb22b15f5eae62cd",
},
{
_id: "ro_ta_bb_expenses_99ce5760a53a430bab4349cd70335a07",
},
],
}
render(ExportModal, {
props: propsCfg,
})
expect(screen.queryByTestId("export-config-table")).toBeNull()
expect(screen.queryByTestId("filters-applied")).toBeNull()
expect(screen.queryByTestId("exporting-n-rows")).toBeVisible()
expect(screen.queryByTestId("exporting-n-rows").textContent).toEqual(
"2 rows will be exported"
)
})
it("Show only the configured sort when no filters are specified", () => {
const propsCfg = {
filters: [...defaultFilters],
sorting: {
sortColumn: "Cost",
sortOrder: "descending",
},
}
render(ExportModal, {
props: propsCfg,
})
expect(screen.queryByTestId("export-config-table")).toBeVisible()
const ele = screen.queryByTestId("export-config-table")
const rows = ele.getElementsByClassName("spectrum-Table-row")
expect(rows.length).toBe(1)
expect(rows[0].textContent?.trim()).toEqual(
`${propsCfg.sorting.sortColumn}Order By${propsCfg.sorting.sortOrder}`
)
})
it("Display all currently configured filters and applied sort", () => {
const propsCfg = {
filters: [
{
id: "MOQkMx9p9",
field: "1:Cost",
operator: "rangeHigh",
value: "100",
valueType: "Value",
type: "number",
noValue: false,
},
{
id: "2ot-aB0gE",
field: "2:Expense Tags",
operator: "contains",
value: ["Equipment", "Services"],
valueType: "Value",
type: "array",
noValue: false,
},
...defaultFilters,
],
sorting: {
sortColumn: "Payment Due",
sortOrder: "ascending",
},
}
render(ExportModal, {
props: propsCfg,
})
const ele = screen.queryByTestId("export-config-table")
expect(ele).toBeVisible()
const rows = ele.getElementsByClassName("spectrum-Table-row")
expect(rows.length).toBe(3)
let rowTextContent1 = rowText(propsCfg.filters[0])
expect(rows[0].textContent?.trim()).toEqual(rowTextContent1)
let rowTextContent2 = rowText(propsCfg.filters[1])
expect(rows[1].textContent?.trim()).toEqual(rowTextContent2)
expect(rows[2].textContent?.trim()).toEqual(
`${propsCfg.sorting.sortColumn}Order By${propsCfg.sorting.sortOrder}`
)
})
it("show only the valid, configured download formats", () => {
const propsCfg = {
formats: ["badger", "json"],
}
render(ExportModal, {
props: propsCfg,
})
let ele = screen.getByTestId("format-select")
expect(ele).toBeVisible()
let formatDisplay = ele.getElementsByTagName("button")[0]
expect(formatDisplay.textContent.trim()).toBe("JSON")
})
it("Load the default format config when no explicit formats are configured", () => {
render(ExportModal, {
props: {},
})
let ele = screen.getByTestId("format-select")
expect(ele).toBeVisible()
let formatDisplay = ele.getElementsByTagName("button")[0]
expect(formatDisplay.textContent.trim()).toBe("CSV")
})
})

View File

@ -1,8 +1,7 @@
<script> <script>
import { get } from "svelte/store" import { get } from "svelte/store"
import { datasources, integrations } from "stores/backend" import { datasources, integrations } from "stores/backend"
import { notifications } from "@budibase/bbui" import { notifications, Input, ModalContent, Modal } from "@budibase/bbui"
import { Input, ModalContent, Modal } from "@budibase/bbui"
import { integrationForDatasource } from "stores/selectors" import { integrationForDatasource } from "stores/selectors"
let error = "" let error = ""

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