Merge remote-tracking branch 'origin/develop' into feature/automation-log-filter-by-license
This commit is contained in:
commit
5067e4682c
|
@ -31,6 +31,9 @@ A clear and concise description of what you expected to happen.
|
|||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**App Export**
|
||||
If possible - please attach an export of your budibase application for debugging/reproduction purposes.
|
||||
|
||||
**Desktop (please complete the following information):**
|
||||
- OS: [e.g. iOS]
|
||||
|
|
|
@ -57,3 +57,12 @@ jobs:
|
|||
platforms: linux/amd64,linux/arm64
|
||||
tags: budibase/budibase,budibase/budibase:v${{ env.RELEASE_VERSION }}
|
||||
file: ./hosting/single/Dockerfile
|
||||
- name: Tag and release Budibase Azure App Service docker image
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
platforms: linux/amd64
|
||||
build-args: TARGETBUILD=aas
|
||||
tags: budibase/budibase-aas,budibase/budibase-aas:v${{ env.RELEASE_VERSION }}
|
||||
file: ./hosting/single/Dockerfile
|
||||
|
|
|
@ -135,13 +135,18 @@ You can learn more about the Budibase API at the following places:
|
|||
|
||||
## 🏁 Get started
|
||||
|
||||
<a href="https://docs.budibase.com/docs/hosting-methods"><img src="https://res.cloudinary.com/daog6scxm/image/upload/v1634808888/logo/deploy_npl9za.png" /></a>
|
||||
|
||||
Deploy Budibase self-hosted in your existing infrastructure, using Docker, Kubernetes, and Digital Ocean.
|
||||
Or use Budibase Cloud if you don't need to self-host, and would like to get started quickly.
|
||||
|
||||
### [Get started with self-hosting Budibase](https://docs.budibase.com/docs/hosting-methods)
|
||||
|
||||
- [Docker - single ARM compatible image](https://docs.budibase.com/docs/docker)
|
||||
- [Docker Compose](https://docs.budibase.com/docs/docker-compose)
|
||||
- [Kubernetes](https://docs.budibase.com/docs/kubernetes-k8s)
|
||||
- [Digital Ocean](https://docs.budibase.com/docs/digitalocean)
|
||||
- [Portainer](https://docs.budibase.com/docs/portainer)
|
||||
|
||||
|
||||
### [Get started with Budibase Cloud](https://budibase.com)
|
||||
|
||||
|
||||
|
|
|
@ -151,6 +151,10 @@ spec:
|
|||
tolerations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{ if .Values.imagePullSecrets }}
|
||||
imagePullSecrets:
|
||||
{{- toYaml .Values.imagePullSecrets | nindent 6 }}
|
||||
{{ end }}
|
||||
restartPolicy: Always
|
||||
serviceAccountName: ""
|
||||
status: {}
|
||||
|
|
|
@ -68,6 +68,10 @@ spec:
|
|||
tolerations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{ if .Values.imagePullSecrets }}
|
||||
imagePullSecrets:
|
||||
{{- toYaml .Values.imagePullSecrets | nindent 6 }}
|
||||
{{ end }}
|
||||
restartPolicy: Always
|
||||
serviceAccountName: ""
|
||||
volumes:
|
||||
|
@ -75,4 +79,4 @@ spec:
|
|||
persistentVolumeClaim:
|
||||
claimName: minio-data
|
||||
status: {}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
|
|
@ -40,6 +40,10 @@ spec:
|
|||
tolerations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{ if .Values.imagePullSecrets }}
|
||||
imagePullSecrets:
|
||||
{{- toYaml .Values.imagePullSecrets | nindent 6 }}
|
||||
{{ end }}
|
||||
restartPolicy: Always
|
||||
serviceAccountName: ""
|
||||
volumes:
|
||||
|
|
|
@ -47,6 +47,10 @@ spec:
|
|||
tolerations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{ if .Values.imagePullSecrets }}
|
||||
imagePullSecrets:
|
||||
{{- toYaml .Values.imagePullSecrets | nindent 6 }}
|
||||
{{ end }}
|
||||
restartPolicy: Always
|
||||
serviceAccountName: ""
|
||||
volumes:
|
||||
|
@ -54,4 +58,4 @@ spec:
|
|||
persistentVolumeClaim:
|
||||
claimName: redis-data
|
||||
status: {}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
|
|
@ -145,6 +145,10 @@ spec:
|
|||
tolerations:
|
||||
{{- toYaml . | nindent 8 }}
|
||||
{{- end }}
|
||||
{{ if .Values.imagePullSecrets }}
|
||||
imagePullSecrets:
|
||||
{{- toYaml .Values.imagePullSecrets | nindent 6 }}
|
||||
{{ end }}
|
||||
restartPolicy: Always
|
||||
serviceAccountName: ""
|
||||
status: {}
|
||||
|
|
|
@ -11,10 +11,11 @@ services:
|
|||
- minio_data:/data
|
||||
ports:
|
||||
- "${MINIO_PORT}:9000"
|
||||
- "9001:9001"
|
||||
environment:
|
||||
MINIO_ACCESS_KEY: ${MINIO_ACCESS_KEY}
|
||||
MINIO_SECRET_KEY: ${MINIO_SECRET_KEY}
|
||||
command: server /data
|
||||
command: server /data --console-address ":9001"
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
|
||||
interval: 30s
|
||||
|
|
|
@ -63,7 +63,7 @@ services:
|
|||
MINIO_ACCESS_KEY: ${MINIO_ACCESS_KEY}
|
||||
MINIO_SECRET_KEY: ${MINIO_SECRET_KEY}
|
||||
MINIO_BROWSER: "off"
|
||||
command: server /data
|
||||
command: server /data --console-address ":9001"
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
|
||||
interval: 30s
|
||||
|
|
|
@ -3,15 +3,15 @@
|
|||
echo ${TARGETBUILD} > /buildtarget.txt
|
||||
if [[ "${TARGETBUILD}" = "aas" ]]; then
|
||||
# Azure AppService uses /home for persisent data & SSH on port 2222
|
||||
mkdir -p /home/budibase/{minio,couchdb}
|
||||
mkdir -p /home/budibase/couchdb/data
|
||||
chown -R couchdb:couchdb /home/budibase/couchdb/
|
||||
mkdir -p /home/{search,minio,couch}
|
||||
mkdir -p /home/couch/{dbs,views}
|
||||
chown -R couchdb:couchdb /home/couch/
|
||||
apt update
|
||||
apt-get install -y openssh-server
|
||||
sed -i 's#dir=/opt/couchdb/data/search#dir=/home/budibase/couchdb/data/search#' /opt/clouseau/clouseau.ini
|
||||
sed -i 's#/minio/minio server /minio &#/minio/minio server /home/budibase/minio &#' /runner.sh
|
||||
sed -i 's#database_dir = ./data#database_dir = /home/budibase/couchdb/data#' /opt/couchdb/etc/default.ini
|
||||
sed -i 's#view_index_dir = ./data#view_index_dir = /home/budibase/couchdb/data#' /opt/couchdb/etc/default.ini
|
||||
sed -i 's#dir=/opt/couchdb/data/search#dir=/home/search#' /opt/clouseau/clouseau.ini
|
||||
sed -i 's#/minio/minio server /minio &#/minio/minio server /home/minio &#' /runner.sh
|
||||
sed -i 's#database_dir = ./data#database_dir = /home/couch/dbs#' /opt/couchdb/etc/default.ini
|
||||
sed -i 's#view_index_dir = ./data#view_index_dir = /home/couch/views#' /opt/couchdb/etc/default.ini
|
||||
sed -i "s/#Port 22/Port 2222/" /etc/ssh/sshd_config
|
||||
/etc/init.d/ssh restart
|
||||
fi
|
||||
|
|
|
@ -108,7 +108,7 @@ RUN chmod +x install.sh && ./install.sh
|
|||
WORKDIR /
|
||||
ADD hosting/single/runner.sh .
|
||||
RUN chmod +x ./runner.sh
|
||||
ADD hosting/scripts/healthcheck.sh .
|
||||
ADD hosting/single/healthcheck.sh .
|
||||
RUN chmod +x ./healthcheck.sh
|
||||
|
||||
ADD hosting/scripts/build-target-paths.sh .
|
||||
|
@ -122,8 +122,7 @@ RUN yarn cache clean -f
|
|||
|
||||
EXPOSE 80
|
||||
EXPOSE 443
|
||||
VOLUME /opt/couchdb/data
|
||||
VOLUME /minio
|
||||
VOLUME /data
|
||||
|
||||
# setup letsencrypt certificate
|
||||
RUN apt-get install -y certbot python3-certbot-nginx
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
#!/usr/bin/env bash
|
||||
healthy=true
|
||||
|
||||
if [ -f "/data/.env" ]; then
|
||||
export $(cat /data/.env | xargs)
|
||||
fi
|
||||
|
||||
if [[ $(curl -Lfk -s -w "%{http_code}\n" http://localhost/ -o /dev/null) -ne 200 ]]; then
|
||||
echo 'ERROR: Budibase is not running';
|
||||
healthy=false
|
|
@ -24,8 +24,8 @@ if [ ! -f "/data/.env" ]; then
|
|||
fi
|
||||
|
||||
# make these directories in runner, incase of mount
|
||||
mkdir -p /data/couch/dbs /data/couch/views
|
||||
chown couchdb:couchdb /data/couch /data/couch/dbs /data/couch/views
|
||||
mkdir -p /data/couch/{dbs,views} /home/couch/{dbs,views}
|
||||
chown -R couchdb:couchdb /data/couch /home/couch
|
||||
redis-server --requirepass $REDIS_PASSWORD &
|
||||
/opt/clouseau/bin/clouseau &
|
||||
/minio/minio server /data/minio &
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"version": "1.1.10-alpha.3",
|
||||
"version": "1.1.32-alpha.1",
|
||||
"npmClient": "yarn",
|
||||
"packages": [
|
||||
"packages/*"
|
||||
|
|
|
@ -26,7 +26,7 @@
|
|||
"build": "lerna run build",
|
||||
"build:dev": "lerna run prebuild && tsc --build --watch --preserveWatchOutput",
|
||||
"release": "lerna publish ${RELEASE_VERSION_TYPE:-patch} --yes --force-publish && yarn release:pro",
|
||||
"release:develop": "lerna publish prerelease --yes --force-publish --dist-tag develop && yarn release:pro:develop",
|
||||
"release:develop": "lerna publish prerelease --yes --force-publish --dist-tag develop --exact && yarn release:pro:develop",
|
||||
"release:pro": "bash scripts/pro/release.sh",
|
||||
"release:pro:develop": "bash scripts/pro/release.sh develop",
|
||||
"restore": "yarn run clean && yarn run bootstrap && yarn run build",
|
||||
|
@ -85,4 +85,4 @@
|
|||
"install:pro": "bash scripts/pro/install.sh",
|
||||
"dep:clean": "yarn clean && yarn bootstrap"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@budibase/backend-core",
|
||||
"version": "1.1.10-alpha.3",
|
||||
"version": "1.1.32-alpha.1",
|
||||
"description": "Budibase backend core libraries used in server and worker",
|
||||
"main": "dist/src/index.js",
|
||||
"types": "dist/src/index.d.ts",
|
||||
|
@ -20,7 +20,7 @@
|
|||
"test:watch": "jest --watchAll"
|
||||
},
|
||||
"dependencies": {
|
||||
"@budibase/types": "^1.1.10-alpha.3",
|
||||
"@budibase/types": "1.1.32-alpha.1",
|
||||
"@techpass/passport-openidconnect": "0.3.2",
|
||||
"aws-sdk": "2.1030.0",
|
||||
"bcrypt": "5.0.1",
|
||||
|
|
|
@ -18,6 +18,8 @@ const {
|
|||
ssoCallbackUrl,
|
||||
csrf,
|
||||
internalApi,
|
||||
adminOnly,
|
||||
joiValidator,
|
||||
} = require("./middleware")
|
||||
|
||||
const { invalidateUser } = require("./cache/user")
|
||||
|
@ -173,4 +175,6 @@ module.exports = {
|
|||
refreshOAuthToken,
|
||||
updateUserOAuth,
|
||||
ssoCallbackUrl,
|
||||
adminOnly,
|
||||
joiValidator,
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import BaseCache from "./base"
|
||||
import { getWritethroughClient } from "../redis/init"
|
||||
import { logWarn } from "../logging"
|
||||
|
||||
const DEFAULT_WRITE_RATE_MS = 10000
|
||||
let CACHE: BaseCache | null = null
|
||||
|
@ -51,10 +52,8 @@ export async function put(
|
|||
if (err.status !== 409) {
|
||||
throw err
|
||||
} else {
|
||||
// get the rev, update over it - this is risky, may change in future
|
||||
const readDoc = await db.get(doc._id)
|
||||
doc._rev = readDoc._rev
|
||||
await writeDb(doc)
|
||||
// Swallow 409s but log them
|
||||
logWarn(`Ignoring conflict in write-through cache`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -67,6 +67,10 @@ export const getTenantIDFromAppID = (appId: string) => {
|
|||
|
||||
// used for automations, API endpoints should always be in context already
|
||||
export const doInTenant = (tenantId: string | null, task: any) => {
|
||||
// make sure default always selected in single tenancy
|
||||
if (!env.MULTI_TENANCY) {
|
||||
tenantId = tenantId || DEFAULT_TENANT_ID
|
||||
}
|
||||
// the internal function is so that we can re-use an existing
|
||||
// context - don't want to close DB on a parent context
|
||||
async function internal(opts = { existing: false }) {
|
||||
|
|
|
@ -11,6 +11,7 @@ export enum AutomationViewModes {
|
|||
}
|
||||
|
||||
export enum ViewNames {
|
||||
USER_BY_APP = "by_app",
|
||||
USER_BY_EMAIL = "by_email2",
|
||||
BY_API_KEY = "by_api_key",
|
||||
USER_BY_BUILDERS = "by_builders",
|
||||
|
@ -28,6 +29,7 @@ export const DeprecatedViews = {
|
|||
|
||||
export enum DocumentTypes {
|
||||
USER = "us",
|
||||
GROUP = "gr",
|
||||
WORKSPACE = "workspace",
|
||||
CONFIG = "config",
|
||||
TEMPLATE = "template",
|
||||
|
|
|
@ -50,3 +50,8 @@ exports.getProdAppID = appId => {
|
|||
const rest = split.join(APP_DEV_PREFIX)
|
||||
return `${APP_PREFIX}${rest}`
|
||||
}
|
||||
|
||||
exports.extractAppUUID = id => {
|
||||
const split = id?.split("_") || []
|
||||
return split.length ? split[split.length - 1] : null
|
||||
}
|
||||
|
|
|
@ -102,6 +102,13 @@ exports.getPouch = (opts = {}) => {
|
|||
}
|
||||
}
|
||||
|
||||
if (opts.onDisk) {
|
||||
POUCH_DB_DEFAULTS = {
|
||||
prefix: undefined,
|
||||
adapter: "leveldb",
|
||||
}
|
||||
}
|
||||
|
||||
if (opts.replication) {
|
||||
const replicationStream = require("pouchdb-replication-stream")
|
||||
PouchDB.plugin(replicationStream.plugin)
|
||||
|
|
|
@ -8,7 +8,7 @@ import { doWithDB, allDbs } from "./index"
|
|||
import { getCouchInfo } from "./pouch"
|
||||
import { getAppMetadata } from "../cache/appMetadata"
|
||||
import { checkSlashesInUrl } from "../helpers"
|
||||
import { isDevApp, isDevAppID } from "./conversions"
|
||||
import { isDevApp, isDevAppID, getProdAppID } from "./conversions"
|
||||
import { APP_PREFIX } from "./constants"
|
||||
import * as events from "../events"
|
||||
|
||||
|
@ -107,6 +107,15 @@ export function getGlobalUserParams(globalId: any, otherProps: any = {}) {
|
|||
}
|
||||
}
|
||||
|
||||
export function getUsersByAppParams(appId: any, otherProps: any = {}) {
|
||||
const prodAppId = getProdAppID(appId)
|
||||
return {
|
||||
...otherProps,
|
||||
startkey: prodAppId,
|
||||
endkey: `${prodAppId}${UNICODE_MAX}`,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a template ID.
|
||||
* @param ownerId The owner/user of the template, this could be global or a workspace level.
|
||||
|
@ -115,6 +124,10 @@ export function generateTemplateID(ownerId: any) {
|
|||
return `${DocumentTypes.TEMPLATE}${SEPARATOR}${ownerId}${SEPARATOR}${newid()}`
|
||||
}
|
||||
|
||||
export function generateAppUserID(prodAppId: string, userId: string) {
|
||||
return `${prodAppId}${SEPARATOR}${userId}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets parameters for retrieving templates. Owner ID must be specified, either global or a workspace level.
|
||||
*/
|
||||
|
@ -442,15 +455,29 @@ export const getPlatformUrl = async (opts = { tenantAware: true }) => {
|
|||
export function pagination(
|
||||
data: any[],
|
||||
pageSize: number,
|
||||
{ paginate, property } = { paginate: true, property: "_id" }
|
||||
{
|
||||
paginate,
|
||||
property,
|
||||
getKey,
|
||||
}: {
|
||||
paginate: boolean
|
||||
property: string
|
||||
getKey?: (doc: any) => string | undefined
|
||||
} = {
|
||||
paginate: true,
|
||||
property: "_id",
|
||||
}
|
||||
) {
|
||||
if (!paginate) {
|
||||
return { data, hasNextPage: false }
|
||||
}
|
||||
const hasNextPage = data.length > pageSize
|
||||
let nextPage = undefined
|
||||
if (!getKey) {
|
||||
getKey = (doc: any) => (property ? doc?.[property] : doc?._id)
|
||||
}
|
||||
if (hasNextPage) {
|
||||
nextPage = property ? data[pageSize]?.[property] : data[pageSize]?._id
|
||||
nextPage = getKey(data[pageSize])
|
||||
}
|
||||
return {
|
||||
data: data.slice(0, pageSize),
|
||||
|
|
|
@ -56,6 +56,33 @@ exports.createNewUserEmailView = async () => {
|
|||
await db.put(designDoc)
|
||||
}
|
||||
|
||||
exports.createUserAppView = async () => {
|
||||
const db = getGlobalDB()
|
||||
let designDoc
|
||||
try {
|
||||
designDoc = await db.get("_design/database")
|
||||
} catch (err) {
|
||||
// no design doc, make one
|
||||
designDoc = DesignDoc()
|
||||
}
|
||||
const view = {
|
||||
// if using variables in a map function need to inject them before use
|
||||
map: `function(doc) {
|
||||
if (doc._id.startsWith("${DocumentTypes.USER}${SEPARATOR}") && doc.roles) {
|
||||
for (let prodAppId of Object.keys(doc.roles)) {
|
||||
let emitted = prodAppId + "${SEPARATOR}" + doc._id
|
||||
emit(emitted, null)
|
||||
}
|
||||
}
|
||||
}`,
|
||||
}
|
||||
designDoc.views = {
|
||||
...designDoc.views,
|
||||
[ViewNames.USER_BY_APP]: view,
|
||||
}
|
||||
await db.put(designDoc)
|
||||
}
|
||||
|
||||
exports.createApiKeyView = async () => {
|
||||
const db = getGlobalDB()
|
||||
let designDoc
|
||||
|
@ -106,6 +133,7 @@ exports.queryGlobalView = async (viewName, params, db = null) => {
|
|||
[ViewNames.USER_BY_EMAIL]: exports.createNewUserEmailView,
|
||||
[ViewNames.BY_API_KEY]: exports.createApiKeyView,
|
||||
[ViewNames.USER_BY_BUILDERS]: exports.createUserBuildersView,
|
||||
[ViewNames.USER_BY_APP]: exports.createUserAppView,
|
||||
}
|
||||
// can pass DB in if working with something specific
|
||||
if (!db) {
|
||||
|
|
|
@ -37,6 +37,7 @@ module.exports = {
|
|||
types,
|
||||
errors: {
|
||||
UsageLimitError: licensing.UsageLimitError,
|
||||
FeatureDisabledError: licensing.FeatureDisabledError,
|
||||
HTTPError: http.HTTPError,
|
||||
},
|
||||
getPublicError,
|
||||
|
|
|
@ -4,6 +4,7 @@ const type = "license_error"
|
|||
|
||||
const codes = {
|
||||
USAGE_LIMIT_EXCEEDED: "usage_limit_exceeded",
|
||||
FEATURE_DISABLED: "feature_disabled",
|
||||
}
|
||||
|
||||
const context = {
|
||||
|
@ -12,6 +13,11 @@ const context = {
|
|||
limitName: err.limitName,
|
||||
}
|
||||
},
|
||||
[codes.FEATURE_DISABLED]: err => {
|
||||
return {
|
||||
featureName: err.featureName,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
class UsageLimitError extends HTTPError {
|
||||
|
@ -21,9 +27,17 @@ class UsageLimitError extends HTTPError {
|
|||
}
|
||||
}
|
||||
|
||||
class FeatureDisabledError extends HTTPError {
|
||||
constructor(message, featureName) {
|
||||
super(message, 400, codes.FEATURE_DISABLED, type)
|
||||
this.featureName = featureName
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
type,
|
||||
codes,
|
||||
context,
|
||||
UsageLimitError,
|
||||
FeatureDisabledError,
|
||||
}
|
||||
|
|
|
@ -0,0 +1,64 @@
|
|||
import { publishEvent } from "../events"
|
||||
import {
|
||||
Event,
|
||||
UserGroup,
|
||||
GroupCreatedEvent,
|
||||
GroupDeletedEvent,
|
||||
GroupUpdatedEvent,
|
||||
GroupUsersAddedEvent,
|
||||
GroupUsersDeletedEvent,
|
||||
GroupAddedOnboardingEvent,
|
||||
UserGroupRoles,
|
||||
} from "@budibase/types"
|
||||
|
||||
export async function created(group: UserGroup, timestamp?: number) {
|
||||
const properties: GroupCreatedEvent = {
|
||||
groupId: group._id as string,
|
||||
}
|
||||
await publishEvent(Event.USER_GROUP_CREATED, properties, timestamp)
|
||||
}
|
||||
|
||||
export async function updated(group: UserGroup) {
|
||||
const properties: GroupUpdatedEvent = {
|
||||
groupId: group._id as string,
|
||||
}
|
||||
await publishEvent(Event.USER_GROUP_UPDATED, properties)
|
||||
}
|
||||
|
||||
export async function deleted(group: UserGroup) {
|
||||
const properties: GroupDeletedEvent = {
|
||||
groupId: group._id as string,
|
||||
}
|
||||
await publishEvent(Event.USER_GROUP_DELETED, properties)
|
||||
}
|
||||
|
||||
export async function usersAdded(count: number, group: UserGroup) {
|
||||
const properties: GroupUsersAddedEvent = {
|
||||
count,
|
||||
groupId: group._id as string,
|
||||
}
|
||||
await publishEvent(Event.USER_GROUP_USERS_ADDED, properties)
|
||||
}
|
||||
|
||||
export async function usersDeleted(emails: string[], group: UserGroup) {
|
||||
const properties: GroupUsersDeletedEvent = {
|
||||
count: emails.length,
|
||||
groupId: group._id as string,
|
||||
}
|
||||
await publishEvent(Event.USER_GROUP_USERS_REMOVED, properties)
|
||||
}
|
||||
|
||||
export async function createdOnboarding(groupId: string) {
|
||||
const properties: GroupAddedOnboardingEvent = {
|
||||
groupId: groupId,
|
||||
onboarding: true,
|
||||
}
|
||||
await publishEvent(Event.USER_GROUP_ONBOARDING, properties)
|
||||
}
|
||||
|
||||
export async function permissionsEdited(roles: UserGroupRoles) {
|
||||
const properties: UserGroupRoles = {
|
||||
...roles,
|
||||
}
|
||||
await publishEvent(Event.USER_GROUP_PERMISSIONS_EDITED, properties)
|
||||
}
|
|
@ -17,3 +17,4 @@ export * as user from "./user"
|
|||
export * as view from "./view"
|
||||
export * as installation from "./installation"
|
||||
export * as backfill from "./backfill"
|
||||
export * as group from "./group"
|
||||
|
|
|
@ -3,6 +3,7 @@ const errorClasses = errors.errors
|
|||
import * as events from "./events"
|
||||
import * as migrations from "./migrations"
|
||||
import * as users from "./users"
|
||||
import * as roles from "./security/roles"
|
||||
import * as accounts from "./cloud/accounts"
|
||||
import * as installation from "./installation"
|
||||
import env from "./environment"
|
||||
|
@ -51,6 +52,7 @@ const core = {
|
|||
installation,
|
||||
errors,
|
||||
logging,
|
||||
roles,
|
||||
...errorClasses,
|
||||
}
|
||||
|
||||
|
|
|
@ -15,6 +15,22 @@ export function logAlert(message: string, e?: any) {
|
|||
console.error(`bb-alert: ${message} ${errorJson}`)
|
||||
}
|
||||
|
||||
export function logAlertWithInfo(
|
||||
message: string,
|
||||
db: string,
|
||||
id: string,
|
||||
error: any
|
||||
) {
|
||||
message = `${message} - db: ${db} - doc: ${id} - error: `
|
||||
logAlert(message, error)
|
||||
}
|
||||
|
||||
export function logWarn(message: string) {
|
||||
console.warn(`bb-warn: ${message}`)
|
||||
}
|
||||
|
||||
export default {
|
||||
logAlert,
|
||||
logAlertWithInfo,
|
||||
logWarn,
|
||||
}
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
module.exports = async (ctx, next) => {
|
||||
if (
|
||||
!ctx.internal &&
|
||||
(!ctx.user || !ctx.user.admin || !ctx.user.admin.global)
|
||||
) {
|
||||
ctx.throw(403, "Admin user only endpoint.")
|
||||
}
|
||||
return next()
|
||||
}
|
|
@ -127,7 +127,7 @@ module.exports = (
|
|||
}
|
||||
if (!user && tenantId) {
|
||||
user = { tenantId }
|
||||
} else {
|
||||
} else if (user) {
|
||||
delete user.password
|
||||
}
|
||||
// be explicit
|
||||
|
|
|
@ -9,7 +9,8 @@ const tenancy = require("./tenancy")
|
|||
const internalApi = require("./internalApi")
|
||||
const datasourceGoogle = require("./passport/datasource/google")
|
||||
const csrf = require("./csrf")
|
||||
|
||||
const adminOnly = require("./adminOnly")
|
||||
const joiValidator = require("./joi-validator")
|
||||
module.exports = {
|
||||
google,
|
||||
oidc,
|
||||
|
@ -25,4 +26,6 @@ module.exports = {
|
|||
google: datasourceGoogle,
|
||||
},
|
||||
csrf,
|
||||
adminOnly,
|
||||
joiValidator,
|
||||
}
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
function validate(schema, property) {
|
||||
// Return a Koa middleware function
|
||||
return (ctx, next) => {
|
||||
if (!schema) {
|
||||
return next()
|
||||
}
|
||||
let params = null
|
||||
if (ctx[property] != null) {
|
||||
params = ctx[property]
|
||||
} else if (ctx.request[property] != null) {
|
||||
params = ctx.request[property]
|
||||
}
|
||||
const { error } = schema.validate(params)
|
||||
if (error) {
|
||||
ctx.throw(400, `Invalid ${property} - ${error.message}`)
|
||||
return
|
||||
}
|
||||
return next()
|
||||
}
|
||||
}
|
||||
|
||||
module.exports.body = schema => {
|
||||
return validate(schema, "body")
|
||||
}
|
||||
|
||||
module.exports.params = schema => {
|
||||
return validate(schema, "params")
|
||||
}
|
|
@ -75,9 +75,11 @@ export const ObjectStore = (bucket: any) => {
|
|||
s3ForcePathStyle: true,
|
||||
signatureVersion: "v4",
|
||||
apiVersion: "2006-03-01",
|
||||
params: {
|
||||
}
|
||||
if (bucket) {
|
||||
config.params = {
|
||||
Bucket: sanitizeBucket(bucket),
|
||||
},
|
||||
}
|
||||
}
|
||||
if (env.MINIO_URL) {
|
||||
config.endpoint = env.MINIO_URL
|
||||
|
@ -292,6 +294,7 @@ export const uploadDirectory = async (
|
|||
}
|
||||
}
|
||||
await Promise.all(uploads)
|
||||
return files
|
||||
}
|
||||
|
||||
exports.downloadTarballDirect = async (url: string, path: string) => {
|
||||
|
|
|
@ -76,7 +76,7 @@ function isBuiltin(role) {
|
|||
/**
|
||||
* Works through the inheritance ranks to see how far up the builtin stack this ID is.
|
||||
*/
|
||||
function builtinRoleToNumber(id) {
|
||||
exports.builtinRoleToNumber = id => {
|
||||
const builtins = exports.getBuiltinRoles()
|
||||
const MAX = Object.values(BUILTIN_IDS).length + 1
|
||||
if (id === BUILTIN_IDS.ADMIN || id === BUILTIN_IDS.BUILDER) {
|
||||
|
@ -104,7 +104,8 @@ exports.lowerBuiltinRoleID = (roleId1, roleId2) => {
|
|||
if (!roleId2) {
|
||||
return roleId1
|
||||
}
|
||||
return builtinRoleToNumber(roleId1) > builtinRoleToNumber(roleId2)
|
||||
return exports.builtinRoleToNumber(roleId1) >
|
||||
exports.builtinRoleToNumber(roleId2)
|
||||
? roleId2
|
||||
: roleId1
|
||||
}
|
||||
|
|
|
@ -1,4 +1,9 @@
|
|||
const { ViewNames } = require("./db/utils")
|
||||
const {
|
||||
ViewNames,
|
||||
getUsersByAppParams,
|
||||
getProdAppID,
|
||||
generateAppUserID,
|
||||
} = require("./db/utils")
|
||||
const { queryGlobalView } = require("./db/views")
|
||||
const { UNICODE_MAX } = require("./db/constants")
|
||||
|
||||
|
@ -13,12 +18,32 @@ exports.getGlobalUserByEmail = async email => {
|
|||
throw "Must supply an email address to view"
|
||||
}
|
||||
|
||||
const response = await queryGlobalView(ViewNames.USER_BY_EMAIL, {
|
||||
return await queryGlobalView(ViewNames.USER_BY_EMAIL, {
|
||||
key: email.toLowerCase(),
|
||||
include_docs: true,
|
||||
})
|
||||
}
|
||||
|
||||
return response
|
||||
exports.searchGlobalUsersByApp = async (appId, opts) => {
|
||||
if (typeof appId !== "string") {
|
||||
throw new Error("Must provide a string based app ID")
|
||||
}
|
||||
const params = getUsersByAppParams(appId, {
|
||||
include_docs: true,
|
||||
})
|
||||
params.startkey = opts && opts.startkey ? opts.startkey : params.startkey
|
||||
let response = await queryGlobalView(ViewNames.USER_BY_APP, params)
|
||||
if (!response) {
|
||||
response = []
|
||||
}
|
||||
return Array.isArray(response) ? response : [response]
|
||||
}
|
||||
|
||||
exports.getGlobalUserByAppPage = (appId, user) => {
|
||||
if (!user) {
|
||||
return
|
||||
}
|
||||
return generateAppUserID(getProdAppID(appId), user._id)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -89,6 +89,14 @@ jest.spyOn(events.user, "passwordUpdated")
|
|||
jest.spyOn(events.user, "passwordResetRequested")
|
||||
jest.spyOn(events.user, "passwordReset")
|
||||
|
||||
jest.spyOn(events.group, "created")
|
||||
jest.spyOn(events.group, "updated")
|
||||
jest.spyOn(events.group, "deleted")
|
||||
jest.spyOn(events.group, "usersAdded")
|
||||
jest.spyOn(events.group, "usersDeleted")
|
||||
jest.spyOn(events.group, "createdOnboarding")
|
||||
jest.spyOn(events.group, "permissionsEdited")
|
||||
|
||||
jest.spyOn(events.serve, "servedBuilder")
|
||||
jest.spyOn(events.serve, "servedApp")
|
||||
jest.spyOn(events.serve, "servedAppPreview")
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "@budibase/bbui",
|
||||
"description": "A UI solution used in the different Budibase projects.",
|
||||
"version": "1.1.10-alpha.3",
|
||||
"version": "1.1.32-alpha.1",
|
||||
"license": "MPL-2.0",
|
||||
"svelte": "src/index.js",
|
||||
"module": "dist/bbui.es.js",
|
||||
|
@ -38,7 +38,7 @@
|
|||
],
|
||||
"dependencies": {
|
||||
"@adobe/spectrum-css-workflow-icons": "^1.2.1",
|
||||
"@budibase/string-templates": "^1.1.10-alpha.3",
|
||||
"@budibase/string-templates": "1.1.32-alpha.1",
|
||||
"@spectrum-css/actionbutton": "^1.0.1",
|
||||
"@spectrum-css/actiongroup": "^1.0.1",
|
||||
"@spectrum-css/avatar": "^3.0.2",
|
||||
|
|
|
@ -84,6 +84,7 @@
|
|||
}
|
||||
:global([dir="ltr"] .spectrum-ActionButton .spectrum-Icon) {
|
||||
margin-left: 0;
|
||||
transition: color ease-out 130ms;
|
||||
}
|
||||
.is-selected:not(.spectrum-ActionButton--emphasized) {
|
||||
background: var(--spectrum-global-color-gray-300);
|
||||
|
@ -92,4 +93,10 @@
|
|||
padding: 0;
|
||||
min-width: 0;
|
||||
}
|
||||
.spectrum-ActionButton--quiet {
|
||||
padding: 0 8px;
|
||||
}
|
||||
.is-selected:not(.emphasized) .spectrum-Icon {
|
||||
color: var(--spectrum-global-color-gray-900);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
["XXS", "--spectrum-alias-avatar-size-50"],
|
||||
["XS", "--spectrum-alias-avatar-size-75"],
|
||||
["S", "--spectrum-alias-avatar-size-200"],
|
||||
["M", "--spectrum-alias-avatar-size-300"],
|
||||
["M", "--spectrum-alias-avatar-size-400"],
|
||||
["L", "--spectrum-alias-avatar-size-500"],
|
||||
["XL", "--spectrum-alias-avatar-size-600"],
|
||||
["XXL", "--spectrum-alias-avatar-size-700"],
|
||||
|
@ -13,6 +13,19 @@
|
|||
export let url = ""
|
||||
export let disabled = false
|
||||
export let initials = "JD"
|
||||
|
||||
const DefaultColor = "#3aab87"
|
||||
|
||||
$: color = getColor(initials)
|
||||
|
||||
const getColor = initials => {
|
||||
if (!initials?.length) {
|
||||
return DefaultColor
|
||||
}
|
||||
const code = initials[0].toLowerCase().charCodeAt(0)
|
||||
const hue = ((code % 26) / 26) * 360
|
||||
return `hsl(${hue}, 50%, 50%)`
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if url}
|
||||
|
@ -25,10 +38,11 @@
|
|||
/>
|
||||
{:else}
|
||||
<div
|
||||
class="spectrum-Avatar"
|
||||
class:is-disabled={disabled}
|
||||
style="width: var({sizes.get(size)}); height: var({sizes.get(
|
||||
size
|
||||
)}); font-size: calc(var({sizes.get(size)}) / 2)"
|
||||
)}); font-size: calc(var({sizes.get(size)}) / 2); background: {color};"
|
||||
>
|
||||
{initials || ""}
|
||||
</div>
|
||||
|
@ -40,7 +54,6 @@
|
|||
display: grid;
|
||||
place-items: center;
|
||||
font-weight: 600;
|
||||
background: #3aab87;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
user-select: none;
|
||||
|
|
|
@ -82,6 +82,7 @@
|
|||
}
|
||||
|
||||
.fillWidth {
|
||||
left: 260px !important;
|
||||
width: calc(100% - 260px) !important;
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,218 @@
|
|||
<script>
|
||||
import "@spectrum-css/inputgroup/dist/index-vars.css"
|
||||
import "@spectrum-css/popover/dist/index-vars.css"
|
||||
import "@spectrum-css/menu/dist/index-vars.css"
|
||||
import { fly } from "svelte/transition"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import clickOutside from "../../Actions/click_outside"
|
||||
|
||||
export let inputValue
|
||||
export let dropdownValue
|
||||
export let id = null
|
||||
export let inputType = "text"
|
||||
export let placeholder = "Choose an option or type"
|
||||
export let disabled = false
|
||||
export let readonly = false
|
||||
export let updateOnChange = true
|
||||
export let error = null
|
||||
export let options = []
|
||||
export let getOptionLabel = option => extractProperty(option, "label")
|
||||
export let getOptionValue = option => extractProperty(option, "value")
|
||||
|
||||
export let isOptionSelected = () => false
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
let open = false
|
||||
let focus = false
|
||||
|
||||
$: fieldText = getFieldText(dropdownValue, options, placeholder)
|
||||
|
||||
const getFieldText = (dropdownValue, options, placeholder) => {
|
||||
// Always use placeholder if no value
|
||||
if (dropdownValue == null || dropdownValue === "") {
|
||||
return placeholder || "Choose an option or type"
|
||||
}
|
||||
|
||||
// Wait for options to load if there is a value but no options
|
||||
if (!options?.length) {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Render the label if the selected option is found, otherwise raw value
|
||||
const selected = options.find(
|
||||
option => getOptionValue(option) === dropdownValue
|
||||
)
|
||||
return selected ? getOptionLabel(selected) : dropdownValue
|
||||
}
|
||||
|
||||
const updateValue = newValue => {
|
||||
if (readonly) {
|
||||
return
|
||||
}
|
||||
dispatch("change", newValue)
|
||||
}
|
||||
|
||||
const onFocus = () => {
|
||||
if (readonly) {
|
||||
return
|
||||
}
|
||||
focus = true
|
||||
}
|
||||
|
||||
const onBlur = event => {
|
||||
if (readonly) {
|
||||
return
|
||||
}
|
||||
focus = false
|
||||
updateValue(event.target.value)
|
||||
}
|
||||
|
||||
const onInput = event => {
|
||||
if (readonly || !updateOnChange) {
|
||||
return
|
||||
}
|
||||
updateValue(event.target.value)
|
||||
}
|
||||
|
||||
const updateValueOnEnter = event => {
|
||||
if (readonly) {
|
||||
return
|
||||
}
|
||||
if (event.key === "Enter") {
|
||||
updateValue(event.target.value)
|
||||
}
|
||||
}
|
||||
|
||||
const onClick = () => {
|
||||
dispatch("click")
|
||||
if (readonly) {
|
||||
return
|
||||
}
|
||||
open = true
|
||||
}
|
||||
|
||||
const onPick = newValue => {
|
||||
dispatch("pick", newValue)
|
||||
open = false
|
||||
}
|
||||
|
||||
const extractProperty = (value, property) => {
|
||||
if (value && typeof value === "object") {
|
||||
return value[property]
|
||||
}
|
||||
return value
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="spectrum-InputGroup"
|
||||
class:is-invalid={!!error}
|
||||
class:is-disabled={disabled}
|
||||
>
|
||||
<div
|
||||
class="spectrum-Textfield spectrum-InputGroup-textfield"
|
||||
class:is-invalid={!!error}
|
||||
class:is-disabled={disabled}
|
||||
class:is-focused={focus}
|
||||
>
|
||||
<input
|
||||
{id}
|
||||
on:click
|
||||
on:blur
|
||||
on:focus
|
||||
on:input
|
||||
on:keyup
|
||||
on:blur={onBlur}
|
||||
on:focus={onFocus}
|
||||
on:input={onInput}
|
||||
on:keyup={updateValueOnEnter}
|
||||
value={inputValue || ""}
|
||||
placeholder={placeholder || ""}
|
||||
{disabled}
|
||||
{readonly}
|
||||
{inputType}
|
||||
class="spectrum-Textfield-input spectrum-InputGroup-input"
|
||||
/>
|
||||
</div>
|
||||
<div style="width: 30%">
|
||||
<button
|
||||
{id}
|
||||
class="spectrum-Picker spectrum-Picker--sizeM override-borders"
|
||||
{disabled}
|
||||
class:is-open={open}
|
||||
aria-haspopup="listbox"
|
||||
on:mousedown={onClick}
|
||||
>
|
||||
<span class="spectrum-Picker-label">
|
||||
<div>
|
||||
{fieldText}
|
||||
</div></span
|
||||
>
|
||||
<svg
|
||||
class="spectrum-Icon spectrum-UIIcon-ChevronDown100 spectrum-Picker-menuIcon"
|
||||
focusable="false"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<use xlink:href="#spectrum-css-icon-Chevron100" />
|
||||
</svg>
|
||||
</button>
|
||||
{#if open}
|
||||
<div
|
||||
use:clickOutside={() => (open = false)}
|
||||
transition:fly|local={{ y: -20, duration: 200 }}
|
||||
class="spectrum-Popover spectrum-Popover--bottom spectrum-Picker-popover is-open"
|
||||
>
|
||||
<ul class="spectrum-Menu" role="listbox">
|
||||
{#each options as option, idx}
|
||||
<li
|
||||
class="spectrum-Menu-item"
|
||||
class:is-selected={isOptionSelected(getOptionValue(option, idx))}
|
||||
role="option"
|
||||
aria-selected="true"
|
||||
tabindex="0"
|
||||
on:click={() => onPick(getOptionValue(option, idx))}
|
||||
>
|
||||
<span class="spectrum-Menu-itemLabel">
|
||||
{getOptionLabel(option, idx)}
|
||||
</span>
|
||||
<svg
|
||||
class="spectrum-Icon spectrum-UIIcon-Checkmark100 spectrum-Menu-checkmark spectrum-Menu-itemIcon"
|
||||
focusable="false"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<use xlink:href="#spectrum-css-icon-Checkmark100" />
|
||||
</svg>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.spectrum-InputGroup {
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.spectrum-InputGroup-input {
|
||||
border-right-width: 1px;
|
||||
}
|
||||
.spectrum-Textfield {
|
||||
width: 100%;
|
||||
}
|
||||
.spectrum-Textfield-input {
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.override-borders {
|
||||
border-top-left-radius: 0px;
|
||||
border-bottom-left-radius: 0px;
|
||||
}
|
||||
.spectrum-Popover {
|
||||
max-height: 240px;
|
||||
z-index: 999;
|
||||
top: 100%;
|
||||
}
|
||||
</style>
|
|
@ -13,6 +13,7 @@
|
|||
export let readonly = false
|
||||
export let autocomplete = false
|
||||
export let sort = false
|
||||
export let autoWidth = false
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
$: selectedLookupMap = getSelectedLookupMap(value)
|
||||
|
@ -85,4 +86,5 @@
|
|||
{getOptionValue}
|
||||
onSelectOption={toggleOption}
|
||||
{sort}
|
||||
{autoWidth}
|
||||
/>
|
||||
|
|
|
@ -87,10 +87,15 @@
|
|||
on:mousedown={onClick}
|
||||
>
|
||||
{#if fieldIcon}
|
||||
<span class="option-icon">
|
||||
<span class="option-extra">
|
||||
<Icon name={fieldIcon} />
|
||||
</span>
|
||||
{/if}
|
||||
{#if fieldColour}
|
||||
<span class="option-extra">
|
||||
<StatusLight square color={fieldColour} />
|
||||
</span>
|
||||
{/if}
|
||||
<span
|
||||
class="spectrum-Picker-label"
|
||||
class:is-placeholder={isPlaceholder}
|
||||
|
@ -108,11 +113,6 @@
|
|||
<use xlink:href="#spectrum-icon-18-Alert" />
|
||||
</svg>
|
||||
{/if}
|
||||
{#if fieldColour}
|
||||
<span class="option-colour">
|
||||
<StatusLight size="L" color={fieldColour} />
|
||||
</span>
|
||||
{/if}
|
||||
<svg
|
||||
class="spectrum-Icon spectrum-UIIcon-ChevronDown100 spectrum-Picker-menuIcon"
|
||||
focusable="false"
|
||||
|
@ -166,10 +166,15 @@
|
|||
on:click={() => onSelectOption(getOptionValue(option, idx))}
|
||||
>
|
||||
{#if getOptionIcon(option, idx)}
|
||||
<span class="option-icon">
|
||||
<span class="option-extra">
|
||||
<Icon name={getOptionIcon(option, idx)} />
|
||||
</span>
|
||||
{/if}
|
||||
{#if getOptionColour(option, idx)}
|
||||
<span class="option-extra">
|
||||
<StatusLight square color={getOptionColour(option, idx)} />
|
||||
</span>
|
||||
{/if}
|
||||
<span class="spectrum-Menu-itemLabel">
|
||||
{getOptionLabel(option, idx)}
|
||||
</span>
|
||||
|
@ -180,11 +185,6 @@
|
|||
>
|
||||
<use xlink:href="#spectrum-css-icon-Checkmark100" />
|
||||
</svg>
|
||||
{#if getOptionColour(option, idx)}
|
||||
<span class="option-colour">
|
||||
<StatusLight size="L" color={getOptionColour(option, idx)} />
|
||||
</span>
|
||||
{/if}
|
||||
</li>
|
||||
{/each}
|
||||
{/if}
|
||||
|
@ -209,6 +209,9 @@
|
|||
width: 100%;
|
||||
box-shadow: none;
|
||||
}
|
||||
.spectrum-Picker-label.auto-width {
|
||||
margin-right: var(--spacing-xs);
|
||||
}
|
||||
.spectrum-Picker-label:not(.auto-width) {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
@ -221,16 +224,16 @@
|
|||
.spectrum-Picker-label.auto-width.is-placeholder {
|
||||
padding-right: 2px;
|
||||
}
|
||||
.auto-width .spectrum-Menu-item {
|
||||
padding-right: var(--spacing-xl);
|
||||
}
|
||||
|
||||
/* Icon and colour alignment */
|
||||
.spectrum-Menu-checkmark {
|
||||
align-self: center;
|
||||
margin-top: 0;
|
||||
}
|
||||
.option-colour {
|
||||
padding-left: 8px;
|
||||
}
|
||||
.option-icon {
|
||||
.option-extra {
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,430 @@
|
|||
<script>
|
||||
import "@spectrum-css/inputgroup/dist/index-vars.css"
|
||||
import "@spectrum-css/popover/dist/index-vars.css"
|
||||
import "@spectrum-css/menu/dist/index-vars.css"
|
||||
import { fly } from "svelte/transition"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import clickOutside from "../../Actions/click_outside"
|
||||
import Icon from "../../Icon/Icon.svelte"
|
||||
import StatusLight from "../../StatusLight/StatusLight.svelte"
|
||||
import Detail from "../../Typography/Detail.svelte"
|
||||
|
||||
export let primaryLabel = ""
|
||||
export let primaryValue = null
|
||||
export let id = null
|
||||
export let placeholder = "Choose an option or type"
|
||||
export let disabled = false
|
||||
export let readonly = false
|
||||
export let updateOnChange = true
|
||||
export let error = null
|
||||
export let secondaryOptions = []
|
||||
export let primaryOptions = []
|
||||
export let secondaryFieldText = ""
|
||||
export let secondaryFieldIcon = ""
|
||||
export let secondaryFieldColour = ""
|
||||
export let getPrimaryOptionLabel = option => option
|
||||
export let getPrimaryOptionValue = option => option
|
||||
export let getPrimaryOptionColour = () => null
|
||||
export let getPrimaryOptionIcon = () => null
|
||||
export let getSecondaryOptionLabel = option => option
|
||||
export let getSecondaryOptionValue = option => option
|
||||
export let getSecondaryOptionColour = () => null
|
||||
export let onSelectOption = () => {}
|
||||
export let autoWidth = false
|
||||
export let autocomplete = false
|
||||
export let isOptionSelected = () => false
|
||||
export let isPlaceholder = false
|
||||
export let placeholderOption = null
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
let primaryOpen = false
|
||||
let secondaryOpen = false
|
||||
let focus = false
|
||||
let searchTerm = null
|
||||
|
||||
$: groupTitles = Object.keys(primaryOptions)
|
||||
$: filteredOptions = getFilteredOptions(
|
||||
primaryOptions,
|
||||
searchTerm,
|
||||
getPrimaryOptionLabel
|
||||
)
|
||||
let iconData
|
||||
/*
|
||||
$: iconData = primaryOptions?.find(x => {
|
||||
return x.name === primaryFieldText
|
||||
})
|
||||
*/
|
||||
const updateValue = newValue => {
|
||||
if (readonly) {
|
||||
return
|
||||
}
|
||||
dispatch("change", newValue)
|
||||
}
|
||||
|
||||
const onClickSecondary = () => {
|
||||
dispatch("click")
|
||||
if (readonly) {
|
||||
return
|
||||
}
|
||||
secondaryOpen = true
|
||||
}
|
||||
|
||||
const onPickPrimary = newValue => {
|
||||
dispatch("pickprimary", newValue)
|
||||
primaryOpen = false
|
||||
}
|
||||
|
||||
const onClearPrimary = () => {
|
||||
dispatch("pickprimary", null)
|
||||
primaryOpen = false
|
||||
}
|
||||
|
||||
const onPickSecondary = newValue => {
|
||||
dispatch("picksecondary", newValue)
|
||||
secondaryOpen = false
|
||||
}
|
||||
|
||||
const onBlur = event => {
|
||||
if (readonly) {
|
||||
return
|
||||
}
|
||||
focus = false
|
||||
updateValue(event.target.value)
|
||||
}
|
||||
|
||||
const onInput = event => {
|
||||
if (readonly || !updateOnChange) {
|
||||
return
|
||||
}
|
||||
updateValue(event.target.value)
|
||||
}
|
||||
|
||||
const updateValueOnEnter = event => {
|
||||
if (readonly) {
|
||||
return
|
||||
}
|
||||
if (event.key === "Enter") {
|
||||
updateValue(event.target.value)
|
||||
}
|
||||
}
|
||||
|
||||
const getFilteredOptions = (options, term, getLabel) => {
|
||||
if (autocomplete && term) {
|
||||
const lowerCaseTerm = term.toLowerCase()
|
||||
return options.filter(option => {
|
||||
return `${getLabel(option)}`.toLowerCase().includes(lowerCaseTerm)
|
||||
})
|
||||
}
|
||||
return options
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="spectrum-InputGroup"
|
||||
class:is-invalid={!!error}
|
||||
class:is-disabled={disabled}
|
||||
>
|
||||
<div
|
||||
class="spectrum-Textfield spectrum-InputGroup-textfield"
|
||||
class:is-invalid={!!error}
|
||||
class:is-disabled={disabled}
|
||||
class:is-focused={focus}
|
||||
class:is-full-width={!secondaryOptions.length}
|
||||
>
|
||||
{#if iconData}
|
||||
<svg
|
||||
width="16px"
|
||||
height="16px"
|
||||
class="spectrum-Icon iconPadding"
|
||||
style="color: {iconData?.color}"
|
||||
focusable="false"
|
||||
>
|
||||
<use xlink:href="#spectrum-icon-18-{iconData?.icon}" />
|
||||
</svg>
|
||||
{/if}
|
||||
<input
|
||||
{id}
|
||||
on:click={() => (primaryOpen = true)}
|
||||
on:blur
|
||||
on:focus
|
||||
on:input
|
||||
on:keyup
|
||||
on:blur={onBlur}
|
||||
on:input={onInput}
|
||||
on:keyup={updateValueOnEnter}
|
||||
value={primaryLabel || ""}
|
||||
placeholder={placeholder || ""}
|
||||
{disabled}
|
||||
{readonly}
|
||||
class="spectrum-Textfield-input spectrum-InputGroup-input"
|
||||
class:labelPadding={iconData}
|
||||
/>
|
||||
{#if primaryValue}
|
||||
<button
|
||||
on:click={() => onClearPrimary()}
|
||||
type="reset"
|
||||
class="spectrum-ClearButton spectrum-Search-clearButton"
|
||||
>
|
||||
<svg
|
||||
class="spectrum-Icon spectrum-UIIcon-Cross75"
|
||||
focusable="false"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<use xlink:href="#spectrum-css-icon-Cross75" />
|
||||
</svg>
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
{#if primaryOpen}
|
||||
<div
|
||||
use:clickOutside={() => (primaryOpen = false)}
|
||||
transition:fly|local={{ y: -20, duration: 200 }}
|
||||
class="spectrum-Popover spectrum-Popover--bottom spectrum-Picker-popover is-open"
|
||||
class:auto-width={autoWidth}
|
||||
class:is-full-width={!secondaryOptions.length}
|
||||
>
|
||||
<ul class="spectrum-Menu" role="listbox">
|
||||
{#if placeholderOption}
|
||||
<li
|
||||
class="spectrum-Menu-item placeholder"
|
||||
class:is-selected={isPlaceholder}
|
||||
role="option"
|
||||
aria-selected="true"
|
||||
tabindex="0"
|
||||
on:click={() => onSelectOption(null)}
|
||||
>
|
||||
<span class="spectrum-Menu-itemLabel">{placeholderOption}</span>
|
||||
<svg
|
||||
class="spectrum-Icon spectrum-UIIcon-Checkmark100 spectrum-Menu-checkmark spectrum-Menu-itemIcon"
|
||||
focusable="false"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<use xlink:href="#spectrum-css-icon-Checkmark100" />
|
||||
</svg>
|
||||
</li>
|
||||
{/if}
|
||||
{#each groupTitles as title}
|
||||
<div class="spectrum-Menu-item">
|
||||
<Detail>{title}</Detail>
|
||||
</div>
|
||||
{#if primaryOptions}
|
||||
{#each primaryOptions[title].data as option, idx}
|
||||
<li
|
||||
class="spectrum-Menu-item"
|
||||
class:is-selected={isOptionSelected(
|
||||
getPrimaryOptionValue(option, idx)
|
||||
)}
|
||||
role="option"
|
||||
aria-selected="true"
|
||||
tabindex="0"
|
||||
on:click={() =>
|
||||
onPickPrimary({
|
||||
value: primaryOptions[title].getValue(option),
|
||||
label: primaryOptions[title].getLabel(option),
|
||||
})}
|
||||
>
|
||||
{#if primaryOptions[title].getIcon(option)}
|
||||
<div
|
||||
style="background: {primaryOptions[title].getColour(
|
||||
option
|
||||
)};"
|
||||
class="circle"
|
||||
>
|
||||
<div>
|
||||
<Icon
|
||||
size="S"
|
||||
name={primaryOptions[title].getIcon(option)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{:else if getPrimaryOptionColour(option, idx)}
|
||||
<span class="option-left">
|
||||
<StatusLight color={getPrimaryOptionColour(option, idx)} />
|
||||
</span>
|
||||
{/if}
|
||||
<span class="spectrum-Menu-itemLabel">
|
||||
<span
|
||||
class:spacing-group={primaryOptions[title].getIcon(option)}
|
||||
>
|
||||
{primaryOptions[title].getLabel(option)}
|
||||
<span />
|
||||
</span>
|
||||
<svg
|
||||
class="spectrum-Icon spectrum-UIIcon-Checkmark100 spectrum-Menu-checkmark spectrum-Menu-itemIcon"
|
||||
focusable="false"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<use xlink:href="#spectrum-css-icon-Checkmark100" />
|
||||
</svg>
|
||||
{#if getPrimaryOptionIcon(option, idx) && getPrimaryOptionColour(option, idx)}
|
||||
<span class="option-right">
|
||||
<StatusLight
|
||||
color={getPrimaryOptionColour(option, idx)}
|
||||
/>
|
||||
</span>
|
||||
{/if}
|
||||
</span>
|
||||
</li>
|
||||
{/each}
|
||||
{/if}
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
{/if}
|
||||
{#if secondaryOptions.length}
|
||||
<div style="width: 30%">
|
||||
<button
|
||||
{id}
|
||||
class="spectrum-Picker spectrum-Picker--sizeM override-borders"
|
||||
{disabled}
|
||||
class:is-open={secondaryOpen}
|
||||
aria-haspopup="listbox"
|
||||
on:mousedown={onClickSecondary}
|
||||
>
|
||||
{#if secondaryFieldIcon}
|
||||
<span class="option-left">
|
||||
<Icon name={secondaryFieldIcon} />
|
||||
</span>
|
||||
{:else if secondaryFieldColour}
|
||||
<span class="option-left">
|
||||
<StatusLight color={secondaryFieldColour} />
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
<span class:auto-width={autoWidth} class="spectrum-Picker-label">
|
||||
{secondaryFieldText}
|
||||
</span>
|
||||
<svg
|
||||
class="spectrum-Icon spectrum-UIIcon-ChevronDown100 spectrum-Picker-menuIcon"
|
||||
focusable="false"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<use xlink:href="#spectrum-css-icon-Chevron100" />
|
||||
</svg>
|
||||
</button>
|
||||
{#if secondaryOpen}
|
||||
<div
|
||||
use:clickOutside={() => (secondaryOpen = false)}
|
||||
transition:fly|local={{ y: -20, duration: 200 }}
|
||||
class="spectrum-Popover spectrum-Popover--bottom spectrum-Picker-popover is-open"
|
||||
style="width: 30%"
|
||||
>
|
||||
<ul class="spectrum-Menu" role="listbox">
|
||||
{#each secondaryOptions as option, idx}
|
||||
<li
|
||||
class="spectrum-Menu-item"
|
||||
class:is-selected={isOptionSelected(
|
||||
getSecondaryOptionValue(option, idx)
|
||||
)}
|
||||
role="option"
|
||||
aria-selected="true"
|
||||
tabindex="0"
|
||||
on:click={() =>
|
||||
onPickSecondary(getSecondaryOptionValue(option, idx))}
|
||||
>
|
||||
{#if getSecondaryOptionColour(option, idx)}
|
||||
<span class="option-left">
|
||||
<StatusLight
|
||||
color={getSecondaryOptionColour(option, idx)}
|
||||
/>
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
<span class="spectrum-Menu-itemLabel">
|
||||
{getSecondaryOptionLabel(option, idx)}
|
||||
</span>
|
||||
<svg
|
||||
class="spectrum-Icon spectrum-UIIcon-Checkmark100 spectrum-Menu-checkmark spectrum-Menu-itemIcon"
|
||||
focusable="false"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<use xlink:href="#spectrum-css-icon-Checkmark100" />
|
||||
</svg>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.spacing-group {
|
||||
margin-left: var(--spacing-m);
|
||||
}
|
||||
.spectrum-InputGroup {
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
}
|
||||
.override-borders {
|
||||
border-top-left-radius: 0px;
|
||||
border-bottom-left-radius: 0px;
|
||||
}
|
||||
|
||||
.spectrum-Popover {
|
||||
max-height: 240px;
|
||||
z-index: 999;
|
||||
top: 100%;
|
||||
}
|
||||
|
||||
.option-left {
|
||||
padding-right: 8px;
|
||||
}
|
||||
.option-right {
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
.circle {
|
||||
border-radius: 50%;
|
||||
height: 28px;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
line-height: 48px;
|
||||
font-size: 1.2em;
|
||||
width: 28px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.circle > div {
|
||||
position: absolute;
|
||||
text-decoration: none;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%) translateY(-50%);
|
||||
}
|
||||
|
||||
.iconPadding {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 10px;
|
||||
transform: translateY(-50%);
|
||||
color: silver;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.labelPadding {
|
||||
padding-left: calc(1em + 10px + 8px);
|
||||
}
|
||||
|
||||
.spectrum-Textfield.spectrum-InputGroup-textfield {
|
||||
width: 70%;
|
||||
}
|
||||
.spectrum-Textfield.spectrum-InputGroup-textfield.is-full-width {
|
||||
width: 100%;
|
||||
}
|
||||
.spectrum-Textfield.spectrum-InputGroup-textfield.is-full-width input {
|
||||
border-right-width: thin;
|
||||
}
|
||||
|
||||
.spectrum-Popover.spectrum-Popover--bottom.spectrum-Picker-popover.is-open {
|
||||
width: 70%;
|
||||
}
|
||||
.spectrum-Popover.spectrum-Popover--bottom.spectrum-Picker-popover.is-open.is-full-width {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.spectrum-Search-clearButton {
|
||||
position: absolute;
|
||||
}
|
||||
</style>
|
|
@ -17,7 +17,6 @@
|
|||
export let autoWidth = false
|
||||
export let autocomplete = false
|
||||
export let sort = false
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
let open = false
|
||||
$: fieldText = getFieldText(value, options, placeholder)
|
||||
|
|
|
@ -0,0 +1,55 @@
|
|||
<script>
|
||||
import Field from "./Field.svelte"
|
||||
import InputDropdown from "./Core/InputDropdown.svelte"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
|
||||
export let inputValue = null
|
||||
export let dropdownValue = null
|
||||
export let inputType = "text"
|
||||
export let label = null
|
||||
export let labelPosition = "above"
|
||||
export let placeholder = null
|
||||
export let disabled = false
|
||||
export let readonly = false
|
||||
export let error = null
|
||||
export let updateOnChange = true
|
||||
export let quiet = false
|
||||
export let dataCy
|
||||
export let autofocus
|
||||
export let options = []
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
const onPick = e => {
|
||||
dropdownValue = e.detail
|
||||
dispatch("pick", e.detail)
|
||||
}
|
||||
const onChange = e => {
|
||||
inputValue = e.detail
|
||||
dispatch("change", e.detail)
|
||||
}
|
||||
</script>
|
||||
|
||||
<Field {label} {labelPosition} {error}>
|
||||
<InputDropdown
|
||||
{dataCy}
|
||||
{updateOnChange}
|
||||
{error}
|
||||
{disabled}
|
||||
{readonly}
|
||||
{inputValue}
|
||||
{dropdownValue}
|
||||
{placeholder}
|
||||
{inputType}
|
||||
{quiet}
|
||||
{autofocus}
|
||||
{options}
|
||||
on:change={onChange}
|
||||
on:pick={onPick}
|
||||
on:click
|
||||
on:input
|
||||
on:blur
|
||||
on:focus
|
||||
on:keyup
|
||||
/>
|
||||
</Field>
|
|
@ -14,7 +14,7 @@
|
|||
export let getOptionLabel = option => option
|
||||
export let getOptionValue = option => option
|
||||
export let sort = false
|
||||
|
||||
export let autoWidth = false
|
||||
const dispatch = createEventDispatcher()
|
||||
const onChange = e => {
|
||||
value = e.detail
|
||||
|
@ -33,6 +33,7 @@
|
|||
{sort}
|
||||
{getOptionLabel}
|
||||
{getOptionValue}
|
||||
{autoWidth}
|
||||
on:change={onChange}
|
||||
on:click
|
||||
/>
|
||||
|
|
|
@ -0,0 +1,125 @@
|
|||
<script>
|
||||
import Field from "./Field.svelte"
|
||||
import PickerDropdown from "./Core/PickerDropdown.svelte"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
|
||||
export let primaryValue = null
|
||||
export let secondaryValue = null
|
||||
export let inputType = "text"
|
||||
export let label = null
|
||||
export let labelPosition = "above"
|
||||
export let secondaryPlaceholder = null
|
||||
export let autocomplete
|
||||
export let placeholder = null
|
||||
export let disabled = false
|
||||
export let readonly = false
|
||||
export let error = null
|
||||
export let updateOnChange = true
|
||||
export let getSecondaryOptionLabel = option =>
|
||||
extractProperty(option, "label")
|
||||
export let getSecondaryOptionValue = option =>
|
||||
extractProperty(option, "value")
|
||||
export let getSecondaryOptionColour = () => {}
|
||||
export let getSecondaryOptionIcon = () => {}
|
||||
export let quiet = false
|
||||
export let dataCy
|
||||
export let autofocus
|
||||
export let primaryOptions = []
|
||||
export let secondaryOptions = []
|
||||
|
||||
let primaryLabel
|
||||
let secondaryLabel
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
$: secondaryFieldText = getSecondaryFieldText(
|
||||
secondaryValue,
|
||||
secondaryOptions,
|
||||
secondaryPlaceholder
|
||||
)
|
||||
$: secondaryFieldIcon = getSecondaryFieldAttribute(
|
||||
getSecondaryOptionIcon,
|
||||
secondaryValue,
|
||||
secondaryOptions
|
||||
)
|
||||
$: secondaryFieldColour = getSecondaryFieldAttribute(
|
||||
getSecondaryOptionColour,
|
||||
secondaryValue,
|
||||
secondaryOptions
|
||||
)
|
||||
|
||||
const getSecondaryFieldAttribute = (getAttribute, value, options) => {
|
||||
// Wait for options to load if there is a value but no options
|
||||
|
||||
if (!options?.length) {
|
||||
return ""
|
||||
}
|
||||
|
||||
const index = options.findIndex(
|
||||
(option, idx) => getSecondaryOptionValue(option, idx) === value
|
||||
)
|
||||
|
||||
return index !== -1 ? getAttribute(options[index], index) : null
|
||||
}
|
||||
|
||||
const getSecondaryFieldText = (value, options, placeholder) => {
|
||||
// Always use placeholder if no value
|
||||
if (value == null || value === "") {
|
||||
return placeholder || "Choose an option"
|
||||
}
|
||||
|
||||
return getSecondaryFieldAttribute(getSecondaryOptionLabel, value, options)
|
||||
}
|
||||
|
||||
const onPickPrimary = e => {
|
||||
primaryLabel = e?.detail?.label || null
|
||||
primaryValue = e?.detail?.value || null
|
||||
dispatch("pickprimary", e?.detail?.value || {})
|
||||
}
|
||||
|
||||
const onPickSecondary = e => {
|
||||
secondaryValue = e.detail
|
||||
dispatch("picksecondary", e.detail)
|
||||
}
|
||||
|
||||
const extractProperty = (value, property) => {
|
||||
if (value && typeof value === "object") {
|
||||
return value[property]
|
||||
}
|
||||
return value
|
||||
}
|
||||
</script>
|
||||
|
||||
<Field {label} {labelPosition} {error}>
|
||||
<PickerDropdown
|
||||
{autocomplete}
|
||||
{dataCy}
|
||||
{updateOnChange}
|
||||
{error}
|
||||
{disabled}
|
||||
{readonly}
|
||||
{placeholder}
|
||||
{inputType}
|
||||
{quiet}
|
||||
{autofocus}
|
||||
{primaryOptions}
|
||||
{secondaryOptions}
|
||||
{getSecondaryOptionLabel}
|
||||
{getSecondaryOptionValue}
|
||||
{getSecondaryOptionIcon}
|
||||
{getSecondaryOptionColour}
|
||||
{secondaryFieldText}
|
||||
{secondaryFieldIcon}
|
||||
{secondaryFieldColour}
|
||||
{primaryValue}
|
||||
{secondaryValue}
|
||||
{primaryLabel}
|
||||
{secondaryLabel}
|
||||
on:pickprimary={onPickPrimary}
|
||||
on:picksecondary={onPickSecondary}
|
||||
on:click
|
||||
on:input
|
||||
on:blur
|
||||
on:focus
|
||||
on:keyup
|
||||
/>
|
||||
</Field>
|
|
@ -0,0 +1,177 @@
|
|||
<script>
|
||||
//import { createEventDispatcher } from "svelte"
|
||||
import "@spectrum-css/popover/dist/index-vars.css"
|
||||
import clickOutside from "../Actions/click_outside"
|
||||
import { fly } from "svelte/transition"
|
||||
import Icon from "../Icon/Icon.svelte"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
|
||||
export let value
|
||||
export let size = "M"
|
||||
export let alignRight = false
|
||||
|
||||
let open = false
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
const iconList = [
|
||||
{
|
||||
label: "Icons",
|
||||
icons: [
|
||||
"Apps",
|
||||
"Actions",
|
||||
"ConversionFunnel",
|
||||
"App",
|
||||
"Briefcase",
|
||||
"Money",
|
||||
"ShoppingCart",
|
||||
"Form",
|
||||
"Help",
|
||||
"Monitoring",
|
||||
"Sandbox",
|
||||
"Project",
|
||||
"Organisations",
|
||||
"Magnify",
|
||||
"Launch",
|
||||
"Car",
|
||||
"Camera",
|
||||
"Bug",
|
||||
"Channel",
|
||||
"Calculator",
|
||||
"Calendar",
|
||||
"GraphDonut",
|
||||
"GraphBarHorizontal",
|
||||
"Demographic",
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const onChange = value => {
|
||||
dispatch("change", value)
|
||||
open = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="container">
|
||||
<div class="preview size--{size || 'M'}" on:click={() => (open = true)}>
|
||||
<div
|
||||
class="fill"
|
||||
style={value ? `background: ${value};` : ""}
|
||||
class:placeholder={!value}
|
||||
>
|
||||
<Icon name={value || "UserGroup"} />
|
||||
</div>
|
||||
</div>
|
||||
{#if open}
|
||||
<div
|
||||
use:clickOutside={() => (open = false)}
|
||||
transition:fly={{ y: -20, duration: 200 }}
|
||||
class="spectrum-Popover spectrum-Popover--bottom spectrum-Picker-popover is-open"
|
||||
class:spectrum-Popover--align-right={alignRight}
|
||||
>
|
||||
{#each iconList as icon}
|
||||
<div class="category">
|
||||
<div class="heading">{icon.label}</div>
|
||||
<div class="icons">
|
||||
{#each icon.icons as icon}
|
||||
<div
|
||||
on:click={() => {
|
||||
onChange(icon)
|
||||
}}
|
||||
>
|
||||
<Icon name={icon} />
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.container {
|
||||
position: relative;
|
||||
}
|
||||
.preview {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
position: relative;
|
||||
box-shadow: 0 0 0 1px var(--spectrum-global-color-gray-400);
|
||||
}
|
||||
.preview:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
.fill {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
}
|
||||
.size--S {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
.size--M {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
.size--L {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
}
|
||||
.spectrum-Popover {
|
||||
width: 210px;
|
||||
z-index: 999;
|
||||
top: 100%;
|
||||
padding: var(--spacing-l) var(--spacing-xl);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: stretch;
|
||||
gap: var(--spacing-xl);
|
||||
}
|
||||
.spectrum-Popover--align-right {
|
||||
right: 0;
|
||||
}
|
||||
.icons {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr;
|
||||
gap: var(--spacing-m);
|
||||
}
|
||||
.heading {
|
||||
font-size: var(--font-size-s);
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.14px;
|
||||
flex: 1 1 auto;
|
||||
text-transform: uppercase;
|
||||
grid-column: 1 / 5;
|
||||
margin-bottom: var(--spacing-s);
|
||||
}
|
||||
|
||||
.icon {
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
border-radius: 100%;
|
||||
box-shadow: 0 0 0 1px var(--spectrum-global-color-gray-300);
|
||||
position: relative;
|
||||
}
|
||||
.icon:hover {
|
||||
cursor: pointer;
|
||||
box-shadow: 0 0 2px 2px var(--spectrum-global-color-gray-300);
|
||||
}
|
||||
.custom {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
align-items: center;
|
||||
gap: var(--spacing-m);
|
||||
margin-right: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.spectrum-wrapper {
|
||||
background-color: transparent;
|
||||
}
|
||||
</style>
|
|
@ -1,53 +0,0 @@
|
|||
<script>
|
||||
import { View } from "svench";
|
||||
import DetailSummary from "./DetailSummary.svelte";
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<link href="https://cdn.jsdelivr.net/npm/remixicon@2.5.0/fonts/remixicon.css" rel="stylesheet">
|
||||
</svelte:head>
|
||||
|
||||
<style>
|
||||
div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: stretch;
|
||||
gap: var(--spacing-m);
|
||||
width: 120px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<View name="default">
|
||||
<div>
|
||||
<DetailSummary name="Category 1">
|
||||
<span>1</span>
|
||||
<span>2</span>
|
||||
<span>3</span>
|
||||
<span>4</span>
|
||||
</DetailSummary>
|
||||
<DetailSummary name="Category 2">
|
||||
<span>1</span>
|
||||
<span>2</span>
|
||||
<span>3</span>
|
||||
<span>4</span>
|
||||
</DetailSummary>
|
||||
</div>
|
||||
</View>
|
||||
|
||||
<View name="thin">
|
||||
<div>
|
||||
<DetailSummary thin name="Category 1">
|
||||
<span>1</span>
|
||||
<span>2</span>
|
||||
<span>3</span>
|
||||
<span>4</span>
|
||||
</DetailSummary>
|
||||
<DetailSummary thin name="Category 2">
|
||||
<span>1</span>
|
||||
<span>2</span>
|
||||
<span>3</span>
|
||||
<span>4</span>
|
||||
</DetailSummary>
|
||||
</div>
|
||||
</View>
|
|
@ -0,0 +1,28 @@
|
|||
<script>
|
||||
import Detail from "../Typography/Detail.svelte"
|
||||
|
||||
export let title = null
|
||||
</script>
|
||||
|
||||
<div>
|
||||
{#if title}
|
||||
<div class="title">
|
||||
<Detail>{title}</Detail>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="list-items">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.title {
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.list-items {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: stretch;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,92 @@
|
|||
<script>
|
||||
import Body from "../Typography/Body.svelte"
|
||||
import Icon from "../Icon/Icon.svelte"
|
||||
import Label from "../Label/Label.svelte"
|
||||
import Avatar from "../Avatar/Avatar.svelte"
|
||||
|
||||
export let icon = null
|
||||
export let iconBackground = null
|
||||
export let avatar = false
|
||||
export let title = null
|
||||
export let subtitle = null
|
||||
|
||||
$: initials = avatar ? title?.[0] : null
|
||||
</script>
|
||||
|
||||
<div class="list-item">
|
||||
<div class="left">
|
||||
{#if icon}
|
||||
<div class="icon" style="background: {iconBackground || `transparent`};">
|
||||
<Icon name={icon} size="S" color={iconBackground ? "white" : null} />
|
||||
</div>
|
||||
{/if}
|
||||
{#if avatar}
|
||||
<Avatar {initials} />
|
||||
{/if}
|
||||
{#if title}
|
||||
<Body>{title}</Body>
|
||||
{/if}
|
||||
{#if subtitle}
|
||||
<Label>{subtitle}</Label>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="right">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.list-item {
|
||||
padding: 0 16px;
|
||||
height: 56px;
|
||||
background: var(--spectrum-alias-background-color-tertiary);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
border: 1px solid var(--spectrum-global-color-gray-300);
|
||||
}
|
||||
.list-item:not(:first-child) {
|
||||
border-top: none;
|
||||
}
|
||||
.list-item:first-child {
|
||||
border-top-left-radius: 4px;
|
||||
border-top-right-radius: 4px;
|
||||
}
|
||||
.list-item:last-child {
|
||||
border-bottom-left-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
}
|
||||
.left,
|
||||
.right {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: var(--spacing-xl);
|
||||
}
|
||||
.left {
|
||||
width: 0;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
.right {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
.list-item :global(.spectrum-Icon),
|
||||
.list-item :global(.spectrum-Avatar) {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
.list-item :global(.spectrum-Body) {
|
||||
color: var(--spectrum-global-color-gray-900);
|
||||
}
|
||||
.list-item :global(.spectrum-Body) {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.icon {
|
||||
width: var(--spectrum-alias-avatar-size-400);
|
||||
height: var(--spectrum-alias-avatar-size-400);
|
||||
display: grid;
|
||||
place-items: center;
|
||||
border-radius: 50%;
|
||||
}
|
||||
</style>
|
|
@ -18,11 +18,16 @@
|
|||
export let disabled = false
|
||||
export let active = false
|
||||
export let color = null
|
||||
export let square = false
|
||||
export let hoverable = false
|
||||
</script>
|
||||
|
||||
<div
|
||||
on:click
|
||||
class="spectrum-StatusLight spectrum-StatusLight--size{size}"
|
||||
class:custom={!!color}
|
||||
class:square
|
||||
class:hoverable
|
||||
style={`--color: ${color};`}
|
||||
class:spectrum-StatusLight--celery={celery}
|
||||
class:spectrum-StatusLight--yellow={yellow}
|
||||
|
@ -54,6 +59,7 @@
|
|||
min-height: 0;
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
transition: color ease-out 130ms;
|
||||
}
|
||||
.spectrum-StatusLight.withText::before {
|
||||
margin-right: 10px;
|
||||
|
@ -61,4 +67,14 @@
|
|||
.custom::before {
|
||||
background: var(--color) !important;
|
||||
}
|
||||
.square::before {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 4px;
|
||||
margin: 0;
|
||||
}
|
||||
.hoverable:hover {
|
||||
cursor: pointer;
|
||||
color: var(--spectrum-global-color-gray-900);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
<script>
|
||||
import Tooltip from "../Tooltip/Tooltip.svelte"
|
||||
import Link from "../Link/Link.svelte"
|
||||
|
||||
export let value
|
||||
|
@ -17,18 +16,16 @@
|
|||
{#each attachments as attachment}
|
||||
{#if isImage(attachment.extension)}
|
||||
<Link quiet target="_blank" href={attachment.url}>
|
||||
<div class="center">
|
||||
<div class="center" title={attachment.name}>
|
||||
<img src={attachment.url} alt={attachment.extension} />
|
||||
</div>
|
||||
</Link>
|
||||
{:else}
|
||||
<Tooltip text={attachment.name} direction="right">
|
||||
<div class="file">
|
||||
<Link quiet target="_blank" href={attachment.url}>
|
||||
{attachment.extension}
|
||||
</Link>
|
||||
</div>
|
||||
</Tooltip>
|
||||
<div class="file" title={attachment.name}>
|
||||
<Link quiet target="_blank" href={attachment.url}>
|
||||
{attachment.extension}
|
||||
</Link>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
{#if leftover}
|
||||
|
@ -52,7 +49,7 @@
|
|||
padding: 0 8px;
|
||||
color: var(--spectrum-global-color-gray-800);
|
||||
border: 1px solid var(--spectrum-global-color-gray-300);
|
||||
border-radius: 2px;
|
||||
border-radius: 4px;
|
||||
text-transform: uppercase;
|
||||
font-weight: 600;
|
||||
font-size: 11px;
|
||||
|
|
|
@ -37,6 +37,7 @@
|
|||
export let autoSortColumns = true
|
||||
export let compact = false
|
||||
export let customPlaceholder = false
|
||||
export let showHeaderBorder = true
|
||||
export let placeholderText = "No rows found"
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
@ -286,6 +287,7 @@
|
|||
<div class="spectrum-Table-head">
|
||||
{#if showEditColumn}
|
||||
<div
|
||||
class:noBorderHeader={!showHeaderBorder}
|
||||
class="spectrum-Table-headCell spectrum-Table-headCell--divider spectrum-Table-headCell--edit"
|
||||
>
|
||||
{#if allowSelectRows}
|
||||
|
@ -301,6 +303,7 @@
|
|||
{#each fields as field}
|
||||
<div
|
||||
class="spectrum-Table-headCell"
|
||||
class:noBorderHeader={!showHeaderBorder}
|
||||
class:spectrum-Table-headCell--alignCenter={schema[field]
|
||||
.align === "Center"}
|
||||
class:spectrum-Table-headCell--alignRight={schema[field].align ===
|
||||
|
@ -348,6 +351,7 @@
|
|||
<div class="spectrum-Table-row">
|
||||
{#if showEditColumn}
|
||||
<div
|
||||
class:noBorderCheckbox={!showHeaderBorder}
|
||||
class="spectrum-Table-cell spectrum-Table-cell--divider spectrum-Table-cell--edit"
|
||||
on:click={e => {
|
||||
toggleSelectRow(row)
|
||||
|
@ -481,6 +485,18 @@
|
|||
.spectrum-Table-headCell:last-of-type {
|
||||
border-right: var(--table-border);
|
||||
}
|
||||
|
||||
.noBorderHeader {
|
||||
border-top: none !important;
|
||||
border-right: none !important;
|
||||
border-left: none !important;
|
||||
}
|
||||
|
||||
.noBorderCheckbox {
|
||||
border-top: none !important;
|
||||
border-right: none !important;
|
||||
}
|
||||
|
||||
.spectrum-Table-headCell--alignCenter {
|
||||
justify-content: center;
|
||||
}
|
||||
|
@ -499,7 +515,7 @@
|
|||
z-index: 3;
|
||||
}
|
||||
.spectrum-Table-headCell .title {
|
||||
overflow: hidden;
|
||||
overflow: visible;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.spectrum-Table-headCell:hover .spectrum-Table-editIcon {
|
||||
|
@ -562,7 +578,7 @@
|
|||
gap: 4px;
|
||||
border-bottom: 1px solid var(--spectrum-alias-border-color-mid);
|
||||
background-color: var(--table-bg);
|
||||
z-index: 1;
|
||||
z-index: auto;
|
||||
}
|
||||
.spectrum-Table-cell--divider {
|
||||
padding-right: var(--cell-padding);
|
||||
|
@ -570,6 +586,7 @@
|
|||
.spectrum-Table-cell--divider + .spectrum-Table-cell {
|
||||
padding-left: var(--cell-padding);
|
||||
}
|
||||
|
||||
.spectrum-Table-cell--edit {
|
||||
position: sticky;
|
||||
left: 0;
|
||||
|
|
|
@ -26,5 +26,9 @@
|
|||
<style>
|
||||
.tooltip {
|
||||
pointer-events: none;
|
||||
background: var(--spectrum-global-color-gray-500);
|
||||
}
|
||||
.spectrum-Tooltip-tip {
|
||||
border-top-color: var(--spectrum-global-color-gray-500);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -23,6 +23,8 @@ export { default as Icon, directions } from "./Icon/Icon.svelte"
|
|||
export { default as Toggle } from "./Form/Toggle.svelte"
|
||||
export { default as RadioGroup } from "./Form/RadioGroup.svelte"
|
||||
export { default as Checkbox } from "./Form/Checkbox.svelte"
|
||||
export { default as InputDropdown } from "./Form/InputDropdown.svelte"
|
||||
export { default as PickerDropdown } from "./Form/PickerDropdown.svelte"
|
||||
export { default as DetailSummary } from "./DetailSummary/DetailSummary.svelte"
|
||||
export { default as Popover } from "./Popover/Popover.svelte"
|
||||
export { default as ProgressBar } from "./ProgressBar/ProgressBar.svelte"
|
||||
|
@ -58,12 +60,15 @@ export { default as Pagination } from "./Pagination/Pagination.svelte"
|
|||
export { default as Badge } from "./Badge/Badge.svelte"
|
||||
export { default as StatusLight } from "./StatusLight/StatusLight.svelte"
|
||||
export { default as ColorPicker } from "./ColorPicker/ColorPicker.svelte"
|
||||
export { default as IconPicker } from "./IconPicker/IconPicker.svelte"
|
||||
export { default as InlineAlert } from "./InlineAlert/InlineAlert.svelte"
|
||||
export { default as Banner } from "./Banner/Banner.svelte"
|
||||
export { default as BannerDisplay } from "./Banner/BannerDisplay.svelte"
|
||||
export { default as MarkdownEditor } from "./Markdown/MarkdownEditor.svelte"
|
||||
export { default as MarkdownViewer } from "./Markdown/MarkdownViewer.svelte"
|
||||
export { default as RichTextField } from "./Form/RichTextField.svelte"
|
||||
export { default as List } from "./List/List.svelte"
|
||||
export { default as ListItem } from "./List/ListItem.svelte"
|
||||
export { default as IconSideNav } from "./IconSideNav/IconSideNav.svelte"
|
||||
export { default as IconSideNavItem } from "./IconSideNav/IconSideNavItem.svelte"
|
||||
export { default as Slider } from "./Form/Slider.svelte"
|
||||
|
@ -71,6 +76,7 @@ export { default as Slider } from "./Form/Slider.svelte"
|
|||
// Renderers
|
||||
export { default as BoldRenderer } from "./Table/BoldRenderer.svelte"
|
||||
export { default as CodeRenderer } from "./Table/CodeRenderer.svelte"
|
||||
export { default as InternalRenderer } from "./Table/InternalRenderer.svelte"
|
||||
|
||||
// Typography
|
||||
export { default as Body } from "./Typography/Body.svelte"
|
||||
|
|
|
@ -28,6 +28,46 @@
|
|||
chalk "^2.0.0"
|
||||
js-tokens "^4.0.0"
|
||||
|
||||
"@jridgewell/gen-mapping@^0.3.0":
|
||||
version "0.3.2"
|
||||
resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz#c1aedc61e853f2bb9f5dfe6d4442d3b565b253b9"
|
||||
integrity sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==
|
||||
dependencies:
|
||||
"@jridgewell/set-array" "^1.0.1"
|
||||
"@jridgewell/sourcemap-codec" "^1.4.10"
|
||||
"@jridgewell/trace-mapping" "^0.3.9"
|
||||
|
||||
"@jridgewell/resolve-uri@^3.0.3":
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz#2203b118c157721addfe69d47b70465463066d78"
|
||||
integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==
|
||||
|
||||
"@jridgewell/set-array@^1.0.1":
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72"
|
||||
integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==
|
||||
|
||||
"@jridgewell/source-map@^0.3.2":
|
||||
version "0.3.2"
|
||||
resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.2.tgz#f45351aaed4527a298512ec72f81040c998580fb"
|
||||
integrity sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw==
|
||||
dependencies:
|
||||
"@jridgewell/gen-mapping" "^0.3.0"
|
||||
"@jridgewell/trace-mapping" "^0.3.9"
|
||||
|
||||
"@jridgewell/sourcemap-codec@^1.4.10":
|
||||
version "1.4.14"
|
||||
resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24"
|
||||
integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==
|
||||
|
||||
"@jridgewell/trace-mapping@^0.3.9":
|
||||
version "0.3.14"
|
||||
resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.14.tgz#b231a081d8f66796e475ad588a1ef473112701ed"
|
||||
integrity sha512-bJWEfQ9lPTvm3SneWwRFVLzrh6nhjwqw7TUFFBEMzwvg7t7PCDenf2lDwqo4NQXzdpgBXyFgDWnQA+2vkruksQ==
|
||||
dependencies:
|
||||
"@jridgewell/resolve-uri" "^3.0.3"
|
||||
"@jridgewell/sourcemap-codec" "^1.4.10"
|
||||
|
||||
"@rollup/plugin-commonjs@^16.0.0":
|
||||
version "16.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@rollup/plugin-commonjs/-/plugin-commonjs-16.0.0.tgz#169004d56cd0f0a1d0f35915d31a036b0efe281f"
|
||||
|
@ -340,6 +380,11 @@ acorn@^7.3.1:
|
|||
resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa"
|
||||
integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==
|
||||
|
||||
acorn@^8.5.0:
|
||||
version "8.7.1"
|
||||
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.7.1.tgz#0197122c843d1bf6d0a5e83220a788f278f63c30"
|
||||
integrity sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A==
|
||||
|
||||
alphanum-sort@^1.0.0:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/alphanum-sort/-/alphanum-sort-1.0.2.tgz#97a1119649b211ad33691d9f9f486a8ec9fbe0a3"
|
||||
|
@ -447,9 +492,9 @@ browserslist@^4.0.0:
|
|||
node-releases "^1.1.71"
|
||||
|
||||
buffer-from@^1.0.0:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef"
|
||||
integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5"
|
||||
integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==
|
||||
|
||||
builtin-modules@^3.1.0:
|
||||
version "3.2.0"
|
||||
|
@ -2372,16 +2417,15 @@ simple-swizzle@^0.2.2:
|
|||
dependencies:
|
||||
is-arrayish "^0.3.1"
|
||||
|
||||
"source-map-fast@npm:source-map@0.7.3", source-map@~0.7.2:
|
||||
name source-map-fast
|
||||
"source-map-fast@npm:source-map@0.7.3":
|
||||
version "0.7.3"
|
||||
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383"
|
||||
integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==
|
||||
|
||||
source-map-support@~0.5.19:
|
||||
version "0.5.19"
|
||||
resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.19.tgz#a98b62f86dcaf4f67399648c085291ab9e8fed61"
|
||||
integrity sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==
|
||||
source-map-support@~0.5.20:
|
||||
version "0.5.21"
|
||||
resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f"
|
||||
integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==
|
||||
dependencies:
|
||||
buffer-from "^1.0.0"
|
||||
source-map "^0.6.0"
|
||||
|
@ -2485,9 +2529,9 @@ svelte-portal@^1.0.0:
|
|||
integrity sha512-nHf+DS/jZ6jjnZSleBMSaZua9JlG5rZv9lOGKgJuaZStfevtjIlUJrkLc3vbV8QdBvPPVmvcjTlazAzfKu0v3Q==
|
||||
|
||||
svelte@^3.38.2:
|
||||
version "3.38.2"
|
||||
resolved "https://registry.yarnpkg.com/svelte/-/svelte-3.38.2.tgz#55e5c681f793ae349b5cc2fe58e5782af4275ef5"
|
||||
integrity sha512-q5Dq0/QHh4BLJyEVWGe7Cej5NWs040LWjMbicBGZ+3qpFWJ1YObRmUDZKbbovddLC9WW7THTj3kYbTOFmU9fbg==
|
||||
version "3.49.0"
|
||||
resolved "https://registry.yarnpkg.com/svelte/-/svelte-3.49.0.tgz#5baee3c672306de1070c3b7888fc2204e36a4029"
|
||||
integrity sha512-+lmjic1pApJWDfPCpUUTc1m8azDqYCG1JN9YEngrx/hUyIcFJo6VZhj0A1Ai0wqoHcEIuQy+e9tk+4uDgdtsFA==
|
||||
|
||||
svgo@^1.0.0:
|
||||
version "1.3.2"
|
||||
|
@ -2509,13 +2553,14 @@ svgo@^1.0.0:
|
|||
util.promisify "~1.0.0"
|
||||
|
||||
terser@^5.0.0:
|
||||
version "5.6.1"
|
||||
resolved "https://registry.yarnpkg.com/terser/-/terser-5.6.1.tgz#a48eeac5300c0a09b36854bf90d9c26fb201973c"
|
||||
integrity sha512-yv9YLFQQ+3ZqgWCUk+pvNJwgUTdlIxUk1WTN+RnaFJe2L7ipG2csPT0ra2XRm7Cs8cxN7QXmK1rFzEwYEQkzXw==
|
||||
version "5.14.2"
|
||||
resolved "https://registry.yarnpkg.com/terser/-/terser-5.14.2.tgz#9ac9f22b06994d736174f4091aa368db896f1c10"
|
||||
integrity sha512-oL0rGeM/WFQCUd0y2QrWxYnq7tfSuKBiqTjRPWrRgB46WD/kiwHwF8T23z78H6Q6kGCuuHcPB+KULHRdxvVGQA==
|
||||
dependencies:
|
||||
"@jridgewell/source-map" "^0.3.2"
|
||||
acorn "^8.5.0"
|
||||
commander "^2.20.0"
|
||||
source-map "~0.7.2"
|
||||
source-map-support "~0.5.19"
|
||||
source-map-support "~0.5.20"
|
||||
|
||||
timsort@^0.3.0:
|
||||
version "0.3.0"
|
||||
|
@ -2580,7 +2625,7 @@ unquote@~1.1.1:
|
|||
util-deprecate@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
|
||||
integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=
|
||||
integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==
|
||||
|
||||
util.promisify@~1.0.0:
|
||||
version "1.0.1"
|
||||
|
|
|
@ -17,16 +17,15 @@ filterTests(['all'], () => {
|
|||
it("should add form with multi select picker, containing 5 options", () => {
|
||||
cy.navigateToFrontend()
|
||||
// Add data provider
|
||||
cy.get(interact.CATEGORY_DATA, { timeout: 500 }).click()
|
||||
cy.get(interact.COMPONENT_DATA_PROVIDER).click()
|
||||
cy.searchAndAddComponent("Data Provider")
|
||||
cy.get(interact.DATASOURCE_PROP_CONTROL).click()
|
||||
cy.get(interact.DROPDOWN).contains("Multi Data").click()
|
||||
// Add Form with schema to match table
|
||||
cy.addComponent("Form", "Form")
|
||||
cy.searchAndAddComponent("Form")
|
||||
cy.get(interact.DATASOURCE_PROP_CONTROL).click()
|
||||
cy.get(interact.DROPDOWN).contains("Multi Data").click()
|
||||
// Add multi-select picker to form
|
||||
cy.addComponent("Form", "Multi-select Picker").then(componentId => {
|
||||
cy.searchAndAddComponent("Multi-select Picker").then(componentId => {
|
||||
cy.get(interact.DATASOURCE_FIELD_CONTROL).type("Test Data").type("{enter}")
|
||||
cy.wait(1000)
|
||||
cy.getComponent(componentId).contains("Choose some options").click()
|
||||
|
|
|
@ -10,15 +10,13 @@ filterTests(['all'], () => {
|
|||
|
||||
it("should add Radio Buttons options picker on form, add data, and confirm", () => {
|
||||
cy.navigateToFrontend()
|
||||
cy.wait(500)
|
||||
cy.addComponent("Form", "Form")
|
||||
cy.addComponent("Form", "Options Picker").then((componentId) => {
|
||||
// Provide field setting
|
||||
cy.searchAndAddComponent("Form")
|
||||
cy.searchAndAddComponent("Options Picker").then((componentId) => {
|
||||
// Provide field setting
|
||||
cy.get(interact.DATASOURCE_FIELD_CONTROL).type("1")
|
||||
// Open dropdown and select Radio buttons
|
||||
cy.get(interact.OPTION_TYPE_PROP_CONTROL).click().then(() => {
|
||||
cy.get(interact.SPECTRUM_POPOVER).contains('Radio buttons')
|
||||
.wait(500)
|
||||
.click()
|
||||
})
|
||||
const radioButtonsTotal = 3
|
||||
|
@ -32,8 +30,8 @@ filterTests(['all'], () => {
|
|||
const addRadioButtonData = (totalRadioButtons) => {
|
||||
cy.get(interact.OPTION_SOURCE_PROP_CONROL).click().then(() => {
|
||||
cy.get(interact.SPECTRUM_POPOVER).contains('Custom')
|
||||
.wait(500)
|
||||
.click()
|
||||
.wait(1000)
|
||||
})
|
||||
cy.addCustomSourceOptions(totalRadioButtons)
|
||||
}
|
||||
|
|
|
@ -19,9 +19,14 @@ filterTests(["smoke", "all"], () => {
|
|||
cy.wait(500)
|
||||
|
||||
// Reset password
|
||||
cy.get(".spectrum-ActionButton-label", { timeout: 2000 }).contains("Force password reset").click({ force: true })
|
||||
cy.get(".title").within(() => {
|
||||
cy.get(interact.SPECTRUM_ICON).click({ force: true })
|
||||
})
|
||||
cy.get(interact.SPECTRUM_MENU).within(() => {
|
||||
cy.get(interact.SPECTRUM_MENU_ITEM).contains("Force Password Reset").click({ force: true })
|
||||
})
|
||||
|
||||
cy.get(".spectrum-Dialog-grid")
|
||||
cy.get(interact.SPECTRUM_DIALOG_GRID)
|
||||
.find(interact.SPECTRUM_TEXTFIELD_INPUT).invoke('val').as('pwd')
|
||||
|
||||
cy.get(interact.SPECTRUM_BUTTON).contains("Reset password").click({ force: true })
|
||||
|
@ -39,23 +44,14 @@ filterTests(["smoke", "all"], () => {
|
|||
cy.logoutNoAppGrid()
|
||||
})
|
||||
|
||||
it("should verify Admin Portal", () => {
|
||||
xit("should verify Admin Portal", () => {
|
||||
cy.login()
|
||||
cy.contains("Users").click()
|
||||
cy.contains("bbuser").click()
|
||||
|
||||
// Enable Development & Administration access
|
||||
cy.wait(500)
|
||||
for (let i = 4; i < 6; i++) {
|
||||
cy.get(interact.FIELD).eq(i).within(() => {
|
||||
cy.get(interact.SPECTRUM_SWITCH_INPUT).click({ force: true })
|
||||
cy.get(interact.SPECTRUM_SWITCH_INPUT).should('be.enabled')
|
||||
})
|
||||
}
|
||||
// Configure user role
|
||||
cy.setUserRole("bbuser", "Admin")
|
||||
bbUserLogin()
|
||||
|
||||
// Verify available options for Admin portal
|
||||
cy.get(".spectrum-SideNav")
|
||||
cy.get(interact.SPECTRUM_SIDENAV)
|
||||
.should('contain', 'Apps')
|
||||
//.and('contain', 'Usage')
|
||||
.and('contain', 'Users')
|
||||
|
@ -72,13 +68,7 @@ filterTests(["smoke", "all"], () => {
|
|||
it("should verify Development Portal", () => {
|
||||
// Only Development access should be enabled
|
||||
cy.login()
|
||||
cy.contains("Users").click()
|
||||
cy.contains("bbuser").click()
|
||||
cy.wait(500)
|
||||
cy.get(interact.FIELD).eq(5).within(() => {
|
||||
cy.get(interact.SPECTRUM_SWITCH_INPUT).click({ force: true })
|
||||
})
|
||||
|
||||
cy.setUserRole("bbuser", "Developer")
|
||||
bbUserLogin()
|
||||
|
||||
// Verify available options for Admin portal
|
||||
|
@ -99,13 +89,7 @@ filterTests(["smoke", "all"], () => {
|
|||
it("should verify Standard Portal", () => {
|
||||
// Development access should be disabled (Admin access is already disabled)
|
||||
cy.login()
|
||||
cy.contains("Users").click()
|
||||
cy.contains("bbuser").click()
|
||||
cy.wait(500)
|
||||
cy.get(interact.FIELD).eq(4).within(() => {
|
||||
cy.get(interact.SPECTRUM_SWITCH_INPUT).click({ force: true })
|
||||
})
|
||||
|
||||
cy.setUserRole("bbuser", "App User")
|
||||
bbUserLogin()
|
||||
|
||||
// Verify Standard Portal
|
||||
|
|
|
@ -15,25 +15,16 @@ filterTests(["smoke", "all"], () => {
|
|||
cy.get(interact.SPECTRUM_TABLE).should("contain", "bbuser")
|
||||
})
|
||||
|
||||
it("should confirm basic permission for a New User", () => {
|
||||
// Basic permission = development & administraton disabled
|
||||
it("should confirm App User role for a New User", () => {
|
||||
cy.contains("bbuser").click()
|
||||
// Confirm development and admin access are disabled
|
||||
for (let i = 4; i < 6; i++) {
|
||||
cy.wait(500)
|
||||
cy.get(interact.FIELD).eq(i).within(() => {
|
||||
//cy.get(interact.SPECTRUM_SWITCH_INPUT).should('be.disabled')
|
||||
cy.get(".spectrum-Switch-switch").should('not.be.checked')
|
||||
})
|
||||
}
|
||||
// Existing apps appear within the No Access table
|
||||
cy.get(interact.SPECTRUM_TABLE, { timeout: 500 }).eq(1).should("not.contain", "No rows found")
|
||||
// Configure roles table should not contain apps
|
||||
cy.get(interact.SPECTRUM_TABLE).eq(0).contains("No rows found")
|
||||
cy.get(".spectrum-Form-itemField").eq(2).should('contain', 'App User')
|
||||
|
||||
// User should not have app access
|
||||
cy.get(interact.LIST_ITEMS, { timeout: 500 }).should("contain", "No apps")
|
||||
})
|
||||
|
||||
if (Cypress.env("TEST_ENV")) {
|
||||
it("should assign role types", () => {
|
||||
xit("should assign role types", () => {
|
||||
// 3 apps minimum required - to assign an app to each role type
|
||||
cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`)
|
||||
.its("body")
|
||||
|
@ -57,6 +48,7 @@ filterTests(["smoke", "all"], () => {
|
|||
cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 5000})
|
||||
cy.get(interact.SPECTRUM_SIDENAV).contains("Users").click()
|
||||
cy.get(interact.SPECTRUM_TABLE, { timeout: 1000 }).contains("bbuser").click()
|
||||
cy.get(interact.SPECTRUM_HEADING).contains("bbuser", { timeout: 2000})
|
||||
for (let i = 0; i < 3; i++) {
|
||||
cy.get(interact.SPECTRUM_TABLE, { timeout: 3000})
|
||||
.eq(1)
|
||||
|
@ -95,7 +87,7 @@ filterTests(["smoke", "all"], () => {
|
|||
})
|
||||
})
|
||||
|
||||
it("should unassign role types", () => {
|
||||
xit("should unassign role types", () => {
|
||||
// Set each app within Configure roles table to 'No Access'
|
||||
cy.get(interact.SPECTRUM_TABLE)
|
||||
.eq(0)
|
||||
|
@ -124,7 +116,7 @@ filterTests(["smoke", "all"], () => {
|
|||
})
|
||||
}
|
||||
|
||||
it("should enable Developer access and verify application access", () => {
|
||||
xit("should enable Developer access and verify application access", () => {
|
||||
// Enable Developer access
|
||||
cy.get(interact.FIELD)
|
||||
.eq(4)
|
||||
|
@ -156,7 +148,7 @@ filterTests(["smoke", "all"], () => {
|
|||
})
|
||||
})
|
||||
|
||||
it("should disable Developer access and verify application access", () => {
|
||||
xit("should disable Developer access and verify application access", () => {
|
||||
// Disable Developer access
|
||||
cy.get(interact.FIELD)
|
||||
.eq(4)
|
||||
|
@ -174,12 +166,12 @@ filterTests(["smoke", "all"], () => {
|
|||
|
||||
it("Should edit user details within user details page", () => {
|
||||
// Add First name
|
||||
cy.get(interact.FIELD, { timeout: 1000 }).eq(2).within(() => {
|
||||
cy.get(interact.FIELD, { timeout: 1000 }).eq(0).within(() => {
|
||||
cy.wait(500)
|
||||
cy.get(interact.SPECTRUM_TEXTFIELD_INPUT, { timeout: 1000 }).wait(500).clear().click().type("bb")
|
||||
})
|
||||
// Add Last name
|
||||
cy.get(interact.FIELD, { timeout: 1000 }).eq(3).within(() => {
|
||||
cy.get(interact.FIELD, { timeout: 1000 }).eq(1).within(() => {
|
||||
cy.wait(500)
|
||||
cy.get(interact.SPECTRUM_TEXTFIELD_INPUT, { timeout: 1000 }).click().wait(500).clear().type("test")
|
||||
})
|
||||
|
@ -188,16 +180,21 @@ filterTests(["smoke", "all"], () => {
|
|||
cy.reload()
|
||||
|
||||
// Confirm details have been saved
|
||||
cy.get(interact.FIELD, { timeout: 1000 }).eq(2).within(() => {
|
||||
cy.get(interact.FIELD, { timeout: 1000 }).eq(0).within(() => {
|
||||
cy.get(interact.SPECTRUM_TEXTFIELD_INPUT).should('have.value', "bb")
|
||||
})
|
||||
cy.get(interact.FIELD, { timeout: 1000 }).eq(3).within(() => {
|
||||
cy.get(interact.FIELD, { timeout: 1000 }).eq(1).within(() => {
|
||||
cy.get(interact.SPECTRUM_TEXTFIELD_INPUT, { timeout: 1000 }).should('have.value', "test")
|
||||
})
|
||||
})
|
||||
|
||||
it("should reset the users password", () => {
|
||||
cy.get(interact.REGENERATE, { timeout: 500 }).contains("Force password reset").click({ force: true })
|
||||
cy.get(".title").within(() => {
|
||||
cy.get(interact.SPECTRUM_ICON).click({ force: true })
|
||||
})
|
||||
cy.get(interact.SPECTRUM_MENU).within(() => {
|
||||
cy.get(interact.SPECTRUM_MENU_ITEM).contains("Force Password Reset").click({ force: true })
|
||||
})
|
||||
|
||||
// Reset password modal
|
||||
cy.get(interact.SPECTRUM_DIALOG_GRID)
|
||||
|
|
|
@ -19,10 +19,10 @@ filterTests(["smoke", "all"], () => {
|
|||
cy.contains("Users").click()
|
||||
cy.contains("test@test.com").click()
|
||||
|
||||
cy.get(interact.FIELD, { timeout: 1000 }).eq(2).within(() => {
|
||||
cy.get(interact.FIELD, { timeout: 1000 }).eq(0).within(() => {
|
||||
cy.get(interact.SPECTRUM_TEXTFIELD_INPUT).should('have.value', fname)
|
||||
})
|
||||
cy.get(interact.FIELD).eq(3).within(() => {
|
||||
cy.get(interact.FIELD).eq(1).within(() => {
|
||||
cy.get(interact.SPECTRUM_TEXTFIELD_INPUT).should('have.value', lname)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -205,7 +205,7 @@ filterTests(["all"], () => {
|
|||
|
||||
cy.navigateToFrontend()
|
||||
|
||||
cy.addComponent("Elements", "Headline").then(componentId => {
|
||||
cy.searchAndAddComponent("Headline").then(componentId => {
|
||||
cy.getComponent(componentId).should("exist")
|
||||
})
|
||||
|
||||
|
|
|
@ -14,11 +14,8 @@ filterTests(['smoke', 'all'], () => {
|
|||
cy.closeModal();
|
||||
|
||||
cy.contains("Design").click()
|
||||
cy.get(interact.LABEL_ADD_CIRCLE).click()
|
||||
cy.get(interact.SPECTRUM_MODAL).within(() => {
|
||||
cy.get(interact.ITEM_DISABLED).contains("Autogenerated screens")
|
||||
cy.get(interact.CONFIRM_WRAP_SPE_BUTTON).should('be.disabled')
|
||||
})
|
||||
cy.navigateToAutogeneratedModal()
|
||||
cy.get(interact.CONFIRM_WRAP_SPE_BUTTON).should('be.disabled')
|
||||
|
||||
cy.deleteAllApps()
|
||||
});
|
||||
|
@ -45,25 +42,25 @@ filterTests(['smoke', 'all'], () => {
|
|||
// Create Autogenerated screens from the internal table
|
||||
cy.createDatasourceScreen(["Cypress Tests"])
|
||||
// Confirm screens have been auto generated
|
||||
cy.get(interact.NAV_ITEMS_CONTAINER).contains("cypress-tests").click({ force: true })
|
||||
cy.get(interact.NAV_ITEMS_CONTAINER).should('contain', 'cypress-tests/:id')
|
||||
cy.get(interact.BODY).should('contain', "cypress-tests")
|
||||
.and('contain', 'cypress-tests/:id')
|
||||
.and('contain', 'cypress-tests/new/row')
|
||||
})
|
||||
|
||||
it("should generate multiple internal table screens at once", () => {
|
||||
// Create a second internal table
|
||||
const initialTable = "Cypress Tests"
|
||||
const secondTable = "Table Two"
|
||||
// Create a second internal table
|
||||
cy.createTable(secondTable)
|
||||
// Create Autogenerated screens from the internal tables
|
||||
cy.createDatasourceScreen([initialTable, secondTable])
|
||||
// Confirm screens have been auto generated
|
||||
cy.get(interact.NAV_ITEMS_CONTAINER).contains("cypress-tests").click({ force: true })
|
||||
// Previously generated tables are suffixed with numbers - as expected
|
||||
cy.get(interact.NAV_ITEMS_CONTAINER).should('contain', 'cypress-tests-2/:id')
|
||||
cy.get(interact.BODY).should('contain', 'cypress-tests-2')
|
||||
.and('contain', 'cypress-tests-2/:id')
|
||||
.and('contain', 'cypress-tests-2/new/row')
|
||||
cy.get(interact.NAV_ITEMS_CONTAINER).contains("table-two").click()
|
||||
cy.get(interact.NAV_ITEMS_CONTAINER).should('contain', 'table-two/:id')
|
||||
.and('contain', 'table-two')
|
||||
.and('contain', 'table-two/:id')
|
||||
.and('contain', 'table-two/new/row')
|
||||
})
|
||||
|
||||
|
@ -73,17 +70,17 @@ filterTests(['smoke', 'all'], () => {
|
|||
cy.createTable("Table Four")
|
||||
cy.createDatasourceScreen(["Table Three", "Table Four"], "Admin")
|
||||
|
||||
cy.get(interact.NAV_ITEMS_CONTAINER).contains("table-three").click()
|
||||
cy.get(interact.NAV_ITEMS_CONTAINER).should('contain', 'table-three/:id')
|
||||
// Filter screens to Admin
|
||||
cy.filterScreensAccessLevel('Admin')
|
||||
|
||||
cy.get(interact.BODY).should('contain', 'table-three')
|
||||
.and('contain', 'table-three/:id')
|
||||
.and('contain', 'table-three/new/row')
|
||||
|
||||
cy.get(interact.NAV_ITEMS_CONTAINER).contains("table-four").click()
|
||||
cy.get(interact.NAV_ITEMS_CONTAINER).should('contain', 'table-four/:id')
|
||||
.and('contain', 'table-four')
|
||||
.and('contain', 'table-four/:id')
|
||||
.and('contain', 'table-four/new/row')
|
||||
|
||||
//The access level should now be set to admin. Previous screens should be filtered.
|
||||
cy.get(interact.NAV_ITEMS_CONTAINER).contains("table-two").should('not.exist')
|
||||
cy.get(interact.NAV_ITEMS_CONTAINER).contains("cypress-tests").should('not.exist')
|
||||
.and('not.contain', 'table-two')
|
||||
.and('not.contain', 'cypress-tests')
|
||||
})
|
||||
|
||||
if (Cypress.env("TEST_ENV")) {
|
||||
|
@ -96,8 +93,8 @@ filterTests(['smoke', 'all'], () => {
|
|||
// Create Autogenerated screens from a MySQL table - MySQL contains books table
|
||||
cy.createDatasourceScreen(["books"])
|
||||
|
||||
cy.get(interact.NAV_ITEMS_CONTAINER).contains("books").click()
|
||||
cy.get(interact.NAV_ITEMS_CONTAINER).should('contain', 'books/:id')
|
||||
cy.get(interact.BODY).should('contain', 'books')
|
||||
.and('contain', 'books/:id')
|
||||
.and('contain', 'books/new/row')
|
||||
})
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@ filterTests(['smoke', 'all'], () => {
|
|||
it("should show the new user UI/UX", () => {
|
||||
cy.visit(`${Cypress.config().baseUrl}/builder/portal/apps/create`, { timeout: 5000 }) //added /portal/apps/create
|
||||
cy.wait(1000)
|
||||
cy.get(interact.CREATE_APP_BUTTON).contains('Start from scratch').should("exist")
|
||||
cy.get(interact.CREATE_APP_BUTTON, { timeout: 10000 }).contains('Start from scratch').should("exist")
|
||||
|
||||
cy.get(interact.TEMPLATE_CATEGORY_FILTER).should("exist")
|
||||
cy.get(interact.TEMPLATE_CATEGORY).should("exist")
|
||||
|
@ -100,24 +100,18 @@ filterTests(['smoke', 'all'], () => {
|
|||
})
|
||||
|
||||
it("should create the first application from scratch, using the users first name as the default app name", () => {
|
||||
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||
cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 5000 })
|
||||
|
||||
cy.updateUserInformation("Ted", "Userman")
|
||||
|
||||
cy.createApp("", false)
|
||||
|
||||
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||
|
||||
cy.applicationInAppTable("Teds app")
|
||||
cy.deleteApp("Teds app")
|
||||
|
||||
//Accomodate names that end in 'S'
|
||||
// Accomodate names that end in 'S'
|
||||
cy.updateUserInformation("Chris", "Userman")
|
||||
|
||||
cy.createApp("", false)
|
||||
|
||||
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||
|
||||
cy.applicationInAppTable("Chris app")
|
||||
cy.deleteApp("Chris app")
|
||||
|
||||
|
|
|
@ -9,13 +9,13 @@ filterTests(['smoke', 'all'], () => {
|
|||
})
|
||||
|
||||
it("should add a current user binding", () => {
|
||||
cy.addComponent("Elements", "Paragraph").then(() => {
|
||||
cy.searchAndAddComponent("Paragraph").then(() => {
|
||||
addSettingBinding("text", "Current User._id")
|
||||
})
|
||||
})
|
||||
|
||||
it("should handle an invalid binding", () => {
|
||||
cy.addComponent("Elements", "Paragraph").then(componentId => {
|
||||
cy.searchAndAddComponent("Paragraph").then(componentId => {
|
||||
// Cypress needs to escape curly brackets
|
||||
cy.get("[data-cy=setting-text] input")
|
||||
.type("{{}{{}{{} Current User._id {}}{}}")
|
||||
|
@ -27,7 +27,7 @@ filterTests(['smoke', 'all'], () => {
|
|||
xit("should add a URL param binding", () => {
|
||||
const paramName = "foo"
|
||||
cy.createScreen(`/test/:${paramName}`)
|
||||
cy.addComponent("Elements", "Paragraph").then(componentId => {
|
||||
cy.searchAndAddComponent("Paragraph").then(componentId => {
|
||||
addSettingBinding("text", `URL.${paramName}`)
|
||||
// The builder preview pages don't have a real URL, so all we can do
|
||||
// is check that we were able to bind to the property, and that the
|
||||
|
@ -37,7 +37,7 @@ filterTests(['smoke', 'all'], () => {
|
|||
})
|
||||
|
||||
it("should add a binding with a handlebars helper", () => {
|
||||
cy.addComponent("Elements", "Paragraph").then(componentId => {
|
||||
cy.searchAndAddComponent("Paragraph").then(componentId => {
|
||||
// Cypress needs to escape curly brackets
|
||||
cy.get("[data-cy=setting-text] input")
|
||||
.type("{{}{{} add 1 2 {}}{}}")
|
||||
|
|
|
@ -31,13 +31,13 @@ filterTests(["all"], () => {
|
|||
}
|
||||
|
||||
it("should add a container", () => {
|
||||
cy.addComponent("Layout", "Container").then(componentId => {
|
||||
cy.searchAndAddComponent("Container").then(componentId => {
|
||||
cy.getComponent(componentId).should("exist")
|
||||
})
|
||||
})
|
||||
|
||||
it("should add a headline", () => {
|
||||
cy.addComponent("Elements", "Headline").then(componentId => {
|
||||
cy.searchAndAddComponent("Headline").then(componentId => {
|
||||
headlineId = componentId
|
||||
cy.getComponent(headlineId).should("exist")
|
||||
})
|
||||
|
@ -63,11 +63,11 @@ filterTests(["all"], () => {
|
|||
})
|
||||
|
||||
it("should create a form and reset to match schema", () => {
|
||||
cy.addComponent("Form", "Form").then(() => {
|
||||
cy.searchAndAddComponent("Form").then(() => {
|
||||
cy.get("[data-cy=setting-dataSource]").contains("Custom").click()
|
||||
cy.get(interact.DROPDOWN).contains("dog").click()
|
||||
cy.wait(500)
|
||||
cy.addComponent("Form", "Field Group").then(fieldGroupId => {
|
||||
cy.searchAndAddComponent("Field Group").then(fieldGroupId => {
|
||||
cy.contains("Update form fields").click()
|
||||
cy.get(".spectrum-Modal")
|
||||
.get(".confirm-wrap .spectrum-Button")
|
||||
|
@ -88,7 +88,7 @@ filterTests(["all"], () => {
|
|||
})
|
||||
|
||||
it("deletes a component", () => {
|
||||
cy.addComponent("Elements", "Paragraph").then(componentId => {
|
||||
cy.searchAndAddComponent("Paragraph").then(componentId => {
|
||||
cy.get("[data-cy=setting-_instanceName] input").type(componentId).blur()
|
||||
cy.get(
|
||||
".nav-items-container .nav-item.selected .actions > div > .icon"
|
||||
|
@ -104,7 +104,7 @@ filterTests(["all"], () => {
|
|||
})
|
||||
|
||||
it("should clear the iframe place holder when a form field has been set", () => {
|
||||
cy.addComponent("Form", "Form").then(formId => {
|
||||
cy.searchAndAddComponent("Form").then(formId => {
|
||||
//For deletion
|
||||
cy.get("[data-cy=setting-_instanceName] input")
|
||||
.clear()
|
||||
|
@ -123,10 +123,7 @@ filterTests(["all"], () => {
|
|||
|
||||
const testFieldFocusOnCreate = componentLabel => {
|
||||
cy.log("Adding: " + componentLabel)
|
||||
return cy.addComponent("Form", componentLabel).then(componentId => {
|
||||
cy.getComponent(componentId)
|
||||
.find(".component-placeholder")
|
||||
.should("exist")
|
||||
return cy.searchAndAddComponent(componentLabel).then(componentId => {
|
||||
cy.get("[data-cy=setting-field] button.spectrum-Picker").click()
|
||||
|
||||
//Click the first appropriate field. They are filtered by type
|
||||
|
@ -157,7 +154,7 @@ filterTests(["all"], () => {
|
|||
})
|
||||
|
||||
it("should populate the provider for charts with a data provider in its path", () => {
|
||||
cy.addComponent("Data", "Data Provider").then(providerId => {
|
||||
cy.searchAndAddComponent("Data Provider").then(providerId => {
|
||||
//For deletion
|
||||
cy.get("[data-cy=setting-_instanceName] input")
|
||||
.clear()
|
||||
|
@ -181,7 +178,7 @@ filterTests(["all"], () => {
|
|||
|
||||
const testFocusOnCreate = chartLabel => {
|
||||
cy.log("Adding: " + chartLabel)
|
||||
cy.addComponent("Chart", chartLabel).then(componentId => {
|
||||
cy.searchAndAddComponent(chartLabel).then(componentId => {
|
||||
cy.get(
|
||||
"[data-cy=dataProvider-prop-control] .spectrum-Picker"
|
||||
).should("not.have.class", "is-focused")
|
||||
|
@ -207,7 +204,7 @@ filterTests(["all"], () => {
|
|||
})
|
||||
|
||||
it("should replace the placeholder when a url is set on an image", () => {
|
||||
cy.addComponent("Elements", "Image").then(imageId => {
|
||||
cy.searchAndAddComponent("Image").then(imageId => {
|
||||
cy.get("[data-cy=setting-_instanceName] input")
|
||||
.clear()
|
||||
.type(imageId)
|
||||
|
@ -229,7 +226,7 @@ filterTests(["all"], () => {
|
|||
})
|
||||
|
||||
it("should add a markdown component.", () => {
|
||||
cy.addComponent("Elements", "Markdown Viewer").then(markdownId => {
|
||||
cy.searchAndAddComponent("Markdown Viewer").then(markdownId => {
|
||||
cy.get("[data-cy=setting-_instanceName] input")
|
||||
.clear()
|
||||
.type(markdownId)
|
||||
|
@ -253,8 +250,7 @@ filterTests(["all"], () => {
|
|||
})
|
||||
|
||||
it("should direct the user when adding an Icon component.", () => {
|
||||
cy.addComponent("Elements", "Icon").then(iconId => {
|
||||
cy.getComponent(iconId).find(".component-placeholder").should("exist")
|
||||
cy.searchAndAddComponent("Icon").then(iconId => {
|
||||
cy.get("[data-cy=setting-_instanceName] input")
|
||||
.clear()
|
||||
.type(iconId)
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import filterTests from "../support/filterTests"
|
||||
const interact = require('../support/interact')
|
||||
|
||||
filterTests(["smoke", "all"], () => {
|
||||
context("Screen Tests", () => {
|
||||
|
@ -10,32 +11,44 @@ filterTests(["smoke", "all"], () => {
|
|||
|
||||
it("Should successfully create a screen", () => {
|
||||
cy.createScreen("test")
|
||||
cy.get(".nav-items-container").within(() => {
|
||||
cy.get(interact.BODY).within(() => {
|
||||
cy.contains("/test").should("exist")
|
||||
})
|
||||
})
|
||||
|
||||
it("Should update the url", () => {
|
||||
cy.createScreen("test with spaces")
|
||||
cy.get(".nav-items-container").within(() => {
|
||||
cy.get(interact.BODY).within(() => {
|
||||
cy.contains("/test-with-spaces").should("exist")
|
||||
})
|
||||
})
|
||||
|
||||
it("Should create a blank screen with the selected access level", () => {
|
||||
cy.createScreen("admin only", "Admin")
|
||||
it("should delete all screens then create first screen via button", () => {
|
||||
cy.deleteAllScreens()
|
||||
|
||||
cy.contains("Create first screen").click()
|
||||
cy.get(interact.BODY, { timeout: 2000 }).should('contain', '/home')
|
||||
})
|
||||
|
||||
cy.get(".nav-items-container").within(() => {
|
||||
cy.contains("/admin-only").should("exist")
|
||||
})
|
||||
it("Should create and filter screens by access level", () => {
|
||||
const accessLevels = ["Basic", "Admin", "Public", "Power"]
|
||||
|
||||
cy.createScreen("open to all", "Public")
|
||||
for (const access of accessLevels){
|
||||
// Create screen with specified access level
|
||||
cy.createScreen(access, access)
|
||||
// Filter by access level and confirm screen visible
|
||||
cy.filterScreensAccessLevel(access)
|
||||
cy.get(interact.BODY).within(() => {
|
||||
cy.get(interact.NAV_ITEM).should('contain', access.toLowerCase())
|
||||
})
|
||||
}
|
||||
|
||||
cy.get(".nav-items-container").within(() => {
|
||||
cy.contains("/open-to-all").should("exist")
|
||||
//The access level should now be set to admin. Previous screens should be filtered.
|
||||
cy.get(".nav-item").contains("/test-screen").should("not.exist")
|
||||
})
|
||||
// Filter by All screens - Confirm all screens visible
|
||||
cy.filterScreensAccessLevel("All screens")
|
||||
cy.get(interact.BODY).should('contain', accessLevels[0])
|
||||
.and('contain', accessLevels[1])
|
||||
.and('contain', accessLevels[2])
|
||||
.and('contain', accessLevels[3])
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -179,7 +179,7 @@ filterTests(["all"], () => {
|
|||
cy.get(".nav-item").should("contain", queryName)
|
||||
})
|
||||
|
||||
it("should duplicate a query", () => {
|
||||
xit("should duplicate a query", () => {
|
||||
/// Get query nav item - QueryName
|
||||
cy.get(".nav-item")
|
||||
.contains(queryName)
|
||||
|
@ -199,15 +199,16 @@ filterTests(["all"], () => {
|
|||
.within(() => {
|
||||
cy.get("input").clear().type(queryRename)
|
||||
})
|
||||
// Save query
|
||||
cy.get(".spectrum-Button").contains("Save Query").click({ force: true })
|
||||
// Click on a nav item
|
||||
cy.get(".nav-item").first().click()
|
||||
// Confirm name change
|
||||
cy.get(".nav-item").should("contain", queryRename)
|
||||
})
|
||||
|
||||
it("should delete a query", () => {
|
||||
// Get query nav item - QueryName
|
||||
cy.get(".nav-item")
|
||||
.contains(queryName)
|
||||
.contains(queryRename)
|
||||
.parent()
|
||||
.within(() => {
|
||||
cy.get(".spectrum-Icon").eq(1).click({ force: true })
|
||||
|
@ -218,7 +219,7 @@ filterTests(["all"], () => {
|
|||
.contains("Delete Query")
|
||||
.click({ force: true })
|
||||
// Confirm deletion
|
||||
cy.get(".nav-item", { timeout: 1000 }).should("not.contain", queryName)
|
||||
cy.get(".nav-item", { timeout: 1000 }).should("not.contain", queryRename)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
|
|
@ -108,7 +108,7 @@ filterTests(["all"], () => {
|
|||
})
|
||||
|
||||
it("should delete a relationship", () => {
|
||||
cy.get(".hierarchy-items-container").contains("PostgreSQL").click()
|
||||
cy.get(".hierarchy-items-container").contains("PostgreSQL").click({ force: true })
|
||||
cy.reload()
|
||||
// Delete one relationship
|
||||
cy.get(".spectrum-Table")
|
||||
|
@ -150,13 +150,15 @@ filterTests(["all"], () => {
|
|||
cy.get("@query").its("response.statusCode").should("eq", 200)
|
||||
cy.get("@query").its("response.body").should("not.be.empty")
|
||||
// Save query
|
||||
cy.intercept("**/queries").as("saveQuery")
|
||||
cy.get(".spectrum-Button").contains("Save Query").click({ force: true })
|
||||
cy.wait("@saveQuery")
|
||||
cy.get(".spectrum-Tabs-content", { timeout: 2000 }).should("contain", queryName)
|
||||
})
|
||||
|
||||
it("should switch to schema with no tables", () => {
|
||||
// Switch Schema - To one without any tables
|
||||
cy.get(".hierarchy-items-container").contains("PostgreSQL").click()
|
||||
cy.get(".hierarchy-items-container").contains("PostgreSQL").click({ force: true })
|
||||
switchSchema("randomText")
|
||||
|
||||
// No tables displayed
|
||||
|
@ -202,7 +204,7 @@ filterTests(["all"], () => {
|
|||
cy.get(".spectrum-Table").eq(1).should("contain", queryName)
|
||||
})
|
||||
|
||||
it("should duplicate a query", () => {
|
||||
xit("should duplicate a query", () => {
|
||||
// Locate previously created query
|
||||
cy.get(".nav-item")
|
||||
.contains(queryName)
|
||||
|
@ -218,8 +220,9 @@ filterTests(["all"], () => {
|
|||
it("should edit a query name", () => {
|
||||
// Access query
|
||||
cy.get(".hierarchy-items-container", { timeout: 2000 })
|
||||
.contains(queryName + " (1)")
|
||||
.click()
|
||||
//.contains(queryName + " (1)")
|
||||
.contains(queryName)
|
||||
.click({ force: true })
|
||||
|
||||
// Rename query
|
||||
cy.wait(1000)
|
||||
|
@ -229,18 +232,16 @@ filterTests(["all"], () => {
|
|||
cy.get("input").clear().type(queryRename)
|
||||
})
|
||||
|
||||
// Run and Save query
|
||||
cy.get(".spectrum-Button", { timeout: 2000 }).contains("Run Query").click({ force: true })
|
||||
cy.wait(1000)
|
||||
cy.get(".spectrum-Button", { timeout: 2000 }).contains("Save Query").click({ force: true })
|
||||
cy.reload({ timeout: 5000 })
|
||||
cy.get(".nav-item", { timeout: 2000 }).should("contain", queryRename)
|
||||
// Click on a nav item and confirm name change
|
||||
cy.get(".nav-item").first().click()
|
||||
// Confirm name change
|
||||
cy.get(".nav-item").should("contain", queryRename)
|
||||
})
|
||||
|
||||
it("should delete a query", () => {
|
||||
// Get query nav item - QueryName
|
||||
cy.get(".nav-item")
|
||||
.contains(queryName)
|
||||
.contains(queryRename)
|
||||
.parent()
|
||||
.within(() => {
|
||||
cy.get(".spectrum-Icon").eq(1).click({ force: true })
|
||||
|
@ -252,7 +253,7 @@ filterTests(["all"], () => {
|
|||
.click({ force: true })
|
||||
// Confirm deletion
|
||||
cy.reload({ timeout: 5000 })
|
||||
cy.get(".nav-item", { timeout: 1000 }).should("not.contain", queryName)
|
||||
cy.get(".nav-item", { timeout: 1000 }).should("not.contain", queryRename)
|
||||
})
|
||||
|
||||
const switchSchema = schema => {
|
||||
|
|
|
@ -2,7 +2,7 @@ import filterTests from "../support/filterTests"
|
|||
const interact = require('../support/interact')
|
||||
|
||||
filterTests(["smoke", "all"], () => {
|
||||
context("Query Level Transformers", () => {
|
||||
xcontext("Query Level Transformers", () => {
|
||||
before(() => {
|
||||
cy.login()
|
||||
cy.createTestApp()
|
||||
|
|
|
@ -15,7 +15,7 @@ filterTests(['smoke', 'all'], () => {
|
|||
})
|
||||
cy.get(interact.SPECTRUM_MODAL).within(() => {
|
||||
// Enter app name before revert
|
||||
cy.get("input").type("Cypress Tests")
|
||||
cy.get(interact.SPECTRUM_TEXTFIELD_INPUT).type("Cypress Tests")
|
||||
cy.intercept('**/revert').as('revertApp')
|
||||
// Click Revert
|
||||
cy.get(interact.SPECTRUM_BUTTON).contains("Revert").click({ force: true })
|
||||
|
@ -30,7 +30,7 @@ filterTests(['smoke', 'all'], () => {
|
|||
cy.navigateToFrontend()
|
||||
|
||||
// Add initial component - Paragraph
|
||||
cy.addComponent("Elements", "Paragraph")
|
||||
cy.searchAndAddComponent("Paragraph")
|
||||
// Publish app
|
||||
cy.get(interact.SPECTRUM_BUTTON).contains("Publish").click({ force: true })
|
||||
cy.get(interact.SPECTRUM_BUTTON_GROUP).within(() => {
|
||||
|
@ -42,7 +42,7 @@ filterTests(['smoke', 'all'], () => {
|
|||
})
|
||||
|
||||
// Add second component - Button
|
||||
cy.addComponent("Elements", "Button")
|
||||
cy.searchAndAddComponent("Button")
|
||||
// Click Revert
|
||||
cy.get(interact.TOP_RIGHT_NAV).within(() => {
|
||||
cy.get(interact.AREA_LABEL_REVERT).click({ force: true })
|
||||
|
|
|
@ -4,31 +4,32 @@ Cypress.on("uncaught:exception", () => {
|
|||
|
||||
// ACCOUNTS & USERS
|
||||
Cypress.Commands.add("login", (email, password) => {
|
||||
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||
cy.wait(2000)
|
||||
cy.url().then(url => {
|
||||
if (url.includes("builder/admin")) {
|
||||
// create admin user
|
||||
cy.get("input").first().type("test@test.com")
|
||||
cy.get('input[type="password"]').first().type("test")
|
||||
cy.get('input[type="password"]').eq(1).type("test")
|
||||
cy.contains("Create super admin user").click({ force: true })
|
||||
}
|
||||
if (url.includes("builder/auth/login") || url.includes("builder/admin")) {
|
||||
// login
|
||||
cy.contains("Sign in to Budibase").then(() => {
|
||||
if (email == null) {
|
||||
cy.get("input").first().type("test@test.com")
|
||||
cy.get('input[type="password"]').type("test")
|
||||
} else {
|
||||
cy.get("input").first().type(email)
|
||||
cy.get('input[type="password"]').type(password)
|
||||
}
|
||||
cy.get("button").first().click({ force: true })
|
||||
cy.wait(1000)
|
||||
})
|
||||
}
|
||||
})
|
||||
cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 10000 })
|
||||
cy.url()
|
||||
.should("include", "/builder/")
|
||||
.then(url => {
|
||||
if (url.includes("builder/admin")) {
|
||||
// create admin user
|
||||
cy.get("input").first().type("test@test.com")
|
||||
cy.get('input[type="password"]').first().type("test")
|
||||
cy.get('input[type="password"]').eq(1).type("test")
|
||||
cy.contains("Create super admin user").click({ force: true })
|
||||
}
|
||||
if (url.includes("builder/auth") || url.includes("builder/admin")) {
|
||||
// login
|
||||
cy.contains("Sign in to Budibase").then(() => {
|
||||
if (email == null) {
|
||||
cy.get("input").first().type("test@test.com")
|
||||
cy.get('input[type="password"]').type("test")
|
||||
} else {
|
||||
cy.get("input").first().type(email)
|
||||
cy.get('input[type="password"]').type(password)
|
||||
}
|
||||
cy.get("button").first().click({ force: true })
|
||||
cy.wait(1000)
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
Cypress.Commands.add("logOut", () => {
|
||||
|
@ -50,23 +51,36 @@ Cypress.Commands.add("logoutNoAppGrid", () => {
|
|||
cy.wait(2000)
|
||||
})
|
||||
|
||||
Cypress.Commands.add("createUser", email => {
|
||||
// quick hacky recorded way to create a user
|
||||
Cypress.Commands.add("createUser", (email, permission) => {
|
||||
cy.contains("Users").click()
|
||||
cy.get(`[data-cy="add-user"]`).click()
|
||||
cy.get(".spectrum-Dialog-grid").within(() => {
|
||||
cy.get(".spectrum-Picker-label").click()
|
||||
cy.get(
|
||||
".spectrum-Menu-item:nth-child(2) > .spectrum-Menu-itemLabel"
|
||||
).click()
|
||||
// Enter email
|
||||
cy.get(".spectrum-Textfield-input").clear().click().type(email)
|
||||
|
||||
// Onboarding type selector
|
||||
cy.get(".spectrum-Textfield-input")
|
||||
.eq(0)
|
||||
.first()
|
||||
.type(email, { force: true })
|
||||
cy.get(".spectrum-Button--cta").click({ force: true })
|
||||
// Select permission, if applicable
|
||||
// Default is App User
|
||||
if (permission != null) {
|
||||
cy.get(".spectrum-Picker-label").click()
|
||||
cy.get(".spectrum-Menu").within(() => {
|
||||
cy.get(".spectrum-Menu-item")
|
||||
.contains(permission)
|
||||
.click({ force: true })
|
||||
})
|
||||
}
|
||||
// Add user and wait for modal to change
|
||||
cy.get(".spectrum-Button").contains("Add user").click({ force: true })
|
||||
cy.get(".spectrum-ActionButton").contains("Add email").should("not.exist")
|
||||
})
|
||||
// Onboarding modal
|
||||
cy.get(".spectrum-Dialog-grid").within(() => {
|
||||
cy.get(".onboarding-type").eq(1).click()
|
||||
cy.get(".spectrum-Button").contains("Done").click({ force: true })
|
||||
cy.get(".spectrum-Button").contains("Cancel").should("not.exist")
|
||||
})
|
||||
|
||||
// Accounts created modal - Click Done button
|
||||
cy.get(".spectrum-Button").contains("Done").click({ force: true })
|
||||
})
|
||||
|
||||
Cypress.Commands.add("deleteUser", email => {
|
||||
|
@ -74,18 +88,13 @@ Cypress.Commands.add("deleteUser", email => {
|
|||
cy.contains("Users", { timeout: 2000 }).click()
|
||||
cy.contains(email).click()
|
||||
|
||||
// Click Delete user button
|
||||
cy.get(".spectrum-Button")
|
||||
.contains("Delete user")
|
||||
.click({ force: true })
|
||||
.then(() => {
|
||||
// Confirm deletion within modal
|
||||
cy.get(".spectrum-Dialog-grid", { timeout: 500 }).within(() => {
|
||||
cy.get(".spectrum-Button")
|
||||
.contains("Delete user")
|
||||
.click({ force: true })
|
||||
})
|
||||
})
|
||||
cy.get(".title").within(() => {
|
||||
cy.get(".spectrum-Icon").click({ force: true })
|
||||
})
|
||||
cy.get(".spectrum-Menu").within(() => {
|
||||
cy.get(".spectrum-Menu-item").contains("Delete").click({ force: true })
|
||||
})
|
||||
cy.get(".spectrum-Dialog-grid").contains("Delete user").click({ force: true })
|
||||
})
|
||||
|
||||
Cypress.Commands.add("updateUserInformation", (firstName, lastName) => {
|
||||
|
@ -120,9 +129,27 @@ Cypress.Commands.add("updateUserInformation", (firstName, lastName) => {
|
|||
.blur()
|
||||
}
|
||||
cy.get("button").contains("Update information").click({ force: true })
|
||||
cy.get(".spectrum-Dialog-grid").should("not.exist")
|
||||
})
|
||||
})
|
||||
|
||||
Cypress.Commands.add("setUserRole", (user, role) => {
|
||||
cy.contains("Users").click()
|
||||
cy.contains(user).click()
|
||||
|
||||
// Set Role
|
||||
cy.wait(500)
|
||||
cy.get(".spectrum-Form-itemField")
|
||||
.eq(2)
|
||||
.within(() => {
|
||||
cy.get(".spectrum-Picker-label").click({ force: true })
|
||||
})
|
||||
cy.get(".spectrum-Menu").within(() => {
|
||||
cy.get(".spectrum-Menu-itemLabel").contains(role).click({ force: true })
|
||||
})
|
||||
cy.get(".spectrum-Form-itemField").eq(2).should("contain", role)
|
||||
})
|
||||
|
||||
// APPLICATIONS
|
||||
Cypress.Commands.add("createTestApp", () => {
|
||||
const appName = "Cypress Tests"
|
||||
|
@ -139,7 +166,9 @@ Cypress.Commands.add("createApp", (name, addDefaultTable) => {
|
|||
cy.get(`[data-cy="create-app-btn"]`, { timeout: 5000 }).click({ force: true })
|
||||
|
||||
// If apps already exist
|
||||
cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`)
|
||||
cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`, {
|
||||
timeout: 5000,
|
||||
})
|
||||
.its("body")
|
||||
.then(val => {
|
||||
if (val.length > 0) {
|
||||
|
@ -208,7 +237,7 @@ Cypress.Commands.add("deleteApp", name => {
|
|||
cy.get(".app-overview-actions-icon").within(() => {
|
||||
cy.get(".spectrum-Icon").click({ force: true })
|
||||
})
|
||||
cy.get(".spectrum-Menu").contains("Delete").click()
|
||||
cy.get(".spectrum-Menu").contains("Delete").click({ force: true })
|
||||
cy.get(".spectrum-Dialog-grid").within(() => {
|
||||
cy.get("input").type(name)
|
||||
})
|
||||
|
@ -223,9 +252,11 @@ Cypress.Commands.add("deleteApp", name => {
|
|||
})
|
||||
|
||||
Cypress.Commands.add("deleteAllApps", () => {
|
||||
cy.visit(`${Cypress.config().baseUrl}/builder`)
|
||||
cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 5000 })
|
||||
cy.wait(500)
|
||||
cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`)
|
||||
cy.request(`${Cypress.config().baseUrl}/api/applications?status=all`, {
|
||||
timeout: 5000,
|
||||
})
|
||||
.its("body")
|
||||
.then(val => {
|
||||
for (let i = 0; i < val.length; i++) {
|
||||
|
@ -285,7 +316,7 @@ Cypress.Commands.add("updateAppName", (changedName, noName) => {
|
|||
})
|
||||
|
||||
Cypress.Commands.add("publishApp", resolvedAppPath => {
|
||||
//Assumes you have navigated to an application first
|
||||
// Assumes you have navigated to an application first
|
||||
cy.get(".toprightnav button.spectrum-Button")
|
||||
.contains("Publish")
|
||||
.click({ force: true })
|
||||
|
@ -297,7 +328,7 @@ Cypress.Commands.add("publishApp", resolvedAppPath => {
|
|||
cy.wait(1000)
|
||||
})
|
||||
|
||||
//Verify that the app url is presented correctly to the user
|
||||
// Verify that the app url is presented correctly to the user
|
||||
cy.get(".spectrum-Modal [data-cy='deploy-app-success-modal']")
|
||||
.should("be.visible")
|
||||
.within(() => {
|
||||
|
@ -377,7 +408,7 @@ Cypress.Commands.add("searchForApplication", appName => {
|
|||
// Assumes there are no others
|
||||
Cypress.Commands.add("applicationInAppTable", appName => {
|
||||
cy.visit(`${Cypress.config().baseUrl}/builder`, { timeout: 10000 })
|
||||
cy.get(".appTable", { timeout: 2000 }).within(() => {
|
||||
cy.get(".appTable", { timeout: 5000 }).within(() => {
|
||||
cy.get(".title").contains(appName).should("exist")
|
||||
})
|
||||
})
|
||||
|
@ -418,7 +449,12 @@ Cypress.Commands.add("createTable", (tableName, initialTable) => {
|
|||
cy.get("input", { timeout: 2000 }).first().type(tableName).blur()
|
||||
cy.get(".spectrum-ButtonGroup").contains("Create").click()
|
||||
})
|
||||
cy.contains(tableName).should("be.visible")
|
||||
// Ensure modal has closed and table is created
|
||||
cy.get(".spectrum-Modal").should("not.exist")
|
||||
cy.get(".spectrum-Tabs-content", { timeout: 1000 }).should(
|
||||
"contain",
|
||||
tableName
|
||||
)
|
||||
})
|
||||
|
||||
Cypress.Commands.add("createTestTableWithData", () => {
|
||||
|
@ -487,26 +523,45 @@ Cypress.Commands.add("selectTable", tableName => {
|
|||
})
|
||||
|
||||
Cypress.Commands.add("addCustomSourceOptions", totalOptions => {
|
||||
cy.get(".spectrum-ActionButton")
|
||||
.contains("Define Options")
|
||||
.click()
|
||||
.then(() => {
|
||||
for (let i = 0; i < totalOptions; i++) {
|
||||
// Add radio button options
|
||||
cy.get(".spectrum-Button")
|
||||
.contains("Add Option")
|
||||
.click({ force: true })
|
||||
.then(() => {
|
||||
cy.get("[placeholder='Label']", { timeout: 500 }).eq(i).type(i)
|
||||
cy.get("[placeholder='Value']").eq(i).type(i)
|
||||
})
|
||||
}
|
||||
// Save options
|
||||
cy.get(".spectrum-Button").contains("Save").click({ force: true })
|
||||
})
|
||||
cy.get('[data-cy="customOptions-prop-control"]').within(() => {
|
||||
cy.get(".spectrum-ActionButton-label").click({ force: true })
|
||||
})
|
||||
for (let i = 0; i < totalOptions; i++) {
|
||||
// Add radio button options
|
||||
cy.get(".spectrum-Button-label", { timeout: 1000 })
|
||||
.contains("Add Option")
|
||||
.click({ force: true })
|
||||
.then(() => {
|
||||
cy.get("[placeholder='Label']", { timeout: 500 }).eq(i).type(i)
|
||||
cy.get("[placeholder='Value']").eq(i).type(i)
|
||||
})
|
||||
}
|
||||
// Save options
|
||||
cy.get(".spectrum-Button").contains("Save").click({ force: true })
|
||||
})
|
||||
|
||||
// DESIGN SECTION
|
||||
Cypress.Commands.add("searchAndAddComponent", component => {
|
||||
// Open component menu
|
||||
cy.get(".spectrum-Button").contains("Component").click({ force: true })
|
||||
|
||||
// Search and add component
|
||||
cy.wait(500)
|
||||
cy.get(".spectrum-Textfield-input").clear().type(component)
|
||||
cy.get(".body").within(() => {
|
||||
cy.get(".component")
|
||||
.contains(new RegExp("^" + component + "$"), { timeout: 3000 })
|
||||
.click({ force: true })
|
||||
})
|
||||
cy.wait(1000)
|
||||
cy.location().then(loc => {
|
||||
const params = loc.pathname.split("/")
|
||||
const componentId = params[params.length - 1]
|
||||
cy.getComponent(componentId, { timeout: 3000 }).should("exist")
|
||||
return cy.wrap(componentId)
|
||||
})
|
||||
})
|
||||
|
||||
// DESIGN AREA
|
||||
Cypress.Commands.add("addComponent", (category, component) => {
|
||||
if (category) {
|
||||
cy.get(`[data-cy="category-${category}"]`, { timeout: 3000 }).click({
|
||||
|
@ -542,7 +597,7 @@ Cypress.Commands.add("getComponent", componentId => {
|
|||
Cypress.Commands.add("createScreen", (route, accessLevelLabel) => {
|
||||
// Blank Screen
|
||||
cy.contains("Design").click()
|
||||
cy.get("[aria-label=AddCircle]").click()
|
||||
cy.get(".header > .add-button").click()
|
||||
cy.get(".spectrum-Modal").within(() => {
|
||||
cy.get("[data-cy='blank-screen']").click()
|
||||
cy.get(".spectrum-Button").contains("Continue").click({ force: true })
|
||||
|
@ -567,7 +622,7 @@ Cypress.Commands.add(
|
|||
"createDatasourceScreen",
|
||||
(datasourceNames, accessLevelLabel) => {
|
||||
cy.contains("Design").click()
|
||||
cy.get("[aria-label=AddCircle]").click()
|
||||
cy.get(".header > .add-button").click()
|
||||
cy.get(".spectrum-Modal").within(() => {
|
||||
cy.get(".item").contains("Autogenerated screens").click()
|
||||
cy.get(".spectrum-Button").contains("Continue").click({ force: true })
|
||||
|
@ -622,13 +677,60 @@ Cypress.Commands.add(
|
|||
}
|
||||
)
|
||||
|
||||
Cypress.Commands.add("filterScreensAccessLevel", accessLevel => {
|
||||
// Filters screens by access level dropdown
|
||||
cy.get(".body").within(() => {
|
||||
cy.get(".spectrum-Form-item").eq(1).click()
|
||||
})
|
||||
cy.get(".spectrum-Menu").within(() => {
|
||||
cy.contains(accessLevel).click()
|
||||
})
|
||||
})
|
||||
|
||||
Cypress.Commands.add("deleteScreen", screen => {
|
||||
// Navigates to Design section and deletes specified screen
|
||||
cy.contains("Design").click()
|
||||
cy.get(".body").within(() => {
|
||||
cy.contains(screen)
|
||||
.siblings(".actions")
|
||||
.within(() => {
|
||||
cy.get(".spectrum-Icon").click({ force: true })
|
||||
})
|
||||
})
|
||||
cy.get(".spectrum-Menu > .spectrum-Menu-item > .spectrum-Menu-itemLabel")
|
||||
.contains("Delete")
|
||||
.click()
|
||||
|
||||
cy.get(
|
||||
".spectrum-Dialog-grid > .spectrum-ButtonGroup > .confirm-wrap > .spectrum-Button"
|
||||
).click({ force: true })
|
||||
cy.get(".spectrum-Dialog-grid", { timeout: 10000 }).should("not.exist")
|
||||
})
|
||||
|
||||
Cypress.Commands.add("deleteAllScreens", () => {
|
||||
// Deletes all screens
|
||||
cy.get(".body")
|
||||
.find(".nav-item")
|
||||
.its("length")
|
||||
.then(len => {
|
||||
for (let i = 0; i < len; i++) {
|
||||
cy.get(".body > .nav-item")
|
||||
.eq(0)
|
||||
.invoke("text")
|
||||
.then(text => {
|
||||
cy.deleteScreen(text.trim())
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// NAVIGATION
|
||||
Cypress.Commands.add("navigateToFrontend", () => {
|
||||
// Clicks on Design tab and then the Home nav item
|
||||
cy.wait(500)
|
||||
cy.contains("Design").click()
|
||||
cy.get(".spectrum-Search", { timeout: 2000 }).type("/")
|
||||
cy.get(".nav-item", { timeout: 2000 }).contains("home").click()
|
||||
cy.get(".nav-item", { timeout: 2000 }).contains("home").click({ force: true })
|
||||
})
|
||||
|
||||
Cypress.Commands.add("navigateToDataSection", () => {
|
||||
|
@ -640,9 +742,11 @@ Cypress.Commands.add("navigateToDataSection", () => {
|
|||
Cypress.Commands.add("navigateToAutogeneratedModal", () => {
|
||||
// Screen name must already exist within data source
|
||||
cy.contains("Design").click()
|
||||
cy.get("[aria-label=AddCircle]").click()
|
||||
cy.get(".header > .add-button").click()
|
||||
cy.get(".spectrum-Modal").within(() => {
|
||||
cy.get(".item").contains("Autogenerated screens").click()
|
||||
cy.get(".item", { timeout: 2000 })
|
||||
.contains("Autogenerated screens")
|
||||
.click({ force: true })
|
||||
cy.get(".spectrum-Button").contains("Continue").click({ force: true })
|
||||
cy.wait(500)
|
||||
})
|
||||
|
|
|
@ -12,7 +12,7 @@ export const APP_NAME_INPUT = "input" // we need to update this with atribute cy
|
|||
export const SPECTRUM_BUTTON_GROUP = ".spectrum-ButtonGroup"
|
||||
export const SPECTRUM_MODAL_INPUT = ".spectrum-Modal input"
|
||||
|
||||
//AddMultiOptionDatatype test
|
||||
//AddMultiOptionDatatype
|
||||
export const CATEGORY_DATA = '[data-cy="category-Data"]'
|
||||
export const COMPONENT_DATA_PROVIDER = '[data-cy="component-Data Provider"]'
|
||||
export const DATASOURCE_PROP_CONTROL = '[data-cy="dataSource-prop-control"]'
|
||||
|
@ -51,7 +51,7 @@ export const LABEL_ADD_CIRCLE = "[aria-label=AddCircle]"
|
|||
export const ITEM_DISABLED = ".item.disabled"
|
||||
export const CONFIRM_WRAP_SPE_BUTTON = ".confirm-wrap .spectrum-Button"
|
||||
export const DATA_SOURCE_ENTRY = ".data-source-entry"
|
||||
export const NAV_ITEMS_CONTAINER = ".nav-items-container"
|
||||
export const BODY = ".body"
|
||||
|
||||
//publishWorkFlow
|
||||
export const DEPLOY_APP_MODAL = ".spectrum-Modal [data-cy=deploy-app-modal]"
|
||||
|
@ -108,6 +108,9 @@ export const CONTAINER = ".container"
|
|||
export const REGENERATE = ".regenerate"
|
||||
export const SPECTRUM_DIALOG_CONTENT = ".spectrum-Dialog-content"
|
||||
export const SPECTRUM_ICON = ".spectrum-Icon"
|
||||
export const SPECTRUM_HEADING = ".spectrum-Heading"
|
||||
export const SPECTRUM_FORM_ITEMFIELD = ".spectrum-Form-itemField"
|
||||
export const LIST_ITEMS = ".list-items"
|
||||
|
||||
//createView
|
||||
export const SPECTRUM_MENU_ITEM_LABEL = ".spectrum-Menu-itemLabel"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@budibase/builder",
|
||||
"version": "1.1.10-alpha.3",
|
||||
"version": "1.1.32-alpha.1",
|
||||
"license": "GPL-3.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
@ -69,10 +69,10 @@
|
|||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@budibase/bbui": "^1.1.10-alpha.3",
|
||||
"@budibase/client": "^1.1.10-alpha.3",
|
||||
"@budibase/frontend-core": "^1.1.10-alpha.3",
|
||||
"@budibase/string-templates": "^1.1.10-alpha.3",
|
||||
"@budibase/bbui": "1.1.32-alpha.1",
|
||||
"@budibase/client": "1.1.32-alpha.1",
|
||||
"@budibase/frontend-core": "1.1.32-alpha.1",
|
||||
"@budibase/string-templates": "1.1.32-alpha.1",
|
||||
"@sentry/browser": "5.19.1",
|
||||
"@spectrum-css/page": "^3.0.1",
|
||||
"@spectrum-css/vars": "^3.0.1",
|
||||
|
|
|
@ -52,8 +52,8 @@ export default class IntercomClient {
|
|||
* @param {Object} user - user to identify
|
||||
* @returns Intercom global object
|
||||
*/
|
||||
show(user = {}) {
|
||||
if (!this.initialised || !user?.admin) return
|
||||
show(user = {}, enabled) {
|
||||
if (!this.initialised || !enabled) return
|
||||
|
||||
return window.Intercom("boot", {
|
||||
app_id: this.token,
|
||||
|
|
|
@ -2,7 +2,6 @@ import { getFrontendStore } from "./store/frontend"
|
|||
import { getAutomationStore } from "./store/automation"
|
||||
import { getThemeStore } from "./store/theme"
|
||||
import { derived } from "svelte/store"
|
||||
import { LAYOUT_NAMES } from "../constants"
|
||||
import { findComponent, findComponentPath } from "./componentUtils"
|
||||
import { RoleUtils } from "@budibase/frontend-core"
|
||||
|
||||
|
@ -28,6 +27,10 @@ export const selectedComponent = derived(
|
|||
}
|
||||
)
|
||||
|
||||
// For legacy compatibility only, but with the new design UI this is just
|
||||
// the selected screen
|
||||
export const currentAsset = selectedScreen
|
||||
|
||||
export const sortedScreens = derived(store, $store => {
|
||||
return $store.screens.slice().sort((a, b) => {
|
||||
// Sort by role first
|
||||
|
@ -66,12 +69,3 @@ export const selectedComponentPath = derived(
|
|||
).map(component => component._id)
|
||||
}
|
||||
)
|
||||
|
||||
export const mainLayout = derived(store, $store => {
|
||||
return $store.layouts?.find(
|
||||
layout => layout._id === LAYOUT_NAMES.MASTER.PRIVATE
|
||||
)
|
||||
})
|
||||
|
||||
// For compatibility
|
||||
export const currentAsset = selectedScreen
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { get, writable } from "svelte/store"
|
||||
import { cloneDeep } from "lodash/fp"
|
||||
import { currentAsset, mainLayout, selectedComponent } from "builderStore"
|
||||
import { selectedScreen, selectedComponent } from "builderStore"
|
||||
import {
|
||||
datasources,
|
||||
integrations,
|
||||
|
@ -11,7 +11,6 @@ import {
|
|||
import { API } from "api"
|
||||
import analytics, { Events } from "analytics"
|
||||
import {
|
||||
findComponentType,
|
||||
findComponentParent,
|
||||
findClosestMatchingComponent,
|
||||
findAllMatchingComponents,
|
||||
|
@ -21,6 +20,7 @@ import {
|
|||
} from "../componentUtils"
|
||||
import { Helpers } from "@budibase/bbui"
|
||||
import { DefaultAppTheme, LAYOUT_NAMES } from "../../constants"
|
||||
import { Utils } from "@budibase/frontend-core"
|
||||
|
||||
const INITIAL_FRONTEND_STATE = {
|
||||
apps: [],
|
||||
|
@ -61,6 +61,26 @@ const INITIAL_FRONTEND_STATE = {
|
|||
export const getFrontendStore = () => {
|
||||
const store = writable({ ...INITIAL_FRONTEND_STATE })
|
||||
|
||||
// This is a fake implementation of a "patch" API endpoint to try and prevent
|
||||
// 409s. All screen doc mutations (aside from creation) use this function,
|
||||
// which queues up invocations sequentially and ensures pending mutations are
|
||||
// always applied to the most up-to-date doc revision.
|
||||
// This is slightly better than just a traditional "patch" endpoint and this
|
||||
// supports deeply mutating the current doc rather than just appending data.
|
||||
const sequentialScreenPatch = Utils.sequential(async (patchFn, screenId) => {
|
||||
const state = get(store)
|
||||
const screen = state.screens.find(screen => screen._id === screenId)
|
||||
if (!screen) {
|
||||
return
|
||||
}
|
||||
let clone = cloneDeep(screen)
|
||||
const result = patchFn(clone)
|
||||
if (result === false) {
|
||||
return
|
||||
}
|
||||
return await store.actions.screens.save(clone)
|
||||
})
|
||||
|
||||
store.actions = {
|
||||
reset: () => {
|
||||
store.set({ ...INITIAL_FRONTEND_STATE })
|
||||
|
@ -137,12 +157,12 @@ export const getFrontendStore = () => {
|
|||
theme: {
|
||||
save: async theme => {
|
||||
const appId = get(store).appId
|
||||
await API.saveAppMetadata({
|
||||
const app = await API.saveAppMetadata({
|
||||
appId,
|
||||
metadata: { theme },
|
||||
})
|
||||
store.update(state => {
|
||||
state.theme = theme
|
||||
state.theme = app.theme
|
||||
return state
|
||||
})
|
||||
},
|
||||
|
@ -150,12 +170,12 @@ export const getFrontendStore = () => {
|
|||
customTheme: {
|
||||
save: async customTheme => {
|
||||
const appId = get(store).appId
|
||||
await API.saveAppMetadata({
|
||||
const app = await API.saveAppMetadata({
|
||||
appId,
|
||||
metadata: { customTheme },
|
||||
})
|
||||
store.update(state => {
|
||||
state.customTheme = customTheme
|
||||
state.customTheme = app.customTheme
|
||||
return state
|
||||
})
|
||||
},
|
||||
|
@ -163,33 +183,35 @@ export const getFrontendStore = () => {
|
|||
navigation: {
|
||||
save: async navigation => {
|
||||
const appId = get(store).appId
|
||||
await API.saveAppMetadata({
|
||||
const app = await API.saveAppMetadata({
|
||||
appId,
|
||||
metadata: { navigation },
|
||||
})
|
||||
store.update(state => {
|
||||
state.navigation = navigation
|
||||
return state
|
||||
})
|
||||
},
|
||||
},
|
||||
routing: {
|
||||
fetch: async () => {
|
||||
const response = await API.fetchAppRoutes()
|
||||
store.update(state => {
|
||||
state.routes = response.routes
|
||||
state.navigation = app.navigation
|
||||
return state
|
||||
})
|
||||
},
|
||||
},
|
||||
screens: {
|
||||
select: screenId => {
|
||||
store.update(state => {
|
||||
let screens = state.screens
|
||||
let screen =
|
||||
screens.find(screen => screen._id === screenId) || screens[0]
|
||||
if (!screen) return state
|
||||
// Check this screen exists
|
||||
const state = get(store)
|
||||
const screen = state.screens.find(screen => screen._id === screenId)
|
||||
if (!screen) {
|
||||
return
|
||||
}
|
||||
|
||||
// Check screen isn't already selected
|
||||
if (
|
||||
state.selectedScreenId === screen._id &&
|
||||
state.selectedComponentId === screen.props?._id
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
// Select new screen
|
||||
store.update(state => {
|
||||
state.selectedScreenId = screen._id
|
||||
state.selectedComponentId = screen.props?._id
|
||||
return state
|
||||
|
@ -198,25 +220,40 @@ export const getFrontendStore = () => {
|
|||
save: async screen => {
|
||||
const creatingNewScreen = screen._id === undefined
|
||||
const savedScreen = await API.saveScreen(screen)
|
||||
const routesResponse = await API.fetchAppRoutes()
|
||||
store.update(state => {
|
||||
// Update screen object
|
||||
const idx = state.screens.findIndex(x => x._id === savedScreen._id)
|
||||
if (idx !== -1) {
|
||||
state.screens.splice(idx, 1, savedScreen)
|
||||
} else {
|
||||
state.screens.push(savedScreen)
|
||||
}
|
||||
|
||||
// Select the new screen if creating a new one
|
||||
if (creatingNewScreen) {
|
||||
state.selectedScreenId = savedScreen._id
|
||||
state.selectedComponentId = savedScreen.props._id
|
||||
}
|
||||
|
||||
// Update routes
|
||||
state.routes = routesResponse.routes
|
||||
|
||||
return state
|
||||
})
|
||||
|
||||
// Refresh routes
|
||||
await store.actions.routing.fetch()
|
||||
|
||||
// Select the new screen if creating a new one
|
||||
if (creatingNewScreen) {
|
||||
store.actions.screens.select(savedScreen._id)
|
||||
}
|
||||
return savedScreen
|
||||
},
|
||||
patch: async (patchFn, screenId) => {
|
||||
// Default to the currently selected screen
|
||||
if (!screenId) {
|
||||
const state = get(store)
|
||||
screenId = state.selectedScreenId
|
||||
}
|
||||
if (!screenId || !patchFn) {
|
||||
return
|
||||
}
|
||||
return await sequentialScreenPatch(patchFn, screenId)
|
||||
},
|
||||
delete: async screens => {
|
||||
const screensToDelete = Array.isArray(screens) ? screens : [screens]
|
||||
|
||||
|
@ -238,60 +275,78 @@ export const getFrontendStore = () => {
|
|||
promises.push(store.actions.links.delete(deleteUrls))
|
||||
await Promise.all(promises)
|
||||
const deletedIds = screensToDelete.map(screen => screen._id)
|
||||
const routesResponse = await API.fetchAppRoutes()
|
||||
store.update(state => {
|
||||
// Remove deleted screens from state
|
||||
state.screens = state.screens.filter(screen => {
|
||||
return !deletedIds.includes(screen._id)
|
||||
})
|
||||
|
||||
// Deselect the current screen if it was deleted
|
||||
if (deletedIds.includes(state.selectedScreenId)) {
|
||||
state.selectedScreenId = null
|
||||
state.selectedComponentId = null
|
||||
}
|
||||
|
||||
// Update routing
|
||||
state.routes = routesResponse.routes
|
||||
|
||||
return state
|
||||
})
|
||||
|
||||
// Refresh routes
|
||||
await store.actions.routing.fetch()
|
||||
},
|
||||
updateHomeScreen: async (screen, makeHomeScreen = true) => {
|
||||
let promises = []
|
||||
|
||||
// Find any existing home screen for this role so we can remove it,
|
||||
// if we are setting this to be the new home screen
|
||||
if (makeHomeScreen) {
|
||||
const roleId = screen.routing.roleId
|
||||
let existingHomeScreen = get(store).screens.find(s => {
|
||||
return (
|
||||
s.routing.roleId === roleId &&
|
||||
s.routing.homeScreen &&
|
||||
s._id !== screen._id
|
||||
)
|
||||
})
|
||||
if (existingHomeScreen) {
|
||||
existingHomeScreen.routing.homeScreen = false
|
||||
promises.push(store.actions.screens.save(existingHomeScreen))
|
||||
}
|
||||
updateSetting: async (screen, name, value) => {
|
||||
if (!screen || !name) {
|
||||
return
|
||||
}
|
||||
|
||||
// Update the passed in screen
|
||||
screen.routing.homeScreen = makeHomeScreen
|
||||
promises.push(store.actions.screens.save(screen))
|
||||
return await Promise.all(promises)
|
||||
// Apply setting update
|
||||
const patch = screen => {
|
||||
if (!screen) {
|
||||
return false
|
||||
}
|
||||
// Skip update if the value is the same
|
||||
if (Helpers.deepGet(screen, name) === value) {
|
||||
return false
|
||||
}
|
||||
Helpers.deepSet(screen, name, value)
|
||||
}
|
||||
await store.actions.screens.patch(patch, screen._id)
|
||||
|
||||
// Ensure we don't have more than one home screen for this new role.
|
||||
// This could happen after updating multiple different settings.
|
||||
const state = get(store)
|
||||
const updatedScreen = state.screens.find(s => s._id === screen._id)
|
||||
if (!updatedScreen) {
|
||||
return
|
||||
}
|
||||
const otherHomeScreens = state.screens.filter(s => {
|
||||
return (
|
||||
s.routing.roleId === updatedScreen.routing.roleId &&
|
||||
s.routing.homeScreen &&
|
||||
s._id !== screen._id
|
||||
)
|
||||
})
|
||||
if (otherHomeScreens.length) {
|
||||
const patch = screen => {
|
||||
screen.routing.homeScreen = false
|
||||
}
|
||||
for (let otherHomeScreen of otherHomeScreens) {
|
||||
await store.actions.screens.patch(patch, otherHomeScreen._id)
|
||||
}
|
||||
}
|
||||
},
|
||||
removeCustomLayout: async screen => {
|
||||
// Pull relevant settings from old layout, if required
|
||||
const layout = get(store).layouts.find(x => x._id === screen.layoutId)
|
||||
screen.layoutId = null
|
||||
screen.showNavigation = layout?.props.navigation !== "None"
|
||||
screen.width = layout?.props.width || "Large"
|
||||
await store.actions.screens.save(screen)
|
||||
const patch = screen => {
|
||||
screen.layoutId = null
|
||||
screen.showNavigation = layout?.props.navigation !== "None"
|
||||
screen.width = layout?.props.width || "Large"
|
||||
}
|
||||
await store.actions.screens.patch(patch, screen._id)
|
||||
},
|
||||
},
|
||||
preview: {
|
||||
saveSelected: async () => {
|
||||
const selectedAsset = get(currentAsset)
|
||||
return await store.actions.screens.save(selectedAsset)
|
||||
},
|
||||
setDevice: device => {
|
||||
store.update(state => {
|
||||
state.previewDevice = device
|
||||
|
@ -301,41 +356,28 @@ export const getFrontendStore = () => {
|
|||
},
|
||||
layouts: {
|
||||
select: layoutId => {
|
||||
// Check this layout exists
|
||||
const state = get(store)
|
||||
const layout = state.layouts.find(layout => layout._id === layoutId)
|
||||
if (!layout) {
|
||||
return
|
||||
}
|
||||
|
||||
// Check layout isn't already selected
|
||||
if (
|
||||
state.selectedLayoutId === layout._id &&
|
||||
state.selectedComponentId === layout.props?._id
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
// Select new layout
|
||||
store.update(state => {
|
||||
const layout =
|
||||
store.actions.layouts.find(layoutId) || get(store).layouts[0]
|
||||
if (!layout) return
|
||||
state.selectedLayoutId = layout._id
|
||||
state.selectedComponentId = layout.props?._id
|
||||
return state
|
||||
})
|
||||
},
|
||||
save: async layout => {
|
||||
const creatingNewLayout = layout._id === undefined
|
||||
const savedLayout = await API.saveLayout(layout)
|
||||
store.update(state => {
|
||||
const idx = state.layouts.findIndex(x => x._id === savedLayout._id)
|
||||
if (idx !== -1) {
|
||||
state.layouts.splice(idx, 1, savedLayout)
|
||||
} else {
|
||||
state.layouts.push(savedLayout)
|
||||
}
|
||||
return state
|
||||
})
|
||||
|
||||
// Select layout if creating a new one
|
||||
if (creatingNewLayout) {
|
||||
store.actions.layouts.select(savedLayout._id)
|
||||
}
|
||||
return savedLayout
|
||||
},
|
||||
find: layoutId => {
|
||||
if (!layoutId) {
|
||||
return get(mainLayout)
|
||||
}
|
||||
const storeContents = get(store)
|
||||
return storeContents.layouts.find(layout => layout._id === layoutId)
|
||||
},
|
||||
delete: async layout => {
|
||||
if (!layout?._id) {
|
||||
return
|
||||
|
@ -345,10 +387,6 @@ export const getFrontendStore = () => {
|
|||
layoutRev: layout._rev,
|
||||
})
|
||||
store.update(state => {
|
||||
// Select main layout if we deleted the selected layout
|
||||
if (layout._id === state.selectedLayoutId) {
|
||||
state.selectedLayoutId = get(mainLayout)._id
|
||||
}
|
||||
state.layouts = state.layouts.filter(x => x._id !== layout._id)
|
||||
return state
|
||||
})
|
||||
|
@ -386,7 +424,7 @@ export const getFrontendStore = () => {
|
|||
}
|
||||
if (componentName.endsWith("/formstep")) {
|
||||
const parentForm = findClosestMatchingComponent(
|
||||
get(currentAsset).props,
|
||||
get(selectedScreen).props,
|
||||
get(selectedComponent)._id,
|
||||
component => component._component.endsWith("/form")
|
||||
)
|
||||
|
@ -407,48 +445,59 @@ export const getFrontendStore = () => {
|
|||
}
|
||||
},
|
||||
create: async (componentName, presetProps) => {
|
||||
const selected = get(selectedComponent)
|
||||
const asset = get(currentAsset)
|
||||
|
||||
// Create new component
|
||||
const state = get(store)
|
||||
const componentInstance = store.actions.components.createInstance(
|
||||
componentName,
|
||||
presetProps
|
||||
)
|
||||
if (!componentInstance || !asset) {
|
||||
if (!componentInstance) {
|
||||
return
|
||||
}
|
||||
|
||||
// Find parent node to attach this component to
|
||||
let parentComponent
|
||||
if (selected) {
|
||||
// Use current screen or layout as parent if no component is selected
|
||||
const definition = store.actions.components.getDefinition(
|
||||
selected._component
|
||||
// Patch selected screen
|
||||
await store.actions.screens.patch(screen => {
|
||||
// Find the selected component
|
||||
const currentComponent = findComponent(
|
||||
screen.props,
|
||||
state.selectedComponentId
|
||||
)
|
||||
if (definition?.hasChildren) {
|
||||
// Use selected component if it allows children
|
||||
parentComponent = selected
|
||||
} else {
|
||||
// Otherwise we need to use the parent of this component
|
||||
parentComponent = findComponentParent(asset?.props, selected._id)
|
||||
if (!currentComponent) {
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
// Use screen or layout if no component is selected
|
||||
parentComponent = asset?.props
|
||||
}
|
||||
|
||||
// Attach component
|
||||
if (!parentComponent) {
|
||||
return
|
||||
}
|
||||
if (!parentComponent._children) {
|
||||
parentComponent._children = []
|
||||
}
|
||||
parentComponent._children.push(componentInstance)
|
||||
// Find parent node to attach this component to
|
||||
let parentComponent
|
||||
if (currentComponent) {
|
||||
// Use selected component as parent if one is selected
|
||||
const definition = store.actions.components.getDefinition(
|
||||
currentComponent._component
|
||||
)
|
||||
if (definition?.hasChildren) {
|
||||
// Use selected component if it allows children
|
||||
parentComponent = currentComponent
|
||||
} else {
|
||||
// Otherwise we need to use the parent of this component
|
||||
parentComponent = findComponentParent(
|
||||
screen.props,
|
||||
currentComponent._id
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// Use screen or layout if no component is selected
|
||||
parentComponent = screen.props
|
||||
}
|
||||
|
||||
// Save components and update UI
|
||||
await store.actions.preview.saveSelected()
|
||||
// Attach new component
|
||||
if (!parentComponent) {
|
||||
return false
|
||||
}
|
||||
if (!parentComponent._children) {
|
||||
parentComponent._children = []
|
||||
}
|
||||
parentComponent._children.push(componentInstance)
|
||||
})
|
||||
|
||||
// Select new component
|
||||
store.update(state => {
|
||||
state.selectedComponentId = componentInstance._id
|
||||
return state
|
||||
|
@ -461,50 +510,58 @@ export const getFrontendStore = () => {
|
|||
|
||||
return componentInstance
|
||||
},
|
||||
patch: async (patchFn, componentId, screenId) => {
|
||||
// Use selected component by default
|
||||
if (!componentId && !screenId) {
|
||||
const state = get(store)
|
||||
componentId = state.selectedComponentId
|
||||
screenId = state.selectedScreenId
|
||||
}
|
||||
// Invalid if only a screen or component ID provided
|
||||
if (!componentId || !screenId || !patchFn) {
|
||||
return
|
||||
}
|
||||
const patchScreen = screen => {
|
||||
let component = findComponent(screen.props, componentId)
|
||||
if (!component) {
|
||||
return false
|
||||
}
|
||||
return patchFn(component, screen)
|
||||
}
|
||||
await store.actions.screens.patch(patchScreen, screenId)
|
||||
},
|
||||
delete: async component => {
|
||||
if (!component) {
|
||||
return
|
||||
}
|
||||
const asset = get(currentAsset)
|
||||
if (!asset) {
|
||||
return
|
||||
}
|
||||
let parentId
|
||||
|
||||
// Fetch full definition
|
||||
component = findComponent(asset.props, component._id)
|
||||
// Patch screen
|
||||
await store.actions.screens.patch(screen => {
|
||||
// Check component exists
|
||||
component = findComponent(screen.props, component._id)
|
||||
if (!component) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Ensure we aren't deleting the screen slot
|
||||
if (component._component?.endsWith("/screenslot")) {
|
||||
throw "You can't delete the screen slot"
|
||||
}
|
||||
|
||||
// Ensure we aren't deleting something that contains the screen slot
|
||||
const screenslot = findComponentType(
|
||||
component,
|
||||
"@budibase/standard-components/screenslot"
|
||||
)
|
||||
if (screenslot != null) {
|
||||
throw "You can't delete a component that contains the screen slot"
|
||||
}
|
||||
|
||||
const parent = findComponentParent(asset.props, component._id)
|
||||
if (parent) {
|
||||
// Check component has a valid parent
|
||||
const parent = findComponentParent(screen.props, component._id)
|
||||
if (!parent) {
|
||||
return false
|
||||
}
|
||||
parentId = parent._id
|
||||
parent._children = parent._children.filter(
|
||||
child => child._id !== component._id
|
||||
)
|
||||
store.update(state => {
|
||||
state.selectedComponentId = parent._id
|
||||
return state
|
||||
})
|
||||
}
|
||||
await store.actions.preview.saveSelected()
|
||||
})
|
||||
|
||||
// Select the deleted component's parent
|
||||
store.update(state => {
|
||||
state.selectedComponentId = parentId
|
||||
return state
|
||||
})
|
||||
},
|
||||
copy: (component, cut = false, selectParent = true) => {
|
||||
const selectedAsset = get(currentAsset)
|
||||
if (!selectedAsset) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Update store with copied component
|
||||
store.update(state => {
|
||||
state.componentToPaste = cloneDeep(component)
|
||||
|
@ -512,13 +569,11 @@ export const getFrontendStore = () => {
|
|||
return state
|
||||
})
|
||||
|
||||
// Remove the component from its parent if we're cutting
|
||||
// Select the parent if cutting
|
||||
if (cut) {
|
||||
const parent = findComponentParent(selectedAsset.props, component._id)
|
||||
const screen = get(selectedScreen)
|
||||
const parent = findComponentParent(screen?.props, component._id)
|
||||
if (parent) {
|
||||
parent._children = parent._children.filter(
|
||||
child => child._id !== component._id
|
||||
)
|
||||
if (selectParent) {
|
||||
store.update(state => {
|
||||
state.selectedComponentId = parent._id
|
||||
|
@ -528,24 +583,42 @@ export const getFrontendStore = () => {
|
|||
}
|
||||
}
|
||||
},
|
||||
paste: async (targetComponent, mode) => {
|
||||
let promises = []
|
||||
store.update(state => {
|
||||
// Stop if we have nothing to paste
|
||||
if (!state.componentToPaste) {
|
||||
return state
|
||||
paste: async (targetComponent, mode, targetScreen) => {
|
||||
const state = get(store)
|
||||
if (!state.componentToPaste) {
|
||||
return
|
||||
}
|
||||
let newComponentId
|
||||
|
||||
// Patch screen
|
||||
const patch = screen => {
|
||||
// Get up to date ref to target
|
||||
targetComponent = findComponent(screen.props, targetComponent._id)
|
||||
if (!targetComponent) {
|
||||
return
|
||||
}
|
||||
const cut = state.componentToPaste.isCut
|
||||
|
||||
// Clone the component to paste and make unique if copying
|
||||
delete state.componentToPaste.isCut
|
||||
const originalId = state.componentToPaste._id
|
||||
let componentToPaste = cloneDeep(state.componentToPaste)
|
||||
if (cut) {
|
||||
state.componentToPaste = null
|
||||
} else {
|
||||
delete componentToPaste.isCut
|
||||
|
||||
// Make new component unique if copying
|
||||
if (!cut) {
|
||||
makeComponentUnique(componentToPaste)
|
||||
}
|
||||
newComponentId = componentToPaste._id
|
||||
|
||||
// Delete old component if cutting
|
||||
if (cut) {
|
||||
const parent = findComponentParent(screen.props, originalId)
|
||||
if (parent?._children) {
|
||||
parent._children = parent._children.filter(
|
||||
component => component._id !== originalId
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Paste new component
|
||||
if (mode === "inside") {
|
||||
// Paste inside target component if chosen
|
||||
if (!targetComponent._children) {
|
||||
|
@ -553,66 +626,106 @@ export const getFrontendStore = () => {
|
|||
}
|
||||
targetComponent._children.push(componentToPaste)
|
||||
} else {
|
||||
// Otherwise find the parent so we can paste in the correct order
|
||||
// in the parents child components
|
||||
const selectedAsset = get(currentAsset)
|
||||
if (!selectedAsset) {
|
||||
return state
|
||||
}
|
||||
// Otherwise paste in the correct order in the parent's children
|
||||
const parent = findComponentParent(
|
||||
selectedAsset.props,
|
||||
screen.props,
|
||||
targetComponent._id
|
||||
)
|
||||
if (!parent) {
|
||||
return state
|
||||
if (!parent?._children) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Insert the component in the correct position
|
||||
const targetIndex = parent._children.indexOf(targetComponent)
|
||||
const targetIndex = parent._children.findIndex(component => {
|
||||
return component._id === targetComponent._id
|
||||
})
|
||||
const index = mode === "above" ? targetIndex : targetIndex + 1
|
||||
parent._children.splice(index, 0, cloneDeep(componentToPaste))
|
||||
parent._children.splice(index, 0, componentToPaste)
|
||||
}
|
||||
}
|
||||
const targetScreenId = targetScreen?._id || state.selectedScreenId
|
||||
await store.actions.screens.patch(patch, targetScreenId)
|
||||
|
||||
// Save and select the new component
|
||||
promises.push(store.actions.preview.saveSelected())
|
||||
state.selectedComponentId = componentToPaste._id
|
||||
store.update(state => {
|
||||
// Remove copied component if cutting
|
||||
if (state.componentToPaste.isCut) {
|
||||
delete state.componentToPaste
|
||||
}
|
||||
state.selectedScreenId = targetScreenId
|
||||
state.selectedComponentId = newComponentId
|
||||
return state
|
||||
})
|
||||
await Promise.all(promises)
|
||||
},
|
||||
moveUp: async component => {
|
||||
await store.actions.screens.patch(screen => {
|
||||
const componentId = component?._id
|
||||
const parent = findComponentParent(screen.props, componentId)
|
||||
if (!parent?._children?.length) {
|
||||
return false
|
||||
}
|
||||
const currentIndex = parent._children.findIndex(
|
||||
child => child._id === componentId
|
||||
)
|
||||
if (currentIndex === 0) {
|
||||
return false
|
||||
}
|
||||
const originalComponent = cloneDeep(parent._children[currentIndex])
|
||||
const newChildren = parent._children.filter(
|
||||
component => component._id !== componentId
|
||||
)
|
||||
newChildren.splice(currentIndex - 1, 0, originalComponent)
|
||||
parent._children = newChildren
|
||||
})
|
||||
},
|
||||
moveDown: async component => {
|
||||
await store.actions.screens.patch(screen => {
|
||||
const componentId = component?._id
|
||||
const parent = findComponentParent(screen.props, componentId)
|
||||
if (!parent?._children?.length) {
|
||||
return false
|
||||
}
|
||||
const currentIndex = parent._children.findIndex(
|
||||
child => child._id === componentId
|
||||
)
|
||||
if (currentIndex === parent._children.length - 1) {
|
||||
return false
|
||||
}
|
||||
const originalComponent = cloneDeep(parent._children[currentIndex])
|
||||
const newChildren = parent._children.filter(
|
||||
component => component._id !== componentId
|
||||
)
|
||||
newChildren.splice(currentIndex + 1, 0, originalComponent)
|
||||
parent._children = newChildren
|
||||
})
|
||||
},
|
||||
updateStyle: async (name, value) => {
|
||||
const selected = get(selectedComponent)
|
||||
if (value == null || value === "") {
|
||||
delete selected._styles.normal[name]
|
||||
} else {
|
||||
selected._styles.normal[name] = value
|
||||
}
|
||||
await store.actions.preview.saveSelected()
|
||||
await store.actions.components.patch(component => {
|
||||
if (value == null || value === "") {
|
||||
delete component._styles.normal[name]
|
||||
} else {
|
||||
component._styles.normal[name] = value
|
||||
}
|
||||
})
|
||||
},
|
||||
updateCustomStyle: async style => {
|
||||
const selected = get(selectedComponent)
|
||||
selected._styles.custom = style
|
||||
await store.actions.preview.saveSelected()
|
||||
await store.actions.components.patch(component => {
|
||||
component._styles.custom = style
|
||||
})
|
||||
},
|
||||
updateConditions: async conditions => {
|
||||
const selected = get(selectedComponent)
|
||||
selected._conditions = conditions
|
||||
await store.actions.preview.saveSelected()
|
||||
},
|
||||
updateProp: async (name, value) => {
|
||||
let component = get(selectedComponent)
|
||||
if (!name || !component) {
|
||||
return
|
||||
}
|
||||
if (component[name] === value) {
|
||||
return
|
||||
}
|
||||
component[name] = value
|
||||
store.update(state => {
|
||||
state.selectedComponentId = component._id
|
||||
return state
|
||||
await store.actions.components.patch(component => {
|
||||
component._conditions = conditions
|
||||
})
|
||||
},
|
||||
updateSetting: async (name, value) => {
|
||||
await store.actions.components.patch(component => {
|
||||
if (!name || !component) {
|
||||
return false
|
||||
}
|
||||
// Skip update if the value is the same
|
||||
if (component[name] === value) {
|
||||
return false
|
||||
}
|
||||
component[name] = value
|
||||
})
|
||||
await store.actions.preview.saveSelected()
|
||||
},
|
||||
},
|
||||
links: {
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
import { createLocalStorageStore } from "@budibase/frontend-core"
|
||||
import { Constants, createLocalStorageStore } from "@budibase/frontend-core"
|
||||
|
||||
export const getThemeStore = () => {
|
||||
const themeElement = document.documentElement
|
||||
|
||||
const initialValue = {
|
||||
theme: "darkest",
|
||||
options: ["lightest", "light", "dark", "darkest", "nord"],
|
||||
}
|
||||
const store = createLocalStorageStore("bb-theme", initialValue)
|
||||
|
||||
|
@ -17,13 +16,19 @@ export const getThemeStore = () => {
|
|||
return
|
||||
}
|
||||
|
||||
state.options.forEach(option => {
|
||||
// Update global class names to use the new theme and remove others
|
||||
Constants.Themes.forEach(option => {
|
||||
themeElement.classList.toggle(
|
||||
`spectrum--${option}`,
|
||||
option === state.theme
|
||||
`spectrum--${option.class}`,
|
||||
option.class === state.theme
|
||||
)
|
||||
themeElement.classList.add("spectrum--darkest")
|
||||
})
|
||||
|
||||
// Add base theme if required
|
||||
const selectedTheme = Constants.Themes.find(x => x.class === state.theme)
|
||||
if (selectedTheme?.base) {
|
||||
themeElement.classList.add(`spectrum--${selectedTheme.base}`)
|
||||
}
|
||||
})
|
||||
|
||||
return store
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
notifications,
|
||||
Modal,
|
||||
} from "@budibase/bbui"
|
||||
import { ActionStepID } from "constants/backend/automations"
|
||||
|
||||
export let automation
|
||||
let testDataModal
|
||||
|
@ -82,7 +83,7 @@
|
|||
in:fly|local={{ x: 500, duration: 500 }}
|
||||
out:fly|local={{ x: 500, duration: 500 }}
|
||||
>
|
||||
{#if block.stepId !== "LOOP"}
|
||||
{#if block.stepId !== ActionStepID.LOOP}
|
||||
<FlowItem {testDataModal} {block} />
|
||||
{/if}
|
||||
</div>
|
||||
|
|
|
@ -10,11 +10,15 @@
|
|||
Select,
|
||||
ActionButton,
|
||||
notifications,
|
||||
Label,
|
||||
} from "@budibase/bbui"
|
||||
import AutomationBlockSetup from "../../SetupPanel/AutomationBlockSetup.svelte"
|
||||
import CreateWebhookModal from "components/automation/Shared/CreateWebhookModal.svelte"
|
||||
import ActionModal from "./ActionModal.svelte"
|
||||
import FlowItemHeader from "./FlowItemHeader.svelte"
|
||||
import RoleSelect from "components/design/settings/controls/RoleSelect.svelte"
|
||||
import { ActionStepID, TriggerStepID } from "constants/backend/automations"
|
||||
import { permissions } from "stores/backend"
|
||||
|
||||
export let block
|
||||
export let testDataModal
|
||||
|
@ -23,9 +27,12 @@
|
|||
let actionModal
|
||||
let blockComplete
|
||||
let showLooping = false
|
||||
let role
|
||||
|
||||
$: automationId = $automationStore.selectedAutomation?.automation._id
|
||||
$: showBindingPicker =
|
||||
block.stepId === "CREATE_ROW" || block.stepId === "UPDATE_ROW"
|
||||
block.stepId === ActionStepID.CREATE_ROW ||
|
||||
block.stepId === ActionStepID.UPDATE_ROW
|
||||
|
||||
$: isTrigger = block.type === "TRIGGER"
|
||||
|
||||
|
@ -45,6 +52,33 @@
|
|||
x => x.blockToLoop === block.id
|
||||
)
|
||||
|
||||
$: isAppAction = block?.stepId === TriggerStepID.APP
|
||||
$: isAppAction && setPermissions(role)
|
||||
$: isAppAction && getPermissions(automationId)
|
||||
|
||||
async function setPermissions(role) {
|
||||
if (!role || !automationId) {
|
||||
return
|
||||
}
|
||||
await permissions.save({
|
||||
level: "execute",
|
||||
role,
|
||||
resource: automationId,
|
||||
})
|
||||
}
|
||||
|
||||
async function getPermissions(automationId) {
|
||||
if (!automationId) {
|
||||
return
|
||||
}
|
||||
const perms = await permissions.forResource(automationId)
|
||||
if (!perms["execute"]) {
|
||||
role = "BASIC"
|
||||
} else {
|
||||
role = perms["execute"]
|
||||
}
|
||||
}
|
||||
|
||||
async function removeLooping() {
|
||||
loopingSelected = false
|
||||
let loopBlock =
|
||||
|
@ -205,6 +239,10 @@
|
|||
</div>
|
||||
{/if}
|
||||
|
||||
{#if isAppAction}
|
||||
<Label>Role</Label>
|
||||
<RoleSelect bind:value={role} />
|
||||
{/if}
|
||||
<AutomationBlockSetup
|
||||
schemaProperties={Object.entries(block.schema.inputs.properties)}
|
||||
{block}
|
||||
|
|
|
@ -32,7 +32,8 @@
|
|||
if (!results) {
|
||||
return {}
|
||||
}
|
||||
if (results.outputs?.status?.toLowerCase() === "stopped") {
|
||||
const lcStatus = results.outputs?.status?.toLowerCase()
|
||||
if (lcStatus === "stopped" || lcStatus === "stopped_error") {
|
||||
return { yellow: true, message: "Stopped" }
|
||||
} else if (results.outputs?.success || isTrigger) {
|
||||
return { positive: true, message: "Success" }
|
||||
|
|
|
@ -15,16 +15,20 @@
|
|||
let trigger = {}
|
||||
let schemaProperties = {}
|
||||
|
||||
// clone the trigger so we're not mutating the reference
|
||||
$: trigger = cloneDeep(
|
||||
$automationStore.selectedAutomation.automation.definition.trigger
|
||||
)
|
||||
$: {
|
||||
// clone the trigger so we're not mutating the reference
|
||||
trigger = cloneDeep(
|
||||
$automationStore.selectedAutomation.automation.definition.trigger
|
||||
)
|
||||
|
||||
// get the outputs so we can define the fields
|
||||
$: schemaProperties = Object.entries(trigger?.schema?.outputs?.properties)
|
||||
// get the outputs so we can define the fields
|
||||
let schema = Object.entries(trigger.schema?.outputs?.properties || {})
|
||||
|
||||
if (!$automationStore.selectedAutomation.automation.testData) {
|
||||
$automationStore.selectedAutomation.automation.testData = {}
|
||||
if (trigger?.event === "app:trigger") {
|
||||
schema = [["fields", { customType: "fields" }]]
|
||||
}
|
||||
|
||||
schemaProperties = schema
|
||||
}
|
||||
|
||||
// check to see if there is existing test data in the store
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<script>
|
||||
import { Icon, Divider, Tabs, Tab, TextArea, Label } from "@budibase/bbui"
|
||||
import FlowItemHeader from "./FlowChart/FlowItemHeader.svelte"
|
||||
import { ActionStepID } from "constants/backend/automations"
|
||||
|
||||
export let automation
|
||||
export let testResults
|
||||
|
@ -10,7 +11,7 @@
|
|||
let blocks
|
||||
|
||||
function prepTestResults(results) {
|
||||
return results?.steps.filter(x => x.stepId !== "LOOP" || [])
|
||||
return results?.steps.filter(x => x.stepId !== ActionStepID.LOOP || [])
|
||||
}
|
||||
|
||||
function textArea(results, message) {
|
||||
|
@ -30,7 +31,7 @@
|
|||
}
|
||||
blocks = blocks
|
||||
.concat(automation.definition.steps || [])
|
||||
.filter(x => x.stepId !== "LOOP")
|
||||
.filter(x => x.stepId !== ActionStepID.LOOP)
|
||||
} else if (filteredResults) {
|
||||
blocks = filteredResults || []
|
||||
// make sure there is an ID for each block being displayed
|
||||
|
@ -45,7 +46,7 @@
|
|||
<div class="container">
|
||||
{#each blocks as block, idx}
|
||||
<div class="block" style={width ? `width: ${width}` : ""}>
|
||||
{#if block.stepId !== "LOOP"}
|
||||
{#if block.stepId !== ActionStepID.LOOP}
|
||||
<FlowItemHeader
|
||||
showTestStatus={true}
|
||||
bind:showParameters
|
||||
|
|
|
@ -2,11 +2,11 @@
|
|||
import { Icon, Divider } from "@budibase/bbui"
|
||||
import TestDisplay from "./TestDisplay.svelte"
|
||||
import { automationStore } from "builderStore"
|
||||
import { ActionStepID } from "constants/backend/automations"
|
||||
|
||||
export let automation
|
||||
export let testResults
|
||||
|
||||
let blocks
|
||||
let blocks, testResults
|
||||
|
||||
$: {
|
||||
blocks = []
|
||||
|
@ -16,16 +16,12 @@
|
|||
}
|
||||
blocks = blocks
|
||||
.concat(automation.definition.steps || [])
|
||||
.filter(x => x.stepId !== "LOOP")
|
||||
} else if (testResults) {
|
||||
blocks = testResults.steps || []
|
||||
}
|
||||
}
|
||||
$: {
|
||||
if (!testResults) {
|
||||
testResults = $automationStore.selectedAutomation?.testResults
|
||||
.filter(x => x.stepId !== ActionStepID.LOOP)
|
||||
} else if ($automationStore.selectedAutomation) {
|
||||
automation = $automationStore.selectedAutomation
|
||||
}
|
||||
}
|
||||
$: testResults = $automationStore.selectedAutomation?.testResults
|
||||
</script>
|
||||
|
||||
<div class="title">
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
Body,
|
||||
Icon,
|
||||
} from "@budibase/bbui"
|
||||
import { TriggerStepID } from "constants/backend/automations"
|
||||
|
||||
let name
|
||||
let selectedTrigger
|
||||
|
@ -35,7 +36,7 @@
|
|||
)
|
||||
|
||||
automationStore.actions.addBlockToAutomation(newBlock)
|
||||
if (triggerVal.stepId === "WEBHOOK") {
|
||||
if (triggerVal.stepId === TriggerStepID.WEBHOOK) {
|
||||
webhookModal.show
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<script>
|
||||
import TableSelector from "./TableSelector.svelte"
|
||||
import RowSelector from "./RowSelector.svelte"
|
||||
import FieldSelector from "./FieldSelector.svelte"
|
||||
import SchemaSetup from "./SchemaSetup.svelte"
|
||||
import {
|
||||
Button,
|
||||
|
@ -30,6 +31,8 @@
|
|||
import { LuceneUtils } from "@budibase/frontend-core"
|
||||
import { getSchemaForTable } from "builderStore/dataBinding"
|
||||
import { Utils } from "@budibase/frontend-core"
|
||||
import { TriggerStepID, ActionStepID } from "constants/backend/automations"
|
||||
import { cloneDeep } from "lodash/fp"
|
||||
|
||||
export let block
|
||||
export let testData
|
||||
|
@ -40,13 +43,25 @@
|
|||
let tempFilters = lookForFilters(schemaProperties) || []
|
||||
let fillWidth = true
|
||||
let codeBindingOpen = false
|
||||
let inputData
|
||||
|
||||
$: stepId = block.stepId
|
||||
$: bindings = getAvailableBindings(
|
||||
block || $automationStore.selectedBlock,
|
||||
$automationStore.selectedAutomation?.automation?.definition
|
||||
)
|
||||
$: inputData = testData ? testData : block.inputs
|
||||
|
||||
$: getInputData(testData, block.inputs)
|
||||
const getInputData = (testData, blockInputs) => {
|
||||
let newInputData = testData || blockInputs
|
||||
|
||||
if (block.event === "app:trigger" && !newInputData?.fields) {
|
||||
newInputData = cloneDeep(blockInputs)
|
||||
}
|
||||
|
||||
inputData = newInputData
|
||||
}
|
||||
|
||||
$: tableId = inputData ? inputData.tableId : null
|
||||
$: table = tableId
|
||||
? $tables.list.find(table => table._id === inputData.tableId)
|
||||
|
@ -54,12 +69,13 @@
|
|||
$: schema = getSchemaForTable(tableId, { searchableSchema: true }).schema
|
||||
$: schemaFields = Object.values(schema || {})
|
||||
$: queryLimit = tableId?.includes("datasource") ? "∞" : "1000"
|
||||
$: isTrigger = block?.type === "TRIGGER"
|
||||
|
||||
const onChange = Utils.sequential(async (e, key) => {
|
||||
try {
|
||||
if (isTestModal) {
|
||||
// Special case for webhook, as it requires a body, but the schema already brings back the body's contents
|
||||
if (stepId === "WEBHOOK") {
|
||||
if (stepId === TriggerStepID.WEBHOOK) {
|
||||
automationStore.actions.addTestDataToAutomation({
|
||||
body: {
|
||||
[key]: e.detail,
|
||||
|
@ -71,15 +87,13 @@
|
|||
[key]: e.detail,
|
||||
})
|
||||
testData[key] = e.detail
|
||||
await automationStore.actions.save(
|
||||
$automationStore.selectedAutomation?.automation
|
||||
)
|
||||
} else {
|
||||
block.inputs[key] = e.detail
|
||||
await automationStore.actions.save(
|
||||
$automationStore.selectedAutomation?.automation
|
||||
)
|
||||
}
|
||||
|
||||
await automationStore.actions.save(
|
||||
$automationStore.selectedAutomation?.automation
|
||||
)
|
||||
} catch (error) {
|
||||
notifications.error("Error saving automation")
|
||||
}
|
||||
|
@ -100,9 +114,9 @@
|
|||
// Extract all outputs from all previous steps as available bindins
|
||||
let bindings = []
|
||||
for (let idx = 0; idx < blockIdx; idx++) {
|
||||
let wasLoopBlock = allSteps[idx]?.stepId === "LOOP"
|
||||
let wasLoopBlock = allSteps[idx]?.stepId === ActionStepID.LOOP
|
||||
let isLoopBlock =
|
||||
allSteps[idx]?.stepId === "LOOP" &&
|
||||
allSteps[idx]?.stepId === ActionStepID.LOOP &&
|
||||
allSteps.find(x => x.blockToLoop === block.id)
|
||||
|
||||
// If the previous block was a loop block, decerement the index so the following
|
||||
|
@ -183,11 +197,13 @@
|
|||
<div class="fields">
|
||||
{#each schemaProperties as [key, value]}
|
||||
<div class="block-field">
|
||||
<Label
|
||||
tooltip={value.title === "Binding / Value"
|
||||
? "If using the String input type, please use a comma or newline separated string"
|
||||
: null}>{value.title || (key === "row" ? "Table" : key)}</Label
|
||||
>
|
||||
{#if key !== "fields"}
|
||||
<Label
|
||||
tooltip={value.title === "Binding / Value"
|
||||
? "If using the String input type, please use a comma or newline separated string"
|
||||
: null}>{value.title || (key === "row" ? "Table" : key)}</Label
|
||||
>
|
||||
{/if}
|
||||
{#if value.type === "string" && value.enum}
|
||||
<Select
|
||||
on:change={e => onChange(e, key)}
|
||||
|
@ -244,6 +260,7 @@
|
|||
{bindings}
|
||||
allowJS={false}
|
||||
updateOnChange={false}
|
||||
drawerLeft="260px"
|
||||
/>
|
||||
{/if}
|
||||
{:else if value.customType === "query"}
|
||||
|
@ -261,6 +278,7 @@
|
|||
/>
|
||||
{:else if value.customType === "table"}
|
||||
<TableSelector
|
||||
{isTrigger}
|
||||
value={inputData[key]}
|
||||
on:change={e => onChange(e, key)}
|
||||
/>
|
||||
|
@ -277,6 +295,14 @@
|
|||
on:change={e => onChange(e, key)}
|
||||
value={inputData[key]}
|
||||
/>
|
||||
{:else if value.customType === "fields"}
|
||||
<FieldSelector
|
||||
{block}
|
||||
value={inputData[key]}
|
||||
on:change={e => onChange(e, key)}
|
||||
{bindings}
|
||||
{isTestModal}
|
||||
/>
|
||||
{:else if value.customType === "triggerSchema"}
|
||||
<SchemaSetup on:change={e => onChange(e, key)} value={inputData[key]} />
|
||||
{:else if value.customType === "code"}
|
||||
|
@ -332,6 +358,7 @@
|
|||
{bindings}
|
||||
updateOnChange={false}
|
||||
placeholder={value.customType === "queryLimit" ? queryLimit : ""}
|
||||
drawerLeft="260px"
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
@ -343,7 +370,7 @@
|
|||
<CreateWebhookModal />
|
||||
</Modal>
|
||||
|
||||
{#if stepId === "WEBHOOK"}
|
||||
{#if stepId === TriggerStepID.WEBHOOK}
|
||||
<Button secondary on:click={() => webhookModal.show()}>Set Up Webhook</Button>
|
||||
{/if}
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import { Button, Select, Input } from "@budibase/bbui"
|
||||
import { Button, Select, Input, Label } from "@budibase/bbui"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
|
@ -9,6 +9,7 @@
|
|||
dispatch("change", e.detail)
|
||||
}
|
||||
|
||||
let touched = false
|
||||
let presets = false
|
||||
|
||||
const CRON_EXPRESSIONS = [
|
||||
|
@ -36,8 +37,10 @@
|
|||
</script>
|
||||
|
||||
<div class="block-field">
|
||||
<Input on:change={onChange} {value} />
|
||||
|
||||
<Input on:change={onChange} {value} on:blur={() => (touched = true)} />
|
||||
{#if touched && !value}
|
||||
<Label><div class="error">Please specify a CRON expression</div></Label>
|
||||
{/if}
|
||||
<div class="presets">
|
||||
<Button on:click={() => (presets = !presets)}
|
||||
>{presets ? "Hide" : "Show"} Presets</Button
|
||||
|
@ -62,4 +65,8 @@
|
|||
.block-field {
|
||||
padding-top: var(--spacing-s);
|
||||
}
|
||||
.error {
|
||||
padding-top: var(--spacing-xs);
|
||||
color: var(--spectrum-global-color-red-500);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -0,0 +1,114 @@
|
|||
<script>
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import RowSelectorTypes from "./RowSelectorTypes.svelte"
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
export let value
|
||||
export let bindings
|
||||
export let block
|
||||
export let isTestModal
|
||||
|
||||
let schemaFields
|
||||
|
||||
$: {
|
||||
let fields = {}
|
||||
|
||||
for (const [key, type] of Object.entries(block?.inputs?.fields)) {
|
||||
fields = {
|
||||
...fields,
|
||||
[key]: {
|
||||
type: type,
|
||||
name: key,
|
||||
fieldName: key,
|
||||
constraints: { type: type },
|
||||
},
|
||||
}
|
||||
|
||||
if (value[key] === type) {
|
||||
value[key] = INITIAL_VALUES[type.toUpperCase()]
|
||||
}
|
||||
}
|
||||
|
||||
schemaFields = Object.entries(fields)
|
||||
}
|
||||
|
||||
const INITIAL_VALUES = {
|
||||
BOOLEAN: null,
|
||||
NUMBER: null,
|
||||
DATETIME: null,
|
||||
STRING: "",
|
||||
OPTIONS: [],
|
||||
ARRAY: [],
|
||||
}
|
||||
|
||||
const coerce = (value, type) => {
|
||||
const re = new RegExp(/{{([^{].*?)}}/g)
|
||||
if (re.test(value)) {
|
||||
return value
|
||||
}
|
||||
|
||||
if (type === "boolean") {
|
||||
if (typeof value === "boolean") {
|
||||
return value
|
||||
}
|
||||
return value === "true"
|
||||
}
|
||||
if (type === "number") {
|
||||
if (typeof value === "number") {
|
||||
return value
|
||||
}
|
||||
return Number(value)
|
||||
}
|
||||
if (type === "options") {
|
||||
return [value]
|
||||
}
|
||||
if (type === "array") {
|
||||
if (Array.isArray(value)) {
|
||||
return value
|
||||
}
|
||||
return value.split(",").map(x => x.trim())
|
||||
}
|
||||
|
||||
if (type === "link") {
|
||||
if (Array.isArray(value)) {
|
||||
return value
|
||||
}
|
||||
|
||||
return [value]
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
const onChange = (e, field, type) => {
|
||||
value[field] = coerce(e.detail, type)
|
||||
dispatch("change", value)
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if schemaFields.length && isTestModal}
|
||||
<div class="schema-fields">
|
||||
{#each schemaFields as [field, schema]}
|
||||
<RowSelectorTypes
|
||||
{isTestModal}
|
||||
{field}
|
||||
{schema}
|
||||
{bindings}
|
||||
{value}
|
||||
{onChange}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.schema-fields {
|
||||
display: grid;
|
||||
grid-gap: var(--spacing-s);
|
||||
margin-top: var(--spacing-s);
|
||||
}
|
||||
.schema-fields :global(label) {
|
||||
text-transform: capitalize;
|
||||
}
|
||||
</style>
|
|
@ -2,10 +2,16 @@
|
|||
import { tables } from "stores/backend"
|
||||
import { Select } from "@budibase/bbui"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import { TableNames } from "constants"
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
export let value
|
||||
export let isTrigger
|
||||
|
||||
$: filteredTables = $tables.list.filter(table => {
|
||||
return !isTrigger || table._id !== TableNames.USERS
|
||||
})
|
||||
|
||||
const onChange = e => {
|
||||
value = e.detail
|
||||
|
@ -16,7 +22,7 @@
|
|||
<Select
|
||||
on:change={onChange}
|
||||
bind:value
|
||||
options={$tables.list}
|
||||
options={filteredTables}
|
||||
getOptionLabel={table => table.name}
|
||||
getOptionValue={table => table._id}
|
||||
/>
|
||||
|
|
|
@ -211,7 +211,6 @@
|
|||
bindings={getAuthBindings()}
|
||||
on:change={e => {
|
||||
form.bearer.token = e.detail
|
||||
console.log(e.detail)
|
||||
onFieldChange()
|
||||
}}
|
||||
on:blur={() => {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import { Icon, StatusLight } from "@budibase/bbui"
|
||||
import { Icon } from "@budibase/bbui"
|
||||
import { createEventDispatcher, getContext } from "svelte"
|
||||
|
||||
export let icon
|
||||
|
@ -14,8 +14,8 @@
|
|||
export let iconText
|
||||
export let iconColor
|
||||
export let scrollable = false
|
||||
export let color
|
||||
export let highlighted = false
|
||||
export let rightAlignIcon = false
|
||||
|
||||
const scrollApi = getContext("scroll")
|
||||
const dispatch = createEventDispatcher()
|
||||
|
@ -78,7 +78,7 @@
|
|||
{iconText}
|
||||
</div>
|
||||
{:else if icon}
|
||||
<div class="icon">
|
||||
<div class="icon" class:right={rightAlignIcon}>
|
||||
<Icon color={iconColor} size="S" name={icon} />
|
||||
</div>
|
||||
{/if}
|
||||
|
@ -88,9 +88,9 @@
|
|||
<slot />
|
||||
</div>
|
||||
{/if}
|
||||
{#if color}
|
||||
<div class="light">
|
||||
<StatusLight size="L" {color} />
|
||||
{#if $$slots.right}
|
||||
<div class="right">
|
||||
<slot name="right" />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
@ -107,7 +107,7 @@
|
|||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
align-items: stretch;
|
||||
}
|
||||
.nav-item.scrollable {
|
||||
flex-direction: column;
|
||||
|
@ -135,10 +135,8 @@
|
|||
align-items: center;
|
||||
gap: var(--spacing-xs);
|
||||
width: max-content;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
padding-left: var(--spacing-l);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Needed to fully display the actions icon */
|
||||
|
@ -153,10 +151,15 @@
|
|||
justify-content: center;
|
||||
align-items: center;
|
||||
color: var(--spectrum-global-color-gray-600);
|
||||
order: 1;
|
||||
}
|
||||
.icon.right {
|
||||
order: 4;
|
||||
}
|
||||
.icon.arrow {
|
||||
flex: 0 0 20px;
|
||||
pointer-events: all;
|
||||
order: 0;
|
||||
}
|
||||
.icon.arrow.absolute {
|
||||
position: absolute;
|
||||
|
@ -188,11 +191,14 @@
|
|||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
flex: 1 1 auto;
|
||||
color: var(--spectrum-global-color-gray-800);
|
||||
color: var(--spectrum-global-color-gray-900);
|
||||
order: 2;
|
||||
width: 0;
|
||||
}
|
||||
.scrollable .text {
|
||||
flex: 0 0 auto;
|
||||
max-width: 160px;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.actions {
|
||||
|
@ -201,18 +207,17 @@
|
|||
display: grid;
|
||||
place-items: center;
|
||||
visibility: hidden;
|
||||
}
|
||||
.actions,
|
||||
.light :global(.spectrum-StatusLight) {
|
||||
order: 3;
|
||||
opacity: 0;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-left: var(--spacing-s);
|
||||
margin-left: var(--spacing-xs);
|
||||
}
|
||||
.light {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
.nav-item.withActions:hover .actions {
|
||||
opacity: 1;
|
||||
}
|
||||
.nav-item.withActions:hover .light {
|
||||
display: none;
|
||||
|
||||
.right {
|
||||
order: 10;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
<script>
|
||||
import { Select } from "@budibase/bbui"
|
||||
import { roles } from "stores/backend"
|
||||
import { RoleUtils } from "@budibase/frontend-core"
|
||||
|
||||
export let value
|
||||
export let error
|
||||
export let placeholder = null
|
||||
export let autoWidth = false
|
||||
export let quiet = false
|
||||
</script>
|
||||
|
||||
<Select
|
||||
{autoWidth}
|
||||
{quiet}
|
||||
bind:value
|
||||
on:change
|
||||
options={$roles}
|
||||
getOptionLabel={role => role.name}
|
||||
getOptionValue={role => role._id}
|
||||
getOptionColour={role => RoleUtils.getRoleColour(role._id)}
|
||||
{placeholder}
|
||||
{error}
|
||||
/>
|
|
@ -18,6 +18,7 @@
|
|||
export let fillWidth
|
||||
export let allowJS = true
|
||||
export let updateOnChange = true
|
||||
export let drawerLeft
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
let bindingDrawer
|
||||
|
@ -53,7 +54,7 @@
|
|||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<Drawer {fillWidth} bind:this={bindingDrawer} {title}>
|
||||
<Drawer {fillWidth} bind:this={bindingDrawer} {title} left={drawerLeft}>
|
||||
<svelte:fragment slot="description">
|
||||
Add the objects on the left to enrich your text.
|
||||
</svelte:fragment>
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
import { ModalContent } from "@budibase/bbui"
|
||||
import { onMount } from "svelte"
|
||||
import WebhookDisplay from "../automation/Shared/WebhookDisplay.svelte"
|
||||
import { TriggerStepID } from "constants/backend/automations"
|
||||
|
||||
let webhookUrls = []
|
||||
|
||||
|
@ -11,7 +12,7 @@
|
|||
onMount(() => {
|
||||
webhookUrls = automations.map(automation => {
|
||||
const trigger = automation.definition.trigger
|
||||
if (trigger?.stepId === "WEBHOOK" && trigger.inputs) {
|
||||
if (trigger?.stepId === TriggerStepID.WEBHOOK && trigger.inputs) {
|
||||
return {
|
||||
type: "Automation",
|
||||
name: automation.name,
|
||||
|
|
|
@ -56,6 +56,10 @@
|
|||
}
|
||||
}
|
||||
|
||||
const previewApp = () => {
|
||||
window.open(`/${application}`)
|
||||
}
|
||||
|
||||
const viewApp = () => {
|
||||
analytics.captureEvent(Events.APP_VIEW_PUBLISHED, {
|
||||
appId: selectedApp.appId,
|
||||
|
@ -174,7 +178,10 @@
|
|||
Are you sure you want to unpublish the app <b>{selectedApp?.name}</b>?
|
||||
</ConfirmDialog>
|
||||
|
||||
<DeployModal onOk={completePublish} />
|
||||
<div class="buttons">
|
||||
<Button on:click={previewApp} newStyles secondary>Preview</Button>
|
||||
<DeployModal onOk={completePublish} />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.publish-popover-actions :global([data-cy="publish-popover-action"]) {
|
||||
|
@ -183,4 +190,11 @@
|
|||
:global([data-cy="publish-popover-menu"]) {
|
||||
padding: 10px;
|
||||
}
|
||||
.buttons {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
gap: var(--spacing-m);
|
||||
}
|
||||
</style>
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue