Merge branch 'master' into BUDI-7580/account_portal_submodule
This commit is contained in:
commit
6abb1b5f70
|
@ -18,8 +18,7 @@ env:
|
|||
BASE_BRANCH: ${{ github.event.pull_request.base.ref}}
|
||||
PERSONAL_ACCESS_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
|
||||
NX_BASE_BRANCH: origin/${{ github.base_ref }}
|
||||
USE_NX_AFFECTED: ${{ github.event_name == 'pull_request' && github.base_ref != 'master'}}
|
||||
NX_CLOUD_ACCESS_TOKEN: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }}
|
||||
USE_NX_AFFECTED: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
|
@ -231,7 +230,7 @@ jobs:
|
|||
cache: "yarn"
|
||||
- run: yarn --frozen-lockfile
|
||||
- name: Build packages
|
||||
run: yarn build --scope @budibase/server --scope @budibase/worker --scope @budibase/client --scope @budibase/backend-core
|
||||
run: yarn build --scope @budibase/server --scope @budibase/worker
|
||||
- name: Run tests
|
||||
run: |
|
||||
cd qa-core
|
||||
|
|
|
@ -4,6 +4,8 @@ on:
|
|||
types: [created]
|
||||
pull_request_target:
|
||||
types: [opened,closed,synchronize]
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
CLAssistant:
|
||||
|
@ -33,4 +35,4 @@ jobs:
|
|||
#custom-pr-sign-comment: 'The signature to be committed in order to sign the CLA'
|
||||
#custom-allsigned-prcomment: 'pull request comment when all contributors has signed, defaults to **CLA Assistant Lite bot** All Contributors have signed the CLA.'
|
||||
#lock-pullrequest-aftermerge: false - if you don't want this bot to automatically lock the pull request after merging (default - true)
|
||||
#use-dco-flag: true - If you are using DCO instead of CLA
|
||||
#use-dco-flag: true - If you are using DCO instead of CLA
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
name: Deploy QA
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
trigger-deploy-to-qa-env:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: peter-evans/repository-dispatch@v2
|
||||
env:
|
||||
PAYLOAD_VERSION: ${{ github.sha }}
|
||||
REF_NAME: ${{ github.ref_name}}
|
||||
with:
|
||||
repository: budibase/budibase-deploys
|
||||
event-type: budicloud-qa-deploy
|
||||
token: ${{ secrets.GH_ACCESS_TOKEN }}
|
|
@ -123,6 +123,7 @@ jobs:
|
|||
- uses: passeidireto/trigger-external-workflow-action@main
|
||||
env:
|
||||
PAYLOAD_VERSION: ${{ env.RELEASE_VERSION }}
|
||||
REF_NAME: ${{ github.ref_name}}
|
||||
with:
|
||||
repository: budibase/budibase-deploys
|
||||
event: budicloud-qa-deploy
|
||||
|
|
|
@ -1,69 +0,0 @@
|
|||
name: Test
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
CI: true
|
||||
PERSONAL_ACCESS_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
|
||||
REGISTRY_URL: registry.hub.docker.com
|
||||
NX_CLOUD_ACCESS_TOKEN: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }}
|
||||
jobs:
|
||||
build:
|
||||
name: "build"
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [18.x]
|
||||
steps:
|
||||
- name: "Checkout"
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: true
|
||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
|
||||
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: "yarn"
|
||||
- name: Setup QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
- name: Setup Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Run Yarn
|
||||
run: yarn
|
||||
- name: Run Yarn Build
|
||||
run: yarn build --scope @budibase/server --scope @budibase/worker
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_API_KEY }}
|
||||
- name: Get the latest release version
|
||||
id: version
|
||||
run: |
|
||||
release_version=$(cat lerna.json | jq -r '.version')
|
||||
echo $release_version
|
||||
echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV
|
||||
- name: Tag and release Budibase service docker image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
pull: true
|
||||
platforms: linux/amd64,linux/arm64
|
||||
tags: budibase/budibase-test:test
|
||||
file: ./hosting/single/Dockerfile.v2
|
||||
cache-from: type=registry,ref=budibase/budibase-test:test
|
||||
cache-to: type=inline
|
||||
- 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-test:aas
|
||||
file: ./hosting/single/Dockerfile.v2
|
|
@ -5,7 +5,7 @@ ENV COUCHDB_PASSWORD admin
|
|||
EXPOSE 5984
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends software-properties-common wget unzip curl && \
|
||||
wget -O - https://packages.adoptium.net/artifactory/api/gpg/key/public | sudo apt-key add - && \
|
||||
wget -O - https://packages.adoptium.net/artifactory/api/gpg/key/public | apt-key add - && \
|
||||
apt-add-repository 'deb http://security.debian.org/debian-security bullseye-security/updates main' && \
|
||||
apt-add-repository 'deb http://archive.debian.org/debian stretch-backports main' && \
|
||||
apt-add-repository 'deb https://packages.adoptium.net/artifactory/deb bullseye main' && \
|
||||
|
|
|
@ -3,3 +3,6 @@
|
|||
[couchdb]
|
||||
database_dir = DATA_DIR/couch/dbs
|
||||
view_index_dir = DATA_DIR/couch/views
|
||||
|
||||
[chttpd_auth]
|
||||
timeout = 7200 ; 2 hours in seconds
|
||||
|
|
|
@ -4,7 +4,11 @@ version: "3"
|
|||
|
||||
services:
|
||||
app-service:
|
||||
build: ../packages/server
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: packages/server/Dockerfile.v2
|
||||
args:
|
||||
- BUDIBASE_VERSION=0.0.0+dev-docker
|
||||
container_name: build-bbapps
|
||||
environment:
|
||||
SELF_HOSTED: 1
|
||||
|
@ -28,11 +32,13 @@ services:
|
|||
depends_on:
|
||||
- worker-service
|
||||
- redis-service
|
||||
# volumes:
|
||||
# - /some/path/to/plugins:/plugins
|
||||
|
||||
worker-service:
|
||||
build: ../packages/worker
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: packages/worker/Dockerfile.v2
|
||||
args:
|
||||
- BUDIBASE_VERSION=0.0.0+dev-docker
|
||||
container_name: build-bbworker
|
||||
environment:
|
||||
SELF_HOSTED: 1
|
||||
|
|
|
@ -51,7 +51,7 @@ http {
|
|||
proxy_buffering off;
|
||||
|
||||
set $csp_default "default-src 'self'";
|
||||
set $csp_script "script-src 'self' 'unsafe-inline' 'unsafe-eval' https://*.budibase.net https://cdn.budi.live https://js.intercomcdn.com https://widget.intercom.io";
|
||||
set $csp_script "script-src 'self' 'unsafe-inline' 'unsafe-eval' https://*.budibase.net https://cdn.budi.live https://js.intercomcdn.com https://widget.intercom.io https://d2l5prqdbvm3op.cloudfront.net";
|
||||
set $csp_style "style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://fonts.googleapis.com https://rsms.me https://maxcdn.bootstrapcdn.com";
|
||||
set $csp_object "object-src 'none'";
|
||||
set $csp_base_uri "base-uri 'self'";
|
||||
|
|
|
@ -19,13 +19,15 @@ COPY packages/string-templates/package.json packages/string-templates/package.js
|
|||
|
||||
COPY scripts/removeWorkspaceDependencies.sh scripts/removeWorkspaceDependencies.sh
|
||||
RUN chmod +x ./scripts/removeWorkspaceDependencies.sh
|
||||
RUN ./scripts/removeWorkspaceDependencies.sh
|
||||
RUN ./scripts/removeWorkspaceDependencies.sh packages/server/package.json
|
||||
RUN ./scripts/removeWorkspaceDependencies.sh packages/worker/package.json
|
||||
|
||||
|
||||
# We will never want to sync pro, but the script is still required
|
||||
RUN echo '' > scripts/syncProPackage.js
|
||||
RUN jq 'del(.scripts.postinstall)' package.json > temp.json && mv temp.json package.json
|
||||
RUN --mount=type=cache,target=/root/.yarn YARN_CACHE_FOLDER=/root/.yarn yarn install --production
|
||||
RUN ./scripts/removeWorkspaceDependencies.sh package.json
|
||||
RUN --mount=type=cache,target=/root/.yarn YARN_CACHE_FOLDER=/root/.yarn yarn install --production
|
||||
|
||||
# copy the actual code
|
||||
COPY packages/server/dist packages/server/dist
|
||||
|
@ -116,6 +118,10 @@ EXPOSE 443
|
|||
EXPOSE 2222
|
||||
VOLUME /data
|
||||
|
||||
ARG BUDIBASE_VERSION
|
||||
# Ensuring the version argument is sent
|
||||
RUN test -n "$BUDIBASE_VERSION"
|
||||
ENV BUDIBASE_VERSION=$BUDIBASE_VERSION
|
||||
|
||||
HEALTHCHECK --interval=15s --timeout=15s --start-period=45s CMD "/healthcheck.sh"
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"version": "2.11.36",
|
||||
"version": "2.11.45",
|
||||
"npmClient": "yarn",
|
||||
"packages": ["packages/*", "packages/account-portal/packages/*"],
|
||||
"useNx": true,
|
||||
|
|
8
nx.json
8
nx.json
|
@ -3,14 +3,16 @@
|
|||
"default": {
|
||||
"runner": "nx-cloud",
|
||||
"options": {
|
||||
"cacheableOperations": ["build", "test", "check:types"],
|
||||
"accessToken": "MmM4OGYxNzItMDBlYy00ZmE3LTk4MTYtNmJhYWMyZjBjZTUyfHJlYWQ="
|
||||
"cacheableOperations": ["build", "test", "check:types"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"targetDefaults": {
|
||||
"build": {
|
||||
"inputs": ["{workspaceRoot}/scripts/build.js"]
|
||||
"inputs": [
|
||||
"{workspaceRoot}/scripts/build.js",
|
||||
"{workspaceRoot}/lerna.json"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,7 +32,6 @@
|
|||
"build:sdk": "lerna run --stream build:sdk",
|
||||
"deps:circular": "madge packages/server/dist/index.js packages/worker/src/index.ts packages/backend-core/dist/src/index.js packages/cli/src/index.js --circular",
|
||||
"release": "lerna publish from-package --yes --force-publish --no-git-tag-version --no-push --no-git-reset",
|
||||
"release:develop": "yarn release --dist-tag develop",
|
||||
"restore": "yarn run clean && yarn && yarn run build",
|
||||
"nuke": "yarn run nuke:packages && yarn run nuke:docker",
|
||||
"nuke:packages": "yarn run restore",
|
||||
|
@ -47,7 +46,7 @@
|
|||
"dev:server": "yarn run kill-server && lerna run --stream dev:builder --scope @budibase/worker --scope @budibase/server",
|
||||
"dev:accountportal": "yarn kill-accountportal && lerna run dev --stream --scope @budibase/account-portal-ui --scope @budibase/account-portal-server",
|
||||
"dev:built": "yarn run kill-all && cd packages/server && yarn dev:stack:up && cd ../../ && lerna run --stream dev:built",
|
||||
"dev:docker": "yarn build && docker-compose -f hosting/docker-compose.build.yaml -f hosting/docker-compose.dev.yaml --env-file hosting/.env up --build --scale proxy-service=0",
|
||||
"dev:docker": "yarn build --scope @budibase/server --scope @budibase/worker && docker-compose -f hosting/docker-compose.build.yaml -f hosting/docker-compose.dev.yaml --env-file hosting/.env up --build --scale proxy-service=0",
|
||||
"test": "lerna run --stream test --stream",
|
||||
"lint:eslint": "eslint packages qa-core --max-warnings=0",
|
||||
"lint:prettier": "prettier --check \"packages/**/*.{js,ts,svelte}\" && prettier --write \"examples/**/*.{js,ts,svelte}\" && prettier --check \"qa-core/**/*.{js,ts,svelte}\"",
|
||||
|
|
|
@ -1 +1 @@
|
|||
Subproject commit afe8d1766e1616a7024c4d402c725d52a1561677
|
||||
Subproject commit a96ef701ea2e0ada8c5c7372b667d1cd26ece4df
|
|
@ -63,7 +63,7 @@
|
|||
"@types/chance": "1.1.3",
|
||||
"@types/cookies": "0.7.8",
|
||||
"@types/jest": "29.5.5",
|
||||
"@types/lodash": "4.14.199",
|
||||
"@types/lodash": "4.14.200",
|
||||
"@types/node": "18.17.0",
|
||||
"@types/node-fetch": "2.6.4",
|
||||
"@types/pouchdb": "6.4.0",
|
||||
|
|
|
@ -33,8 +33,8 @@ function isInvalid(metadata?: { state: string }) {
|
|||
* Get the requested app metadata by id.
|
||||
* Use redis cache to first read the app metadata.
|
||||
* If not present fallback to loading the app metadata directly and re-caching.
|
||||
* @param {string} appId the id of the app to get metadata from.
|
||||
* @returns {object} the app metadata.
|
||||
* @param appId the id of the app to get metadata from.
|
||||
* @returns the app metadata.
|
||||
*/
|
||||
export async function getAppMetadata(appId: string): Promise<App | DeletedApp> {
|
||||
const client = await getAppClient()
|
||||
|
@ -72,9 +72,9 @@ export async function getAppMetadata(appId: string): Promise<App | DeletedApp> {
|
|||
|
||||
/**
|
||||
* Invalidate/reset the cached metadata when a change occurs in the db.
|
||||
* @param appId {string} the cache key to bust/update.
|
||||
* @param newMetadata {object|undefined} optional - can simply provide the new metadata to update with.
|
||||
* @return {Promise<void>} will respond with success when cache is updated.
|
||||
* @param appId the cache key to bust/update.
|
||||
* @param newMetadata optional - can simply provide the new metadata to update with.
|
||||
* @return will respond with success when cache is updated.
|
||||
*/
|
||||
export async function invalidateAppMetadata(appId: string, newMetadata?: any) {
|
||||
if (!appId) {
|
||||
|
|
|
@ -61,9 +61,9 @@ async function populateUsersFromDB(
|
|||
* Get the requested user by id.
|
||||
* Use redis cache to first read the user.
|
||||
* If not present fallback to loading the user directly and re-caching.
|
||||
* @param {*} userId the id of the user to get
|
||||
* @param {*} tenantId the tenant of the user to get
|
||||
* @param {*} populateUser function to provide the user for re-caching. default to couch db
|
||||
* @param userId the id of the user to get
|
||||
* @param tenantId the tenant of the user to get
|
||||
* @param populateUser function to provide the user for re-caching. default to couch db
|
||||
* @returns
|
||||
*/
|
||||
export async function getUser(
|
||||
|
@ -111,8 +111,8 @@ export async function getUser(
|
|||
* Get the requested users by id.
|
||||
* Use redis cache to first read the users.
|
||||
* If not present fallback to loading the users directly and re-caching.
|
||||
* @param {*} userIds the ids of the user to get
|
||||
* @param {*} tenantId the tenant of the users to get
|
||||
* @param userIds the ids of the user to get
|
||||
* @param tenantId the tenant of the users to get
|
||||
* @returns
|
||||
*/
|
||||
export async function getUsers(
|
||||
|
|
|
@ -23,7 +23,7 @@ import environment from "../environment"
|
|||
|
||||
/**
|
||||
* Generates a new configuration ID.
|
||||
* @returns {string} The new configuration ID which the config doc can be stored under.
|
||||
* @returns The new configuration ID which the config doc can be stored under.
|
||||
*/
|
||||
export function generateConfigID(type: ConfigType) {
|
||||
return `${DocumentType.CONFIG}${SEPARATOR}${type}`
|
||||
|
|
|
@ -62,7 +62,7 @@ export function isTenancyEnabled() {
|
|||
|
||||
/**
|
||||
* Given an app ID this will attempt to retrieve the tenant ID from it.
|
||||
* @return {null|string} The tenant ID found within the app ID.
|
||||
* @return The tenant ID found within the app ID.
|
||||
*/
|
||||
export function getTenantIDFromAppID(appId: string) {
|
||||
if (!appId) {
|
||||
|
|
|
@ -8,8 +8,8 @@ class Replication {
|
|||
|
||||
/**
|
||||
*
|
||||
* @param {String} source - the DB you want to replicate or rollback to
|
||||
* @param {String} target - the DB you want to replicate to, or rollback from
|
||||
* @param source - the DB you want to replicate or rollback to
|
||||
* @param target - the DB you want to replicate to, or rollback from
|
||||
*/
|
||||
constructor({ source, target }: any) {
|
||||
this.source = getPouchDB(source)
|
||||
|
@ -38,7 +38,7 @@ class Replication {
|
|||
|
||||
/**
|
||||
* Two way replication operation, intended to be promise based.
|
||||
* @param {Object} opts - PouchDB replication options
|
||||
* @param opts - PouchDB replication options
|
||||
*/
|
||||
sync(opts = {}) {
|
||||
this.replication = this.promisify(this.source.sync, opts)
|
||||
|
@ -47,7 +47,7 @@ class Replication {
|
|||
|
||||
/**
|
||||
* One way replication operation, intended to be promise based.
|
||||
* @param {Object} opts - PouchDB replication options
|
||||
* @param opts - PouchDB replication options
|
||||
*/
|
||||
replicate(opts = {}) {
|
||||
this.replication = this.promisify(this.source.replicate.to, opts)
|
||||
|
|
|
@ -599,10 +599,10 @@ async function runQuery<T>(
|
|||
* Gets round the fixed limit of 200 results from a query by fetching as many
|
||||
* pages as required and concatenating the results. This recursively operates
|
||||
* until enough results have been found.
|
||||
* @param dbName {string} Which database to run a lucene query on
|
||||
* @param index {string} Which search index to utilise
|
||||
* @param query {object} The JSON query structure
|
||||
* @param params {object} The search params including:
|
||||
* @param dbName Which database to run a lucene query on
|
||||
* @param index Which search index to utilise
|
||||
* @param query The JSON query structure
|
||||
* @param params The search params including:
|
||||
* tableId {string} The table ID to search
|
||||
* sort {string} The sort column
|
||||
* sortOrder {string} The sort order ("ascending" or "descending")
|
||||
|
@ -655,10 +655,10 @@ async function recursiveSearch<T>(
|
|||
* Performs a paginated search. A bookmark will be returned to allow the next
|
||||
* page to be fetched. There is a max limit off 200 results per page in a
|
||||
* paginated search.
|
||||
* @param dbName {string} Which database to run a lucene query on
|
||||
* @param index {string} Which search index to utilise
|
||||
* @param query {object} The JSON query structure
|
||||
* @param params {object} The search params including:
|
||||
* @param dbName Which database to run a lucene query on
|
||||
* @param index Which search index to utilise
|
||||
* @param query The JSON query structure
|
||||
* @param params The search params including:
|
||||
* tableId {string} The table ID to search
|
||||
* sort {string} The sort column
|
||||
* sortOrder {string} The sort order ("ascending" or "descending")
|
||||
|
@ -722,10 +722,10 @@ export async function paginatedSearch<T>(
|
|||
* desired amount of results. There is a limit of 1000 results to avoid
|
||||
* heavy performance hits, and to avoid client components breaking from
|
||||
* handling too much data.
|
||||
* @param dbName {string} Which database to run a lucene query on
|
||||
* @param index {string} Which search index to utilise
|
||||
* @param query {object} The JSON query structure
|
||||
* @param params {object} The search params including:
|
||||
* @param dbName Which database to run a lucene query on
|
||||
* @param index Which search index to utilise
|
||||
* @param query The JSON query structure
|
||||
* @param params The search params including:
|
||||
* tableId {string} The table ID to search
|
||||
* sort {string} The sort column
|
||||
* sortOrder {string} The sort order ("ascending" or "descending")
|
||||
|
|
|
@ -45,7 +45,7 @@ export async function getAllDbs(opts = { efficient: false }) {
|
|||
* Lots of different points in the system need to find the full list of apps, this will
|
||||
* enumerate the entire CouchDB cluster and get the list of databases (every app).
|
||||
*
|
||||
* @return {Promise<object[]>} returns the app information document stored in each app database.
|
||||
* @return returns the app information document stored in each app database.
|
||||
*/
|
||||
export async function getAllApps({
|
||||
dev,
|
||||
|
|
|
@ -25,7 +25,7 @@ export function isDevApp(app: App) {
|
|||
|
||||
/**
|
||||
* Generates a development app ID from a real app ID.
|
||||
* @returns {string} the dev app ID which can be used for dev database.
|
||||
* @returns the dev app ID which can be used for dev database.
|
||||
*/
|
||||
export function getDevelopmentAppID(appId: string) {
|
||||
if (!appId || appId.startsWith(APP_DEV_PREFIX)) {
|
||||
|
|
|
@ -8,7 +8,7 @@ import { newid } from "./newid"
|
|||
|
||||
/**
|
||||
* Generates a new app ID.
|
||||
* @returns {string} The new app ID which the app doc can be stored under.
|
||||
* @returns The new app ID which the app doc can be stored under.
|
||||
*/
|
||||
export const generateAppID = (tenantId?: string | null) => {
|
||||
let id = APP_PREFIX
|
||||
|
@ -20,9 +20,9 @@ export const generateAppID = (tenantId?: string | null) => {
|
|||
|
||||
/**
|
||||
* Gets a new row ID for the specified table.
|
||||
* @param {string} tableId The table which the row is being created for.
|
||||
* @param {string|null} id If an ID is to be used then the UUID can be substituted for this.
|
||||
* @returns {string} The new ID which a row doc can be stored under.
|
||||
* @param tableId The table which the row is being created for.
|
||||
* @param id If an ID is to be used then the UUID can be substituted for this.
|
||||
* @returns The new ID which a row doc can be stored under.
|
||||
*/
|
||||
export function generateRowID(tableId: string, id?: string) {
|
||||
id = id || newid()
|
||||
|
@ -31,7 +31,7 @@ export function generateRowID(tableId: string, id?: string) {
|
|||
|
||||
/**
|
||||
* Generates a new workspace ID.
|
||||
* @returns {string} The new workspace ID which the workspace doc can be stored under.
|
||||
* @returns The new workspace ID which the workspace doc can be stored under.
|
||||
*/
|
||||
export function generateWorkspaceID() {
|
||||
return `${DocumentType.WORKSPACE}${SEPARATOR}${newid()}`
|
||||
|
@ -39,7 +39,7 @@ export function generateWorkspaceID() {
|
|||
|
||||
/**
|
||||
* Generates a new global user ID.
|
||||
* @returns {string} The new user ID which the user doc can be stored under.
|
||||
* @returns The new user ID which the user doc can be stored under.
|
||||
*/
|
||||
export function generateGlobalUserID(id?: any) {
|
||||
return `${DocumentType.USER}${SEPARATOR}${id || newid()}`
|
||||
|
@ -52,8 +52,8 @@ export function isGlobalUserID(id: string) {
|
|||
|
||||
/**
|
||||
* Generates a new user ID based on the passed in global ID.
|
||||
* @param {string} globalId The ID of the global user.
|
||||
* @returns {string} The new user ID which the user doc can be stored under.
|
||||
* @param globalId The ID of the global user.
|
||||
* @returns The new user ID which the user doc can be stored under.
|
||||
*/
|
||||
export function generateUserMetadataID(globalId: string) {
|
||||
return generateRowID(InternalTable.USER_METADATA, globalId)
|
||||
|
@ -84,7 +84,7 @@ export function generateAppUserID(prodAppId: string, userId: string) {
|
|||
|
||||
/**
|
||||
* Generates a new role ID.
|
||||
* @returns {string} The new role ID which the role doc can be stored under.
|
||||
* @returns The new role ID which the role doc can be stored under.
|
||||
*/
|
||||
export function generateRoleID(name: string) {
|
||||
const prefix = `${DocumentType.ROLE}${SEPARATOR}`
|
||||
|
@ -103,7 +103,7 @@ export function prefixRoleID(name: string) {
|
|||
|
||||
/**
|
||||
* Generates a new dev info document ID - this is scoped to a user.
|
||||
* @returns {string} The new dev info ID which info for dev (like api key) can be stored under.
|
||||
* @returns The new dev info ID which info for dev (like api key) can be stored under.
|
||||
*/
|
||||
export const generateDevInfoID = (userId: any) => {
|
||||
return `${DocumentType.DEV_INFO}${SEPARATOR}${userId}`
|
||||
|
@ -111,7 +111,7 @@ export const generateDevInfoID = (userId: any) => {
|
|||
|
||||
/**
|
||||
* Generates a new plugin ID - to be used in the global DB.
|
||||
* @returns {string} The new plugin ID which a plugin metadata document can be stored under.
|
||||
* @returns The new plugin ID which a plugin metadata document can be stored under.
|
||||
*/
|
||||
export const generatePluginID = (name: string) => {
|
||||
return `${DocumentType.PLUGIN}${SEPARATOR}${name}`
|
||||
|
|
|
@ -12,12 +12,12 @@ import { getProdAppID } from "./conversions"
|
|||
* is usually the case as most of our docs are top level e.g. tables, automations, users and so on.
|
||||
* More complex cases such as link docs and rows which have multiple levels of IDs that their
|
||||
* ID consists of need their own functions to build the allDocs parameters.
|
||||
* @param {string} docType The type of document which input params are being built for, e.g. user,
|
||||
* @param docType The type of document which input params are being built for, e.g. user,
|
||||
* link, app, table and so on.
|
||||
* @param {string|null} docId The ID of the document minus its type - this is only needed if looking
|
||||
* @param docId The ID of the document minus its type - this is only needed if looking
|
||||
* for a singular document.
|
||||
* @param {object} otherProps Add any other properties onto the request, e.g. include_docs.
|
||||
* @returns {object} Parameters which can then be used with an allDocs request.
|
||||
* @param otherProps Add any other properties onto the request, e.g. include_docs.
|
||||
* @returns Parameters which can then be used with an allDocs request.
|
||||
*/
|
||||
export function getDocParams(
|
||||
docType: string,
|
||||
|
@ -36,11 +36,11 @@ export function getDocParams(
|
|||
|
||||
/**
|
||||
* Gets the DB allDocs/query params for retrieving a row.
|
||||
* @param {string|null} tableId The table in which the rows have been stored.
|
||||
* @param {string|null} rowId The ID of the row which is being specifically queried for. This can be
|
||||
* @param tableId The table in which the rows have been stored.
|
||||
* @param rowId The ID of the row which is being specifically queried for. This can be
|
||||
* left null to get all the rows in the table.
|
||||
* @param {object} otherProps Any other properties to add to the request.
|
||||
* @returns {object} Parameters which can then be used with an allDocs request.
|
||||
* @param otherProps Any other properties to add to the request.
|
||||
* @returns Parameters which can then be used with an allDocs request.
|
||||
*/
|
||||
export function getRowParams(
|
||||
tableId?: string | null,
|
||||
|
|
|
@ -75,12 +75,12 @@ function getPackageJsonFields(): {
|
|||
const content = readFileSync(packageJsonFile!, "utf-8")
|
||||
const parsedContent = JSON.parse(content)
|
||||
return {
|
||||
VERSION: parsedContent.version,
|
||||
VERSION: process.env.BUDIBASE_VERSION || parsedContent.version,
|
||||
SERVICE_NAME: parsedContent.name,
|
||||
}
|
||||
} catch {
|
||||
// throwing an error here is confusing/causes backend-core to be hard to import
|
||||
return { VERSION: "", SERVICE_NAME: "" }
|
||||
return { VERSION: process.env.BUDIBASE_VERSION || "", SERVICE_NAME: "" }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
/**
|
||||
* Makes sure that a URL has the correct number of slashes, while maintaining the
|
||||
* http(s):// double slashes.
|
||||
* @param {string} url The URL to test and remove any extra double slashes.
|
||||
* @return {string} The updated url.
|
||||
* @param url The URL to test and remove any extra double slashes.
|
||||
* @return The updated url.
|
||||
*/
|
||||
export function checkSlashesInUrl(url: string) {
|
||||
return url.replace(/(https?:\/\/)|(\/)+/g, "$1$2")
|
||||
|
|
|
@ -13,10 +13,10 @@ export const options = {
|
|||
|
||||
/**
|
||||
* Passport Local Authentication Middleware.
|
||||
* @param {*} ctx the request structure
|
||||
* @param {*} email username to login with
|
||||
* @param {*} password plain text password to log in with
|
||||
* @param {*} done callback from passport to return user information and errors
|
||||
* @param ctx the request structure
|
||||
* @param email username to login with
|
||||
* @param password plain text password to log in with
|
||||
* @param done callback from passport to return user information and errors
|
||||
* @returns The authenticated user, or errors if they occur
|
||||
*/
|
||||
export async function authenticate(
|
||||
|
|
|
@ -17,15 +17,15 @@ const OIDCStrategy = require("@techpass/passport-openidconnect").Strategy
|
|||
|
||||
export function buildVerifyFn(saveUserFn: SaveSSOUserFunction) {
|
||||
/**
|
||||
* @param {*} issuer The identity provider base URL
|
||||
* @param {*} sub The user ID
|
||||
* @param {*} profile The user profile information. Created by passport from the /userinfo response
|
||||
* @param {*} jwtClaims The parsed id_token claims
|
||||
* @param {*} accessToken The access_token for contacting the identity provider - may or may not be a JWT
|
||||
* @param {*} refreshToken The refresh_token for obtaining a new access_token - usually not a JWT
|
||||
* @param {*} idToken The id_token - always a JWT
|
||||
* @param {*} params The response body from requesting an access_token
|
||||
* @param {*} done The passport callback: err, user, info
|
||||
* @param issuer The identity provider base URL
|
||||
* @param sub The user ID
|
||||
* @param profile The user profile information. Created by passport from the /userinfo response
|
||||
* @param jwtClaims The parsed id_token claims
|
||||
* @param accessToken The access_token for contacting the identity provider - may or may not be a JWT
|
||||
* @param refreshToken The refresh_token for obtaining a new access_token - usually not a JWT
|
||||
* @param idToken The id_token - always a JWT
|
||||
* @param params The response body from requesting an access_token
|
||||
* @param done The passport callback: err, user, info
|
||||
*/
|
||||
return async (
|
||||
issuer: string,
|
||||
|
@ -61,8 +61,8 @@ export function buildVerifyFn(saveUserFn: SaveSSOUserFunction) {
|
|||
}
|
||||
|
||||
/**
|
||||
* @param {*} profile The structured profile created by passport using the user info endpoint
|
||||
* @param {*} jwtClaims The claims returned in the id token
|
||||
* @param profile The structured profile created by passport using the user info endpoint
|
||||
* @param jwtClaims The claims returned in the id token
|
||||
*/
|
||||
function getEmail(profile: SSOProfile, jwtClaims: JwtClaims) {
|
||||
// profile not guaranteed to contain email e.g. github connected azure ad account
|
||||
|
|
|
@ -5,9 +5,9 @@ import { ConfigType, GoogleInnerConfig } from "@budibase/types"
|
|||
/**
|
||||
* Utility to handle authentication errors.
|
||||
*
|
||||
* @param {*} done The passport callback.
|
||||
* @param {*} message Message that will be returned in the response body
|
||||
* @param {*} err (Optional) error that will be logged
|
||||
* @param done The passport callback.
|
||||
* @param message Message that will be returned in the response body
|
||||
* @param err (Optional) error that will be logged
|
||||
*/
|
||||
|
||||
export function authError(done: Function, message: string, err?: any) {
|
||||
|
|
|
@ -1,37 +1,50 @@
|
|||
import env from "../../environment"
|
||||
import * as objectStore from "../objectStore"
|
||||
import * as cloudfront from "../cloudfront"
|
||||
import qs from "querystring"
|
||||
import { DEFAULT_TENANT_ID, getTenantId } from "../../context"
|
||||
|
||||
export function clientLibraryPath(appId: string) {
|
||||
return `${objectStore.sanitizeKey(appId)}/budibase-client.js`
|
||||
}
|
||||
|
||||
/**
|
||||
* In production the client library is stored in the object store, however in development
|
||||
* we use the symlinked version produced by lerna, located in node modules. We link to this
|
||||
* via a specific endpoint (under /api/assets/client).
|
||||
* @param {string} appId In production we need the appId to look up the correct bucket, as the
|
||||
* version of the client lib may differ between apps.
|
||||
* @param {string} version The version to retrieve.
|
||||
* @return {string} The URL to be inserted into appPackage response or server rendered
|
||||
* app index file.
|
||||
* Previously we used to serve the client library directly from Cloudfront, however
|
||||
* due to issues with the domain we were unable to continue doing this - keeping
|
||||
* incase we are able to switch back to CDN path again in future.
|
||||
*/
|
||||
export const clientLibraryUrl = (appId: string, version: string) => {
|
||||
if (env.isProd()) {
|
||||
let file = `${objectStore.sanitizeKey(appId)}/budibase-client.js`
|
||||
if (env.CLOUDFRONT_CDN) {
|
||||
// append app version to bust the cache
|
||||
if (version) {
|
||||
file += `?v=${version}`
|
||||
}
|
||||
// don't need to use presigned for client with cloudfront
|
||||
// file is public
|
||||
return cloudfront.getUrl(file)
|
||||
} else {
|
||||
return objectStore.getPresignedUrl(env.APPS_BUCKET_NAME, file)
|
||||
export function clientLibraryCDNUrl(appId: string, version: string) {
|
||||
let file = clientLibraryPath(appId)
|
||||
if (env.CLOUDFRONT_CDN) {
|
||||
// append app version to bust the cache
|
||||
if (version) {
|
||||
file += `?v=${version}`
|
||||
}
|
||||
// don't need to use presigned for client with cloudfront
|
||||
// file is public
|
||||
return cloudfront.getUrl(file)
|
||||
} else {
|
||||
return `/api/assets/client`
|
||||
return objectStore.getPresignedUrl(env.APPS_BUCKET_NAME, file)
|
||||
}
|
||||
}
|
||||
|
||||
export const getAppFileUrl = (s3Key: string) => {
|
||||
export function clientLibraryUrl(appId: string, version: string) {
|
||||
let tenantId, qsParams: { appId: string; version: string; tenantId?: string }
|
||||
try {
|
||||
tenantId = getTenantId()
|
||||
} finally {
|
||||
qsParams = {
|
||||
appId,
|
||||
version,
|
||||
}
|
||||
}
|
||||
if (tenantId && tenantId !== DEFAULT_TENANT_ID) {
|
||||
qsParams.tenantId = tenantId
|
||||
}
|
||||
return `/api/assets/client?${qs.encode(qsParams)}`
|
||||
}
|
||||
|
||||
export function getAppFileUrl(s3Key: string) {
|
||||
if (env.CLOUDFRONT_CDN) {
|
||||
return cloudfront.getPresignedUrl(s3Key)
|
||||
} else {
|
||||
|
|
|
@ -6,7 +6,7 @@ import { Plugin } from "@budibase/types"
|
|||
|
||||
// URLS
|
||||
|
||||
export const enrichPluginURLs = (plugins: Plugin[]) => {
|
||||
export function enrichPluginURLs(plugins: Plugin[]) {
|
||||
if (!plugins || !plugins.length) {
|
||||
return []
|
||||
}
|
||||
|
@ -17,12 +17,12 @@ export const enrichPluginURLs = (plugins: Plugin[]) => {
|
|||
})
|
||||
}
|
||||
|
||||
const getPluginJSUrl = (plugin: Plugin) => {
|
||||
function getPluginJSUrl(plugin: Plugin) {
|
||||
const s3Key = getPluginJSKey(plugin)
|
||||
return getPluginUrl(s3Key)
|
||||
}
|
||||
|
||||
const getPluginIconUrl = (plugin: Plugin): string | undefined => {
|
||||
function getPluginIconUrl(plugin: Plugin): string | undefined {
|
||||
const s3Key = getPluginIconKey(plugin)
|
||||
if (!s3Key) {
|
||||
return
|
||||
|
@ -30,7 +30,7 @@ const getPluginIconUrl = (plugin: Plugin): string | undefined => {
|
|||
return getPluginUrl(s3Key)
|
||||
}
|
||||
|
||||
const getPluginUrl = (s3Key: string) => {
|
||||
function getPluginUrl(s3Key: string) {
|
||||
if (env.CLOUDFRONT_CDN) {
|
||||
return cloudfront.getPresignedUrl(s3Key)
|
||||
} else {
|
||||
|
@ -40,11 +40,11 @@ const getPluginUrl = (s3Key: string) => {
|
|||
|
||||
// S3 KEYS
|
||||
|
||||
export const getPluginJSKey = (plugin: Plugin) => {
|
||||
export function getPluginJSKey(plugin: Plugin) {
|
||||
return getPluginS3Key(plugin, "plugin.min.js")
|
||||
}
|
||||
|
||||
export const getPluginIconKey = (plugin: Plugin) => {
|
||||
export function getPluginIconKey(plugin: Plugin) {
|
||||
// stored iconUrl is deprecated - hardcode to icon.svg in this case
|
||||
const iconFileName = plugin.iconUrl ? "icon.svg" : plugin.iconFileName
|
||||
if (!iconFileName) {
|
||||
|
@ -53,12 +53,12 @@ export const getPluginIconKey = (plugin: Plugin) => {
|
|||
return getPluginS3Key(plugin, iconFileName)
|
||||
}
|
||||
|
||||
const getPluginS3Key = (plugin: Plugin, fileName: string) => {
|
||||
function getPluginS3Key(plugin: Plugin, fileName: string) {
|
||||
const s3Key = getPluginS3Dir(plugin.name)
|
||||
return `${s3Key}/${fileName}`
|
||||
}
|
||||
|
||||
export const getPluginS3Dir = (pluginName: string) => {
|
||||
export function getPluginS3Dir(pluginName: string) {
|
||||
let s3Key = `${pluginName}`
|
||||
if (env.MULTI_TENANCY) {
|
||||
const tenantId = context.getTenantId()
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import * as app from "../app"
|
||||
import { getAppFileUrl } from "../app"
|
||||
import { testEnv } from "../../../../tests/extra"
|
||||
|
||||
describe("app", () => {
|
||||
|
@ -7,6 +6,15 @@ describe("app", () => {
|
|||
testEnv.nodeJest()
|
||||
})
|
||||
|
||||
function baseCheck(url: string, tenantId?: string) {
|
||||
expect(url).toContain("/api/assets/client")
|
||||
if (tenantId) {
|
||||
expect(url).toContain(`tenantId=${tenantId}`)
|
||||
}
|
||||
expect(url).toContain("appId=app_123")
|
||||
expect(url).toContain("version=2.0.0")
|
||||
}
|
||||
|
||||
describe("clientLibraryUrl", () => {
|
||||
function getClientUrl() {
|
||||
return app.clientLibraryUrl("app_123/budibase-client.js", "2.0.0")
|
||||
|
@ -20,31 +28,19 @@ describe("app", () => {
|
|||
it("gets url in dev", () => {
|
||||
testEnv.nodeDev()
|
||||
const url = getClientUrl()
|
||||
expect(url).toBe("/api/assets/client")
|
||||
})
|
||||
|
||||
it("gets url with embedded minio", () => {
|
||||
testEnv.withMinio()
|
||||
const url = getClientUrl()
|
||||
expect(url).toBe(
|
||||
"/files/signed/prod-budi-app-assets/app_123/budibase-client.js/budibase-client.js"
|
||||
)
|
||||
baseCheck(url)
|
||||
})
|
||||
|
||||
it("gets url with custom S3", () => {
|
||||
testEnv.withS3()
|
||||
const url = getClientUrl()
|
||||
expect(url).toBe(
|
||||
"http://s3.example.com/prod-budi-app-assets/app_123/budibase-client.js/budibase-client.js"
|
||||
)
|
||||
baseCheck(url)
|
||||
})
|
||||
|
||||
it("gets url with cloudfront + s3", () => {
|
||||
testEnv.withCloudfront()
|
||||
const url = getClientUrl()
|
||||
expect(url).toBe(
|
||||
"http://cf.example.com/app_123/budibase-client.js/budibase-client.js?v=2.0.0"
|
||||
)
|
||||
baseCheck(url)
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -57,7 +53,7 @@ describe("app", () => {
|
|||
testEnv.nodeDev()
|
||||
await testEnv.withTenant(tenantId => {
|
||||
const url = getClientUrl()
|
||||
expect(url).toBe("/api/assets/client")
|
||||
baseCheck(url, tenantId)
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -65,9 +61,7 @@ describe("app", () => {
|
|||
await testEnv.withTenant(tenantId => {
|
||||
testEnv.withMinio()
|
||||
const url = getClientUrl()
|
||||
expect(url).toBe(
|
||||
"/files/signed/prod-budi-app-assets/app_123/budibase-client.js/budibase-client.js"
|
||||
)
|
||||
baseCheck(url, tenantId)
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -75,9 +69,7 @@ describe("app", () => {
|
|||
await testEnv.withTenant(tenantId => {
|
||||
testEnv.withS3()
|
||||
const url = getClientUrl()
|
||||
expect(url).toBe(
|
||||
"http://s3.example.com/prod-budi-app-assets/app_123/budibase-client.js/budibase-client.js"
|
||||
)
|
||||
baseCheck(url, tenantId)
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -85,9 +77,7 @@ describe("app", () => {
|
|||
await testEnv.withTenant(tenantId => {
|
||||
testEnv.withCloudfront()
|
||||
const url = getClientUrl()
|
||||
expect(url).toBe(
|
||||
"http://cf.example.com/app_123/budibase-client.js/budibase-client.js?v=2.0.0"
|
||||
)
|
||||
baseCheck(url, tenantId)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
const sanitize = require("sanitize-s3-objectkey")
|
||||
import AWS from "aws-sdk"
|
||||
import stream from "stream"
|
||||
import stream, { Readable } from "stream"
|
||||
import fetch from "node-fetch"
|
||||
import tar from "tar-fs"
|
||||
import zlib from "zlib"
|
||||
|
@ -61,15 +61,15 @@ export function sanitizeBucket(input: string) {
|
|||
|
||||
/**
|
||||
* Gets a connection to the object store using the S3 SDK.
|
||||
* @param {string} bucket the name of the bucket which blobs will be uploaded/retrieved from.
|
||||
* @param {object} opts configuration for the object store.
|
||||
* @return {Object} an S3 object store object, check S3 Nodejs SDK for usage.
|
||||
* @param bucket the name of the bucket which blobs will be uploaded/retrieved from.
|
||||
* @param opts configuration for the object store.
|
||||
* @return an S3 object store object, check S3 Nodejs SDK for usage.
|
||||
* @constructor
|
||||
*/
|
||||
export const ObjectStore = (
|
||||
export function ObjectStore(
|
||||
bucket: string,
|
||||
opts: { presigning: boolean } = { presigning: false }
|
||||
) => {
|
||||
) {
|
||||
const config: any = {
|
||||
s3ForcePathStyle: true,
|
||||
signatureVersion: "v4",
|
||||
|
@ -104,7 +104,7 @@ export const ObjectStore = (
|
|||
* Given an object store and a bucket name this will make sure the bucket exists,
|
||||
* if it does not exist then it will create it.
|
||||
*/
|
||||
export const makeSureBucketExists = async (client: any, bucketName: string) => {
|
||||
export async function makeSureBucketExists(client: any, bucketName: string) {
|
||||
bucketName = sanitizeBucket(bucketName)
|
||||
try {
|
||||
await client
|
||||
|
@ -139,13 +139,13 @@ export const makeSureBucketExists = async (client: any, bucketName: string) => {
|
|||
* Uploads the contents of a file given the required parameters, useful when
|
||||
* temp files in use (for example file uploaded as an attachment).
|
||||
*/
|
||||
export const upload = async ({
|
||||
export async function upload({
|
||||
bucket: bucketName,
|
||||
filename,
|
||||
path,
|
||||
type,
|
||||
metadata,
|
||||
}: UploadParams) => {
|
||||
}: UploadParams) {
|
||||
const extension = filename.split(".").pop()
|
||||
const fileBytes = fs.readFileSync(path)
|
||||
|
||||
|
@ -180,12 +180,12 @@ export const upload = async ({
|
|||
* Similar to the upload function but can be used to send a file stream
|
||||
* through to the object store.
|
||||
*/
|
||||
export const streamUpload = async (
|
||||
export async function streamUpload(
|
||||
bucketName: string,
|
||||
filename: string,
|
||||
stream: any,
|
||||
extra = {}
|
||||
) => {
|
||||
) {
|
||||
const objectStore = ObjectStore(bucketName)
|
||||
await makeSureBucketExists(objectStore, bucketName)
|
||||
|
||||
|
@ -215,7 +215,7 @@ export const streamUpload = async (
|
|||
* retrieves the contents of a file from the object store, if it is a known content type it
|
||||
* will be converted, otherwise it will be returned as a buffer stream.
|
||||
*/
|
||||
export const retrieve = async (bucketName: string, filepath: string) => {
|
||||
export async function retrieve(bucketName: string, filepath: string) {
|
||||
const objectStore = ObjectStore(bucketName)
|
||||
const params = {
|
||||
Bucket: sanitizeBucket(bucketName),
|
||||
|
@ -230,7 +230,7 @@ export const retrieve = async (bucketName: string, filepath: string) => {
|
|||
}
|
||||
}
|
||||
|
||||
export const listAllObjects = async (bucketName: string, path: string) => {
|
||||
export async function listAllObjects(bucketName: string, path: string) {
|
||||
const objectStore = ObjectStore(bucketName)
|
||||
const list = (params: ListParams = {}) => {
|
||||
return objectStore
|
||||
|
@ -261,11 +261,11 @@ export const listAllObjects = async (bucketName: string, path: string) => {
|
|||
/**
|
||||
* Generate a presigned url with a default TTL of 1 hour
|
||||
*/
|
||||
export const getPresignedUrl = (
|
||||
export function getPresignedUrl(
|
||||
bucketName: string,
|
||||
key: string,
|
||||
durationSeconds: number = 3600
|
||||
) => {
|
||||
) {
|
||||
const objectStore = ObjectStore(bucketName, { presigning: true })
|
||||
const params = {
|
||||
Bucket: sanitizeBucket(bucketName),
|
||||
|
@ -291,7 +291,7 @@ export const getPresignedUrl = (
|
|||
/**
|
||||
* Same as retrieval function but puts to a temporary file.
|
||||
*/
|
||||
export const retrieveToTmp = async (bucketName: string, filepath: string) => {
|
||||
export async function retrieveToTmp(bucketName: string, filepath: string) {
|
||||
bucketName = sanitizeBucket(bucketName)
|
||||
filepath = sanitizeKey(filepath)
|
||||
const data = await retrieve(bucketName, filepath)
|
||||
|
@ -300,7 +300,7 @@ export const retrieveToTmp = async (bucketName: string, filepath: string) => {
|
|||
return outputPath
|
||||
}
|
||||
|
||||
export const retrieveDirectory = async (bucketName: string, path: string) => {
|
||||
export async function retrieveDirectory(bucketName: string, path: string) {
|
||||
let writePath = join(budibaseTempDir(), v4())
|
||||
fs.mkdirSync(writePath)
|
||||
const objects = await listAllObjects(bucketName, path)
|
||||
|
@ -324,7 +324,7 @@ export const retrieveDirectory = async (bucketName: string, path: string) => {
|
|||
/**
|
||||
* Delete a single file.
|
||||
*/
|
||||
export const deleteFile = async (bucketName: string, filepath: string) => {
|
||||
export async function deleteFile(bucketName: string, filepath: string) {
|
||||
const objectStore = ObjectStore(bucketName)
|
||||
await makeSureBucketExists(objectStore, bucketName)
|
||||
const params = {
|
||||
|
@ -334,7 +334,7 @@ export const deleteFile = async (bucketName: string, filepath: string) => {
|
|||
return objectStore.deleteObject(params).promise()
|
||||
}
|
||||
|
||||
export const deleteFiles = async (bucketName: string, filepaths: string[]) => {
|
||||
export async function deleteFiles(bucketName: string, filepaths: string[]) {
|
||||
const objectStore = ObjectStore(bucketName)
|
||||
await makeSureBucketExists(objectStore, bucketName)
|
||||
const params = {
|
||||
|
@ -349,10 +349,10 @@ export const deleteFiles = async (bucketName: string, filepaths: string[]) => {
|
|||
/**
|
||||
* Delete a path, including everything within.
|
||||
*/
|
||||
export const deleteFolder = async (
|
||||
export async function deleteFolder(
|
||||
bucketName: string,
|
||||
folder: string
|
||||
): Promise<any> => {
|
||||
): Promise<any> {
|
||||
bucketName = sanitizeBucket(bucketName)
|
||||
folder = sanitizeKey(folder)
|
||||
const client = ObjectStore(bucketName)
|
||||
|
@ -383,11 +383,11 @@ export const deleteFolder = async (
|
|||
}
|
||||
}
|
||||
|
||||
export const uploadDirectory = async (
|
||||
export async function uploadDirectory(
|
||||
bucketName: string,
|
||||
localPath: string,
|
||||
bucketPath: string
|
||||
) => {
|
||||
) {
|
||||
bucketName = sanitizeBucket(bucketName)
|
||||
let uploads = []
|
||||
const files = fs.readdirSync(localPath, { withFileTypes: true })
|
||||
|
@ -404,11 +404,11 @@ export const uploadDirectory = async (
|
|||
return files
|
||||
}
|
||||
|
||||
export const downloadTarballDirect = async (
|
||||
export async function downloadTarballDirect(
|
||||
url: string,
|
||||
path: string,
|
||||
headers = {}
|
||||
) => {
|
||||
) {
|
||||
path = sanitizeKey(path)
|
||||
const response = await fetch(url, { headers })
|
||||
if (!response.ok) {
|
||||
|
@ -418,11 +418,11 @@ export const downloadTarballDirect = async (
|
|||
await streamPipeline(response.body, zlib.createUnzip(), tar.extract(path))
|
||||
}
|
||||
|
||||
export const downloadTarball = async (
|
||||
export async function downloadTarball(
|
||||
url: string,
|
||||
bucketName: string,
|
||||
path: string
|
||||
) => {
|
||||
) {
|
||||
bucketName = sanitizeBucket(bucketName)
|
||||
path = sanitizeKey(path)
|
||||
const response = await fetch(url)
|
||||
|
@ -438,3 +438,17 @@ export const downloadTarball = async (
|
|||
// return the temporary path incase there is a use for it
|
||||
return tmpPath
|
||||
}
|
||||
|
||||
export async function getReadStream(
|
||||
bucketName: string,
|
||||
path: string
|
||||
): Promise<Readable> {
|
||||
bucketName = sanitizeBucket(bucketName)
|
||||
path = sanitizeKey(path)
|
||||
const client = ObjectStore(bucketName)
|
||||
const params = {
|
||||
Bucket: bucketName,
|
||||
Key: path,
|
||||
}
|
||||
return client.getObject(params).createReadStream()
|
||||
}
|
||||
|
|
|
@ -5,9 +5,9 @@ import { timeout } from "../utils"
|
|||
* Bull works with a Job wrapper around all messages that contains a lot more information about
|
||||
* the state of the message, this object constructor implements the same schema of Bull jobs
|
||||
* for the sake of maintaining API consistency.
|
||||
* @param {string} queue The name of the queue which the message will be carried on.
|
||||
* @param {object} message The JSON message which will be passed back to the consumer.
|
||||
* @returns {Object} A new job which can now be put onto the queue, this is mostly an
|
||||
* @param queue The name of the queue which the message will be carried on.
|
||||
* @param message The JSON message which will be passed back to the consumer.
|
||||
* @returns A new job which can now be put onto the queue, this is mostly an
|
||||
* internal structure so that an in memory queue can be easily swapped for a Bull queue.
|
||||
*/
|
||||
function newJob(queue: string, message: any) {
|
||||
|
@ -32,8 +32,8 @@ class InMemoryQueue {
|
|||
_addCount: number
|
||||
/**
|
||||
* The constructor the queue, exactly the same as that of Bulls.
|
||||
* @param {string} name The name of the queue which is being configured.
|
||||
* @param {object|null} opts This is not used by the in memory queue as there is no real use
|
||||
* @param name The name of the queue which is being configured.
|
||||
* @param opts This is not used by the in memory queue as there is no real use
|
||||
* case when in memory, but is the same API as Bull
|
||||
*/
|
||||
constructor(name: string, opts = null) {
|
||||
|
@ -49,7 +49,7 @@ class InMemoryQueue {
|
|||
* Same callback API as Bull, each callback passed to this will consume messages as they are
|
||||
* available. Please note this is a queue service, not a notification service, so each
|
||||
* consumer will receive different messages.
|
||||
* @param {function<object>} func The callback function which will return a "Job", the same
|
||||
* @param func The callback function which will return a "Job", the same
|
||||
* as the Bull API, within this job the property "data" contains the JSON message. Please
|
||||
* note this is incredibly limited compared to Bull as in reality the Job would contain
|
||||
* a lot more information about the queue and current status of Bull cluster.
|
||||
|
@ -73,9 +73,9 @@ class InMemoryQueue {
|
|||
* Simple function to replicate the add message functionality of Bull, putting
|
||||
* a new message on the queue. This then emits an event which will be used to
|
||||
* return the message to a consumer (if one is attached).
|
||||
* @param {object} msg A message to be transported over the queue, this should be
|
||||
* @param msg A message to be transported over the queue, this should be
|
||||
* a JSON message as this is required by Bull.
|
||||
* @param {boolean} repeat serves no purpose for the import queue.
|
||||
* @param repeat serves no purpose for the import queue.
|
||||
*/
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
add(msg: any, repeat: boolean) {
|
||||
|
@ -96,7 +96,7 @@ class InMemoryQueue {
|
|||
|
||||
/**
|
||||
* This removes a cron which has been implemented, this is part of Bull API.
|
||||
* @param {string} cronJobId The cron which is to be removed.
|
||||
* @param cronJobId The cron which is to be removed.
|
||||
*/
|
||||
removeRepeatableByKey(cronJobId: string) {
|
||||
// TODO: implement for testing
|
||||
|
|
|
@ -142,7 +142,7 @@ function waitForConnection(selectDb: number = DEFAULT_SELECT_DB) {
|
|||
* this can only be done with redis streams because they will have an end.
|
||||
* @param stream A redis stream, specifically as this type of stream will have an end.
|
||||
* @param client The client to use for further lookups.
|
||||
* @return {Promise<object>} The final output of the stream
|
||||
* @return The final output of the stream
|
||||
*/
|
||||
function promisifyStream(stream: any, client: RedisWrapper) {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
|
|
@ -36,8 +36,8 @@ export function levelToNumber(perm: PermissionLevel) {
|
|||
|
||||
/**
|
||||
* Given the specified permission level for the user return the levels they are allowed to carry out.
|
||||
* @param {string} userPermLevel The permission level of the user.
|
||||
* @return {string[]} All the permission levels this user is allowed to carry out.
|
||||
* @param userPermLevel The permission level of the user.
|
||||
* @return All the permission levels this user is allowed to carry out.
|
||||
*/
|
||||
export function getAllowedLevels(userPermLevel: PermissionLevel): string[] {
|
||||
switch (userPermLevel) {
|
||||
|
|
|
@ -149,9 +149,9 @@ export function lowerBuiltinRoleID(roleId1?: string, roleId2?: string): string {
|
|||
/**
|
||||
* Gets the role object, this is mainly useful for two purposes, to check if the level exists and
|
||||
* to check if the role inherits any others.
|
||||
* @param {string|null} roleId The level ID to lookup.
|
||||
* @param {object|null} opts options for the function, like whether to halt errors, instead return public.
|
||||
* @returns {Promise<Role|object|null>} The role object, which may contain an "inherits" property.
|
||||
* @param roleId The level ID to lookup.
|
||||
* @param opts options for the function, like whether to halt errors, instead return public.
|
||||
* @returns The role object, which may contain an "inherits" property.
|
||||
*/
|
||||
export async function getRole(
|
||||
roleId?: string,
|
||||
|
@ -225,8 +225,8 @@ export async function getUserRoleIdHierarchy(
|
|||
/**
|
||||
* Returns an ordered array of the user's inherited role IDs, this can be used
|
||||
* to determine if a user can access something that requires a specific role.
|
||||
* @param {string} userRoleId The user's role ID, this can be found in their access token.
|
||||
* @returns {Promise<object[]>} returns an ordered array of the roles, with the first being their
|
||||
* @param userRoleId The user's role ID, this can be found in their access token.
|
||||
* @returns returns an ordered array of the roles, with the first being their
|
||||
* highest level of access and the last being the lowest level.
|
||||
*/
|
||||
export async function getUserRoleHierarchy(userRoleId?: string) {
|
||||
|
@ -258,7 +258,7 @@ export async function getAllRoleIds(appId?: string) {
|
|||
|
||||
/**
|
||||
* Given an app ID this will retrieve all of the roles that are currently within that app.
|
||||
* @return {Promise<object[]>} An array of the role objects that were found.
|
||||
* @return An array of the role objects that were found.
|
||||
*/
|
||||
export async function getAllRoles(appId?: string): Promise<RoleDoc[]> {
|
||||
if (appId) {
|
||||
|
|
|
@ -21,7 +21,6 @@ import {
|
|||
User,
|
||||
UserStatus,
|
||||
UserGroup,
|
||||
ContextUser,
|
||||
} from "@budibase/types"
|
||||
import {
|
||||
getAccountHolderFromUserIds,
|
||||
|
@ -135,7 +134,7 @@ export class UserDB {
|
|||
if (!fullUser.roles) {
|
||||
fullUser.roles = {}
|
||||
}
|
||||
// add the active status to a user if its not provided
|
||||
// add the active status to a user if it's not provided
|
||||
if (fullUser.status == null) {
|
||||
fullUser.status = UserStatus.ACTIVE
|
||||
}
|
||||
|
@ -160,14 +159,14 @@ export class UserDB {
|
|||
}
|
||||
}
|
||||
|
||||
static async getUsersByAppAccess(appId?: string) {
|
||||
const opts: any = {
|
||||
static async getUsersByAppAccess(opts: { appId?: string; limit?: number }) {
|
||||
const params: any = {
|
||||
include_docs: true,
|
||||
limit: 50,
|
||||
limit: opts.limit || 50,
|
||||
}
|
||||
let response: User[] = await usersCore.searchGlobalUsersByAppAccess(
|
||||
appId,
|
||||
opts
|
||||
opts.appId,
|
||||
params
|
||||
)
|
||||
return response
|
||||
}
|
||||
|
|
|
@ -19,9 +19,11 @@ import {
|
|||
SearchQueryOperators,
|
||||
SearchUsersRequest,
|
||||
User,
|
||||
DatabaseQueryOpts,
|
||||
} from "@budibase/types"
|
||||
import * as context from "../context"
|
||||
import { getGlobalDB } from "../context"
|
||||
import * as context from "../context"
|
||||
import { isCreator } from "./utils"
|
||||
|
||||
type GetOpts = { cleanup?: boolean }
|
||||
|
||||
|
@ -240,12 +242,14 @@ export const paginatedUsers = async ({
|
|||
bookmark,
|
||||
query,
|
||||
appId,
|
||||
limit,
|
||||
}: SearchUsersRequest = {}) => {
|
||||
const db = getGlobalDB()
|
||||
const pageLimit = limit ? limit + 1 : PAGE_LIMIT + 1
|
||||
// get one extra document, to have the next page
|
||||
const opts: any = {
|
||||
const opts: DatabaseQueryOpts = {
|
||||
include_docs: true,
|
||||
limit: PAGE_LIMIT + 1,
|
||||
limit: pageLimit,
|
||||
}
|
||||
// add a startkey if the page was specified (anchor)
|
||||
if (bookmark) {
|
||||
|
@ -268,7 +272,7 @@ export const paginatedUsers = async ({
|
|||
const response = await db.allDocs(getGlobalUserParams(null, opts))
|
||||
userList = response.rows.map((row: any) => row.doc)
|
||||
}
|
||||
return pagination(userList, PAGE_LIMIT, {
|
||||
return pagination(userList, pageLimit, {
|
||||
paginate: true,
|
||||
property,
|
||||
getKey,
|
||||
|
@ -283,6 +287,19 @@ export async function getUserCount() {
|
|||
return response.total_rows
|
||||
}
|
||||
|
||||
export async function getCreatorCount() {
|
||||
let creators = 0
|
||||
async function iterate(startPage?: string) {
|
||||
const page = await paginatedUsers({ bookmark: startPage })
|
||||
creators += page.data.filter(isCreator).length
|
||||
if (page.hasNextPage) {
|
||||
await iterate(page.nextPage)
|
||||
}
|
||||
}
|
||||
await iterate()
|
||||
return creators
|
||||
}
|
||||
|
||||
// used to remove the builder/admin permissions, for processing the
|
||||
// user as an app user (they may have some specific role/group
|
||||
export function removePortalUserPermissions(user: User | ContextUser) {
|
||||
|
|
|
@ -10,6 +10,7 @@ import { getAccountByTenantId } from "../accounts"
|
|||
// extract from shared-core to make easily accessible from backend-core
|
||||
export const isBuilder = sdk.users.isBuilder
|
||||
export const isAdmin = sdk.users.isAdmin
|
||||
export const isCreator = sdk.users.isCreator
|
||||
export const isGlobalBuilder = sdk.users.isGlobalBuilder
|
||||
export const isAdminOrBuilder = sdk.users.isAdminOrBuilder
|
||||
export const hasAdminPermissions = sdk.users.hasAdminPermissions
|
||||
|
|
|
@ -79,8 +79,8 @@ export function isPublicApiRequest(ctx: Ctx): boolean {
|
|||
|
||||
/**
|
||||
* Given a request tries to find the appId, which can be located in various places
|
||||
* @param {object} ctx The main request body to look through.
|
||||
* @returns {string|undefined} If an appId was found it will be returned.
|
||||
* @param ctx The main request body to look through.
|
||||
* @returns If an appId was found it will be returned.
|
||||
*/
|
||||
export async function getAppIdFromCtx(ctx: Ctx) {
|
||||
// look in headers
|
||||
|
@ -135,7 +135,7 @@ function parseAppIdFromUrl(url?: string) {
|
|||
|
||||
/**
|
||||
* opens the contents of the specified encrypted JWT.
|
||||
* @return {object} the contents of the token.
|
||||
* @return the contents of the token.
|
||||
*/
|
||||
export function openJwt(token: string) {
|
||||
if (!token) {
|
||||
|
@ -169,8 +169,8 @@ export function isValidInternalAPIKey(apiKey: string) {
|
|||
|
||||
/**
|
||||
* Get a cookie from context, and decrypt if necessary.
|
||||
* @param {object} ctx The request which is to be manipulated.
|
||||
* @param {string} name The name of the cookie to get.
|
||||
* @param ctx The request which is to be manipulated.
|
||||
* @param name The name of the cookie to get.
|
||||
*/
|
||||
export function getCookie(ctx: Ctx, name: string) {
|
||||
const cookie = ctx.cookies.get(name)
|
||||
|
@ -184,10 +184,10 @@ export function getCookie(ctx: Ctx, name: string) {
|
|||
|
||||
/**
|
||||
* Store a cookie for the request - it will not expire.
|
||||
* @param {object} ctx The request which is to be manipulated.
|
||||
* @param {string} name The name of the cookie to set.
|
||||
* @param {string|object} value The value of cookie which will be set.
|
||||
* @param {object} opts options like whether to sign.
|
||||
* @param ctx The request which is to be manipulated.
|
||||
* @param name The name of the cookie to set.
|
||||
* @param value The value of cookie which will be set.
|
||||
* @param opts options like whether to sign.
|
||||
*/
|
||||
export function setCookie(
|
||||
ctx: Ctx,
|
||||
|
@ -223,8 +223,8 @@ export function clearCookie(ctx: Ctx, name: string) {
|
|||
/**
|
||||
* Checks if the API call being made (based on the provided ctx object) is from the client. If
|
||||
* the call is not from a client app then it is from the builder.
|
||||
* @param {object} ctx The koa context object to be tested.
|
||||
* @return {boolean} returns true if the call is from the client lib (a built app rather than the builder).
|
||||
* @param ctx The koa context object to be tested.
|
||||
* @return returns true if the call is from the client lib (a built app rather than the builder).
|
||||
*/
|
||||
export function isClient(ctx: Ctx) {
|
||||
return ctx.headers[Header.TYPE] === "client"
|
||||
|
|
|
@ -72,6 +72,11 @@ export function quotas(): Quotas {
|
|||
value: 1,
|
||||
triggers: [],
|
||||
},
|
||||
creators: {
|
||||
name: "Creators",
|
||||
value: 1,
|
||||
triggers: [],
|
||||
},
|
||||
userGroups: {
|
||||
name: "User Groups",
|
||||
value: 1,
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { MonthlyQuotaName, QuotaUsage } from "@budibase/types"
|
||||
|
||||
export const usage = (): QuotaUsage => {
|
||||
export const usage = (users: number = 0, creators: number = 0): QuotaUsage => {
|
||||
return {
|
||||
_id: "usage_quota",
|
||||
quotaReset: new Date().toISOString(),
|
||||
|
@ -58,7 +58,8 @@ export const usage = (): QuotaUsage => {
|
|||
usageQuota: {
|
||||
apps: 0,
|
||||
plugins: 0,
|
||||
users: 0,
|
||||
users,
|
||||
creators,
|
||||
userGroups: 0,
|
||||
rows: 0,
|
||||
triggers: {},
|
||||
|
|
|
@ -106,6 +106,13 @@
|
|||
name: fieldName,
|
||||
}
|
||||
}
|
||||
|
||||
// Delete numeric only widths as these are grid widths and should be
|
||||
// ignored
|
||||
const width = fixedSchema[fieldName].width
|
||||
if (width != null && `${width}`.trim().match(/^[0-9]+$/)) {
|
||||
delete fixedSchema[fieldName].width
|
||||
}
|
||||
})
|
||||
return fixedSchema
|
||||
}
|
||||
|
|
|
@ -23,7 +23,7 @@ class AnalyticsHub {
|
|||
posthog.identify(id)
|
||||
}
|
||||
|
||||
captureException(err) {}
|
||||
captureException(_err) {}
|
||||
|
||||
captureEvent(eventName, props = {}) {
|
||||
posthog.captureEvent(eventName, props)
|
||||
|
|
|
@ -5,6 +5,7 @@ import {
|
|||
encodeJSBinding,
|
||||
findHBSBlocks,
|
||||
} from "@budibase/string-templates"
|
||||
import { capitalise } from "helpers"
|
||||
|
||||
/**
|
||||
* Recursively searches for a specific component ID
|
||||
|
@ -235,3 +236,13 @@ export const makeComponentUnique = component => {
|
|||
// Recurse on all children
|
||||
return JSON.parse(definition)
|
||||
}
|
||||
|
||||
export const getComponentText = component => {
|
||||
if (component?._instanceName) {
|
||||
return component._instanceName
|
||||
}
|
||||
const type =
|
||||
component._component.replace("@budibase/standard-components/", "") ||
|
||||
"component"
|
||||
return capitalise(type)
|
||||
}
|
||||
|
|
|
@ -2,14 +2,14 @@ import sanitizeUrl from "./utils/sanitizeUrl"
|
|||
import { Screen } from "./utils/Screen"
|
||||
import { Component } from "./utils/Component"
|
||||
|
||||
export default function (datasources) {
|
||||
export default function (datasources, mode = "table") {
|
||||
if (!Array.isArray(datasources)) {
|
||||
return []
|
||||
}
|
||||
return datasources.map(datasource => {
|
||||
return {
|
||||
name: `${datasource.label} - List`,
|
||||
create: () => createScreen(datasource),
|
||||
create: () => createScreen(datasource, mode),
|
||||
id: ROW_LIST_TEMPLATE,
|
||||
resourceId: datasource.resourceId,
|
||||
}
|
||||
|
@ -40,10 +40,24 @@ const generateTableBlock = datasource => {
|
|||
return tableBlock
|
||||
}
|
||||
|
||||
const createScreen = datasource => {
|
||||
const generateGridBlock = datasource => {
|
||||
const gridBlock = new Component("@budibase/standard-components/gridblock")
|
||||
gridBlock
|
||||
.customProps({
|
||||
table: datasource,
|
||||
})
|
||||
.instanceName(`${datasource.label} - Grid block`)
|
||||
return gridBlock
|
||||
}
|
||||
|
||||
const createScreen = (datasource, mode) => {
|
||||
return new Screen()
|
||||
.route(rowListUrl(datasource))
|
||||
.instanceName(`${datasource.label} - List`)
|
||||
.addChild(generateTableBlock(datasource))
|
||||
.addChild(
|
||||
mode === "table"
|
||||
? generateTableBlock(datasource)
|
||||
: generateGridBlock(datasource)
|
||||
)
|
||||
.json()
|
||||
}
|
||||
|
|
|
@ -3,13 +3,10 @@
|
|||
import { goto, params } from "@roxi/routify"
|
||||
import { Table, Heading, Layout } from "@budibase/bbui"
|
||||
import Spinner from "components/common/Spinner.svelte"
|
||||
import {
|
||||
TableNames,
|
||||
UNEDITABLE_USER_FIELDS,
|
||||
UNSORTABLE_TYPES,
|
||||
} from "constants"
|
||||
import { TableNames, UNEDITABLE_USER_FIELDS } from "constants"
|
||||
import RoleCell from "./cells/RoleCell.svelte"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import { canBeSortColumn } from "@budibase/shared-core"
|
||||
|
||||
export let schema = {}
|
||||
export let data = []
|
||||
|
@ -27,19 +24,23 @@
|
|||
|
||||
let selectedRows = []
|
||||
let customRenderers = []
|
||||
let parsedSchema = {}
|
||||
|
||||
$: if (schema) {
|
||||
parsedSchema = Object.keys(schema).reduce((acc, key) => {
|
||||
acc[key] =
|
||||
typeof schema[key] === "string" ? { type: schema[key] } : schema[key]
|
||||
|
||||
if (!canBeSortColumn(acc[key].type)) {
|
||||
acc[key].sortable = false
|
||||
}
|
||||
return acc
|
||||
}, {})
|
||||
}
|
||||
|
||||
$: selectedRows, dispatch("selectionUpdated", selectedRows)
|
||||
$: isUsersTable = tableId === TableNames.USERS
|
||||
$: data && resetSelectedRows()
|
||||
$: {
|
||||
UNSORTABLE_TYPES.forEach(type => {
|
||||
Object.values(schema || {}).forEach(col => {
|
||||
if (col.type === type) {
|
||||
col.sortable = false
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
$: {
|
||||
if (isUsersTable) {
|
||||
customRenderers = [
|
||||
|
@ -49,24 +50,24 @@
|
|||
},
|
||||
]
|
||||
UNEDITABLE_USER_FIELDS.forEach(field => {
|
||||
if (schema[field]) {
|
||||
schema[field].editable = false
|
||||
if (parsedSchema[field]) {
|
||||
parsedSchema[field].editable = false
|
||||
}
|
||||
})
|
||||
if (schema.email) {
|
||||
schema.email.displayName = "Email"
|
||||
if (parsedSchema.email) {
|
||||
parsedSchema.email.displayName = "Email"
|
||||
}
|
||||
if (schema.roleId) {
|
||||
schema.roleId.displayName = "Role"
|
||||
if (parsedSchema.roleId) {
|
||||
parsedSchema.roleId.displayName = "Role"
|
||||
}
|
||||
if (schema.firstName) {
|
||||
schema.firstName.displayName = "First Name"
|
||||
if (parsedSchema.firstName) {
|
||||
parsedSchema.firstName.displayName = "First Name"
|
||||
}
|
||||
if (schema.lastName) {
|
||||
schema.lastName.displayName = "Last Name"
|
||||
if (parsedSchema.lastName) {
|
||||
parsedSchema.lastName.displayName = "Last Name"
|
||||
}
|
||||
if (schema.status) {
|
||||
schema.status.displayName = "Status"
|
||||
if (parsedSchema.status) {
|
||||
parsedSchema.status.displayName = "Status"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -102,7 +103,7 @@
|
|||
<div class="table-wrapper">
|
||||
<Table
|
||||
{data}
|
||||
{schema}
|
||||
schema={parsedSchema}
|
||||
{loading}
|
||||
{customRenderers}
|
||||
{rowCount}
|
||||
|
|
|
@ -20,6 +20,7 @@
|
|||
let type = "internal"
|
||||
|
||||
$: name = view.name
|
||||
$: schema = view.schema
|
||||
$: calculation = view.calculation
|
||||
|
||||
$: supportedFormats = Object.values(ROW_EXPORT_FORMATS).filter(key => {
|
||||
|
@ -61,7 +62,7 @@
|
|||
|
||||
<Table
|
||||
title={decodeURI(name)}
|
||||
schema={view.schema}
|
||||
{schema}
|
||||
tableId={view.tableId}
|
||||
{data}
|
||||
{loading}
|
||||
|
|
|
@ -777,7 +777,8 @@
|
|||
disabled={deleteColName !== originalName}
|
||||
>
|
||||
<p>
|
||||
Are you sure you wish to delete the column <b>{originalName}?</b>
|
||||
Are you sure you wish to delete the column
|
||||
<b on:click={() => (deleteColName = originalName)}>{originalName}?</b>
|
||||
Your data will be deleted and this action cannot be undone - enter the column
|
||||
name to confirm.
|
||||
</p>
|
||||
|
@ -810,4 +811,11 @@
|
|||
gap: 8px;
|
||||
display: flex;
|
||||
}
|
||||
b {
|
||||
transition: color 130ms ease-out;
|
||||
}
|
||||
b:hover {
|
||||
cursor: pointer;
|
||||
color: var(--spectrum-global-color-gray-900);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
export let closeButtonIcon = "Close"
|
||||
|
||||
$: customHeaderContent = $$slots["panel-header-content"]
|
||||
$: customTitleContent = $$slots["panel-title-content"]
|
||||
</script>
|
||||
|
||||
<div
|
||||
|
@ -33,7 +34,11 @@
|
|||
<Icon name={icon} />
|
||||
{/if}
|
||||
<div class="title">
|
||||
<Body size="S">{title}</Body>
|
||||
{#if customTitleContent}
|
||||
<slot name="panel-title-content" />
|
||||
{:else}
|
||||
<Body size="S">{title || ""}</Body>
|
||||
{/if}
|
||||
</div>
|
||||
{#if showAddButton}
|
||||
<div class="add-button" on:click={onClickAddButton}>
|
||||
|
@ -134,4 +139,7 @@
|
|||
.custom-content-wrap {
|
||||
border-bottom: var(--border-light);
|
||||
}
|
||||
.title {
|
||||
display: flex;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -23,6 +23,7 @@ import BasicColumnEditor from "./controls/ColumnEditor/BasicColumnEditor.svelte"
|
|||
import GridColumnEditor from "./controls/ColumnEditor/GridColumnEditor.svelte"
|
||||
import BarButtonList from "./controls/BarButtonList.svelte"
|
||||
import FieldConfiguration from "./controls/FieldConfiguration/FieldConfiguration.svelte"
|
||||
import ButtonConfiguration from "./controls/ButtonConfiguration/ButtonConfiguration.svelte"
|
||||
import RelationshipFilterEditor from "./controls/RelationshipFilterEditor.svelte"
|
||||
|
||||
const componentMap = {
|
||||
|
@ -48,6 +49,7 @@ const componentMap = {
|
|||
"filter/relationship": RelationshipFilterEditor,
|
||||
url: URLSelect,
|
||||
fieldConfiguration: FieldConfiguration,
|
||||
buttonConfiguration: ButtonConfiguration,
|
||||
columns: ColumnEditor,
|
||||
"columns/basic": BasicColumnEditor,
|
||||
"columns/grid": GridColumnEditor,
|
||||
|
|
|
@ -0,0 +1,134 @@
|
|||
<script>
|
||||
import DraggableList from "../DraggableList/DraggableList.svelte"
|
||||
import ButtonSetting from "./ButtonSetting.svelte"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import { store } from "builderStore"
|
||||
import { Helpers } from "@budibase/bbui"
|
||||
|
||||
export let componentBindings
|
||||
export let bindings
|
||||
export let value
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let focusItem
|
||||
|
||||
$: buttonList = sanitizeValue(value) || []
|
||||
$: buttonCount = buttonList.length
|
||||
$: itemProps = {
|
||||
componentBindings: componentBindings || [],
|
||||
bindings,
|
||||
removeButton,
|
||||
canRemove: buttonCount > 1,
|
||||
}
|
||||
|
||||
const sanitizeValue = val => {
|
||||
return val?.map(button => {
|
||||
return button._component ? button : buildPseudoInstance(button)
|
||||
})
|
||||
}
|
||||
|
||||
const processItemUpdate = e => {
|
||||
const updatedField = e.detail
|
||||
const newButtonList = [...buttonList]
|
||||
const fieldIdx = newButtonList.findIndex(pSetting => {
|
||||
return pSetting._id === updatedField?._id
|
||||
})
|
||||
if (fieldIdx === -1) {
|
||||
newButtonList.push(updatedField)
|
||||
} else {
|
||||
newButtonList[fieldIdx] = updatedField
|
||||
}
|
||||
dispatch("change", newButtonList)
|
||||
}
|
||||
|
||||
const listUpdated = e => {
|
||||
dispatch("change", [...e.detail])
|
||||
}
|
||||
|
||||
const buildPseudoInstance = cfg => {
|
||||
return store.actions.components.createInstance(
|
||||
`@budibase/standard-components/button`,
|
||||
{
|
||||
_instanceName: Helpers.uuid(),
|
||||
text: cfg.text,
|
||||
type: cfg.type || "primary",
|
||||
},
|
||||
{}
|
||||
)
|
||||
}
|
||||
|
||||
const addButton = () => {
|
||||
const newButton = buildPseudoInstance({
|
||||
text: `Button ${buttonCount + 1}`,
|
||||
})
|
||||
dispatch("change", [...buttonList, newButton])
|
||||
focusItem = newButton._id
|
||||
}
|
||||
|
||||
const removeButton = id => {
|
||||
dispatch(
|
||||
"change",
|
||||
buttonList.filter(button => button._id !== id)
|
||||
)
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="button-configuration">
|
||||
{#if buttonCount}
|
||||
<DraggableList
|
||||
on:change={listUpdated}
|
||||
on:itemChange={processItemUpdate}
|
||||
items={buttonList}
|
||||
listItemKey={"_id"}
|
||||
listType={ButtonSetting}
|
||||
listTypeProps={itemProps}
|
||||
focus={focusItem}
|
||||
draggable={buttonCount > 1}
|
||||
/>
|
||||
|
||||
<div class="list-footer" on:click={addButton}>
|
||||
<div class="add-button">Add button</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.button-configuration :global(.spectrum-ActionButton) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.button-configuration :global(.list-wrap > li:last-child),
|
||||
.button-configuration :global(.list-wrap) {
|
||||
border-bottom-left-radius: unset;
|
||||
border-bottom-right-radius: unset;
|
||||
border-bottom: 0px;
|
||||
}
|
||||
|
||||
.list-footer {
|
||||
width: 100%;
|
||||
border-bottom-left-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
background-color: var(
|
||||
--spectrum-table-background-color,
|
||||
var(--spectrum-global-color-gray-50)
|
||||
);
|
||||
transition: background-color ease-in-out 130ms;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
border: 1px solid
|
||||
var(--spectrum-table-border-color, var(--spectrum-alias-border-color-mid));
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.add-button {
|
||||
margin: var(--spacing-s);
|
||||
}
|
||||
|
||||
.list-footer:hover {
|
||||
background-color: var(
|
||||
--spectrum-table-row-background-color-hover,
|
||||
var(--spectrum-alias-highlight-hover)
|
||||
);
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,64 @@
|
|||
<script>
|
||||
import EditComponentPopover from "../EditComponentPopover.svelte"
|
||||
import { Icon } from "@budibase/bbui"
|
||||
import { runtimeToReadableBinding } from "builderStore/dataBinding"
|
||||
import { isJSBinding } from "@budibase/string-templates"
|
||||
|
||||
export let item
|
||||
export let componentBindings
|
||||
export let bindings
|
||||
export let anchor
|
||||
export let removeButton
|
||||
export let canRemove
|
||||
|
||||
$: readableText = isJSBinding(item.text)
|
||||
? "(JavaScript function)"
|
||||
: runtimeToReadableBinding([...bindings, componentBindings], item.text)
|
||||
</script>
|
||||
|
||||
<div class="list-item-body">
|
||||
<div class="list-item-left">
|
||||
<EditComponentPopover
|
||||
{anchor}
|
||||
componentInstance={item}
|
||||
{componentBindings}
|
||||
{bindings}
|
||||
on:change
|
||||
/>
|
||||
<div class="field-label">{readableText || "Button"}</div>
|
||||
</div>
|
||||
<div class="list-item-right">
|
||||
<Icon
|
||||
disabled={!canRemove}
|
||||
size="S"
|
||||
name="Close"
|
||||
hoverable
|
||||
on:click={() => removeButton(item._id)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.field-label {
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
.list-item-body,
|
||||
.list-item-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-m);
|
||||
min-width: 0;
|
||||
}
|
||||
.list-item-body {
|
||||
margin-top: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.list-item-right :global(div.spectrum-Switch) {
|
||||
margin: 0px;
|
||||
}
|
||||
.list-item-body {
|
||||
justify-content: space-between;
|
||||
}
|
||||
</style>
|
|
@ -16,7 +16,11 @@
|
|||
<DrawerContent>
|
||||
<div class="container">
|
||||
<Layout noPadding gap="S">
|
||||
<Input bind:value={column.width} label="Width" placeholder="Auto" />
|
||||
<Input
|
||||
bind:value={column.width}
|
||||
label="Width (must include a unit like px or %)"
|
||||
placeholder="Auto"
|
||||
/>
|
||||
<Select
|
||||
label="Alignment"
|
||||
bind:value={column.align}
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
<script>
|
||||
import { getContextProviderComponents } from "builderStore/dataBinding"
|
||||
import {
|
||||
getContextProviderComponents,
|
||||
readableToRuntimeBinding,
|
||||
runtimeToReadableBinding,
|
||||
} from "builderStore/dataBinding"
|
||||
import {
|
||||
Button,
|
||||
Popover,
|
||||
|
@ -9,6 +13,11 @@
|
|||
Heading,
|
||||
Drawer,
|
||||
DrawerContent,
|
||||
Icon,
|
||||
Modal,
|
||||
ModalContent,
|
||||
CoreDropzone,
|
||||
notifications,
|
||||
} from "@budibase/bbui"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import { store, currentAsset } from "builderStore"
|
||||
|
@ -22,6 +31,8 @@
|
|||
import BindingBuilder from "components/integration/QueryBindingBuilder.svelte"
|
||||
import IntegrationQueryEditor from "components/integration/index.svelte"
|
||||
import { makePropSafe as safe } from "@budibase/string-templates"
|
||||
import ClientBindingPanel from "components/common/bindings/ClientBindingPanel.svelte"
|
||||
import { API } from "api"
|
||||
|
||||
export let value = {}
|
||||
export let otherSources
|
||||
|
@ -31,9 +42,13 @@
|
|||
|
||||
const dispatch = createEventDispatcher()
|
||||
const arrayTypes = ["attachment", "array"]
|
||||
|
||||
let anchorRight, dropdownRight
|
||||
let drawer
|
||||
let tmpQueryParams
|
||||
let tmpCustomData
|
||||
let customDataValid = true
|
||||
let modal
|
||||
|
||||
$: text = value?.label ?? "Choose an option"
|
||||
$: tables = $tablesStore.list.map(m => ({
|
||||
|
@ -125,6 +140,10 @@
|
|||
value: `{{ literal ${runtimeBinding} }}`,
|
||||
}
|
||||
})
|
||||
$: custom = {
|
||||
type: "custom",
|
||||
label: "JSON / CSV",
|
||||
}
|
||||
|
||||
const handleSelected = selected => {
|
||||
dispatch("change", selected)
|
||||
|
@ -151,6 +170,11 @@
|
|||
drawer.show()
|
||||
}
|
||||
|
||||
const openCustomDrawer = () => {
|
||||
tmpCustomData = runtimeToReadableBinding(bindings, value.data || "")
|
||||
drawer.show()
|
||||
}
|
||||
|
||||
const getQueryValue = queries => {
|
||||
return queries.find(q => q._id === value._id) || value
|
||||
}
|
||||
|
@ -162,6 +186,35 @@
|
|||
})
|
||||
drawer.hide()
|
||||
}
|
||||
|
||||
const saveCustomData = () => {
|
||||
handleSelected({
|
||||
...value,
|
||||
data: readableToRuntimeBinding(bindings, tmpCustomData),
|
||||
})
|
||||
drawer.hide()
|
||||
}
|
||||
|
||||
const promptForCSV = () => {
|
||||
drawer.hide()
|
||||
modal.show()
|
||||
}
|
||||
|
||||
const handleCSV = async e => {
|
||||
try {
|
||||
const csv = await e.detail[0]?.text()
|
||||
if (csv?.length) {
|
||||
const js = await API.csvToJson(csv)
|
||||
tmpCustomData = JSON.stringify(js)
|
||||
}
|
||||
modal.hide()
|
||||
saveCustomData()
|
||||
} catch (error) {
|
||||
notifications.error("Failed to parse CSV")
|
||||
modal.hide()
|
||||
drawer.show()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="container" bind:this={anchorRight}>
|
||||
|
@ -172,7 +225,9 @@
|
|||
on:click={dropdownRight.show}
|
||||
/>
|
||||
{#if value?.type === "query"}
|
||||
<i class="ri-settings-5-line" on:click={openQueryParamsDrawer} />
|
||||
<div class="icon">
|
||||
<Icon hoverable name="Settings" on:click={openQueryParamsDrawer} />
|
||||
</div>
|
||||
<Drawer title={"Query Bindings"} bind:this={drawer}>
|
||||
<Button slot="buttons" cta on:click={saveQueryParams}>Save</Button>
|
||||
<DrawerContent slot="body">
|
||||
|
@ -198,6 +253,29 @@
|
|||
</DrawerContent>
|
||||
</Drawer>
|
||||
{/if}
|
||||
{#if value?.type === "custom"}
|
||||
<div class="icon">
|
||||
<Icon hoverable name="Settings" on:click={openCustomDrawer} />
|
||||
</div>
|
||||
<Drawer title="Custom data" bind:this={drawer}>
|
||||
<div slot="buttons" style="display:contents">
|
||||
<Button primary on:click={promptForCSV}>Load CSV</Button>
|
||||
<Button cta on:click={saveCustomData} disabled={!customDataValid}>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
<div slot="description">Provide a JSON array to use as data</div>
|
||||
<ClientBindingPanel
|
||||
slot="body"
|
||||
bind:valid={customDataValid}
|
||||
value={tmpCustomData}
|
||||
on:change={event => (tmpCustomData = event.detail)}
|
||||
{bindings}
|
||||
allowJS
|
||||
allowHelpers
|
||||
/>
|
||||
</Drawer>
|
||||
{/if}
|
||||
</div>
|
||||
<Popover bind:this={dropdownRight} anchor={anchorRight}>
|
||||
<div class="dropdown">
|
||||
|
@ -285,20 +363,27 @@
|
|||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
{#if otherSources?.length}
|
||||
<Divider />
|
||||
<div class="title">
|
||||
<Heading size="XS">Other</Heading>
|
||||
</div>
|
||||
<ul>
|
||||
<Divider />
|
||||
<div class="title">
|
||||
<Heading size="XS">Other</Heading>
|
||||
</div>
|
||||
<ul>
|
||||
<li on:click={() => handleSelected(custom)}>{custom.label}</li>
|
||||
{#if otherSources?.length}
|
||||
{#each otherSources as source}
|
||||
<li on:click={() => handleSelected(source)}>{source.label}</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
{/if}
|
||||
</ul>
|
||||
</div>
|
||||
</Popover>
|
||||
|
||||
<Modal bind:this={modal}>
|
||||
<ModalContent title="Load CSV" showConfirmButton={false}>
|
||||
<CoreDropzone compact extensions=".csv" on:change={handleCSV} />
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
|
||||
<style>
|
||||
.container {
|
||||
display: flex;
|
||||
|
@ -340,16 +425,7 @@
|
|||
background-color: var(--spectrum-global-color-gray-200);
|
||||
}
|
||||
|
||||
i {
|
||||
margin-left: 5px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
i:hover {
|
||||
transform: scale(1.1);
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
.icon {
|
||||
margin-left: 8px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
<script>
|
||||
import { Icon } from "@budibase/bbui"
|
||||
import { dndzone } from "svelte-dnd-action"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import { generate } from "shortid"
|
||||
import { setContext } from "svelte"
|
||||
import { writable } from "svelte/store"
|
||||
import { writable, get } from "svelte/store"
|
||||
import DragHandle from "./drag-handle.svelte"
|
||||
|
||||
export let items = []
|
||||
export let showHandle = true
|
||||
|
@ -12,6 +12,7 @@
|
|||
export let listTypeProps = {}
|
||||
export let listItemKey
|
||||
export let draggable = true
|
||||
export let focus
|
||||
|
||||
let store = writable({
|
||||
selected: null,
|
||||
|
@ -27,6 +28,10 @@
|
|||
|
||||
setContext("draggable", store)
|
||||
|
||||
$: if (focus && store) {
|
||||
get(store).actions.select(focus)
|
||||
}
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const flipDurationMs = 150
|
||||
|
||||
|
@ -82,13 +87,16 @@
|
|||
>
|
||||
{#each draggableItems as draggable (draggable.id)}
|
||||
<li
|
||||
on:mousedown={() => {
|
||||
get(store).actions.select()
|
||||
}}
|
||||
bind:this={anchors[draggable.id]}
|
||||
class:highlighted={draggable.id === $store.selected}
|
||||
>
|
||||
<div class="left-content">
|
||||
{#if showHandle}
|
||||
<div class="handle" aria-label="drag-handle">
|
||||
<Icon name="DragHandle" size="XL" />
|
||||
<div class="handle">
|
||||
<DragHandle />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
@ -142,8 +150,9 @@
|
|||
border-top-right-radius: 4px;
|
||||
}
|
||||
.list-wrap > li:last-child {
|
||||
border-top-left-radius: var(--spectrum-table-regular-border-radius);
|
||||
border-top-right-radius: var(--spectrum-table-regular-border-radius);
|
||||
border-bottom-left-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
border-bottom: 0px;
|
||||
}
|
||||
.right-content {
|
||||
flex: 1;
|
||||
|
@ -153,4 +162,15 @@
|
|||
padding-left: var(--spacing-s);
|
||||
padding-right: var(--spacing-s);
|
||||
}
|
||||
.handle {
|
||||
display: flex;
|
||||
height: var(--spectrum-global-dimension-size-150);
|
||||
}
|
||||
.handle :global(svg) {
|
||||
fill: var(--spectrum-global-color-gray-500);
|
||||
margin-right: var(--spacing-m);
|
||||
margin-left: 2px;
|
||||
width: var(--spectrum-global-dimension-size-65);
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,31 @@
|
|||
<svg
|
||||
class="drag-handle spectrum-Icon spectrum-Icon--sizeS"
|
||||
focusable="false"
|
||||
aria-hidden="true"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="m1,11c0.55228,0 1,-0.4477 1,-1c0,-0.5523 -0.44772,-1 -1,-1c-0.55228,0 -1,0.4477 -1,1c0,0.5523 0.44772,1 1,1z"
|
||||
/>
|
||||
<path
|
||||
d="m1,8c0.55228,0 1,-0.4477 1,-1c0,-0.55228 -0.44772,-1 -1,-1c-0.55228,0 -1,0.44772 -1,1c0,0.5523 0.44772,1 1,1z"
|
||||
/>
|
||||
<path
|
||||
d="m1,5c0.55228,0 1,-0.44772 1,-1c0,-0.55228 -0.44772,-1 -1,-1c-0.55228,0 -1,0.44772 -1,1c0,0.55228 0.44772,1 1,1z"
|
||||
/>
|
||||
<path
|
||||
d="m1,2c0.55228,0 1,-0.44772 1,-1c0,-0.55228 -0.44772,-1 -1,-1c-0.55228,0 -1,0.44772 -1,1c0,0.55228 0.44772,1 1,1z"
|
||||
/>
|
||||
<path
|
||||
d="m4,11c0.5523,0 1,-0.4477 1,-1c0,-0.5523 -0.4477,-1 -1,-1c-0.55228,0 -1,0.4477 -1,1c0,0.5523 0.44772,1 1,1z"
|
||||
/>
|
||||
<path
|
||||
d="m4,8c0.5523,0 1,-0.4477 1,-1c0,-0.55228 -0.4477,-1 -1,-1c-0.55228,0 -1,0.44772 -1,1c0,0.5523 0.44772,1 1,1z"
|
||||
/>
|
||||
<path
|
||||
d="m4,5c0.5523,0 1,-0.44772 1,-1c0,-0.55228 -0.4477,-1 -1,-1c-0.55228,0 -1,0.44772 -1,1c0,0.55228 0.44772,1 1,1z"
|
||||
/>
|
||||
<path
|
||||
d="m4,2c0.5523,0 1,-0.44772 1,-1c0,-0.55228 -0.4477,-1 -1,-1c-0.55228,0 -1,0.44772 -1,1c0,0.55228 0.44772,1 1,1z"
|
||||
/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.2 KiB |
|
@ -3,31 +3,35 @@
|
|||
import { store } from "builderStore"
|
||||
import { cloneDeep } from "lodash/fp"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import ComponentSettingsSection from "../../../../../pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Component/ComponentSettingsSection.svelte"
|
||||
import ComponentSettingsSection from "../../../../pages/builder/app/[application]/design/[screenId]/[componentId]/_components/Component/ComponentSettingsSection.svelte"
|
||||
import { getContext } from "svelte"
|
||||
|
||||
export let anchor
|
||||
export let field
|
||||
export let componentInstance
|
||||
export let componentBindings
|
||||
export let bindings
|
||||
export let parseSettings
|
||||
|
||||
const draggable = getContext("draggable")
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let popover
|
||||
let drawers = []
|
||||
let pseudoComponentInstance
|
||||
let open = false
|
||||
|
||||
$: if (open && $draggable.selected && $draggable.selected != field._id) {
|
||||
// Auto hide the component when another item is selected
|
||||
$: if (open && $draggable.selected != componentInstance._id) {
|
||||
popover.hide()
|
||||
}
|
||||
|
||||
$: if (field) {
|
||||
pseudoComponentInstance = field
|
||||
// Open automatically if the component is marked as selected
|
||||
$: if (!open && $draggable.selected === componentInstance._id && popover) {
|
||||
popover.show()
|
||||
open = true
|
||||
}
|
||||
|
||||
$: componentDef = store.actions.components.getDefinition(
|
||||
pseudoComponentInstance._component
|
||||
componentInstance._component
|
||||
)
|
||||
$: parsedComponentDef = processComponentDefinitionSettings(componentDef)
|
||||
|
||||
|
@ -36,17 +40,16 @@
|
|||
return {}
|
||||
}
|
||||
const clone = cloneDeep(componentDef)
|
||||
const updatedSettings = clone.settings
|
||||
.filter(setting => setting.key !== "field")
|
||||
.map(setting => {
|
||||
return { ...setting, nested: true }
|
||||
})
|
||||
clone.settings = updatedSettings
|
||||
|
||||
if (typeof parseSettings === "function") {
|
||||
clone.settings = parseSettings(clone.settings)
|
||||
}
|
||||
|
||||
return clone
|
||||
}
|
||||
|
||||
const updateSetting = async (setting, value) => {
|
||||
const nestedComponentInstance = cloneDeep(pseudoComponentInstance)
|
||||
const nestedComponentInstance = cloneDeep(componentInstance)
|
||||
|
||||
const patchFn = store.actions.components.updateComponentSetting(
|
||||
setting.key,
|
||||
|
@ -54,12 +57,26 @@
|
|||
)
|
||||
patchFn(nestedComponentInstance)
|
||||
|
||||
const update = {
|
||||
...nestedComponentInstance,
|
||||
active: pseudoComponentInstance.active,
|
||||
dispatch("change", nestedComponentInstance)
|
||||
}
|
||||
|
||||
const customPositionHandler = (anchorBounds, eleBounds, cfg) => {
|
||||
let { left, top } = cfg
|
||||
let percentageOffset = 30
|
||||
// left-outside
|
||||
left = anchorBounds.left - eleBounds.width - 18
|
||||
|
||||
// shift up from the anchor, if space allows
|
||||
let offsetPos = Math.floor(eleBounds.height / 100) * percentageOffset
|
||||
let defaultTop = anchorBounds.top - offsetPos
|
||||
|
||||
if (window.innerHeight - defaultTop < eleBounds.height) {
|
||||
top = window.innerHeight - eleBounds.height - 5
|
||||
} else {
|
||||
top = anchorBounds.top - offsetPos
|
||||
}
|
||||
|
||||
dispatch("change", update)
|
||||
return { ...cfg, left, top }
|
||||
}
|
||||
</script>
|
||||
|
||||
|
@ -79,11 +96,11 @@
|
|||
bind:this={popover}
|
||||
on:open={() => {
|
||||
drawers = []
|
||||
$draggable.actions.select(field._id)
|
||||
$draggable.actions.select(componentInstance._id)
|
||||
}}
|
||||
on:close={() => {
|
||||
open = false
|
||||
if ($draggable.selected == field._id) {
|
||||
if ($draggable.selected == componentInstance._id) {
|
||||
$draggable.actions.select()
|
||||
}
|
||||
}}
|
||||
|
@ -92,33 +109,13 @@
|
|||
showPopover={drawers.length == 0}
|
||||
clickOutsideOverride={drawers.length > 0}
|
||||
maxHeight={600}
|
||||
handlePostionUpdate={(anchorBounds, eleBounds, cfg) => {
|
||||
let { left, top } = cfg
|
||||
let percentageOffset = 30
|
||||
// left-outside
|
||||
left = anchorBounds.left - eleBounds.width - 18
|
||||
|
||||
// shift up from the anchor, if space allows
|
||||
let offsetPos = Math.floor(eleBounds.height / 100) * percentageOffset
|
||||
let defaultTop = anchorBounds.top - offsetPos
|
||||
|
||||
if (window.innerHeight - defaultTop < eleBounds.height) {
|
||||
top = window.innerHeight - eleBounds.height - 5
|
||||
} else {
|
||||
top = anchorBounds.top - offsetPos
|
||||
}
|
||||
|
||||
return { ...cfg, left, top }
|
||||
}}
|
||||
handlePostionUpdate={customPositionHandler}
|
||||
>
|
||||
<span class="popover-wrap">
|
||||
<Layout noPadding noGap>
|
||||
<div class="type-icon">
|
||||
<Icon name={parsedComponentDef.icon} />
|
||||
<span>{field.field}</span>
|
||||
</div>
|
||||
<slot name="header" />
|
||||
<ComponentSettingsSection
|
||||
componentInstance={pseudoComponentInstance}
|
||||
{componentInstance}
|
||||
componentDefinition={parsedComponentDef}
|
||||
isScreen={false}
|
||||
onUpdateSetting={updateSetting}
|
||||
|
@ -141,20 +138,4 @@
|
|||
.popover-wrap {
|
||||
background-color: var(--spectrum-alias-background-color-primary);
|
||||
}
|
||||
.type-icon {
|
||||
display: flex;
|
||||
gap: var(--spacing-m);
|
||||
margin: var(--spacing-xl);
|
||||
margin-bottom: 0px;
|
||||
height: var(--spectrum-alias-item-height-m);
|
||||
padding: 0px var(--spectrum-alias-item-padding-m);
|
||||
border-width: var(--spectrum-actionbutton-border-size);
|
||||
border-radius: var(--spectrum-alias-border-radius-regular);
|
||||
border: 1px solid
|
||||
var(
|
||||
--spectrum-actionbutton-m-border-color,
|
||||
var(--spectrum-alias-border-color)
|
||||
);
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
|
@ -7,7 +7,7 @@
|
|||
getComponentBindableProperties,
|
||||
} from "builderStore/dataBinding"
|
||||
import { currentAsset } from "builderStore"
|
||||
import DraggableList from "../DraggableList.svelte"
|
||||
import DraggableList from "../DraggableList/DraggableList.svelte"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import { store, selectedScreen } from "builderStore"
|
||||
import FieldSetting from "./FieldSetting.svelte"
|
||||
|
@ -50,7 +50,7 @@
|
|||
updateSanitsedFields(sanitisedValue)
|
||||
unconfigured = buildUnconfiguredOptions(schema, sanitisedFields)
|
||||
fieldList = [...sanitisedFields, ...unconfigured]
|
||||
.map(buildSudoInstance)
|
||||
.map(buildPseudoInstance)
|
||||
.filter(x => x != null)
|
||||
}
|
||||
|
||||
|
@ -104,7 +104,7 @@
|
|||
})
|
||||
}
|
||||
|
||||
const buildSudoInstance = instance => {
|
||||
const buildPseudoInstance = instance => {
|
||||
if (instance._component) {
|
||||
return instance
|
||||
}
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
<script>
|
||||
import EditFieldPopover from "./EditFieldPopover.svelte"
|
||||
import { Toggle } from "@budibase/bbui"
|
||||
import EditComponentPopover from "../EditComponentPopover.svelte"
|
||||
import { Toggle, Icon } from "@budibase/bbui"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import { cloneDeep } from "lodash/fp"
|
||||
import { store } from "builderStore"
|
||||
import { runtimeToReadableBinding } from "builderStore/dataBinding"
|
||||
import { isJSBinding } from "@budibase/string-templates"
|
||||
|
||||
export let item
|
||||
export let componentBindings
|
||||
|
@ -16,18 +19,43 @@
|
|||
dispatch("change", { ...cloneDeep(item), active: e.detail })
|
||||
}
|
||||
}
|
||||
const getReadableText = () => {
|
||||
if (item.label) {
|
||||
return isJSBinding(item.label)
|
||||
? "(JavaScript function)"
|
||||
: runtimeToReadableBinding([...bindings, componentBindings], item.label)
|
||||
}
|
||||
return item.field
|
||||
}
|
||||
|
||||
const parseSettings = settings => {
|
||||
return settings
|
||||
.filter(setting => setting.key !== "field")
|
||||
.map(setting => {
|
||||
return { ...setting, nested: true }
|
||||
})
|
||||
}
|
||||
|
||||
$: readableText = getReadableText(item)
|
||||
$: componentDef = store.actions.components.getDefinition(item._component)
|
||||
</script>
|
||||
|
||||
<div class="list-item-body">
|
||||
<div class="list-item-left">
|
||||
<EditFieldPopover
|
||||
<EditComponentPopover
|
||||
{anchor}
|
||||
field={item}
|
||||
componentInstance={item}
|
||||
{componentBindings}
|
||||
{bindings}
|
||||
{parseSettings}
|
||||
on:change
|
||||
/>
|
||||
<div class="field-label">{item.label || item.field}</div>
|
||||
>
|
||||
<div slot="header" class="type-icon">
|
||||
<Icon name={componentDef.icon} />
|
||||
<span>{item.field}</span>
|
||||
</div>
|
||||
</EditComponentPopover>
|
||||
<div class="field-label">{readableText}</div>
|
||||
</div>
|
||||
<div class="list-item-right">
|
||||
<Toggle on:change={onToggle(item)} text="" value={item.active} thin />
|
||||
|
@ -53,4 +81,20 @@
|
|||
.list-item-body {
|
||||
justify-content: space-between;
|
||||
}
|
||||
.type-icon {
|
||||
display: flex;
|
||||
gap: var(--spacing-m);
|
||||
margin: var(--spacing-xl);
|
||||
margin-bottom: 0px;
|
||||
height: var(--spectrum-alias-item-height-m);
|
||||
padding: 0px var(--spectrum-alias-item-padding-m);
|
||||
border-width: var(--spectrum-actionbutton-border-size);
|
||||
border-radius: var(--spectrum-alias-border-radius-regular);
|
||||
border: 1px solid
|
||||
var(
|
||||
--spectrum-actionbutton-m-border-color,
|
||||
var(--spectrum-alias-border-color)
|
||||
);
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
} from "builderStore/dataBinding"
|
||||
import { currentAsset } from "builderStore"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import { UNSORTABLE_TYPES } from "constants"
|
||||
import { canBeSortColumn } from "@budibase/shared-core"
|
||||
|
||||
export let componentInstance = {}
|
||||
export let value = ""
|
||||
|
@ -20,7 +20,7 @@
|
|||
|
||||
const getSortableFields = schema => {
|
||||
return Object.entries(schema || {})
|
||||
.filter(entry => !UNSORTABLE_TYPES.includes(entry[1].type))
|
||||
.filter(entry => canBeSortColumn(entry[1].type))
|
||||
.map(entry => entry[0])
|
||||
}
|
||||
|
||||
|
|
|
@ -196,8 +196,36 @@
|
|||
}
|
||||
}
|
||||
|
||||
const validateQuery = async () => {
|
||||
const forbiddenBindings = /{{\s?user(\.(\w|\$)*\s?|\s?)}}/g
|
||||
const bindingError = new Error(
|
||||
"'user' is a protected binding and cannot be used"
|
||||
)
|
||||
|
||||
if (forbiddenBindings.test(url)) {
|
||||
throw bindingError
|
||||
}
|
||||
|
||||
if (forbiddenBindings.test(query.fields.requestBody ?? "")) {
|
||||
throw bindingError
|
||||
}
|
||||
|
||||
Object.values(requestBindings).forEach(bindingValue => {
|
||||
if (forbiddenBindings.test(bindingValue)) {
|
||||
throw bindingError
|
||||
}
|
||||
})
|
||||
|
||||
Object.values(query.fields.headers).forEach(headerValue => {
|
||||
if (forbiddenBindings.test(headerValue)) {
|
||||
throw bindingError
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function runQuery() {
|
||||
try {
|
||||
await validateQuery()
|
||||
response = await queries.preview(buildQuery())
|
||||
if (response.rows.length === 0) {
|
||||
notifications.info("Request did not return any data")
|
||||
|
|
|
@ -34,8 +34,6 @@ export const UNEDITABLE_USER_FIELDS = [
|
|||
"lastName",
|
||||
]
|
||||
|
||||
export const UNSORTABLE_TYPES = ["formula", "attachment", "array", "link"]
|
||||
|
||||
export const LAYOUT_NAMES = {
|
||||
MASTER: {
|
||||
PRIVATE: "layout_private_master",
|
||||
|
|
|
@ -114,8 +114,9 @@
|
|||
query: {
|
||||
appId: query || !filterByAppAccess ? null : prodAppId,
|
||||
email: query,
|
||||
paginated: query || !filterByAppAccess ? null : false,
|
||||
},
|
||||
limit: 50,
|
||||
paginate: query || !filterByAppAccess ? null : false,
|
||||
})
|
||||
await usersFetch.refresh()
|
||||
|
||||
|
|
|
@ -23,5 +23,7 @@
|
|||
</script>
|
||||
|
||||
{#key $params.datasourceId}
|
||||
<slot />
|
||||
{#if $datasources.selected}
|
||||
<slot />
|
||||
{/if}
|
||||
{/key}
|
||||
|
|
|
@ -16,8 +16,7 @@
|
|||
let selectedPanel = null
|
||||
let panelOptions = []
|
||||
|
||||
// datasources.selected can return null temporarily on datasource deletion
|
||||
$: datasource = $datasources.selected || {}
|
||||
$: datasource = $datasources.selected
|
||||
|
||||
$: getOptions(datasource)
|
||||
|
||||
|
|
|
@ -53,7 +53,8 @@
|
|||
}
|
||||
.alert-wrap {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
flex: 0 0 auto;
|
||||
margin: -28px -40px 14px -40px;
|
||||
}
|
||||
.alert-wrap :global(> *) {
|
||||
flex: 1;
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
<script>
|
||||
import Panel from "components/design/Panel.svelte"
|
||||
import { store, selectedComponent, selectedScreen } from "builderStore"
|
||||
import { getComponentText } from "builderStore/componentUtils"
|
||||
import ComponentSettingsSection from "./ComponentSettingsSection.svelte"
|
||||
import DesignSection from "./DesignSection.svelte"
|
||||
import CustomStylesSection from "./CustomStylesSection.svelte"
|
||||
import ConditionalUISection from "./ConditionalUISection.svelte"
|
||||
import { notifications } from "@budibase/bbui"
|
||||
|
||||
import {
|
||||
getBindableProperties,
|
||||
|
@ -13,6 +15,14 @@
|
|||
import { ActionButton } from "@budibase/bbui"
|
||||
import { capitalise } from "helpers"
|
||||
|
||||
const onUpdateName = async value => {
|
||||
try {
|
||||
await store.actions.components.updateSetting("_instanceName", value)
|
||||
} catch (error) {
|
||||
notifications.error("Error updating component name")
|
||||
}
|
||||
}
|
||||
|
||||
$: componentInstance = $selectedComponent
|
||||
$: componentDefinition = store.actions.components.getDefinition(
|
||||
$selectedComponent?._component
|
||||
|
@ -39,6 +49,22 @@
|
|||
{#if $selectedComponent}
|
||||
{#key $selectedComponent._id}
|
||||
<Panel {title} icon={componentDefinition?.icon} borderLeft wide>
|
||||
<span class="panel-title-content" slot="panel-title-content">
|
||||
<input
|
||||
class="input"
|
||||
value={title}
|
||||
{title}
|
||||
placeholder={getComponentText(componentInstance)}
|
||||
on:keypress={e => {
|
||||
if (e.key.toLowerCase() === "enter") {
|
||||
e.target.blur()
|
||||
}
|
||||
}}
|
||||
on:change={e => {
|
||||
onUpdateName(e.target.value)
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
<span slot="panel-header-content">
|
||||
<div class="settings-tabs">
|
||||
{#each tabs as tab}
|
||||
|
@ -65,7 +91,12 @@
|
|||
/>
|
||||
{/if}
|
||||
{#if section == "styles"}
|
||||
<DesignSection {componentInstance} {componentDefinition} {bindings} />
|
||||
<DesignSection
|
||||
{componentInstance}
|
||||
{componentBindings}
|
||||
{componentDefinition}
|
||||
{bindings}
|
||||
/>
|
||||
<CustomStylesSection
|
||||
{componentInstance}
|
||||
{componentDefinition}
|
||||
|
@ -90,4 +121,24 @@
|
|||
padding: 0 var(--spacing-l);
|
||||
padding-bottom: var(--spacing-l);
|
||||
}
|
||||
.input {
|
||||
color: inherit;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.panel-title-content {
|
||||
display: contents;
|
||||
}
|
||||
.input:focus {
|
||||
outline: none;
|
||||
}
|
||||
input::placeholder {
|
||||
color: var(--spectrum-global-color-gray-600);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<script>
|
||||
import { isEmpty } from "lodash/fp"
|
||||
import { Input, DetailSummary, notifications } from "@budibase/bbui"
|
||||
import { helpers } from "@budibase/shared-core"
|
||||
import { DetailSummary, notifications } from "@budibase/bbui"
|
||||
import { store } from "builderStore"
|
||||
import PropertyControl from "components/design/settings/controls/PropertyControl.svelte"
|
||||
import ResetFieldsButton from "components/design/settings/controls/ResetFieldsButton.svelte"
|
||||
|
@ -16,19 +16,32 @@
|
|||
export let isScreen = false
|
||||
export let onUpdateSetting
|
||||
export let showSectionTitle = true
|
||||
export let showInstanceName = true
|
||||
export let tag
|
||||
|
||||
$: sections = getSections(componentInstance, componentDefinition, isScreen)
|
||||
$: sections = getSections(
|
||||
componentInstance,
|
||||
componentDefinition,
|
||||
isScreen,
|
||||
tag
|
||||
)
|
||||
|
||||
const getSections = (instance, definition, isScreen) => {
|
||||
const getSections = (instance, definition, isScreen, tag) => {
|
||||
const settings = definition?.settings ?? []
|
||||
const generalSettings = settings.filter(setting => !setting.section)
|
||||
const customSections = settings.filter(setting => setting.section)
|
||||
const generalSettings = settings.filter(
|
||||
setting => !setting.section && setting.tag === tag
|
||||
)
|
||||
const customSections = settings.filter(
|
||||
setting => setting.section && setting.tag === tag
|
||||
)
|
||||
let sections = [
|
||||
{
|
||||
name: "General",
|
||||
settings: generalSettings,
|
||||
},
|
||||
...(generalSettings?.length
|
||||
? [
|
||||
{
|
||||
name: "General",
|
||||
settings: generalSettings,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(customSections || []),
|
||||
]
|
||||
|
||||
|
@ -70,41 +83,43 @@
|
|||
}
|
||||
|
||||
const shouldDisplay = (instance, setting) => {
|
||||
// Parse dependant settings
|
||||
if (setting.dependsOn) {
|
||||
let dependantSetting = setting.dependsOn
|
||||
let dependantValue = null
|
||||
let invert = !!setting.dependsOn.invert
|
||||
if (typeof setting.dependsOn === "object") {
|
||||
dependantSetting = setting.dependsOn.setting
|
||||
dependantValue = setting.dependsOn.value
|
||||
let dependsOn = setting.dependsOn
|
||||
if (dependsOn && !Array.isArray(dependsOn)) {
|
||||
dependsOn = [dependsOn]
|
||||
}
|
||||
if (!dependsOn?.length) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Ensure all conditions are met
|
||||
return dependsOn.every(condition => {
|
||||
let dependantSetting = condition
|
||||
let dependantValues = null
|
||||
let invert = !!condition.invert
|
||||
if (typeof condition === "object") {
|
||||
dependantSetting = condition.setting
|
||||
dependantValues = condition.value
|
||||
}
|
||||
if (!dependantSetting) {
|
||||
return false
|
||||
}
|
||||
|
||||
// If no specific value is depended upon, check if a value exists at all
|
||||
// for the dependent setting
|
||||
if (dependantValue == null) {
|
||||
const currentValue = instance[dependantSetting]
|
||||
if (currentValue === false) {
|
||||
return false
|
||||
}
|
||||
if (currentValue === true) {
|
||||
return true
|
||||
}
|
||||
return !isEmpty(currentValue)
|
||||
// Ensure values is an array
|
||||
if (!Array.isArray(dependantValues)) {
|
||||
dependantValues = [dependantValues]
|
||||
}
|
||||
|
||||
// Otherwise check the value matches
|
||||
if (invert) {
|
||||
return instance[dependantSetting] !== dependantValue
|
||||
} else {
|
||||
return instance[dependantSetting] === dependantValue
|
||||
}
|
||||
}
|
||||
|
||||
return typeof setting.visible == "boolean" ? setting.visible : true
|
||||
// If inverting, we want to ensure that we don't have any matches.
|
||||
// If not inverting, we want to ensure that we do have any matches.
|
||||
const currentVal = helpers.deepGet(instance, dependantSetting)
|
||||
const anyMatches = dependantValues.some(dependantVal => {
|
||||
if (dependantVal == null) {
|
||||
return currentVal != null && currentVal !== false && currentVal !== ""
|
||||
}
|
||||
return dependantVal === currentVal
|
||||
})
|
||||
return anyMatches !== invert
|
||||
})
|
||||
}
|
||||
|
||||
const canRenderControl = (instance, setting, isScreen) => {
|
||||
|
@ -125,28 +140,19 @@
|
|||
{#if section.visible}
|
||||
<DetailSummary
|
||||
name={showSectionTitle ? section.name : ""}
|
||||
collapsible={false}
|
||||
show={section.collapsed !== true}
|
||||
>
|
||||
{#if section.info}
|
||||
<div class="section-info">
|
||||
<InfoDisplay body={section.info} />
|
||||
</div>
|
||||
{:else if idx === 0 && section.name === "General" && componentDefinition.info}
|
||||
{:else if idx === 0 && section.name === "General" && componentDefinition?.info && !tag}
|
||||
<InfoDisplay
|
||||
title={componentDefinition.name}
|
||||
body={componentDefinition.info}
|
||||
/>
|
||||
{/if}
|
||||
<div class="settings">
|
||||
{#if idx === 0 && !componentInstance._component.endsWith("/layout") && !isScreen && showInstanceName}
|
||||
<PropertyControl
|
||||
control={Input}
|
||||
label="Name"
|
||||
key="_instanceName"
|
||||
value={componentInstance._instanceName}
|
||||
onChange={val => updateSetting({ key: "_instanceName" }, val)}
|
||||
/>
|
||||
{/if}
|
||||
{#each section.settings as setting (setting.key)}
|
||||
{#if setting.visible}
|
||||
<PropertyControl
|
||||
|
@ -189,7 +195,7 @@
|
|||
</DetailSummary>
|
||||
{/if}
|
||||
{/each}
|
||||
{#if componentDefinition?.block}
|
||||
{#if componentDefinition?.block && !tag}
|
||||
<DetailSummary name="Eject" collapsible={false}>
|
||||
<EjectBlockButton />
|
||||
</DetailSummary>
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
<script>
|
||||
import StyleSection from "./StyleSection.svelte"
|
||||
import * as ComponentStyles from "./componentStyles"
|
||||
import ComponentSettingsSection from "./ComponentSettingsSection.svelte"
|
||||
|
||||
export let componentDefinition
|
||||
export let componentInstance
|
||||
export let bindings
|
||||
export let componentBindings
|
||||
|
||||
const getStyles = def => {
|
||||
if (!def?.styles?.length) {
|
||||
|
@ -22,6 +24,19 @@
|
|||
$: styles = getStyles(componentDefinition)
|
||||
</script>
|
||||
|
||||
<!--
|
||||
Load any general settings or sections tagged as "style"
|
||||
-->
|
||||
<ComponentSettingsSection
|
||||
{componentInstance}
|
||||
{componentDefinition}
|
||||
isScreen={false}
|
||||
showInstanceName={false}
|
||||
{bindings}
|
||||
{componentBindings}
|
||||
tag="style"
|
||||
/>
|
||||
|
||||
{#if styles?.length > 0}
|
||||
{#each styles as style}
|
||||
<StyleSection
|
||||
|
|
|
@ -36,6 +36,7 @@
|
|||
"heading",
|
||||
"text",
|
||||
"button",
|
||||
"buttongroup",
|
||||
"tag",
|
||||
"spectrumcard",
|
||||
"cardstat",
|
||||
|
|
|
@ -2,14 +2,16 @@
|
|||
import { store, userSelectedResourceMap } from "builderStore"
|
||||
import ComponentDropdownMenu from "./ComponentDropdownMenu.svelte"
|
||||
import NavItem from "components/common/NavItem.svelte"
|
||||
import { capitalise } from "helpers"
|
||||
import { notifications } from "@budibase/bbui"
|
||||
import {
|
||||
selectedComponentPath,
|
||||
selectedComponent,
|
||||
selectedScreen,
|
||||
} from "builderStore"
|
||||
import { findComponentPath } from "builderStore/componentUtils"
|
||||
import {
|
||||
findComponentPath,
|
||||
getComponentText,
|
||||
} from "builderStore/componentUtils"
|
||||
import { get } from "svelte/store"
|
||||
import { dndStore } from "./dndStore"
|
||||
|
||||
|
@ -35,16 +37,6 @@
|
|||
return false
|
||||
}
|
||||
|
||||
const getComponentText = component => {
|
||||
if (component._instanceName) {
|
||||
return component._instanceName
|
||||
}
|
||||
const type =
|
||||
component._component.replace("@budibase/standard-components/", "") ||
|
||||
"component"
|
||||
return capitalise(type)
|
||||
}
|
||||
|
||||
const getComponentIcon = component => {
|
||||
const def = store.actions.components.getDefinition(component?._component)
|
||||
return def?.icon
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
import { capitalise } from "helpers"
|
||||
import { goto } from "@roxi/routify"
|
||||
|
||||
let mode
|
||||
let pendingScreen
|
||||
|
||||
// Modal refs
|
||||
|
@ -100,14 +101,15 @@
|
|||
}
|
||||
|
||||
// Handler for NewScreenModal
|
||||
export const show = mode => {
|
||||
export const show = newMode => {
|
||||
mode = newMode
|
||||
selectedTemplates = null
|
||||
blankScreenUrl = null
|
||||
screenMode = mode
|
||||
pendingScreen = null
|
||||
screenAccessRole = Roles.BASIC
|
||||
|
||||
if (mode === "table") {
|
||||
if (mode === "table" || mode === "grid") {
|
||||
datasourceModal.show()
|
||||
} else if (mode === "blank") {
|
||||
let templates = getTemplates($tables.list)
|
||||
|
@ -123,6 +125,7 @@
|
|||
|
||||
// Handler for DatasourceModal confirmation, move to screen access select
|
||||
const confirmScreenDatasources = async ({ templates }) => {
|
||||
console.log(templates)
|
||||
selectedTemplates = templates
|
||||
screenAccessRoleModal.show()
|
||||
}
|
||||
|
@ -177,6 +180,7 @@
|
|||
|
||||
<Modal bind:this={datasourceModal} autoFocus={false}>
|
||||
<DatasourceModal
|
||||
{mode}
|
||||
onConfirm={confirmScreenDatasources}
|
||||
initialScreens={!selectedTemplates ? [] : [...selectedTemplates]}
|
||||
/>
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
import rowListScreen from "builderStore/store/screenTemplates/rowListScreen"
|
||||
import DatasourceTemplateRow from "./DatasourceTemplateRow.svelte"
|
||||
|
||||
export let mode
|
||||
export let onCancel
|
||||
export let onConfirm
|
||||
export let initialScreens = []
|
||||
|
@ -24,7 +25,10 @@
|
|||
screen => screen.resourceId !== resourceId
|
||||
)
|
||||
} else {
|
||||
selectedScreens = [...selectedScreens, rowListScreen([datasource])[0]]
|
||||
selectedScreens = [
|
||||
...selectedScreens,
|
||||
rowListScreen([datasource], mode)[0],
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 24 KiB |
|
@ -3,6 +3,7 @@
|
|||
import CreationPage from "components/common/CreationPage.svelte"
|
||||
import blankImage from "./blank.png"
|
||||
import tableImage from "./table.png"
|
||||
import gridImage from "./grid.png"
|
||||
import CreateScreenModal from "./CreateScreenModal.svelte"
|
||||
import { store } from "builderStore"
|
||||
|
||||
|
@ -43,6 +44,16 @@
|
|||
<Body size="XS">View, edit and delete rows on a table</Body>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" on:click={() => createScreenModal.show("grid")}>
|
||||
<div class="image">
|
||||
<img alt="" src={gridImage} />
|
||||
</div>
|
||||
<div class="text">
|
||||
<Body size="S">Grid</Body>
|
||||
<Body size="XS">View and manipulate rows on a grid</Body>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CreationPage>
|
||||
</div>
|
||||
|
|
|
@ -43,7 +43,7 @@
|
|||
})
|
||||
</script>
|
||||
|
||||
<TestimonialPage>
|
||||
<TestimonialPage enabled={$organisation.testimonialsEnabled}>
|
||||
<Layout gap="S" noPadding>
|
||||
<img alt="logo" src={$organisation.logoUrl || Logo} />
|
||||
<span class="heading-wrap">
|
||||
|
|
|
@ -53,7 +53,7 @@
|
|||
})
|
||||
</script>
|
||||
|
||||
<TestimonialPage>
|
||||
<TestimonialPage enabled={$organisation.testimonialsEnabled}>
|
||||
<Layout gap="S" noPadding>
|
||||
{#if loaded}
|
||||
<img alt="logo" src={$organisation.logoUrl || Logo} />
|
||||
|
|
|
@ -81,9 +81,9 @@ export function createDatasourcesStore() {
|
|||
}))
|
||||
}
|
||||
|
||||
const updateDatasource = response => {
|
||||
const updateDatasource = (response, { ignoreErrors } = {}) => {
|
||||
const { datasource, errors } = response
|
||||
if (errors && Object.keys(errors).length > 0) {
|
||||
if (!ignoreErrors && errors && Object.keys(errors).length > 0) {
|
||||
throw new TableImportError(errors)
|
||||
}
|
||||
replaceDatasource(datasource._id, datasource)
|
||||
|
@ -137,7 +137,7 @@ export function createDatasourcesStore() {
|
|||
fetchSchema: integration.plus,
|
||||
})
|
||||
|
||||
return updateDatasource(response)
|
||||
return updateDatasource(response, { ignoreErrors: true })
|
||||
}
|
||||
|
||||
const update = async ({ integration, datasource }) => {
|
||||
|
|
|
@ -258,6 +258,186 @@
|
|||
"description": "Contains your app screens",
|
||||
"static": true
|
||||
},
|
||||
"buttongroup": {
|
||||
"name": "Button group",
|
||||
"icon": "Button",
|
||||
"hasChildren": false,
|
||||
"settings": [
|
||||
{
|
||||
"section": true,
|
||||
"name": "Buttons",
|
||||
"settings": [
|
||||
{
|
||||
"type": "buttonConfiguration",
|
||||
"key": "buttons",
|
||||
"nested": true,
|
||||
"defaultValue": [
|
||||
{
|
||||
"type": "cta",
|
||||
"text": "Button 1"
|
||||
},
|
||||
{
|
||||
"type": "primary",
|
||||
"text": "Button 2"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"section": true,
|
||||
"name": "Layout",
|
||||
"settings": [
|
||||
{
|
||||
"type": "select",
|
||||
"label": "Direction",
|
||||
"key": "direction",
|
||||
"showInBar": true,
|
||||
"barStyle": "buttons",
|
||||
"options": [
|
||||
{
|
||||
"label": "Column",
|
||||
"value": "column",
|
||||
"barIcon": "ViewColumn",
|
||||
"barTitle": "Column layout"
|
||||
},
|
||||
{
|
||||
"label": "Row",
|
||||
"value": "row",
|
||||
"barIcon": "ViewRow",
|
||||
"barTitle": "Row layout"
|
||||
}
|
||||
],
|
||||
"defaultValue": "row"
|
||||
},
|
||||
{
|
||||
"type": "select",
|
||||
"label": "Horiz. align",
|
||||
"key": "hAlign",
|
||||
"showInBar": true,
|
||||
"barStyle": "buttons",
|
||||
"options": [
|
||||
{
|
||||
"label": "Left",
|
||||
"value": "left",
|
||||
"barIcon": "AlignLeft",
|
||||
"barTitle": "Align left"
|
||||
},
|
||||
{
|
||||
"label": "Center",
|
||||
"value": "center",
|
||||
"barIcon": "AlignCenter",
|
||||
"barTitle": "Align center"
|
||||
},
|
||||
{
|
||||
"label": "Right",
|
||||
"value": "right",
|
||||
"barIcon": "AlignRight",
|
||||
"barTitle": "Align right"
|
||||
},
|
||||
{
|
||||
"label": "Stretch",
|
||||
"value": "stretch",
|
||||
"barIcon": "MoveLeftRight",
|
||||
"barTitle": "Align stretched horizontally"
|
||||
}
|
||||
],
|
||||
"defaultValue": "left"
|
||||
},
|
||||
{
|
||||
"type": "select",
|
||||
"label": "Vert. align",
|
||||
"key": "vAlign",
|
||||
"showInBar": true,
|
||||
"barStyle": "buttons",
|
||||
"options": [
|
||||
{
|
||||
"label": "Top",
|
||||
"value": "top",
|
||||
"barIcon": "AlignTop",
|
||||
"barTitle": "Align top"
|
||||
},
|
||||
{
|
||||
"label": "Middle",
|
||||
"value": "middle",
|
||||
"barIcon": "AlignMiddle",
|
||||
"barTitle": "Align middle"
|
||||
},
|
||||
{
|
||||
"label": "Bottom",
|
||||
"value": "bottom",
|
||||
"barIcon": "AlignBottom",
|
||||
"barTitle": "Align bottom"
|
||||
},
|
||||
{
|
||||
"label": "Stretch",
|
||||
"value": "stretch",
|
||||
"barIcon": "MoveUpDown",
|
||||
"barTitle": "Align stretched vertically"
|
||||
}
|
||||
],
|
||||
"defaultValue": "top"
|
||||
},
|
||||
{
|
||||
"type": "select",
|
||||
"label": "Size",
|
||||
"key": "size",
|
||||
"showInBar": true,
|
||||
"barStyle": "buttons",
|
||||
"options": [
|
||||
{
|
||||
"label": "Shrink",
|
||||
"value": "shrink",
|
||||
"barIcon": "Minimize",
|
||||
"barTitle": "Shrink container"
|
||||
},
|
||||
{
|
||||
"label": "Grow",
|
||||
"value": "grow",
|
||||
"barIcon": "Maximize",
|
||||
"barTitle": "Grow container"
|
||||
}
|
||||
],
|
||||
"defaultValue": "shrink"
|
||||
},
|
||||
{
|
||||
"type": "select",
|
||||
"label": "Gap",
|
||||
"key": "gap",
|
||||
"showInBar": true,
|
||||
"barStyle": "picker",
|
||||
"options": [
|
||||
{
|
||||
"label": "None",
|
||||
"value": "N"
|
||||
},
|
||||
{
|
||||
"label": "Small",
|
||||
"value": "S"
|
||||
},
|
||||
{
|
||||
"label": "Medium",
|
||||
"value": "M"
|
||||
},
|
||||
{
|
||||
"label": "Large",
|
||||
"value": "L"
|
||||
}
|
||||
],
|
||||
"defaultValue": "M"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"label": "Wrap",
|
||||
"key": "wrap",
|
||||
"showInBar": true,
|
||||
"barIcon": "ModernGridView",
|
||||
"barTitle": "Wrap"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"button": {
|
||||
"name": "Button",
|
||||
"description": "A basic html button that is ready for styling",
|
||||
|
@ -2409,7 +2589,6 @@
|
|||
"key": "disabled",
|
||||
"defaultValue": false
|
||||
},
|
||||
|
||||
{
|
||||
"type": "text",
|
||||
"label": "Initial form step",
|
||||
|
@ -5305,6 +5484,12 @@
|
|||
"key": "title",
|
||||
"nested": true
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"label": "Description",
|
||||
"key": "description",
|
||||
"nested": true
|
||||
},
|
||||
{
|
||||
"section": true,
|
||||
"dependsOn": {
|
||||
|
@ -5385,38 +5570,6 @@
|
|||
"section": true,
|
||||
"name": "Fields",
|
||||
"settings": [
|
||||
{
|
||||
"type": "select",
|
||||
"label": "Align labels",
|
||||
"key": "labelPosition",
|
||||
"defaultValue": "left",
|
||||
"options": [
|
||||
{
|
||||
"label": "Left",
|
||||
"value": "left"
|
||||
},
|
||||
{
|
||||
"label": "Above",
|
||||
"value": "above"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "select",
|
||||
"label": "Size",
|
||||
"key": "size",
|
||||
"options": [
|
||||
{
|
||||
"label": "Medium",
|
||||
"value": "spectrum--medium"
|
||||
},
|
||||
{
|
||||
"label": "Large",
|
||||
"value": "spectrum--large"
|
||||
}
|
||||
],
|
||||
"defaultValue": "spectrum--medium"
|
||||
},
|
||||
{
|
||||
"type": "fieldConfiguration",
|
||||
"key": "fields",
|
||||
|
@ -5436,6 +5589,40 @@
|
|||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tag": "style",
|
||||
"type": "select",
|
||||
"label": "Align labels",
|
||||
"key": "labelPosition",
|
||||
"defaultValue": "left",
|
||||
"options": [
|
||||
{
|
||||
"label": "Left",
|
||||
"value": "left"
|
||||
},
|
||||
{
|
||||
"label": "Above",
|
||||
"value": "above"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tag": "style",
|
||||
"type": "select",
|
||||
"label": "Size",
|
||||
"key": "size",
|
||||
"options": [
|
||||
{
|
||||
"label": "Medium",
|
||||
"value": "spectrum--medium"
|
||||
},
|
||||
{
|
||||
"label": "Large",
|
||||
"value": "spectrum--large"
|
||||
}
|
||||
],
|
||||
"defaultValue": "spectrum--medium"
|
||||
}
|
||||
],
|
||||
"context": [
|
||||
|
@ -5556,10 +5743,9 @@
|
|||
"width": 600,
|
||||
"height": 400
|
||||
},
|
||||
"info": "Grid Blocks are only compatible with internal or SQL tables",
|
||||
"settings": [
|
||||
{
|
||||
"type": "table",
|
||||
"type": "dataSource",
|
||||
"label": "Data",
|
||||
"key": "table",
|
||||
"required": true
|
||||
|
@ -5568,18 +5754,35 @@
|
|||
"type": "columns/grid",
|
||||
"label": "Columns",
|
||||
"key": "columns",
|
||||
"dependsOn": "table"
|
||||
"dependsOn": [
|
||||
"table",
|
||||
{
|
||||
"setting": "table.type",
|
||||
"value": "custom",
|
||||
"invert": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "filter",
|
||||
"label": "Filtering",
|
||||
"key": "initialFilter"
|
||||
"key": "initialFilter",
|
||||
"dependsOn": {
|
||||
"setting": "table.type",
|
||||
"value": "custom",
|
||||
"invert": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "field/sortable",
|
||||
"label": "Sort column",
|
||||
"key": "initialSortColumn",
|
||||
"placeholder": "Default"
|
||||
"placeholder": "Default",
|
||||
"dependsOn": {
|
||||
"setting": "table.type",
|
||||
"value": "custom",
|
||||
"invert": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "select",
|
||||
|
@ -5618,29 +5821,37 @@
|
|||
"label": "Clicked row",
|
||||
"key": "row"
|
||||
}
|
||||
],
|
||||
"dependsOn": {
|
||||
"setting": "allowEditRows",
|
||||
"value": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"label": "Add rows",
|
||||
"key": "allowAddRows",
|
||||
"defaultValue": true
|
||||
"defaultValue": true,
|
||||
"dependsOn": {
|
||||
"setting": "table.type",
|
||||
"value": ["table", "viewV2"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"label": "Edit rows",
|
||||
"key": "allowEditRows",
|
||||
"defaultValue": true
|
||||
"defaultValue": true,
|
||||
"dependsOn": {
|
||||
"setting": "table.type",
|
||||
"value": ["table", "viewV2"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"label": "Delete rows",
|
||||
"key": "allowDeleteRows",
|
||||
"defaultValue": true
|
||||
"defaultValue": true,
|
||||
"dependsOn": {
|
||||
"setting": "table.type",
|
||||
"value": ["table", "viewV2"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
|
@ -5713,4 +5924,4 @@
|
|||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
<script>
|
||||
import BlockComponent from "../BlockComponent.svelte"
|
||||
import Block from "../Block.svelte"
|
||||
|
||||
export let buttons = []
|
||||
export let direction
|
||||
export let hAlign
|
||||
export let vAlign
|
||||
export let gap = "S"
|
||||
</script>
|
||||
|
||||
<Block>
|
||||
<BlockComponent
|
||||
type="container"
|
||||
props={{
|
||||
direction,
|
||||
hAlign,
|
||||
vAlign,
|
||||
gap,
|
||||
wrap: true,
|
||||
}}
|
||||
>
|
||||
{#each buttons as { text, type, quiet, disabled, onClick, size }}
|
||||
<BlockComponent
|
||||
type="button"
|
||||
props={{
|
||||
text: text || "Button",
|
||||
onClick,
|
||||
type,
|
||||
quiet,
|
||||
disabled,
|
||||
size,
|
||||
}}
|
||||
/>
|
||||
{/each}
|
||||
</BlockComponent>
|
||||
</Block>
|
|
@ -81,6 +81,7 @@
|
|||
sortOrder: $fetch.sortOrder,
|
||||
},
|
||||
limit,
|
||||
primaryDisplay: $fetch.definition?.primaryDisplay,
|
||||
}
|
||||
|
||||
const createFetch = datasource => {
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
import { getContext } from "svelte"
|
||||
import { Grid } from "@budibase/frontend-core"
|
||||
|
||||
// table is actually any datasource, but called table for legacy compatibility
|
||||
export let table
|
||||
export let allowAddRows = true
|
||||
export let allowEditRows = true
|
||||
|
@ -21,7 +22,6 @@
|
|||
|
||||
$: columnWhitelist = columns?.map(col => col.name)
|
||||
$: schemaOverrides = getSchemaOverrides(columns)
|
||||
$: handleRowClick = allowEditRows ? undefined : onRowClick
|
||||
|
||||
const getSchemaOverrides = columns => {
|
||||
let overrides = {}
|
||||
|
@ -58,7 +58,7 @@
|
|||
showControls={false}
|
||||
notifySuccess={notificationStore.actions.success}
|
||||
notifyError={notificationStore.actions.error}
|
||||
on:rowclick={e => handleRowClick?.({ row: e.detail })}
|
||||
on:rowclick={e => onRowClick?.({ row: e.detail })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
export let fields
|
||||
export let labelPosition
|
||||
export let title
|
||||
export let description
|
||||
export let showDeleteButton
|
||||
export let showSaveButton
|
||||
export let saveButtonLabel
|
||||
|
@ -98,6 +99,7 @@
|
|||
fields: fieldsOrDefault,
|
||||
labelPosition,
|
||||
title,
|
||||
description,
|
||||
saveButtonLabel: saveLabel,
|
||||
deleteButtonLabel: deleteLabel,
|
||||
schema,
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
export let fields
|
||||
export let labelPosition
|
||||
export let title
|
||||
export let description
|
||||
export let saveButtonLabel
|
||||
export let deleteButtonLabel
|
||||
export let schema
|
||||
|
@ -160,55 +161,71 @@
|
|||
<BlockComponent
|
||||
type="container"
|
||||
props={{
|
||||
direction: "row",
|
||||
hAlign: "stretch",
|
||||
vAlign: "center",
|
||||
gap: "M",
|
||||
wrap: true,
|
||||
direction: "column",
|
||||
gap: "S",
|
||||
}}
|
||||
order={0}
|
||||
>
|
||||
<BlockComponent
|
||||
type="heading"
|
||||
props={{ text: title || "" }}
|
||||
type="container"
|
||||
props={{
|
||||
direction: "row",
|
||||
hAlign: "stretch",
|
||||
vAlign: "center",
|
||||
gap: "M",
|
||||
wrap: true,
|
||||
}}
|
||||
order={0}
|
||||
/>
|
||||
{#if renderButtons}
|
||||
>
|
||||
<BlockComponent
|
||||
type="container"
|
||||
props={{
|
||||
direction: "row",
|
||||
hAlign: "stretch",
|
||||
vAlign: "center",
|
||||
gap: "M",
|
||||
wrap: true,
|
||||
}}
|
||||
type="heading"
|
||||
props={{ text: title || "" }}
|
||||
order={0}
|
||||
/>
|
||||
{#if renderButtons}
|
||||
<BlockComponent
|
||||
type="container"
|
||||
props={{
|
||||
direction: "row",
|
||||
hAlign: "stretch",
|
||||
vAlign: "center",
|
||||
gap: "M",
|
||||
wrap: true,
|
||||
}}
|
||||
order={1}
|
||||
>
|
||||
{#if renderDeleteButton}
|
||||
<BlockComponent
|
||||
type="button"
|
||||
props={{
|
||||
text: deleteButtonLabel,
|
||||
onClick: onDelete,
|
||||
quiet: true,
|
||||
type: "secondary",
|
||||
}}
|
||||
order={0}
|
||||
/>
|
||||
{/if}
|
||||
{#if renderSaveButton}
|
||||
<BlockComponent
|
||||
type="button"
|
||||
props={{
|
||||
text: saveButtonLabel,
|
||||
onClick: onSave,
|
||||
type: "cta",
|
||||
}}
|
||||
order={1}
|
||||
/>
|
||||
{/if}
|
||||
</BlockComponent>
|
||||
{/if}
|
||||
</BlockComponent>
|
||||
{#if description}
|
||||
<BlockComponent
|
||||
type="text"
|
||||
props={{ text: description }}
|
||||
order={1}
|
||||
>
|
||||
{#if renderDeleteButton}
|
||||
<BlockComponent
|
||||
type="button"
|
||||
props={{
|
||||
text: deleteButtonLabel,
|
||||
onClick: onDelete,
|
||||
quiet: true,
|
||||
type: "secondary",
|
||||
}}
|
||||
order={0}
|
||||
/>
|
||||
{/if}
|
||||
{#if renderSaveButton}
|
||||
<BlockComponent
|
||||
type="button"
|
||||
props={{
|
||||
text: saveButtonLabel,
|
||||
onClick: onSave,
|
||||
type: "cta",
|
||||
}}
|
||||
order={1}
|
||||
/>
|
||||
{/if}
|
||||
</BlockComponent>
|
||||
/>
|
||||
{/if}
|
||||
</BlockComponent>
|
||||
{/if}
|
||||
|
|
|
@ -19,6 +19,7 @@ export { default as dataprovider } from "./DataProvider.svelte"
|
|||
export { default as divider } from "./Divider.svelte"
|
||||
export { default as screenslot } from "./ScreenSlot.svelte"
|
||||
export { default as button } from "./Button.svelte"
|
||||
export { default as buttongroup } from "./ButtonGroup.svelte"
|
||||
export { default as repeater } from "./Repeater.svelte"
|
||||
export { default as text } from "./Text.svelte"
|
||||
export { default as layout } from "./Layout.svelte"
|
||||
|
|
|
@ -2,8 +2,8 @@
|
|||
import { getContext } from "svelte"
|
||||
import { Table } from "@budibase/bbui"
|
||||
import SlotRenderer from "./SlotRenderer.svelte"
|
||||
import { UnsortableTypes } from "../../../constants"
|
||||
import { onDestroy } from "svelte"
|
||||
import { canBeSortColumn } from "@budibase/shared-core"
|
||||
|
||||
export let dataProvider
|
||||
export let columns
|
||||
|
@ -32,7 +32,8 @@
|
|||
$: loading = dataProvider?.loading ?? false
|
||||
$: data = dataProvider?.rows || []
|
||||
$: fullSchema = dataProvider?.schema ?? {}
|
||||
$: fields = getFields(fullSchema, columns, false)
|
||||
$: primaryDisplay = dataProvider?.primaryDisplay
|
||||
$: fields = getFields(fullSchema, columns, false, primaryDisplay)
|
||||
$: schema = getFilteredSchema(fullSchema, fields, hasChildren)
|
||||
$: setSorting = getAction(
|
||||
dataProvider?.id,
|
||||
|
@ -55,18 +56,13 @@
|
|||
}
|
||||
}
|
||||
|
||||
const getFields = (schema, customColumns, showAutoColumns) => {
|
||||
// Check for an invalid column selection
|
||||
let invalid = false
|
||||
customColumns?.forEach(column => {
|
||||
const columnName = typeof column === "string" ? column : column.name
|
||||
if (schema[columnName] == null) {
|
||||
invalid = true
|
||||
}
|
||||
})
|
||||
|
||||
// Use column selection if it exists
|
||||
if (!invalid && customColumns?.length) {
|
||||
const getFields = (
|
||||
schema,
|
||||
customColumns,
|
||||
showAutoColumns,
|
||||
primaryDisplay
|
||||
) => {
|
||||
if (customColumns?.length) {
|
||||
return customColumns
|
||||
}
|
||||
|
||||
|
@ -74,13 +70,38 @@
|
|||
let columns = []
|
||||
let autoColumns = []
|
||||
Object.entries(schema).forEach(([field, fieldSchema]) => {
|
||||
if (fieldSchema.visible === false) {
|
||||
return
|
||||
}
|
||||
if (!fieldSchema?.autocolumn) {
|
||||
columns.push(field)
|
||||
} else if (showAutoColumns) {
|
||||
autoColumns.push(field)
|
||||
}
|
||||
})
|
||||
return columns.concat(autoColumns)
|
||||
|
||||
// Sort columns to respect grid metadata
|
||||
const allCols = columns.concat(autoColumns)
|
||||
return allCols.sort((a, b) => {
|
||||
if (a === primaryDisplay) {
|
||||
return -1
|
||||
}
|
||||
if (b === primaryDisplay) {
|
||||
return 1
|
||||
}
|
||||
const aOrder = schema[a].order
|
||||
const bOrder = schema[b].order
|
||||
if (aOrder === bOrder) {
|
||||
return 0
|
||||
}
|
||||
if (aOrder == null) {
|
||||
return 1
|
||||
}
|
||||
if (bOrder == null) {
|
||||
return -1
|
||||
}
|
||||
return aOrder < bOrder ? -1 : 1
|
||||
})
|
||||
}
|
||||
|
||||
const getFilteredSchema = (schema, fields, hasChildren) => {
|
||||
|
@ -102,7 +123,7 @@
|
|||
return
|
||||
}
|
||||
newSchema[columnName] = schema[columnName]
|
||||
if (UnsortableTypes.includes(schema[columnName].type)) {
|
||||
if (!canBeSortColumn(schema[columnName].type)) {
|
||||
newSchema[columnName].sortable = false
|
||||
}
|
||||
|
||||
|
|
|
@ -1,13 +1,5 @@
|
|||
import { FieldType as FieldTypes } from "@budibase/types"
|
||||
export { FieldType as FieldTypes } from "@budibase/types"
|
||||
|
||||
export const UnsortableTypes = [
|
||||
FieldTypes.FORMULA,
|
||||
FieldTypes.ATTACHMENT,
|
||||
FieldTypes.ARRAY,
|
||||
FieldTypes.LINK,
|
||||
]
|
||||
|
||||
export const ActionTypes = {
|
||||
ValidateForm: "ValidateForm",
|
||||
UpdateFieldValue: "UpdateFieldValue",
|
||||
|
|
|
@ -9,7 +9,9 @@ export const buildRelationshipEndpoints = API => ({
|
|||
if (!tableId || !rowId) {
|
||||
return []
|
||||
}
|
||||
const response = await API.get({ url: `/api/${tableId}/${rowId}/enrich` })
|
||||
const response = await API.get({
|
||||
url: `/api/${tableId}/${rowId}/enrich?field=${fieldName}`,
|
||||
})
|
||||
if (!fieldName) {
|
||||
return response || []
|
||||
} else {
|
||||
|
|
|
@ -21,6 +21,7 @@
|
|||
export let invertX = false
|
||||
export let invertY = false
|
||||
export let contentLines = 1
|
||||
export let hidden = false
|
||||
|
||||
const emptyError = writable(null)
|
||||
|
||||
|
@ -34,7 +35,7 @@
|
|||
column.schema.autocolumn ||
|
||||
column.schema.disabled ||
|
||||
column.schema.type === "formula" ||
|
||||
(!$config.canEditRows && row._id)
|
||||
(!$config.canEditRows && !row._isNewRow)
|
||||
|
||||
// Register this cell API if the row is focused
|
||||
$: {
|
||||
|
@ -78,6 +79,7 @@
|
|||
{focused}
|
||||
{selectedUser}
|
||||
{readonly}
|
||||
{hidden}
|
||||
error={$error}
|
||||
on:click={() => focusedCellId.set(cellId)}
|
||||
on:contextmenu={e => menu.actions.open(cellId, e)}
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
export let defaultHeight = false
|
||||
export let center = false
|
||||
export let readonly = false
|
||||
export let hidden = false
|
||||
|
||||
$: style = getStyle(width, selectedUser)
|
||||
|
||||
|
@ -30,6 +31,7 @@
|
|||
class:error
|
||||
class:center
|
||||
class:readonly
|
||||
class:hidden
|
||||
class:default-height={defaultHeight}
|
||||
class:selected-other={selectedUser != null}
|
||||
class:alt={rowIdx % 2 === 1}
|
||||
|
@ -81,6 +83,9 @@
|
|||
.cell.center {
|
||||
align-items: center;
|
||||
}
|
||||
.cell.hidden {
|
||||
content-visibility: hidden;
|
||||
}
|
||||
|
||||
/* Cell border */
|
||||
.cell.focused:after,
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
<script>
|
||||
import { getContext, onMount, tick } from "svelte"
|
||||
import { canBeDisplayColumn } from "@budibase/shared-core"
|
||||
import { canBeDisplayColumn, canBeSortColumn } from "@budibase/shared-core"
|
||||
import { Icon, Popover, Menu, MenuItem, clickOutside } from "@budibase/bbui"
|
||||
import GridCell from "./GridCell.svelte"
|
||||
import { getColumnIcon } from "../lib/utils"
|
||||
import { debounce } from "../../../utils/utils"
|
||||
import { FieldType, FormulaTypes } from "@budibase/types"
|
||||
|
||||
export let column
|
||||
export let idx
|
||||
|
@ -15,7 +17,7 @@
|
|||
isResizing,
|
||||
rand,
|
||||
sort,
|
||||
renderedColumns,
|
||||
visibleColumns,
|
||||
dispatch,
|
||||
subscribe,
|
||||
config,
|
||||
|
@ -23,23 +25,70 @@
|
|||
columns,
|
||||
definition,
|
||||
datasource,
|
||||
schema,
|
||||
focusedCellId,
|
||||
filter,
|
||||
inlineFilters,
|
||||
} = getContext("grid")
|
||||
|
||||
const searchableTypes = [
|
||||
FieldType.STRING,
|
||||
FieldType.OPTIONS,
|
||||
FieldType.NUMBER,
|
||||
FieldType.BIGINT,
|
||||
FieldType.ARRAY,
|
||||
FieldType.LONGFORM,
|
||||
]
|
||||
|
||||
let anchor
|
||||
let open = false
|
||||
let editIsOpen = false
|
||||
let timeout
|
||||
let popover
|
||||
let searchValue
|
||||
let input
|
||||
|
||||
$: sortedBy = column.name === $sort.column
|
||||
$: canMoveLeft = orderable && idx > 0
|
||||
$: canMoveRight = orderable && idx < $renderedColumns.length - 1
|
||||
$: ascendingLabel = ["number", "bigint"].includes(column.schema?.type)
|
||||
? "low-high"
|
||||
: "A-Z"
|
||||
$: descendingLabel = ["number", "bigint"].includes(column.schema?.type)
|
||||
? "high-low"
|
||||
: "Z-A"
|
||||
$: canMoveRight = orderable && idx < $visibleColumns.length - 1
|
||||
$: sortingLabels = getSortingLabels(column.schema?.type)
|
||||
$: searchable = isColumnSearchable(column)
|
||||
$: resetSearchValue(column.name)
|
||||
$: searching = searchValue != null
|
||||
$: debouncedUpdateFilter(searchValue)
|
||||
|
||||
const getSortingLabels = type => {
|
||||
switch (type) {
|
||||
case FieldType.NUMBER:
|
||||
case FieldType.BIGINT:
|
||||
return {
|
||||
ascending: "low-high",
|
||||
descending: "high-low",
|
||||
}
|
||||
case FieldType.DATETIME:
|
||||
return {
|
||||
ascending: "old-new",
|
||||
descending: "new-old",
|
||||
}
|
||||
default:
|
||||
return {
|
||||
ascending: "A-Z",
|
||||
descending: "Z-A",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const resetSearchValue = name => {
|
||||
searchValue = $inlineFilters?.find(x => x.id === `inline-${name}`)?.value
|
||||
}
|
||||
|
||||
const isColumnSearchable = col => {
|
||||
const { type, formulaType } = col.schema
|
||||
return (
|
||||
searchableTypes.includes(type) ||
|
||||
(type === FieldType.FORMULA && formulaType === FormulaTypes.STATIC)
|
||||
)
|
||||
}
|
||||
|
||||
const editColumn = async () => {
|
||||
editIsOpen = true
|
||||
|
@ -119,16 +168,16 @@
|
|||
// Generate new name
|
||||
let newName = `${column.name} copy`
|
||||
let attempts = 2
|
||||
while ($definition.schema[newName]) {
|
||||
while ($schema[newName]) {
|
||||
newName = `${column.name} copy ${attempts++}`
|
||||
}
|
||||
|
||||
// Save schema with new column
|
||||
const existingColumnDefinition = $definition.schema[column.name]
|
||||
const existingColumnDefinition = $schema[column.name]
|
||||
await datasource.actions.saveDefinition({
|
||||
...$definition,
|
||||
schema: {
|
||||
...$definition.schema,
|
||||
...$schema,
|
||||
[newName]: {
|
||||
...existingColumnDefinition,
|
||||
name: newName,
|
||||
|
@ -140,12 +189,46 @@
|
|||
})
|
||||
}
|
||||
|
||||
const startSearching = async () => {
|
||||
$focusedCellId = null
|
||||
searchValue = ""
|
||||
await tick()
|
||||
input?.focus()
|
||||
}
|
||||
|
||||
const onInputKeyDown = e => {
|
||||
if (e.key === "Enter") {
|
||||
updateFilter()
|
||||
} else if (e.key === "Escape") {
|
||||
input?.blur()
|
||||
}
|
||||
}
|
||||
|
||||
const stopSearching = () => {
|
||||
searchValue = null
|
||||
updateFilter()
|
||||
}
|
||||
|
||||
const onBlurInput = () => {
|
||||
if (searchValue === "") {
|
||||
searchValue = null
|
||||
}
|
||||
updateFilter()
|
||||
}
|
||||
|
||||
const updateFilter = () => {
|
||||
filter.actions.addInlineFilter(column, searchValue)
|
||||
}
|
||||
const debouncedUpdateFilter = debounce(updateFilter, 250)
|
||||
|
||||
onMount(() => subscribe("close-edit-column", cancelEdit))
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="header-cell"
|
||||
class:open
|
||||
class:searchable
|
||||
class:searching
|
||||
style="flex: 0 0 {column.width}px;"
|
||||
bind:this={anchor}
|
||||
class:disabled={$isReordering || $isResizing}
|
||||
|
@ -160,30 +243,49 @@
|
|||
defaultHeight
|
||||
center
|
||||
>
|
||||
<Icon
|
||||
size="S"
|
||||
name={getColumnIcon(column)}
|
||||
color={`var(--spectrum-global-color-gray-600)`}
|
||||
/>
|
||||
{#if searching}
|
||||
<input
|
||||
bind:this={input}
|
||||
type="text"
|
||||
bind:value={searchValue}
|
||||
on:blur={onBlurInput}
|
||||
on:click={() => focusedCellId.set(null)}
|
||||
on:keydown={onInputKeyDown}
|
||||
data-grid-ignore
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<div class="column-icon">
|
||||
<Icon size="S" name={getColumnIcon(column)} />
|
||||
</div>
|
||||
<div class="search-icon" on:click={startSearching}>
|
||||
<Icon hoverable size="S" name="Search" />
|
||||
</div>
|
||||
|
||||
<div class="name">
|
||||
{column.label}
|
||||
</div>
|
||||
{#if sortedBy}
|
||||
<div class="sort-indicator">
|
||||
<Icon
|
||||
size="S"
|
||||
name={$sort.order === "descending" ? "SortOrderDown" : "SortOrderUp"}
|
||||
color="var(--spectrum-global-color-gray-600)"
|
||||
/>
|
||||
|
||||
{#if searching}
|
||||
<div class="clear-icon" on:click={stopSearching}>
|
||||
<Icon hoverable size="S" name="Close" />
|
||||
</div>
|
||||
{:else}
|
||||
{#if sortedBy}
|
||||
<div class="sort-indicator">
|
||||
<Icon
|
||||
hoverable
|
||||
size="S"
|
||||
name={$sort.order === "descending"
|
||||
? "SortOrderDown"
|
||||
: "SortOrderUp"}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="more-icon" on:click={() => (open = true)}>
|
||||
<Icon hoverable size="S" name="MoreVertical" />
|
||||
</div>
|
||||
{/if}
|
||||
<div class="more" on:click={() => (open = true)}>
|
||||
<Icon
|
||||
size="S"
|
||||
name="MoreVertical"
|
||||
color="var(--spectrum-global-color-gray-600)"
|
||||
/>
|
||||
</div>
|
||||
</GridCell>
|
||||
</div>
|
||||
|
||||
|
@ -231,16 +333,18 @@
|
|||
<MenuItem
|
||||
icon="SortOrderUp"
|
||||
on:click={sortAscending}
|
||||
disabled={column.name === $sort.column && $sort.order === "ascending"}
|
||||
disabled={!canBeSortColumn(column.schema.type) ||
|
||||
(column.name === $sort.column && $sort.order === "ascending")}
|
||||
>
|
||||
Sort {ascendingLabel}
|
||||
Sort {sortingLabels.ascending}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
icon="SortOrderDown"
|
||||
on:click={sortDescending}
|
||||
disabled={column.name === $sort.column && $sort.order === "descending"}
|
||||
disabled={!canBeSortColumn(column.schema.type) ||
|
||||
(column.name === $sort.column && $sort.order === "descending")}
|
||||
>
|
||||
Sort {descendingLabel}
|
||||
Sort {sortingLabels.descending}
|
||||
</MenuItem>
|
||||
<MenuItem disabled={!canMoveLeft} icon="ChevronLeft" on:click={moveLeft}>
|
||||
Move left
|
||||
|
@ -280,6 +384,29 @@
|
|||
background: var(--grid-background-alt);
|
||||
}
|
||||
|
||||
/* Icon colors */
|
||||
.header-cell :global(.spectrum-Icon) {
|
||||
color: var(--spectrum-global-color-gray-600);
|
||||
}
|
||||
.header-cell :global(.spectrum-Icon.hoverable:hover) {
|
||||
color: var(--spectrum-global-color-gray-800) !important;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Search icon */
|
||||
.search-icon {
|
||||
display: none;
|
||||
}
|
||||
.header-cell.searchable:not(.open):hover .search-icon,
|
||||
.header-cell.searchable.searching .search-icon {
|
||||
display: block;
|
||||
}
|
||||
.header-cell.searchable:not(.open):hover .column-icon,
|
||||
.header-cell.searchable.searching .column-icon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Main center content */
|
||||
.name {
|
||||
flex: 1 1 auto;
|
||||
width: 0;
|
||||
|
@ -287,23 +414,45 @@
|
|||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
.header-cell.searching .name {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
input {
|
||||
display: none;
|
||||
font-family: var(--font-sans);
|
||||
outline: none;
|
||||
border: 1px solid transparent;
|
||||
background: transparent;
|
||||
color: var(--spectrum-global-color-gray-800);
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 0 30px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
input:focus {
|
||||
border: 1px solid var(--accent-color);
|
||||
}
|
||||
input:not(:focus) {
|
||||
background: var(--spectrum-global-color-gray-200);
|
||||
}
|
||||
.header-cell.searching input {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.more {
|
||||
/* Right icons */
|
||||
.more-icon {
|
||||
display: none;
|
||||
padding: 4px;
|
||||
margin: 0 -4px;
|
||||
}
|
||||
.header-cell.open .more,
|
||||
.header-cell:hover .more {
|
||||
.header-cell.open .more-icon,
|
||||
.header-cell:hover .more-icon {
|
||||
display: block;
|
||||
}
|
||||
.more:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
.more:hover :global(.spectrum-Icon) {
|
||||
color: var(--spectrum-global-color-gray-800) !important;
|
||||
}
|
||||
|
||||
.header-cell.open .sort-indicator,
|
||||
.header-cell:hover .sort-indicator {
|
||||
display: none;
|
||||
|
|
|
@ -260,29 +260,31 @@
|
|||
class:wrap={editable || contentLines > 1}
|
||||
on:wheel={e => (focused ? e.stopPropagation() : null)}
|
||||
>
|
||||
{#each value || [] as relationship}
|
||||
{#if relationship[primaryDisplay] || relationship.primaryDisplay}
|
||||
<div class="badge">
|
||||
<span
|
||||
on:click={editable
|
||||
? () => showRelationship(relationship._id)
|
||||
: null}
|
||||
>
|
||||
{readable(
|
||||
relationship[primaryDisplay] || relationship.primaryDisplay
|
||||
)}
|
||||
</span>
|
||||
{#if editable}
|
||||
<Icon
|
||||
name="Close"
|
||||
size="XS"
|
||||
hoverable
|
||||
on:click={() => toggleRow(relationship)}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
{#if Array.isArray(value) && value.length}
|
||||
{#each value as relationship}
|
||||
{#if relationship[primaryDisplay] || relationship.primaryDisplay}
|
||||
<div class="badge">
|
||||
<span
|
||||
on:click={editable
|
||||
? () => showRelationship(relationship._id)
|
||||
: null}
|
||||
>
|
||||
{readable(
|
||||
relationship[primaryDisplay] || relationship.primaryDisplay
|
||||
)}
|
||||
</span>
|
||||
{#if editable}
|
||||
<Icon
|
||||
name="Close"
|
||||
size="XS"
|
||||
hoverable
|
||||
on:click={() => toggleRow(relationship)}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
{/if}
|
||||
{#if editable}
|
||||
<div class="add" on:click={open}>
|
||||
<Icon name="Add" size="S" />
|
||||
|
@ -318,7 +320,7 @@
|
|||
<div class="searching">
|
||||
<ProgressCircle size="S" />
|
||||
</div>
|
||||
{:else if searchResults?.length}
|
||||
{:else if Array.isArray(searchResults) && searchResults.length}
|
||||
<div class="results">
|
||||
{#each searchResults as row, idx}
|
||||
<div
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<script>
|
||||
import { getContext } from "svelte"
|
||||
import { ActionButton, Popover, Select } from "@budibase/bbui"
|
||||
import { canBeSortColumn } from "@budibase/shared-core"
|
||||
|
||||
const { sort, columns, stickyColumn } = getContext("grid")
|
||||
|
||||
|
@ -19,7 +20,7 @@
|
|||
type: stickyColumn.schema?.type,
|
||||
})
|
||||
}
|
||||
return [
|
||||
options = [
|
||||
...options,
|
||||
...columns.map(col => ({
|
||||
label: col.label || col.name,
|
||||
|
@ -27,6 +28,7 @@
|
|||
type: col.schema?.type,
|
||||
})),
|
||||
]
|
||||
return options.filter(col => canBeSortColumn(col.type))
|
||||
}
|
||||
|
||||
const getOrderOptions = (column, columnOptions) => {
|
||||
|
|
|
@ -141,7 +141,14 @@
|
|||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{#if $loaded}
|
||||
{#if $error}
|
||||
<div class="grid-error">
|
||||
<div class="grid-error-title">There was a problem loading your grid</div>
|
||||
<div class="grid-error-subtitle">
|
||||
{$error}
|
||||
</div>
|
||||
</div>
|
||||
{:else if $loaded}
|
||||
<div class="grid-data-outer" use:clickOutside={ui.actions.blur}>
|
||||
<div class="grid-data-inner">
|
||||
<StickyColumn>
|
||||
|
@ -171,13 +178,6 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else if $error}
|
||||
<div class="grid-error">
|
||||
<div class="grid-error-title">There was a problem loading your grid</div>
|
||||
<div class="grid-error-subtitle">
|
||||
{$error}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{#if $loading && !$error}
|
||||
<div in:fade|local={{ duration: 130 }} class="grid-loading">
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
const {
|
||||
bounds,
|
||||
renderedRows,
|
||||
renderedColumns,
|
||||
visibleColumns,
|
||||
rowVerticalInversionIndex,
|
||||
hoveredRowId,
|
||||
dispatch,
|
||||
|
@ -17,7 +17,7 @@
|
|||
|
||||
let body
|
||||
|
||||
$: renderColumnsWidth = $renderedColumns.reduce(
|
||||
$: columnsWidth = $visibleColumns.reduce(
|
||||
(total, col) => (total += col.width),
|
||||
0
|
||||
)
|
||||
|
@ -47,7 +47,7 @@
|
|||
<div
|
||||
class="blank"
|
||||
class:highlighted={$hoveredRowId === BlankRowID}
|
||||
style="width:{renderColumnsWidth}px"
|
||||
style="width:{columnsWidth}px"
|
||||
on:mouseenter={$isDragging ? null : () => ($hoveredRowId = BlankRowID)}
|
||||
on:mouseleave={$isDragging ? null : () => ($hoveredRowId = null)}
|
||||
on:click={() => dispatch("add-row-inline")}
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
focusedCellId,
|
||||
reorder,
|
||||
selectedRows,
|
||||
renderedColumns,
|
||||
visibleColumns,
|
||||
hoveredRowId,
|
||||
selectedCellMap,
|
||||
focusedRow,
|
||||
|
@ -18,6 +18,8 @@
|
|||
contentLines,
|
||||
isDragging,
|
||||
dispatch,
|
||||
rows,
|
||||
columnRenderMap,
|
||||
} = getContext("grid")
|
||||
|
||||
$: rowSelected = !!$selectedRows[row._id]
|
||||
|
@ -31,9 +33,9 @@
|
|||
on:focus
|
||||
on:mouseenter={$isDragging ? null : () => ($hoveredRowId = row._id)}
|
||||
on:mouseleave={$isDragging ? null : () => ($hoveredRowId = null)}
|
||||
on:click={() => dispatch("rowclick", row)}
|
||||
on:click={() => dispatch("rowclick", rows.actions.cleanRow(row))}
|
||||
>
|
||||
{#each $renderedColumns as column, columnIdx (column.name)}
|
||||
{#each $visibleColumns as column, columnIdx}
|
||||
{@const cellId = `${row._id}-${column.name}`}
|
||||
<DataCell
|
||||
{cellId}
|
||||
|
@ -50,6 +52,7 @@
|
|||
selectedUser={$selectedCellMap[cellId]}
|
||||
width={column.width}
|
||||
contentLines={$contentLines}
|
||||
hidden={!$columnRenderMap[column.name]}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
|
|
|
@ -11,7 +11,6 @@
|
|||
maxScrollLeft,
|
||||
bounds,
|
||||
hoveredRowId,
|
||||
hiddenColumnsWidth,
|
||||
menu,
|
||||
} = getContext("grid")
|
||||
|
||||
|
@ -23,10 +22,10 @@
|
|||
let initialTouchX
|
||||
let initialTouchY
|
||||
|
||||
$: style = generateStyle($scroll, $rowHeight, $hiddenColumnsWidth)
|
||||
$: style = generateStyle($scroll, $rowHeight)
|
||||
|
||||
const generateStyle = (scroll, rowHeight, hiddenWidths) => {
|
||||
const offsetX = scrollHorizontally ? -1 * scroll.left + hiddenWidths : 0
|
||||
const generateStyle = (scroll, rowHeight) => {
|
||||
const offsetX = scrollHorizontally ? -1 * scroll.left : 0
|
||||
const offsetY = scrollVertically ? -1 * (scroll.top % rowHeight) : 0
|
||||
return `transform: translate3d(${offsetX}px, ${offsetY}px, 0);`
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue