Merge branch 'develop' into lab-theme-binding
This commit is contained in:
commit
32892b1ef9
|
@ -7,7 +7,4 @@ packages/worker/coverage
|
|||
packages/backend-core/coverage
|
||||
packages/server/client
|
||||
packages/builder/.routify
|
||||
packages/builder/cypress/support/queryLevelTransformerFunction.js
|
||||
packages/builder/cypress/support/queryLevelTransformerFunctionWithData.js
|
||||
packages/builder/cypress/reports
|
||||
packages/sdk/sdk
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
# Budibase CI Pipelines
|
||||
|
||||
Welcome to the budibase CI pipelines directory. This document details what each of the CI pipelines are for, and come common combinations.
|
||||
|
@ -6,27 +5,34 @@ Welcome to the budibase CI pipelines directory. This document details what each
|
|||
## All CI Pipelines
|
||||
|
||||
### Note
|
||||
- When running workflow dispatch jobs, ensure you always run them off the `master` branch. It defaults to `develop`, so double check before running any jobs. The exception to this case is the `deploy-release` job which requires the develop branch.
|
||||
|
||||
- When running workflow dispatch jobs, ensure you always run them off the `master` branch. It defaults to `develop`, so double check before running any jobs. The exception to this case is the `deploy-release` job which requires the develop branch.
|
||||
|
||||
### Standard CI Build Job (budibase_ci.yml)
|
||||
|
||||
Triggers:
|
||||
|
||||
- PR or push to develop
|
||||
- PR or push to master
|
||||
|
||||
The standard CI Build job is what runs when you raise a PR to develop or master.
|
||||
The standard CI Build job is what runs when you raise a PR to develop or master.
|
||||
|
||||
- Installs all dependencies,
|
||||
- builds the project
|
||||
- builds the project
|
||||
- run the unit tests
|
||||
- Generate test coverage metrics with codecov
|
||||
- Run the cypress tests
|
||||
- Run the integration tests
|
||||
|
||||
### Release Develop Job (release-develop.yml)
|
||||
|
||||
Triggers:
|
||||
|
||||
- Push to develop
|
||||
|
||||
The job responsible for building, tagging and pushing docker images out to the test and release environments.
|
||||
The job responsible for building, tagging and pushing docker images out to the test and release environments.
|
||||
|
||||
- Installs all dependencies
|
||||
- builds the project
|
||||
- builds the project
|
||||
- run the unit tests
|
||||
- publish the budibase JS packages under a prerelease tag to NPM
|
||||
- build, tag and push docker images under the `develop` tag to docker hub
|
||||
|
@ -34,23 +40,29 @@ The job responsible for building, tagging and pushing docker images out to the t
|
|||
These images will then be pulled by the test and release environments, updating the latest automatically. Discord notifications are sent to the #infra channel when this occurs.
|
||||
|
||||
### Release Job (release.yml)
|
||||
|
||||
Triggers:
|
||||
|
||||
- Push to master
|
||||
|
||||
This job is responsible for building and pushing the latest code to NPM and docker hub, so that it can be deployed.
|
||||
|
||||
- Installs all dependencies
|
||||
- builds the project
|
||||
- builds the project
|
||||
- run the unit tests
|
||||
- publish the budibase JS packages under a release tag to NPM (always incremented by patch versions)
|
||||
- build, tag and push docker images under the `v.x.x.x` (the tag of the NPM release) tag to docker hub
|
||||
|
||||
### Release Selfhost Job (release-selfhost.yml)
|
||||
|
||||
Triggers:
|
||||
|
||||
- Manual Workflow Dispatch Trigger
|
||||
|
||||
This job is responsible for delivering the latest version of budibase to those that are self-hosting.
|
||||
This job is responsible for delivering the latest version of budibase to those that are self-hosting.
|
||||
|
||||
This job relies on the release job to have run first, so the latest image is pushed to dockerhub. This job then will pull the latest version from `lerna.json` and try to find an image in dockerhub corresponding to that version. For example, if the version in `lerna.json` is `1.0.0`:
|
||||
|
||||
- Pull the images for all budibase services tagged `v1.0.0` from dockerhub
|
||||
- Tag these images as `latest`
|
||||
- Push them back to dockerhub. This now means anyone who pulls `latest` (self hosters using docker-compose) will get the latest version.
|
||||
|
@ -58,53 +70,61 @@ This job relies on the release job to have run first, so the latest image is pus
|
|||
- Perform a github release with the latest version. You can see previous releases here (https://github.com/Budibase/budibase/releases)
|
||||
|
||||
### Deploy Release (deploy-release.yml)
|
||||
|
||||
Triggers:
|
||||
|
||||
- Manual Workflow Dispatch Trigger
|
||||
|
||||
This job is responsible for deploying to our release, cloud kubernetes environment. You must run the release job first, to ensure that the latest images have been built and pushed to docker hub. After kicking off this job, the following will occur:
|
||||
|
||||
- Checks out the release branch
|
||||
- Checks out the release branch
|
||||
- Pulls the latest `values.yaml` from budibase infra, a private repo containing budibases infrastructure configuration
|
||||
- Gets the latest budibase version from `lerna.json`, if it hasn't been specified in the workflow when you kicked it off
|
||||
- Configures AWS Credentials
|
||||
- Configures AWS Credentials
|
||||
- Deploys the helm chart in the budibase repo to our preproduction EKS cluster, injecting the `values.yaml` we pulled from budibase-infra
|
||||
- Fires off a discord webhook in the #infra channel to show that the deployment completely successfully.
|
||||
|
||||
### Deploy Preprod (deploy-preprod.yml)
|
||||
|
||||
Triggers:
|
||||
|
||||
- Manual Workflow Dispatch Trigger
|
||||
|
||||
This job is responsible for deploying to our preprod, cloud kubernetes environment. You must run the release job first, to ensure that the latest images have been built and pushed to docker hub. After kicking off this job, the following will occur:
|
||||
|
||||
- Checks out the master branch
|
||||
- Checks out the master branch
|
||||
- Pulls the latest `values.yaml` from budibase infra, a private repo containing budibases infrastructure configuration
|
||||
- Gets the latest budibase version from `lerna.json`, if it hasn't been specified in the workflow when you kicked it off
|
||||
- Configures AWS Credentials
|
||||
- Configures AWS Credentials
|
||||
- Deploys the helm chart in the budibase repo to our preprod EKS cluster, injecting the `values.yaml` we pulled from budibase-infra
|
||||
- Fires off a discord webhook in the #infra channel to show that the deployment completely successfully.
|
||||
|
||||
### Deploy Production (deploy-cloud.yml)
|
||||
|
||||
Triggers:
|
||||
|
||||
- Manual Workflow Dispatch Trigger
|
||||
|
||||
This job is responsible for deploying to our production, cloud kubernetes environment. You must run the release job first, to ensure that the latest images have been built and pushed to docker hub. You can also manually enter a version number for this job, so you can perform rollbacks or upgrade to a specific version. After kicking off this job, the following will occur:
|
||||
|
||||
- Checks out the master branch
|
||||
- Checks out the master branch
|
||||
- Pulls the latest `values.yaml` from budibase infra, a private repo containing budibases infrastructure configuration
|
||||
- Gets the latest budibase version from `lerna.json`, if it hasn't been specified in the workflow when you kicked it off
|
||||
- Configures AWS Credentials
|
||||
- Configures AWS Credentials
|
||||
- Deploys the helm chart in the budibase repo to our production EKS cluster, injecting the `values.yaml` we pulled from budibase-infra
|
||||
- Fires off a discord webhook in the #infra channel to show that the deployment completely successfully.
|
||||
|
||||
## Common Workflows
|
||||
|
||||
### Deploy Changes to Production (Release)
|
||||
|
||||
- Merge `develop` into `master`
|
||||
- Wait for budibase CI job and release job to run
|
||||
- Run cloud deploy job
|
||||
- Run release selfhost job
|
||||
|
||||
### Deploy Changes to Production (Hotfix)
|
||||
|
||||
- Branch off `master`
|
||||
- Perform your hotfix
|
||||
- Merge back into `master`
|
||||
|
@ -113,79 +133,7 @@ This job is responsible for deploying to our production, cloud kubernetes enviro
|
|||
- Run release selfhost job
|
||||
|
||||
### Rollback A Bad Cloud Deployment
|
||||
|
||||
- Kick off cloud deploy job
|
||||
- Ensure you are running off master
|
||||
- Enter the version number of the last known good version of budibase. For example `1.0.0`
|
||||
|
||||
## Pro
|
||||
|
||||
| **NOTE**: When developing for both pro / budibase repositories, your branch names need to match, or else the correct pro doesn't get run within your CI job.
|
||||
|
||||
### Installing Pro
|
||||
|
||||
The pro package is always installed from source in our CI jobs.
|
||||
|
||||
This is done to prevent pro needing to be published prior to CI runs in budiabse. This is required for two reasons:
|
||||
- To reduce developer need to manually bump versions, i.e:
|
||||
- release pro, bump pro dep in budibase, now ci can run successfully
|
||||
- The cyclic dependency on backend-core, i.e:
|
||||
- pro depends on backend-core
|
||||
- server depends on pro
|
||||
- backend-core lives in the monorepo, so it can't be released independently to be used in pro
|
||||
- therefore the only option is to pull pro from source and release it as a part of the monorepo release, as if it were a mono package
|
||||
|
||||
The install is performed using the same steps as local development, via the `yarn bootstrap` command, see the [Contributing Guide#Pro](../../docs/CONTRIBUTING.md#pro)
|
||||
|
||||
The branch to install pro from can vary depending on ref of the commit that triggered the budibase CI job. This is done to enable branches which have changes in both the monorepo and the pro repo to have their CI pass successfully.
|
||||
|
||||
This is done using the [pro/install.sh](../../scripts/pro/install.sh) script. The script will:
|
||||
- Clone pro to it's default branch (`develop`)
|
||||
- Check if the clone worked, on forked versions of budibase this will fail due to no access
|
||||
- This is fine as the `yarn` command will install the version from NPM
|
||||
- Community PRs should never touch pro so this will always work
|
||||
- Checkout the `BRANCH` argument, if this fails fallback to `BASE_BRANCH`
|
||||
- This enables the more complex case of a feature branch being merged to another feature branch, e.g.
|
||||
- I am working on a branch `epic/stonks` which exists on budibase and pro.
|
||||
- I want to merge a change to this branch in budibase from `feature/stonks-ui`, which only exists in budibase
|
||||
- The base branch ensures that `epic/stonks` in pro will still be checked out for the CI run, rather than falling back to `develop`
|
||||
- Run `yarn setup` to build and install dependencies
|
||||
- `yarn`
|
||||
- `yarn bootstrap`
|
||||
- `yarn build`
|
||||
- The will build .ts files, and also update the `main` and `types` of `package.json` to point to `dist` rather than src
|
||||
- The build command will only ever work in CI, it is prevented in local dev
|
||||
|
||||
#### `BRANCH` and `BASE_BRANCH` arguments
|
||||
These arguments are supplied by the various budibase build and release pipelines
|
||||
- `budibase_ci`
|
||||
- `BRANCH: ${{ github.event.pull_request.head.ref }}` -> The branch being merged
|
||||
- `BASE_BRANCH: ${{ github.event.pull_request.base.ref}}` -> The base branch
|
||||
- `release-develop`
|
||||
- `BRANCH: develop` -> always use the `develop` branch in pro
|
||||
- `release`
|
||||
- `BRANCH: master` -> always use the `master` branch in pro
|
||||
|
||||
|
||||
### Releasing Pro
|
||||
After budibase dependencies have been released we will release the new version of pro to match the release version of budibase dependencies. This is to ensure that we are always keeping the version of `backend-core` in sync in the pro package and in budibase packages. Without this we could run into scenarios where different versions are being used when installed via `yarn` inside the docker images, creating very difficult to debug cases.
|
||||
|
||||
Pro is released using the [pro/release.sh](../../scripts/pro/release.sh) script. The script will:
|
||||
- Inspect the `VERSION` from the `lerna.json` file in budibase
|
||||
- Determine whether to use the `latest` or `develop` tag based on the command argument
|
||||
- Go to pro directory
|
||||
- install npm creds
|
||||
- update the version of `backend-core` to be `VERSION`, the version just released by lerna
|
||||
- publish to npm. Uses a `lerna publish` command, pro itself is a mono repo.
|
||||
- force the version to be the same as `VERSION` to keep pro and budibase in sync
|
||||
- reverts the changes to `main` and `types` in `package.json` that were made by the build step, to point back to source
|
||||
- commit & push: `Prep next development iteration`
|
||||
- Go to budibase
|
||||
- Update to the new version of pro in `server` and `worker` so the latest pro version is used in the docker builds
|
||||
- commit & push: `Update pro version to $VERSION`
|
||||
|
||||
|
||||
#### `COMMAND` argument
|
||||
This argument is supplied by the existing `release` and `release:develop` budibase commands, which invoke the pro release
|
||||
- `release` will supply no command and default to use `latest`
|
||||
- `release:develop` will supply `develop`
|
||||
|
||||
|
|
|
@ -214,6 +214,7 @@ jobs:
|
|||
with:
|
||||
submodules: true
|
||||
token: ${{ secrets.PERSONAL_ACCESS_TOKEN || github.token }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Check pro commit
|
||||
id: get_pro_commits
|
||||
|
@ -251,4 +252,4 @@ jobs:
|
|||
process.exit(1);
|
||||
} else {
|
||||
console.log('All good, the submodule had been merged and setup correctly!')
|
||||
}
|
||||
}
|
|
@ -36,7 +36,7 @@ jobs:
|
|||
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 14.x
|
||||
node-version: 18.x
|
||||
|
||||
- run: yarn install --frozen-lockfile
|
||||
- name: Update versions
|
||||
|
|
|
@ -28,10 +28,10 @@ jobs:
|
|||
exit 1
|
||||
fi
|
||||
|
||||
- name: Use Node.js 14.x
|
||||
- name: Use Node.js 18.x
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 14.x
|
||||
node-version: 18.x
|
||||
|
||||
- name: Get the latest budibase release version
|
||||
id: version
|
||||
|
@ -67,7 +67,6 @@ jobs:
|
|||
- name: Bootstrap and build (CLI)
|
||||
run: |
|
||||
yarn
|
||||
yarn bootstrap
|
||||
yarn build
|
||||
|
||||
- name: Build OpenAPI spec
|
||||
|
|
|
@ -13,7 +13,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [14.x]
|
||||
node-version: [18.x]
|
||||
steps:
|
||||
- name: Maximize build space
|
||||
uses: easimon/maximize-build-space@master
|
||||
|
|
|
@ -97,8 +97,6 @@ typings/
|
|||
|
||||
bin/
|
||||
hosting/.generated*
|
||||
packages/builder/cypress.env.json
|
||||
packages/builder/cypress/reports
|
||||
stats.html
|
||||
|
||||
|
||||
|
|
|
@ -9,6 +9,4 @@ packages/backend-core/coverage
|
|||
packages/server/client
|
||||
packages/server/src/definitions/openapi.ts
|
||||
packages/builder/.routify
|
||||
packages/builder/cypress/support/queryLevelTransformerFunction.js
|
||||
packages/builder/cypress/support/queryLevelTransformerFunctionWithData.js
|
||||
packages/sdk/sdk
|
|
@ -1,4 +1,5 @@
|
|||
{{- if .Values.globals.createSecrets -}}
|
||||
{{- $existingSecret := lookup "v1" "Secret" .Release.Namespace (include "budibase.fullname" .) }}
|
||||
{{- if .Values.globals.createSecrets }}
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
|
@ -10,8 +11,15 @@ metadata:
|
|||
heritage: "{{ .Release.Service }}"
|
||||
type: Opaque
|
||||
data:
|
||||
{{- if $existingSecret }}
|
||||
internalApiKey: {{ index $existingSecret.data "internalApiKey" }}
|
||||
jwtSecret: {{ index $existingSecret.data "jwtSecret" }}
|
||||
objectStoreAccess: {{ index $existingSecret.data "objectStoreAccess" }}
|
||||
objectStoreSecret: {{ index $existingSecret.data "objectStoreSecret" }}
|
||||
{{- else }}
|
||||
internalApiKey: {{ template "budibase.defaultsecret" .Values.globals.internalApiKey }}
|
||||
jwtSecret: {{ template "budibase.defaultsecret" .Values.globals.jwtSecret }}
|
||||
objectStoreAccess: {{ template "budibase.defaultsecret" .Values.services.objectStore.accessKey }}
|
||||
objectStoreSecret: {{ template "budibase.defaultsecret" .Values.services.objectStore.secretKey }}
|
||||
{{- end -}}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
|
|
@ -264,16 +264,14 @@ Sometimes, things go wrong. This can be due to incompatible updates on the budib
|
|||
|
||||
### Running tests
|
||||
|
||||
#### End-to-end Tests
|
||||
#### Unit Tests
|
||||
|
||||
Budibase uses Cypress to run a number of E2E tests. To run the tests execute the following command in the root folder:
|
||||
Budibase uses Jest to run a number of tests. To run the tests execute the following command in the root folder:
|
||||
|
||||
```
|
||||
yarn test:e2e
|
||||
yarn test
|
||||
```
|
||||
|
||||
Or if you are in the builder you can run `yarn cy:test`.
|
||||
|
||||
### Other Useful Information
|
||||
|
||||
- The contributors are listed in [AUTHORS.md](https://github.com/Budibase/budibase/blob/master/.github/AUTHORS.md) (add yourself).
|
||||
|
|
|
@ -55,7 +55,7 @@ yarn setup
|
|||
The yarn setup command runs several build steps i.e.
|
||||
|
||||
```
|
||||
node ./hosting/scripts/setup.js && yarn && yarn bootstrap && yarn build && yarn dev
|
||||
node ./hosting/scripts/setup.js && yarn && yarn build && yarn dev
|
||||
```
|
||||
|
||||
So this command will actually run the application in dev mode. It creates .env files under `./packages/server` and `./packages/worker` and runs docker containers for each service via docker-compose.
|
||||
|
|
|
@ -55,7 +55,7 @@ yarn setup
|
|||
The yarn setup command runs several build steps i.e.
|
||||
|
||||
```
|
||||
node ./hosting/scripts/setup.js && yarn && yarn bootstrap && yarn build && yarn dev
|
||||
node ./hosting/scripts/setup.js && yarn && yarn build && yarn dev
|
||||
```
|
||||
|
||||
So this command will actually run the application in dev mode. It creates .env files under `./packages/server` and `./packages/worker` and runs docker containers for each service via docker-compose.
|
||||
|
|
|
@ -74,7 +74,7 @@ yarn setup
|
|||
The yarn setup command runs several build steps i.e.
|
||||
|
||||
```
|
||||
node ./hosting/scripts/setup.js && yarn && yarn bootstrap && yarn build && yarn dev
|
||||
node ./hosting/scripts/setup.js && yarn && yarn build && yarn dev
|
||||
```
|
||||
|
||||
So this command will actually run the application in dev mode. It creates .env files under `./packages/server` and `./packages/worker` and runs docker containers for each service via docker-compose.
|
||||
|
|
|
@ -58,7 +58,6 @@ Node setup:
|
|||
```
|
||||
node ./hosting/scripts/setup.js
|
||||
yarn
|
||||
yarn bootstrap
|
||||
yarn build
|
||||
```
|
||||
#### Build Image
|
||||
|
|
|
@ -47,7 +47,6 @@ Node setup:
|
|||
```
|
||||
node ./hosting/scripts/setup.js
|
||||
yarn
|
||||
yarn bootstrap
|
||||
yarn build
|
||||
```
|
||||
#### Build Image
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"version": "2.9.33-alpha.3",
|
||||
"version": "2.9.40-alpha.8",
|
||||
"npmClient": "yarn",
|
||||
"packages": [
|
||||
"packages/*"
|
||||
|
|
15
package.json
15
package.json
|
@ -9,7 +9,6 @@
|
|||
"esbuild": "^0.18.17",
|
||||
"esbuild-node-externals": "^1.8.0",
|
||||
"eslint": "^8.44.0",
|
||||
"eslint-plugin-cypress": "^2.11.3",
|
||||
"husky": "^8.0.3",
|
||||
"js-yaml": "^4.1.0",
|
||||
"kill-port": "^1.6.1",
|
||||
|
@ -33,25 +32,22 @@
|
|||
"scripts": {
|
||||
"preinstall": "node scripts/syncProPackage.js",
|
||||
"setup": "git config submodule.recurse true && git submodule update && node ./hosting/scripts/setup.js && yarn && yarn build && yarn dev",
|
||||
"bootstrap": "./scripts/link-dependencies.sh && echo '***BOOTSTRAP ONLY REQUIRED FOR USE WITH ACCOUNT PORTAL***'",
|
||||
"build": "lerna run build --stream",
|
||||
"build:dev": "lerna run --stream prebuild && yarn nx run-many --target=build --output-style=dynamic --watch --preserveWatchOutput",
|
||||
"check:types": "lerna run check:types",
|
||||
"backend:bootstrap": "./scripts/scopeBackend.sh && yarn run bootstrap",
|
||||
"backend:build": "./scripts/scopeBackend.sh 'lerna run --stream build'",
|
||||
"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 run bootstrap && yarn run build",
|
||||
"restore": "yarn run clean && yarn && yarn run build",
|
||||
"nuke": "yarn run nuke:packages && yarn run nuke:docker",
|
||||
"nuke:packages": "yarn run restore",
|
||||
"nuke:docker": "lerna run --stream dev:stack:nuke",
|
||||
"clean": "lerna clean",
|
||||
"clean": "lerna clean -y",
|
||||
"kill-builder": "kill-port 3000",
|
||||
"kill-server": "kill-port 4001 4002",
|
||||
"kill-all": "yarn run kill-builder && yarn run kill-server",
|
||||
"dev": "yarn run kill-all && lerna run --stream dev:builder",
|
||||
"dev": "yarn run kill-all && lerna run --parallel prebuild && lerna run --stream dev:builder",
|
||||
"dev:noserver": "yarn run kill-builder && lerna run --stream dev:stack:up && lerna run --stream dev:builder --ignore @budibase/backend-core --ignore @budibase/server --ignore @budibase/worker",
|
||||
"dev:server": "yarn run kill-server && lerna run --stream dev:builder --scope @budibase/worker --scope @budibase/server",
|
||||
"dev:built": "yarn run kill-all && cd packages/server && yarn dev:stack:up && cd ../../ && lerna run --stream dev:built",
|
||||
|
@ -93,9 +89,8 @@
|
|||
"mode:account": "yarn mode:cloud && yarn env:account:enable",
|
||||
"security:audit": "node scripts/audit.js",
|
||||
"postinstall": "husky install",
|
||||
"dep:clean": "yarn clean -y && yarn bootstrap",
|
||||
"submodules:load": "git submodule init && git submodule update && yarn && yarn bootstrap",
|
||||
"submodules:unload": "git submodule deinit --all && yarn && yarn bootstrap"
|
||||
"submodules:load": "git submodule init && git submodule update && yarn",
|
||||
"submodules:unload": "git submodule deinit --all && yarn"
|
||||
},
|
||||
"workspaces": {
|
||||
"packages": [
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
*
|
||||
!dist/**/*
|
||||
dist/tsconfig.build.tsbuildinfo
|
||||
!package.json
|
||||
!package.json
|
||||
!src/**
|
||||
!tests/**
|
|
@ -6,7 +6,7 @@
|
|||
"types": "dist/src/index.d.ts",
|
||||
"exports": {
|
||||
".": "./dist/index.js",
|
||||
"./tests": "./dist/tests.js",
|
||||
"./tests": "./dist/tests/index.js",
|
||||
"./*": "./dist/*.js"
|
||||
},
|
||||
"author": "Budibase",
|
||||
|
@ -14,7 +14,7 @@
|
|||
"scripts": {
|
||||
"prebuild": "rimraf dist/",
|
||||
"prepack": "cp package.json dist",
|
||||
"build": "node ./scripts/build.js && tsc -p tsconfig.build.json --emitDeclarationOnly --paths null",
|
||||
"build": "tsc -p tsconfig.build.json --paths null && node ./scripts/build.js",
|
||||
"build:dev": "yarn prebuild && tsc --build --watch --preserveWatchOutput",
|
||||
"check:types": "tsc -p tsconfig.json --noEmit --paths null",
|
||||
"test": "bash scripts/test.sh",
|
||||
|
@ -68,8 +68,8 @@
|
|||
"@types/jest": "29.5.3",
|
||||
"@types/koa": "2.13.4",
|
||||
"@types/lodash": "4.14.180",
|
||||
"@types/node": "14.18.20",
|
||||
"@types/node-fetch": "2.6.1",
|
||||
"@types/node": "18.17.0",
|
||||
"@types/node-fetch": "2.6.4",
|
||||
"@types/pouchdb": "6.4.0",
|
||||
"@types/redlock": "4.0.3",
|
||||
"@types/semver": "7.3.7",
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
#!/usr/bin/node
|
||||
const coreBuild = require("../../../scripts/build")
|
||||
|
||||
coreBuild("./src/plugin/index.ts", "./dist/plugins.js")
|
||||
coreBuild("./src/index.ts", "./dist/index.js")
|
||||
coreBuild("./tests/index.ts", "./dist/tests.js")
|
||||
|
|
|
@ -55,7 +55,7 @@ export async function getAppMetadata(appId: string): Promise<App | DeletedApp> {
|
|||
throw err
|
||||
}
|
||||
}
|
||||
// needed for cypress/some scenarios where the caching happens
|
||||
// needed for some scenarios where the caching happens
|
||||
// so quickly the requests can get slightly out of sync
|
||||
// might store its invalid just before it stores its valid
|
||||
if (isInvalid(metadata)) {
|
||||
|
|
|
@ -22,6 +22,8 @@ export enum Header {
|
|||
TENANT_ID = "x-budibase-tenant-id",
|
||||
VERIFICATION_CODE = "x-budibase-verification-code",
|
||||
RETURN_VERIFICATION_CODE = "x-budibase-return-verification-code",
|
||||
RESET_PASSWORD_CODE = "x-budibase-reset-password-code",
|
||||
RETURN_RESET_PASSWORD_CODE = "x-budibase-return-reset-password-code",
|
||||
TOKEN = "x-budibase-token",
|
||||
CSRF_TOKEN = "x-csrf-token",
|
||||
CORRELATION_ID = "x-budibase-correlation-id",
|
||||
|
|
|
@ -8,7 +8,6 @@ import {
|
|||
DatabasePutOpts,
|
||||
DatabaseCreateIndexOpts,
|
||||
DatabaseDeleteIndexOpts,
|
||||
DocExistsResponse,
|
||||
Document,
|
||||
isDocument,
|
||||
} from "@budibase/types"
|
||||
|
@ -121,19 +120,6 @@ export class DatabaseImpl implements Database {
|
|||
return this.updateOutput(() => db.get(id))
|
||||
}
|
||||
|
||||
async docExists(docId: string): Promise<DocExistsResponse> {
|
||||
const db = await this.checkSetup()
|
||||
let _rev, exists
|
||||
try {
|
||||
const { etag } = await db.head(docId)
|
||||
_rev = etag
|
||||
exists = true
|
||||
} catch (err) {
|
||||
exists = false
|
||||
}
|
||||
return { _rev, exists }
|
||||
}
|
||||
|
||||
async remove(idOrDoc: string | Document, rev?: string) {
|
||||
const db = await this.checkSetup()
|
||||
let _id: string
|
||||
|
|
|
@ -11,7 +11,11 @@ export function getDB(dbName?: string, opts?: any): Database {
|
|||
// we have to use a callback for this so that we can close
|
||||
// the DB when we're done, without this manual requests would
|
||||
// need to close the database when done with it to avoid memory leaks
|
||||
export async function doWithDB(dbName: string, cb: any, opts = {}) {
|
||||
export async function doWithDB<T>(
|
||||
dbName: string,
|
||||
cb: (db: Database) => Promise<T>,
|
||||
opts = {}
|
||||
) {
|
||||
const db = getDB(dbName, opts)
|
||||
// need this to be async so that we can correctly close DB after all
|
||||
// async operations have been completed
|
||||
|
|
|
@ -2,15 +2,15 @@ import { existsSync, readFileSync } from "fs"
|
|||
import { ServiceType } from "@budibase/types"
|
||||
|
||||
function isTest() {
|
||||
return isCypress() || isJest()
|
||||
return isJest()
|
||||
}
|
||||
|
||||
function isJest() {
|
||||
return !!(process.env.NODE_ENV === "jest" || process.env.JEST_WORKER_ID)
|
||||
}
|
||||
|
||||
function isCypress() {
|
||||
return process.env.NODE_ENV === "cypress"
|
||||
return (
|
||||
process.env.NODE_ENV === "jest" ||
|
||||
(process.env.JEST_WORKER_ID != null &&
|
||||
process.env.JEST_WORKER_ID !== "null")
|
||||
)
|
||||
}
|
||||
|
||||
function isDev() {
|
||||
|
|
|
@ -87,6 +87,7 @@ export const BUILTIN_PERMISSIONS = {
|
|||
new Permission(PermissionType.QUERY, PermissionLevel.WRITE),
|
||||
new Permission(PermissionType.TABLE, PermissionLevel.WRITE),
|
||||
new Permission(PermissionType.AUTOMATION, PermissionLevel.EXECUTE),
|
||||
new Permission(PermissionType.LEGACY_VIEW, PermissionLevel.READ),
|
||||
],
|
||||
},
|
||||
POWER: {
|
||||
|
@ -97,6 +98,7 @@ export const BUILTIN_PERMISSIONS = {
|
|||
new Permission(PermissionType.USER, PermissionLevel.READ),
|
||||
new Permission(PermissionType.AUTOMATION, PermissionLevel.EXECUTE),
|
||||
new Permission(PermissionType.WEBHOOK, PermissionLevel.READ),
|
||||
new Permission(PermissionType.LEGACY_VIEW, PermissionLevel.READ),
|
||||
],
|
||||
},
|
||||
ADMIN: {
|
||||
|
@ -108,6 +110,7 @@ export const BUILTIN_PERMISSIONS = {
|
|||
new Permission(PermissionType.AUTOMATION, PermissionLevel.ADMIN),
|
||||
new Permission(PermissionType.WEBHOOK, PermissionLevel.READ),
|
||||
new Permission(PermissionType.QUERY, PermissionLevel.ADMIN),
|
||||
new Permission(PermissionType.LEGACY_VIEW, PermissionLevel.READ),
|
||||
],
|
||||
},
|
||||
}
|
||||
|
|
|
@ -253,7 +253,7 @@ export function checkForRoleResourceArray(
|
|||
* 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.
|
||||
*/
|
||||
export async function getAllRoles(appId?: string) {
|
||||
export async function getAllRoles(appId?: string): Promise<RoleDoc[]> {
|
||||
if (appId) {
|
||||
return doWithDB(appId, internal)
|
||||
} else {
|
||||
|
@ -312,37 +312,6 @@ export async function getAllRoles(appId?: string) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This retrieves the required role for a resource
|
||||
* @param permLevel The level of request
|
||||
* @param resourceId The resource being requested
|
||||
* @param subResourceId The sub resource being requested
|
||||
* @return {Promise<{permissions}|Object>} returns the permissions required to access.
|
||||
*/
|
||||
export async function getRequiredResourceRole(
|
||||
permLevel: string,
|
||||
{ resourceId, subResourceId }: { resourceId?: string; subResourceId?: string }
|
||||
) {
|
||||
const roles = await getAllRoles()
|
||||
let main = [],
|
||||
sub = []
|
||||
for (let role of roles) {
|
||||
// no permissions, ignore it
|
||||
if (!role.permissions) {
|
||||
continue
|
||||
}
|
||||
const mainRes = resourceId ? role.permissions[resourceId] : undefined
|
||||
const subRes = subResourceId ? role.permissions[subResourceId] : undefined
|
||||
if (mainRes && mainRes.indexOf(permLevel) !== -1) {
|
||||
main.push(role._id)
|
||||
} else if (subRes && subRes.indexOf(permLevel) !== -1) {
|
||||
sub.push(role._id)
|
||||
}
|
||||
}
|
||||
// for now just return the IDs
|
||||
return main.concat(sub)
|
||||
}
|
||||
|
||||
export class AccessController {
|
||||
userHierarchies: { [key: string]: string[] }
|
||||
constructor() {
|
||||
|
@ -411,8 +380,8 @@ export function getDBRoleID(roleName: string) {
|
|||
export function getExternalRoleID(roleId: string, version?: string) {
|
||||
// for built-in roles we want to remove the DB role ID element (role_)
|
||||
if (
|
||||
(roleId.startsWith(DocumentType.ROLE) && isBuiltin(roleId)) ||
|
||||
version === RoleIDVersion.NAME
|
||||
roleId.startsWith(DocumentType.ROLE) &&
|
||||
(isBuiltin(roleId) || version === RoleIDVersion.NAME)
|
||||
) {
|
||||
return roleId.split(`${DocumentType.ROLE}${SEPARATOR}`)[1]
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@ export default function positionDropdown(element, opts) {
|
|||
useAnchorWidth,
|
||||
offset = 5,
|
||||
customUpdate,
|
||||
offsetBelow,
|
||||
} = opts
|
||||
if (!anchor) {
|
||||
return
|
||||
|
@ -47,7 +48,7 @@ export default function positionDropdown(element, opts) {
|
|||
styles.top = anchorBounds.top - elementBounds.height - offset
|
||||
styles.maxHeight = maxHeight || 240
|
||||
} else {
|
||||
styles.top = anchorBounds.bottom + offset
|
||||
styles.top = anchorBounds.bottom + (offsetBelow || offset)
|
||||
styles.maxHeight =
|
||||
maxHeight || window.innerHeight - anchorBounds.bottom - 20
|
||||
}
|
||||
|
|
|
@ -2,8 +2,9 @@
|
|||
import { createEventDispatcher } from "svelte"
|
||||
import FancyField from "./FancyField.svelte"
|
||||
import Icon from "../Icon/Icon.svelte"
|
||||
import Popover from "../Popover/Popover.svelte"
|
||||
import FancyFieldLabel from "./FancyFieldLabel.svelte"
|
||||
import StatusLight from "../StatusLight/StatusLight.svelte"
|
||||
import Picker from "../Form/Core/Picker.svelte"
|
||||
|
||||
export let label
|
||||
export let value
|
||||
|
@ -11,18 +12,30 @@
|
|||
export let error = null
|
||||
export let validate = null
|
||||
export let options = []
|
||||
export let isOptionEnabled = () => true
|
||||
export let getOptionLabel = option => extractProperty(option, "label")
|
||||
export let getOptionValue = option => extractProperty(option, "value")
|
||||
|
||||
export let getOptionSubtitle = option => extractProperty(option, "subtitle")
|
||||
export let getOptionColour = () => null
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let open = false
|
||||
let popover
|
||||
let wrapper
|
||||
|
||||
$: placeholder = !value
|
||||
$: selectedLabel = getSelectedLabel(value)
|
||||
$: fieldColour = getFieldAttribute(getOptionColour, value, options)
|
||||
|
||||
const getFieldAttribute = (getAttribute, value, options) => {
|
||||
// Wait for options to load if there is a value but no options
|
||||
if (!options?.length) {
|
||||
return ""
|
||||
}
|
||||
const index = options.findIndex(
|
||||
(option, idx) => getOptionValue(option, idx) === value
|
||||
)
|
||||
return index !== -1 ? getAttribute(options[index], index) : null
|
||||
}
|
||||
const extractProperty = (value, property) => {
|
||||
if (value && typeof value === "object") {
|
||||
return value[property]
|
||||
|
@ -64,46 +77,45 @@
|
|||
<FancyFieldLabel {placeholder}>{label}</FancyFieldLabel>
|
||||
{/if}
|
||||
|
||||
{#if fieldColour}
|
||||
<span class="align">
|
||||
<StatusLight square color={fieldColour} />
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
<div class="value" class:placeholder>
|
||||
{selectedLabel || ""}
|
||||
</div>
|
||||
|
||||
<div class="arrow">
|
||||
<div class="align arrow-alignment">
|
||||
<Icon name="ChevronDown" />
|
||||
</div>
|
||||
</FancyField>
|
||||
|
||||
<Popover
|
||||
anchor={wrapper}
|
||||
align="left"
|
||||
portalTarget={document.documentElement}
|
||||
bind:this={popover}
|
||||
{open}
|
||||
on:close={() => (open = false)}
|
||||
useAnchorWidth={true}
|
||||
maxWidth={null}
|
||||
>
|
||||
<div class="popover-content">
|
||||
{#if options.length}
|
||||
{#each options as option, idx}
|
||||
<div
|
||||
class="popover-option"
|
||||
tabindex="0"
|
||||
on:click={() => onChange(getOptionValue(option, idx))}
|
||||
>
|
||||
<span class="option-text">
|
||||
{getOptionLabel(option, idx)}
|
||||
</span>
|
||||
{#if value === getOptionValue(option, idx)}
|
||||
<Icon name="Checkmark" />
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
</Popover>
|
||||
<div id="picker-wrapper">
|
||||
<Picker
|
||||
customAnchor={wrapper}
|
||||
onlyPopover={true}
|
||||
bind:open
|
||||
{error}
|
||||
{disabled}
|
||||
{options}
|
||||
{getOptionLabel}
|
||||
{getOptionValue}
|
||||
{getOptionSubtitle}
|
||||
{getOptionColour}
|
||||
{isOptionEnabled}
|
||||
isPlaceholder={value == null || value === ""}
|
||||
placeholderOption={placeholder === false ? null : placeholder}
|
||||
onSelectOption={onChange}
|
||||
isOptionSelected={option => option === value}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
#picker-wrapper :global(.spectrum-Picker) {
|
||||
display: none;
|
||||
}
|
||||
.value {
|
||||
display: block;
|
||||
flex: 1 1 auto;
|
||||
|
@ -118,30 +130,23 @@
|
|||
width: 0;
|
||||
transform: translateY(9px);
|
||||
}
|
||||
|
||||
.align {
|
||||
display: block;
|
||||
font-size: 15px;
|
||||
line-height: 17px;
|
||||
color: var(--spectrum-global-color-gray-900);
|
||||
transition: transform 130ms ease-out, opacity 130ms ease-out;
|
||||
transform: translateY(9px);
|
||||
}
|
||||
|
||||
.arrow-alignment {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
.value.placeholder {
|
||||
transform: translateY(0);
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
margin-top: 0;
|
||||
}
|
||||
.popover-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: stretch;
|
||||
padding: 7px 0;
|
||||
}
|
||||
.popover-option {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 7px 16px;
|
||||
transition: background 130ms ease-out;
|
||||
font-size: 15px;
|
||||
}
|
||||
.popover-option:hover {
|
||||
background: var(--spectrum-global-color-gray-200);
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -17,6 +17,9 @@
|
|||
export let fetchTerm = null
|
||||
export let useFetch = false
|
||||
export let customPopoverHeight
|
||||
export let customPopoverOffsetBelow
|
||||
export let customPopoverMaxHeight
|
||||
export let open = false
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
|
@ -88,6 +91,7 @@
|
|||
isPlaceholder={!arrayValue.length}
|
||||
{autocomplete}
|
||||
bind:fetchTerm
|
||||
bind:open
|
||||
{useFetch}
|
||||
{isOptionSelected}
|
||||
{getOptionLabel}
|
||||
|
@ -96,4 +100,6 @@
|
|||
{sort}
|
||||
{autoWidth}
|
||||
{customPopoverHeight}
|
||||
{customPopoverOffsetBelow}
|
||||
{customPopoverMaxHeight}
|
||||
/>
|
||||
|
|
|
@ -8,6 +8,8 @@
|
|||
import Icon from "../../Icon/Icon.svelte"
|
||||
import StatusLight from "../../StatusLight/StatusLight.svelte"
|
||||
import Popover from "../../Popover/Popover.svelte"
|
||||
import Tags from "../../Tags/Tags.svelte"
|
||||
import Tag from "../../Tags/Tag.svelte"
|
||||
|
||||
export let id = null
|
||||
export let disabled = false
|
||||
|
@ -26,6 +28,7 @@
|
|||
export let getOptionIcon = () => null
|
||||
export let useOptionIconImage = false
|
||||
export let getOptionColour = () => null
|
||||
export let getOptionSubtitle = () => null
|
||||
export let open = false
|
||||
export let readonly = false
|
||||
export let quiet = false
|
||||
|
@ -35,9 +38,11 @@
|
|||
export let fetchTerm = null
|
||||
export let useFetch = false
|
||||
export let customPopoverHeight
|
||||
export let customPopoverOffsetBelow
|
||||
export let customPopoverMaxHeight
|
||||
export let align = "left"
|
||||
export let footer = null
|
||||
|
||||
export let customAnchor = null
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let searchTerm = null
|
||||
|
@ -139,16 +144,17 @@
|
|||
<use xlink:href="#spectrum-css-icon-Chevron100" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<Popover
|
||||
anchor={button}
|
||||
anchor={customAnchor ? customAnchor : button}
|
||||
align={align || "left"}
|
||||
bind:this={popover}
|
||||
{open}
|
||||
on:close={() => (open = false)}
|
||||
useAnchorWidth={!autoWidth}
|
||||
maxWidth={autoWidth ? 400 : null}
|
||||
maxHeight={customPopoverMaxHeight}
|
||||
customHeight={customPopoverHeight}
|
||||
offsetBelow={customPopoverOffsetBelow}
|
||||
>
|
||||
<div
|
||||
class="popover-content"
|
||||
|
@ -215,8 +221,21 @@
|
|||
</span>
|
||||
{/if}
|
||||
<span class="spectrum-Menu-itemLabel">
|
||||
{#if getOptionSubtitle(option, idx)}
|
||||
<span class="subtitle-text"
|
||||
>{getOptionSubtitle(option, idx)}</span
|
||||
>
|
||||
{/if}
|
||||
|
||||
{getOptionLabel(option, idx)}
|
||||
</span>
|
||||
{#if option.tag}
|
||||
<span class="option-tag">
|
||||
<Tags>
|
||||
<Tag icon="LockClosed">{option.tag}</Tag>
|
||||
</Tags>
|
||||
</span>
|
||||
{/if}
|
||||
<svg
|
||||
class="spectrum-Icon spectrum-UIIcon-Checkmark100 spectrum-Menu-checkmark spectrum-Menu-itemIcon"
|
||||
focusable="false"
|
||||
|
@ -242,6 +261,17 @@
|
|||
width: 100%;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.subtitle-text {
|
||||
font-size: 12px;
|
||||
line-height: 15px;
|
||||
font-weight: 500;
|
||||
top: 10px;
|
||||
color: var(--spectrum-global-color-gray-600);
|
||||
display: block;
|
||||
margin-bottom: var(--spacing-s);
|
||||
}
|
||||
|
||||
.spectrum-Picker-label.auto-width {
|
||||
margin-right: var(--spacing-xs);
|
||||
}
|
||||
|
@ -321,4 +351,12 @@
|
|||
.option-extra.icon.field-icon {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.option-tag {
|
||||
margin: 0 var(--spacing-m) 0 var(--spacing-m);
|
||||
}
|
||||
|
||||
.option-tag :global(.spectrum-Tags-item > .spectrum-Icon) {
|
||||
margin-top: 2px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -21,11 +21,13 @@
|
|||
export let sort = false
|
||||
export let align
|
||||
export let footer = null
|
||||
export let open = false
|
||||
export let tag = null
|
||||
export let customPopoverOffsetBelow
|
||||
export let customPopoverMaxHeight
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let open = false
|
||||
|
||||
$: fieldText = getFieldText(value, options, placeholder)
|
||||
$: fieldIcon = getFieldAttribute(getOptionIcon, value, options)
|
||||
$: fieldColour = getFieldAttribute(getOptionColour, value, options)
|
||||
|
@ -83,6 +85,9 @@
|
|||
{isOptionEnabled}
|
||||
{autocomplete}
|
||||
{sort}
|
||||
{tag}
|
||||
{customPopoverOffsetBelow}
|
||||
{customPopoverMaxHeight}
|
||||
isPlaceholder={value == null || value === ""}
|
||||
placeholderOption={placeholder === false ? null : placeholder}
|
||||
isOptionSelected={option => option === value}
|
||||
|
|
|
@ -25,7 +25,7 @@
|
|||
export let customPopoverHeight
|
||||
export let align
|
||||
export let footer = null
|
||||
|
||||
export let tag = null
|
||||
const dispatch = createEventDispatcher()
|
||||
const onChange = e => {
|
||||
value = e.detail
|
||||
|
@ -61,6 +61,7 @@
|
|||
{isOptionEnabled}
|
||||
{autocomplete}
|
||||
{customPopoverHeight}
|
||||
{tag}
|
||||
on:change={onChange}
|
||||
on:click
|
||||
/>
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
export let fixed = false
|
||||
export let inline = false
|
||||
export let disableCancel = false
|
||||
export let autoFocus = true
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
let visible = fixed || inline
|
||||
|
@ -53,6 +54,9 @@
|
|||
}
|
||||
|
||||
async function focusModal(node) {
|
||||
if (!autoFocus) {
|
||||
return
|
||||
}
|
||||
await tick()
|
||||
|
||||
// Try to focus first input
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
export let useAnchorWidth = false
|
||||
export let dismissible = true
|
||||
export let offset = 5
|
||||
export let offsetBelow
|
||||
export let customHeight
|
||||
export let animate = true
|
||||
export let customZindex
|
||||
|
@ -89,6 +90,7 @@
|
|||
maxWidth,
|
||||
useAnchorWidth,
|
||||
offset,
|
||||
offsetBelow,
|
||||
customUpdate: handlePostionUpdate,
|
||||
}}
|
||||
use:clickOutside={{
|
||||
|
|
|
@ -57,10 +57,8 @@
|
|||
function calculateIndicatorLength() {
|
||||
if (!vertical) {
|
||||
width = $tab.info?.width + "px"
|
||||
height = $tab.info?.height
|
||||
} else {
|
||||
height = $tab.info?.height + 4 + "px"
|
||||
width = $tab.info?.width
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
export let text = null
|
||||
export let condition = true
|
||||
export let duration = 3000
|
||||
export let duration = 5000
|
||||
export let position
|
||||
export let type
|
||||
|
||||
|
|
|
@ -5,6 +5,4 @@ package-lock.json
|
|||
release/
|
||||
dist/
|
||||
routify
|
||||
cypress/videos
|
||||
cypress/screenshots
|
||||
.routify/
|
|
@ -43,21 +43,10 @@
|
|||
"/node_modules/(?!svelte).+\\.js$",
|
||||
".*string-templates.*"
|
||||
],
|
||||
"modulePathIgnorePatterns": [
|
||||
"<rootDir>/cypress/"
|
||||
],
|
||||
"setupFilesAfterEnv": [
|
||||
"@testing-library/jest-dom/extend-expect"
|
||||
]
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"plugin:cypress/recommended"
|
||||
],
|
||||
"rules": {
|
||||
"cypress/no-unnecessary-waiting": "off"
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@budibase/bbui": "0.0.0",
|
||||
"@budibase/frontend-core": "0.0.0",
|
||||
|
@ -104,9 +93,6 @@
|
|||
"@testing-library/jest-dom": "5.17.0",
|
||||
"@testing-library/svelte": "^3.2.2",
|
||||
"babel-jest": "29.6.2",
|
||||
"cypress": "^9.3.1",
|
||||
"cypress-multi-reporters": "^1.6.0",
|
||||
"cypress-terminal-report": "^1.4.1",
|
||||
"identity-obj-proxy": "^3.0.0",
|
||||
"jest": "29.6.2",
|
||||
"jsdom": "^21.1.1",
|
||||
|
|
|
@ -351,12 +351,19 @@ const getProviderContextBindings = (asset, dataProviders) => {
|
|||
schema = info.schema
|
||||
table = info.table
|
||||
|
||||
// For JSON arrays, use the array name as the readable prefix.
|
||||
// Otherwise use the table name
|
||||
// Determine what to prefix bindings with
|
||||
if (datasource.type === "jsonarray") {
|
||||
// For JSON arrays, use the array name as the readable prefix
|
||||
const split = datasource.label.split(".")
|
||||
readablePrefix = split[split.length - 1]
|
||||
} else if (datasource.type === "viewV2") {
|
||||
// For views, use the view name
|
||||
const view = Object.values(table?.views || {}).find(
|
||||
view => view.id === datasource.id
|
||||
)
|
||||
readablePrefix = view?.name
|
||||
} else {
|
||||
// Otherwise use the table name
|
||||
readablePrefix = info.table?.name
|
||||
}
|
||||
}
|
||||
|
@ -464,7 +471,7 @@ const getComponentBindingCategory = (component, context, def) => {
|
|||
*/
|
||||
export const getUserBindings = () => {
|
||||
let bindings = []
|
||||
const { schema } = getSchemaForTable(TableNames.USERS)
|
||||
const { schema } = getSchemaForDatasourcePlus(TableNames.USERS)
|
||||
const keys = Object.keys(schema).sort()
|
||||
const safeUser = makePropSafe("user")
|
||||
|
||||
|
@ -725,17 +732,25 @@ export const getActionBindings = (actions, actionId) => {
|
|||
}
|
||||
|
||||
/**
|
||||
* Gets the schema for a certain table ID.
|
||||
* Gets the schema for a certain datasource plus.
|
||||
* The options which can be passed in are:
|
||||
* formSchema: whether the schema is for a form
|
||||
* searchableSchema: whether to generate a searchable schema, which may have
|
||||
* fewer fields than a readable schema
|
||||
* @param tableId the table ID to get the schema for
|
||||
* @param resourceId the DS+ resource ID
|
||||
* @param options options for generating the schema
|
||||
* @return {{schema: Object, table: Object}}
|
||||
*/
|
||||
export const getSchemaForTable = (tableId, options) => {
|
||||
return getSchemaForDatasource(null, { type: "table", tableId }, options)
|
||||
export const getSchemaForDatasourcePlus = (resourceId, options) => {
|
||||
const isViewV2 = resourceId?.includes("view_")
|
||||
const datasource = isViewV2
|
||||
? {
|
||||
type: "viewV2",
|
||||
id: resourceId,
|
||||
tableId: resourceId.split("_").slice(1, 3).join("_"),
|
||||
}
|
||||
: { type: "table", tableId: resourceId }
|
||||
return getSchemaForDatasource(null, datasource, options)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -812,9 +827,21 @@ export const getSchemaForDatasource = (asset, datasource, options) => {
|
|||
// Determine the schema from the backing entity if not already determined
|
||||
if (table && !schema) {
|
||||
if (type === "view") {
|
||||
// For views, the schema is pulled from the `views` property of the
|
||||
// table
|
||||
// Old views
|
||||
schema = cloneDeep(table.views?.[datasource.name]?.schema)
|
||||
} else if (type === "viewV2") {
|
||||
// New views which are DS+
|
||||
const view = Object.values(table.views || {}).find(
|
||||
view => view.id === datasource.id
|
||||
)
|
||||
schema = cloneDeep(view?.schema)
|
||||
|
||||
// Strip hidden fields
|
||||
Object.keys(schema || {}).forEach(field => {
|
||||
if (!schema[field].visible) {
|
||||
delete schema[field]
|
||||
}
|
||||
})
|
||||
} else if (
|
||||
type === "query" &&
|
||||
(options.formSchema || options.searchableSchema)
|
||||
|
@ -860,12 +887,12 @@ export const getSchemaForDatasource = (asset, datasource, options) => {
|
|||
|
||||
// Determine if we should add ID and rev to the schema
|
||||
const isInternal = table && !table.sql
|
||||
const isTable = ["table", "link"].includes(datasource.type)
|
||||
const isDSPlus = ["table", "link", "viewV2"].includes(datasource.type)
|
||||
|
||||
// ID is part of the readable schema for all tables
|
||||
// Rev is part of the readable schema for internal tables only
|
||||
let addId = isTable
|
||||
let addRev = isTable && isInternal
|
||||
let addId = isDSPlus
|
||||
let addRev = isDSPlus && isInternal
|
||||
|
||||
// Don't add ID or rev for form schemas
|
||||
if (options.formSchema) {
|
||||
|
@ -875,7 +902,7 @@ export const getSchemaForDatasource = (asset, datasource, options) => {
|
|||
|
||||
// ID is only searchable for internal tables
|
||||
else if (options.searchableSchema) {
|
||||
addId = isTable && isInternal
|
||||
addId = isDSPlus && isInternal
|
||||
}
|
||||
|
||||
// Add schema properties if required
|
||||
|
@ -939,7 +966,9 @@ export const buildFormSchema = (component, asset) => {
|
|||
const patched = convertOldFieldFormat(component.fields || [])
|
||||
patched?.forEach(({ field, active }) => {
|
||||
if (!active) return
|
||||
schema[field] = { type: info?.schema[field].type }
|
||||
if (info?.schema[field]) {
|
||||
schema[field] = { type: info?.schema[field].type }
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -627,6 +627,7 @@ export const getFrontendStore = () => {
|
|||
component[setting.key] = {
|
||||
label: defaultDS.name,
|
||||
tableId: defaultDS._id,
|
||||
resourceId: defaultDS._id,
|
||||
type: "table",
|
||||
}
|
||||
} else if (setting.type === "dataProvider") {
|
||||
|
@ -1245,6 +1246,13 @@ export const getFrontendStore = () => {
|
|||
const settings = getComponentSettings(component._component)
|
||||
const updatedSetting = settings.find(setting => setting.key === name)
|
||||
|
||||
const resetFields = settings.filter(
|
||||
setting => name === setting.resetOn
|
||||
)
|
||||
resetFields?.forEach(setting => {
|
||||
component[setting.key] = null
|
||||
})
|
||||
|
||||
if (
|
||||
updatedSetting?.type === "dataSource" ||
|
||||
updatedSetting?.type === "table"
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import rowListScreen from "./rowListScreen"
|
||||
import createFromScratchScreen from "./createFromScratchScreen"
|
||||
|
||||
const allTemplates = tables => [...rowListScreen(tables)]
|
||||
const allTemplates = datasources => [...rowListScreen(datasources)]
|
||||
|
||||
// Allows us to apply common behaviour to all create() functions
|
||||
const createTemplateOverride = (frontendState, template) => () => {
|
||||
const createTemplateOverride = template => () => {
|
||||
const screen = template.create()
|
||||
screen.name = screen.props._id
|
||||
screen.routing.route = screen.routing.route.toLowerCase()
|
||||
|
@ -12,14 +12,13 @@ const createTemplateOverride = (frontendState, template) => () => {
|
|||
return screen
|
||||
}
|
||||
|
||||
export default (frontendState, tables) => {
|
||||
export default datasources => {
|
||||
const enrichTemplate = template => ({
|
||||
...template,
|
||||
create: createTemplateOverride(frontendState, template),
|
||||
create: createTemplateOverride(template),
|
||||
})
|
||||
|
||||
const fromScratch = enrichTemplate(createFromScratchScreen)
|
||||
const tableTemplates = allTemplates(tables).map(enrichTemplate)
|
||||
const tableTemplates = allTemplates(datasources).map(enrichTemplate)
|
||||
return [
|
||||
fromScratch,
|
||||
...tableTemplates.sort((templateA, templateB) => {
|
||||
|
|
|
@ -2,31 +2,29 @@ import sanitizeUrl from "./utils/sanitizeUrl"
|
|||
import { Screen } from "./utils/Screen"
|
||||
import { Component } from "./utils/Component"
|
||||
|
||||
export default function (tables) {
|
||||
return tables.map(table => {
|
||||
export default function (datasources) {
|
||||
if (!Array.isArray(datasources)) {
|
||||
return []
|
||||
}
|
||||
return datasources.map(datasource => {
|
||||
return {
|
||||
name: `${table.name} - List`,
|
||||
create: () => createScreen(table),
|
||||
name: `${datasource.label} - List`,
|
||||
create: () => createScreen(datasource),
|
||||
id: ROW_LIST_TEMPLATE,
|
||||
table: table._id,
|
||||
resourceId: datasource.resourceId,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const ROW_LIST_TEMPLATE = "ROW_LIST_TEMPLATE"
|
||||
export const rowListUrl = table => sanitizeUrl(`/${table.name}`)
|
||||
export const rowListUrl = datasource => sanitizeUrl(`/${datasource.label}`)
|
||||
|
||||
const generateTableBlock = table => {
|
||||
const generateTableBlock = datasource => {
|
||||
const tableBlock = new Component("@budibase/standard-components/tableblock")
|
||||
tableBlock
|
||||
.customProps({
|
||||
title: table.name,
|
||||
dataSource: {
|
||||
label: table.name,
|
||||
name: table._id,
|
||||
tableId: table._id,
|
||||
type: "table",
|
||||
},
|
||||
title: datasource.label,
|
||||
dataSource: datasource,
|
||||
sortOrder: "Ascending",
|
||||
size: "spectrum--medium",
|
||||
paginate: true,
|
||||
|
@ -36,14 +34,14 @@ const generateTableBlock = table => {
|
|||
titleButtonText: "Create row",
|
||||
titleButtonClickBehaviour: "new",
|
||||
})
|
||||
.instanceName(`${table.name} - Table block`)
|
||||
.instanceName(`${datasource.label} - Table block`)
|
||||
return tableBlock
|
||||
}
|
||||
|
||||
const createScreen = table => {
|
||||
const createScreen = datasource => {
|
||||
return new Screen()
|
||||
.route(rowListUrl(table))
|
||||
.instanceName(`${table.name} - List`)
|
||||
.addChild(generateTableBlock(table))
|
||||
.route(rowListUrl(datasource))
|
||||
.instanceName(`${datasource.label} - List`)
|
||||
.addChild(generateTableBlock(datasource))
|
||||
.json()
|
||||
}
|
||||
|
|
|
@ -73,7 +73,7 @@
|
|||
if (!perms["execute"]) {
|
||||
role = "BASIC"
|
||||
} else {
|
||||
role = perms["execute"]
|
||||
role = perms["execute"].role
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -39,7 +39,7 @@
|
|||
import FilterDrawer from "components/design/settings/controls/FilterEditor/FilterDrawer.svelte"
|
||||
import { LuceneUtils } from "@budibase/frontend-core"
|
||||
import {
|
||||
getSchemaForTable,
|
||||
getSchemaForDatasourcePlus,
|
||||
getEnvironmentBindings,
|
||||
} from "builderStore/dataBinding"
|
||||
import { Utils } from "@budibase/frontend-core"
|
||||
|
@ -67,7 +67,9 @@
|
|||
$: table = tableId
|
||||
? $tables.list.find(table => table._id === inputData.tableId)
|
||||
: { schema: {} }
|
||||
$: schema = getSchemaForTable(tableId, { searchableSchema: true }).schema
|
||||
$: schema = getSchemaForDatasourcePlus(tableId, {
|
||||
searchableSchema: true,
|
||||
}).schema
|
||||
$: schemaFields = Object.values(schema || {})
|
||||
$: queryLimit = tableId?.includes("datasource") ? "∞" : "1000"
|
||||
$: isTrigger = block?.type === "TRIGGER"
|
||||
|
@ -158,7 +160,7 @@
|
|||
// instead fetch the schema in the backend at runtime.
|
||||
let schema
|
||||
if (e.detail?.tableId) {
|
||||
schema = getSchemaForTable(e.detail.tableId, {
|
||||
schema = getSchemaForDatasourcePlus(e.detail.tableId, {
|
||||
searchableSchema: true,
|
||||
}).schema
|
||||
}
|
||||
|
|
|
@ -26,12 +26,14 @@
|
|||
$: id = $tables.selected?._id
|
||||
$: isUsersTable = id === TableNames.USERS
|
||||
$: isInternal = $tables.selected?.type !== "external"
|
||||
|
||||
$: datasource = $datasources.list.find(datasource => {
|
||||
$: gridDatasource = {
|
||||
type: "table",
|
||||
tableId: id,
|
||||
}
|
||||
$: tableDatasource = $datasources.list.find(datasource => {
|
||||
return datasource._id === $tables.selected?.sourceId
|
||||
})
|
||||
|
||||
$: relationshipsEnabled = relationshipSupport(datasource)
|
||||
$: relationshipsEnabled = relationshipSupport(tableDatasource)
|
||||
|
||||
const relationshipSupport = datasource => {
|
||||
const integration = $integrations[datasource?.source]
|
||||
|
@ -54,12 +56,12 @@
|
|||
<div class="wrapper">
|
||||
<Grid
|
||||
{API}
|
||||
tableId={id}
|
||||
allowAddRows={!isUsersTable}
|
||||
allowDeleteRows={!isUsersTable}
|
||||
datasource={gridDatasource}
|
||||
canAddRows={!isUsersTable}
|
||||
canDeleteRows={!isUsersTable}
|
||||
schemaOverrides={isUsersTable ? userSchemaOverrides : null}
|
||||
showAvatars={false}
|
||||
on:updatetable={handleGridTableUpdate}
|
||||
on:updatedatasource={handleGridTableUpdate}
|
||||
>
|
||||
<svelte:fragment slot="filter">
|
||||
<GridFilterButton />
|
||||
|
@ -72,9 +74,7 @@
|
|||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="controls">
|
||||
{#if isInternal}
|
||||
<GridCreateViewButton />
|
||||
{/if}
|
||||
<GridCreateViewButton />
|
||||
<GridManageAccessButton />
|
||||
{#if relationshipsEnabled}
|
||||
<GridRelationshipButton />
|
|
@ -10,6 +10,7 @@
|
|||
import ManageAccessButton from "./buttons/ManageAccessButton.svelte"
|
||||
import HideAutocolumnButton from "./buttons/HideAutocolumnButton.svelte"
|
||||
import { notifications } from "@budibase/bbui"
|
||||
import { ROW_EXPORT_FORMATS } from "constants/backend"
|
||||
|
||||
export let view = {}
|
||||
|
||||
|
@ -19,6 +20,14 @@
|
|||
let type = "internal"
|
||||
|
||||
$: name = view.name
|
||||
$: calculation = view.calculation
|
||||
|
||||
$: supportedFormats = Object.values(ROW_EXPORT_FORMATS).filter(key => {
|
||||
if (calculation && key === ROW_EXPORT_FORMATS.JSON_WITH_SCHEMA) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
// Fetch rows for specified view
|
||||
$: fetchViewData(name, view.field, view.groupBy, view.calculation)
|
||||
|
@ -68,5 +77,5 @@
|
|||
{/if}
|
||||
<ManageAccessButton resourceId={decodeURI(name)} />
|
||||
<HideAutocolumnButton bind:hideAutocolumns />
|
||||
<ExportButton view={view.name} />
|
||||
<ExportButton view={view.name} formats={supportedFormats} />
|
||||
</Table>
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
<script>
|
||||
import { viewsV2 } from "stores/backend"
|
||||
import { Grid } from "@budibase/frontend-core"
|
||||
import { API } from "api"
|
||||
import GridCreateEditRowModal from "components/backend/DataTable/modals/grid/GridCreateEditRowModal.svelte"
|
||||
import GridFilterButton from "components/backend/DataTable/buttons/grid/GridFilterButton.svelte"
|
||||
import GridManageAccessButton from "components/backend/DataTable/buttons/grid/GridManageAccessButton.svelte"
|
||||
|
||||
$: id = $viewsV2.selected?.id
|
||||
$: datasource = {
|
||||
type: "viewV2",
|
||||
id,
|
||||
tableId: $viewsV2.selected?.tableId,
|
||||
}
|
||||
|
||||
const handleGridViewUpdate = async e => {
|
||||
viewsV2.replaceView(id, e.detail)
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="wrapper">
|
||||
<Grid
|
||||
{API}
|
||||
{datasource}
|
||||
allowAddRows
|
||||
allowDeleteRows
|
||||
showAvatars={false}
|
||||
on:updatedatasource={handleGridViewUpdate}
|
||||
>
|
||||
<svelte:fragment slot="filter">
|
||||
<GridFilterButton />
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="controls">
|
||||
<GridCreateEditRowModal />
|
||||
<GridManageAccessButton />
|
||||
</svelte:fragment>
|
||||
</Grid>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.wrapper {
|
||||
flex: 1 1 auto;
|
||||
margin: -28px -40px -40px -40px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--background);
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
|
@ -7,6 +7,7 @@
|
|||
export let sorting
|
||||
export let disabled = false
|
||||
export let selectedRows
|
||||
export let formats
|
||||
|
||||
let modal
|
||||
</script>
|
||||
|
@ -15,5 +16,5 @@
|
|||
Export
|
||||
</ActionButton>
|
||||
<Modal bind:this={modal}>
|
||||
<ExportModal {view} {filters} {sorting} {selectedRows} />
|
||||
<ExportModal {view} {filters} {sorting} {selectedRows} {formats} />
|
||||
</Modal>
|
||||
|
|
|
@ -9,19 +9,15 @@
|
|||
let modal
|
||||
let resourcePermissions
|
||||
|
||||
async function openDropdown() {
|
||||
resourcePermissions = await permissions.forResource(resourceId)
|
||||
async function openModal() {
|
||||
resourcePermissions = await permissions.forResourceDetailed(resourceId)
|
||||
modal.show()
|
||||
}
|
||||
</script>
|
||||
|
||||
<ActionButton icon="LockClosed" quiet on:click={openDropdown} {disabled}>
|
||||
<ActionButton icon="LockClosed" quiet on:click={openModal} {disabled}>
|
||||
Access
|
||||
</ActionButton>
|
||||
<Modal bind:this={modal}>
|
||||
<ManageAccessModal
|
||||
{resourceId}
|
||||
levels={$permissions}
|
||||
permissions={resourcePermissions}
|
||||
/>
|
||||
<ManageAccessModal {resourceId} permissions={resourcePermissions} />
|
||||
</Modal>
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
$: tempValue = filters || []
|
||||
$: schemaFields = Object.values(schema || {})
|
||||
$: text = getText(filters)
|
||||
$: selected = tempValue.filter(x => !x.onEmptyFilter)?.length > 0
|
||||
|
||||
const getText = filters => {
|
||||
const count = filters?.filter(filter => filter.field)?.length
|
||||
|
@ -22,13 +23,7 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<ActionButton
|
||||
icon="Filter"
|
||||
quiet
|
||||
{disabled}
|
||||
on:click={modal.show}
|
||||
selected={tempValue?.length > 0}
|
||||
>
|
||||
<ActionButton icon="Filter" quiet {disabled} on:click={modal.show} {selected}>
|
||||
{text}
|
||||
</ActionButton>
|
||||
<Modal bind:this={modal}>
|
||||
|
|
|
@ -1,18 +1,30 @@
|
|||
<script>
|
||||
import { getContext } from "svelte"
|
||||
import { Modal, ActionButton } from "@budibase/bbui"
|
||||
import CreateViewModal from "../../modals/CreateViewModal.svelte"
|
||||
import { Modal, ActionButton, TooltipType, TempTooltip } from "@budibase/bbui"
|
||||
import GridCreateViewModal from "../../modals/grid/GridCreateViewModal.svelte"
|
||||
|
||||
const { rows, columns } = getContext("grid")
|
||||
const { rows, columns, filter } = getContext("grid")
|
||||
|
||||
let modal
|
||||
let firstFilterUsage = false
|
||||
|
||||
$: disabled = !$columns.length || !$rows.length
|
||||
$: {
|
||||
if ($filter?.length && !firstFilterUsage) {
|
||||
firstFilterUsage = true
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<ActionButton {disabled} icon="CollectionAdd" quiet on:click={modal.show}>
|
||||
Add view
|
||||
</ActionButton>
|
||||
<TempTooltip
|
||||
text="Create a view to save your filters"
|
||||
type={TooltipType.Info}
|
||||
condition={firstFilterUsage}
|
||||
>
|
||||
<ActionButton {disabled} icon="CollectionAdd" quiet on:click={modal.show}>
|
||||
Create view
|
||||
</ActionButton>
|
||||
</TempTooltip>
|
||||
<Modal bind:this={modal}>
|
||||
<CreateViewModal />
|
||||
<GridCreateViewModal />
|
||||
</Modal>
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
import ExportButton from "../ExportButton.svelte"
|
||||
import { getContext } from "svelte"
|
||||
|
||||
const { rows, columns, tableId, sort, selectedRows, filter } =
|
||||
const { rows, columns, datasource, sort, selectedRows, filter } =
|
||||
getContext("grid")
|
||||
|
||||
$: disabled = !$rows.length || !$columns.length
|
||||
|
@ -12,7 +12,7 @@
|
|||
<span data-ignore-click-outside="true">
|
||||
<ExportButton
|
||||
{disabled}
|
||||
view={$tableId}
|
||||
view={$datasource.tableId}
|
||||
filters={$filter}
|
||||
sorting={{
|
||||
sortColumn: $sort.column,
|
||||
|
|
|
@ -2,22 +2,19 @@
|
|||
import TableFilterButton from "../TableFilterButton.svelte"
|
||||
import { getContext } from "svelte"
|
||||
|
||||
const { columns, tableId, filter, table } = getContext("grid")
|
||||
|
||||
// Wipe filter whenever table ID changes to avoid using stale filters
|
||||
$: $tableId, filter.set([])
|
||||
const { columns, datasource, filter, definition } = getContext("grid")
|
||||
|
||||
const onFilter = e => {
|
||||
filter.set(e.detail || [])
|
||||
}
|
||||
</script>
|
||||
|
||||
{#key $tableId}
|
||||
{#key $datasource}
|
||||
<TableFilterButton
|
||||
schema={$table?.schema}
|
||||
schema={$definition?.schema}
|
||||
filters={$filter}
|
||||
on:change={onFilter}
|
||||
disabled={!$columns.length}
|
||||
tableId={$tableId}
|
||||
tableId={$datasource.tableId}
|
||||
/>
|
||||
{/key}
|
||||
|
|
|
@ -4,12 +4,12 @@
|
|||
|
||||
export let disabled = false
|
||||
|
||||
const { rows, tableId, table } = getContext("grid")
|
||||
const { rows, datasource, definition } = getContext("grid")
|
||||
</script>
|
||||
|
||||
<ImportButton
|
||||
{disabled}
|
||||
tableId={$tableId}
|
||||
tableType={$table?.type}
|
||||
tableId={$datasource?.tableId}
|
||||
tableType={$definition?.type}
|
||||
on:importrows={rows.actions.refreshData}
|
||||
/>
|
||||
|
|
|
@ -2,7 +2,16 @@
|
|||
import ManageAccessButton from "../ManageAccessButton.svelte"
|
||||
import { getContext } from "svelte"
|
||||
|
||||
const { tableId } = getContext("grid")
|
||||
const { datasource } = getContext("grid")
|
||||
|
||||
$: resourceId = getResourceID($datasource)
|
||||
|
||||
const getResourceID = datasource => {
|
||||
if (!datasource) {
|
||||
return null
|
||||
}
|
||||
return datasource.type === "table" ? datasource.tableId : datasource.id
|
||||
}
|
||||
</script>
|
||||
|
||||
<ManageAccessButton resourceId={$tableId} />
|
||||
<ManageAccessButton {resourceId} />
|
||||
|
|
|
@ -2,12 +2,12 @@
|
|||
import ExistingRelationshipButton from "../ExistingRelationshipButton.svelte"
|
||||
import { getContext } from "svelte"
|
||||
|
||||
const { table, rows } = getContext("grid")
|
||||
const { definition, rows } = getContext("grid")
|
||||
</script>
|
||||
|
||||
{#if $table}
|
||||
{#if $definition}
|
||||
<ExistingRelationshipButton
|
||||
table={$table}
|
||||
table={$definition}
|
||||
on:updatecolumns={() => rows.actions.refreshData()}
|
||||
/>
|
||||
{/if}
|
||||
|
|
|
@ -55,7 +55,7 @@
|
|||
let linkEditDisabled
|
||||
let primaryDisplay
|
||||
let indexes = [...($tables.selected.indexes || [])]
|
||||
let isCreating
|
||||
let isCreating = undefined
|
||||
|
||||
let table = $tables.selected
|
||||
let confirmDeleteDialog
|
||||
|
@ -75,11 +75,11 @@
|
|||
}
|
||||
|
||||
const initialiseField = (field, savingColumn) => {
|
||||
isCreating = !field
|
||||
if (field && !savingColumn) {
|
||||
editableColumn = cloneDeep(field)
|
||||
originalName = editableColumn.name ? editableColumn.name + "" : null
|
||||
linkEditDisabled = originalName != null
|
||||
isCreating = originalName == null
|
||||
primaryDisplay =
|
||||
$tables.selected.primaryDisplay == null ||
|
||||
$tables.selected.primaryDisplay === editableColumn.name
|
||||
|
@ -584,6 +584,7 @@
|
|||
{ label: "Dynamic", value: "dynamic" },
|
||||
{ label: "Static", value: "static" },
|
||||
]}
|
||||
disabled={!isCreating}
|
||||
getOptionLabel={option => option.label}
|
||||
getOptionValue={option => option.value}
|
||||
tooltip="Dynamic formula are calculated when retrieved, but cannot be filtered or sorted by,
|
||||
|
|
|
@ -1,38 +0,0 @@
|
|||
<script>
|
||||
import { Input, notifications, ModalContent } from "@budibase/bbui"
|
||||
import { goto } from "@roxi/routify"
|
||||
import { views as viewsStore } from "stores/backend"
|
||||
import { tables } from "stores/backend"
|
||||
|
||||
let name
|
||||
let field
|
||||
|
||||
$: views = $tables.list.flatMap(table => Object.keys(table.views || {}))
|
||||
|
||||
const saveView = async () => {
|
||||
name = name?.trim()
|
||||
if (views.includes(name)) {
|
||||
notifications.error(`View exists with name ${name}`)
|
||||
return
|
||||
}
|
||||
try {
|
||||
await viewsStore.save({
|
||||
name,
|
||||
tableId: $tables.selected._id,
|
||||
field,
|
||||
})
|
||||
notifications.success(`View ${name} created`)
|
||||
$goto(`../../view/${encodeURIComponent(name)}`)
|
||||
} catch (error) {
|
||||
notifications.error("Error creating view")
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<ModalContent
|
||||
title="Create View"
|
||||
confirmText="Create View"
|
||||
onConfirm={saveView}
|
||||
>
|
||||
<Input label="View Name" thin bind:value={name} />
|
||||
</ModalContent>
|
|
@ -9,30 +9,43 @@
|
|||
import download from "downloadjs"
|
||||
import { API } from "api"
|
||||
import { Constants, LuceneUtils } from "@budibase/frontend-core"
|
||||
|
||||
const FORMATS = [
|
||||
{
|
||||
name: "CSV",
|
||||
key: "csv",
|
||||
},
|
||||
{
|
||||
name: "JSON",
|
||||
key: "json",
|
||||
},
|
||||
{
|
||||
name: "JSON with Schema",
|
||||
key: "jsonWithSchema",
|
||||
},
|
||||
]
|
||||
import { ROW_EXPORT_FORMATS } from "constants/backend"
|
||||
|
||||
export let view
|
||||
export let filters
|
||||
export let sorting
|
||||
export let selectedRows = []
|
||||
export let formats
|
||||
|
||||
let exportFormat = FORMATS[0].key
|
||||
const FORMATS = [
|
||||
{
|
||||
name: "CSV",
|
||||
key: ROW_EXPORT_FORMATS.CSV,
|
||||
},
|
||||
{
|
||||
name: "JSON",
|
||||
key: ROW_EXPORT_FORMATS.JSON,
|
||||
},
|
||||
{
|
||||
name: "JSON with Schema",
|
||||
key: ROW_EXPORT_FORMATS.JSON_WITH_SCHEMA,
|
||||
},
|
||||
]
|
||||
|
||||
$: options = FORMATS.filter(format => {
|
||||
if (formats && !formats.includes(format.key)) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
let exportFormat
|
||||
let filterLookup
|
||||
|
||||
$: if (options && !exportFormat) {
|
||||
exportFormat = Array.isArray(options) ? options[0]?.key : []
|
||||
}
|
||||
|
||||
$: luceneFilter = LuceneUtils.buildLuceneQuery(filters)
|
||||
$: exportOpDisplay = buildExportOpDisplay(sorting, filterDisplay, filters)
|
||||
|
||||
|
@ -190,7 +203,7 @@
|
|||
<Select
|
||||
label="Format"
|
||||
bind:value={exportFormat}
|
||||
options={FORMATS}
|
||||
{options}
|
||||
placeholder={null}
|
||||
getOptionLabel={x => x.name}
|
||||
getOptionValue={x => x.key}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
<script>
|
||||
import { PermissionSource } from "@budibase/types"
|
||||
import { roles, permissions as permissionsStore } from "stores/backend"
|
||||
import {
|
||||
Label,
|
||||
|
@ -7,45 +8,130 @@
|
|||
notifications,
|
||||
Body,
|
||||
ModalContent,
|
||||
Tags,
|
||||
Tag,
|
||||
Icon,
|
||||
} from "@budibase/bbui"
|
||||
import { capitalise } from "helpers"
|
||||
import { get } from "svelte/store"
|
||||
|
||||
export let resourceId
|
||||
export let permissions
|
||||
|
||||
const inheritedRoleId = "inherited"
|
||||
|
||||
async function changePermission(level, role) {
|
||||
try {
|
||||
await permissionsStore.save({
|
||||
level,
|
||||
role,
|
||||
resource: resourceId,
|
||||
})
|
||||
if (role === inheritedRoleId) {
|
||||
await permissionsStore.remove({
|
||||
level,
|
||||
role,
|
||||
resource: resourceId,
|
||||
})
|
||||
} else {
|
||||
await permissionsStore.save({
|
||||
level,
|
||||
role,
|
||||
resource: resourceId,
|
||||
})
|
||||
}
|
||||
|
||||
// Show updated permissions in UI: REMOVE
|
||||
permissions = await permissionsStore.forResource(resourceId)
|
||||
permissions = await permissionsStore.forResourceDetailed(resourceId)
|
||||
notifications.success("Updated permissions")
|
||||
} catch (error) {
|
||||
notifications.error("Error updating permissions")
|
||||
}
|
||||
}
|
||||
|
||||
$: computedPermissions = Object.entries(permissions.permissions).reduce(
|
||||
(p, [level, roleInfo]) => {
|
||||
p[level] = {
|
||||
selectedValue:
|
||||
roleInfo.permissionType === PermissionSource.INHERITED
|
||||
? inheritedRoleId
|
||||
: roleInfo.role,
|
||||
options: [...get(roles)],
|
||||
}
|
||||
|
||||
if (roleInfo.inheritablePermission) {
|
||||
p[level].inheritOption = roleInfo.inheritablePermission
|
||||
p[level].options.unshift({
|
||||
_id: inheritedRoleId,
|
||||
name: `Inherit (${
|
||||
get(roles).find(x => x._id === roleInfo.inheritablePermission).name
|
||||
})`,
|
||||
})
|
||||
}
|
||||
return p
|
||||
},
|
||||
{}
|
||||
)
|
||||
|
||||
$: requiresPlanToModify = permissions.requiresPlanToModify
|
||||
|
||||
let dependantsInfoMessage
|
||||
async function loadDependantInfo() {
|
||||
const dependantsInfo = await permissionsStore.getDependantsInfo(resourceId)
|
||||
|
||||
const resourceByType = dependantsInfo?.resourceByType
|
||||
|
||||
if (resourceByType) {
|
||||
const total = Object.values(resourceByType).reduce((p, c) => p + c, 0)
|
||||
let resourceDisplay =
|
||||
Object.keys(resourceByType).length === 1 && resourceByType.view
|
||||
? "view"
|
||||
: "resource"
|
||||
|
||||
if (total === 1) {
|
||||
dependantsInfoMessage = `1 ${resourceDisplay} is inheriting this access.`
|
||||
} else if (total > 1) {
|
||||
dependantsInfoMessage = `${total} ${resourceDisplay}s are inheriting this access.`
|
||||
}
|
||||
}
|
||||
}
|
||||
loadDependantInfo()
|
||||
</script>
|
||||
|
||||
<ModalContent title="Manage Access" showCancelButton={false} confirmText="Done">
|
||||
<ModalContent showCancelButton={false} confirmText="Done">
|
||||
<span slot="header">
|
||||
Manage Access
|
||||
{#if requiresPlanToModify}
|
||||
<span class="lock-tag">
|
||||
<Tags>
|
||||
<Tag icon="LockClosed">{capitalise(requiresPlanToModify)}</Tag>
|
||||
</Tags>
|
||||
</span>
|
||||
{/if}
|
||||
</span>
|
||||
<Body size="S">Specify the minimum access level role for this data.</Body>
|
||||
<div class="row">
|
||||
<Label extraSmall grey>Level</Label>
|
||||
<Label extraSmall grey>Role</Label>
|
||||
{#each Object.keys(permissions) as level}
|
||||
{#each Object.keys(computedPermissions) as level}
|
||||
<Input value={capitalise(level)} disabled />
|
||||
<Select
|
||||
value={permissions[level]}
|
||||
disabled={requiresPlanToModify}
|
||||
placeholder={false}
|
||||
value={computedPermissions[level].selectedValue}
|
||||
on:change={e => changePermission(level, e.detail)}
|
||||
options={$roles}
|
||||
options={computedPermissions[level].options}
|
||||
getOptionLabel={x => x.name}
|
||||
getOptionValue={x => x._id}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
{#if dependantsInfoMessage}
|
||||
<div class="inheriting-resources">
|
||||
<Icon name="Alert" />
|
||||
<Body size="S">
|
||||
<i>
|
||||
{dependantsInfoMessage}
|
||||
</i>
|
||||
</Body>
|
||||
</div>
|
||||
{/if}
|
||||
</ModalContent>
|
||||
|
||||
<style>
|
||||
|
@ -54,4 +140,13 @@
|
|||
grid-template-columns: 1fr 1fr;
|
||||
grid-gap: var(--spacing-s);
|
||||
}
|
||||
|
||||
.lock-tag {
|
||||
padding-left: var(--spacing-s);
|
||||
}
|
||||
|
||||
.inheriting-resources {
|
||||
display: flex;
|
||||
gap: var(--spacing-s);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
import { getContext } from "svelte"
|
||||
import CreateEditColumn from "components/backend/DataTable/modals/CreateEditColumn.svelte"
|
||||
|
||||
const { rows } = getContext("grid")
|
||||
const { datasource } = getContext("grid")
|
||||
</script>
|
||||
|
||||
<CreateEditColumn on:updatecolumns={rows.actions.refreshTableDefinition} />
|
||||
<CreateEditColumn on:updatecolumns={datasource.actions.refreshDefinition} />
|
||||
|
|
|
@ -0,0 +1,60 @@
|
|||
<script>
|
||||
import { getContext } from "svelte"
|
||||
import { Input, notifications, ModalContent } from "@budibase/bbui"
|
||||
import { goto } from "@roxi/routify"
|
||||
import { viewsV2 } from "stores/backend"
|
||||
|
||||
const { filter, sort, definition } = getContext("grid")
|
||||
|
||||
let name
|
||||
|
||||
$: views = Object.keys($definition?.views || {}).map(x => x.toLowerCase())
|
||||
$: nameExists = views.includes(name?.trim().toLowerCase())
|
||||
|
||||
const enrichSchema = schema => {
|
||||
// We need to sure that "visible" is set to true for any fields which have
|
||||
// not yet been saved with grid metadata attached
|
||||
const cloned = { ...schema }
|
||||
Object.entries(cloned).forEach(([field, fieldSchema]) => {
|
||||
if (fieldSchema.visible == null) {
|
||||
cloned[field] = { ...cloned[field], visible: true }
|
||||
}
|
||||
})
|
||||
return cloned
|
||||
}
|
||||
|
||||
const saveView = async () => {
|
||||
name = name?.trim()
|
||||
try {
|
||||
const newView = await viewsV2.create({
|
||||
name,
|
||||
tableId: $definition._id,
|
||||
query: $filter,
|
||||
sort: {
|
||||
field: $sort.column,
|
||||
order: $sort.order,
|
||||
},
|
||||
schema: enrichSchema($definition.schema),
|
||||
primaryDisplay: $definition.primaryDisplay,
|
||||
})
|
||||
notifications.success(`View ${name} created`)
|
||||
$goto(`../../view/v2/${newView.id}`)
|
||||
} catch (error) {
|
||||
notifications.error("Error creating view")
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<ModalContent
|
||||
title="Create view"
|
||||
confirmText="Create view"
|
||||
onConfirm={saveView}
|
||||
disabled={nameExists}
|
||||
>
|
||||
<Input
|
||||
label="View name"
|
||||
thin
|
||||
bind:value={name}
|
||||
error={nameExists ? "A view already exists with that name" : null}
|
||||
/>
|
||||
</ModalContent>
|
|
@ -1,7 +1,14 @@
|
|||
<script>
|
||||
import { goto, isActive, params } from "@roxi/routify"
|
||||
import { BUDIBASE_INTERNAL_DB_ID } from "constants/backend"
|
||||
import { database, datasources, queries, tables, views } from "stores/backend"
|
||||
import {
|
||||
database,
|
||||
datasources,
|
||||
queries,
|
||||
tables,
|
||||
views,
|
||||
viewsV2,
|
||||
} from "stores/backend"
|
||||
import EditDatasourcePopover from "./popovers/EditDatasourcePopover.svelte"
|
||||
import EditQueryPopover from "./popovers/EditQueryPopover.svelte"
|
||||
import NavItem from "components/common/NavItem.svelte"
|
||||
|
@ -24,6 +31,7 @@
|
|||
$tables,
|
||||
$queries,
|
||||
$views,
|
||||
$viewsV2,
|
||||
openDataSources
|
||||
)
|
||||
$: openDataSource = enrichedDataSources.find(x => x.open)
|
||||
|
@ -41,6 +49,7 @@
|
|||
tables,
|
||||
queries,
|
||||
views,
|
||||
viewsV2,
|
||||
openDataSources
|
||||
) => {
|
||||
if (!datasources?.list?.length) {
|
||||
|
@ -57,7 +66,8 @@
|
|||
isActive,
|
||||
tables,
|
||||
queries,
|
||||
views
|
||||
views,
|
||||
viewsV2
|
||||
)
|
||||
const onlySource = datasources.list.length === 1
|
||||
return {
|
||||
|
@ -106,7 +116,8 @@
|
|||
isActive,
|
||||
tables,
|
||||
queries,
|
||||
views
|
||||
views,
|
||||
viewsV2
|
||||
) => {
|
||||
// Check for being on a datasource page
|
||||
if (params.datasourceId === datasource._id) {
|
||||
|
@ -152,10 +163,16 @@
|
|||
|
||||
// Check for a matching view
|
||||
const selectedView = views.selected?.name
|
||||
const table = options.find(table => {
|
||||
const viewTable = options.find(table => {
|
||||
return table.views?.[selectedView] != null
|
||||
})
|
||||
return table != null
|
||||
if (viewTable) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check for a matching viewV2
|
||||
const viewV2Table = options.find(x => x._id === viewsV2.selected?.tableId)
|
||||
return viewV2Table != null
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
|
@ -290,11 +290,11 @@
|
|||
datasource.entities[getTable(toId).name].schema[toRelationship.name] =
|
||||
toRelationship
|
||||
|
||||
await save()
|
||||
await save({ action: "saved" })
|
||||
}
|
||||
async function deleteRelationship() {
|
||||
removeExistingRelationship()
|
||||
await save()
|
||||
await save({ action: "deleted" })
|
||||
await tables.fetch()
|
||||
close()
|
||||
}
|
||||
|
|
|
@ -33,7 +33,7 @@
|
|||
}
|
||||
|
||||
// action is one of 'created', 'updated' or 'deleted'
|
||||
async function saveRelationship(action) {
|
||||
async function saveRelationship({ action }) {
|
||||
try {
|
||||
await beforeSave({ action, datasource })
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import { tables, views, database } from "stores/backend"
|
||||
import { tables, views, viewsV2, database } from "stores/backend"
|
||||
import { TableNames } from "constants"
|
||||
import EditTablePopover from "./popovers/EditTablePopover.svelte"
|
||||
import EditViewPopover from "./popovers/EditViewPopover.svelte"
|
||||
|
@ -7,9 +7,6 @@
|
|||
import { goto, isActive } from "@roxi/routify"
|
||||
import { userSelectedResourceMap } from "builderStore"
|
||||
|
||||
const alphabetical = (a, b) =>
|
||||
a.name?.toLowerCase() > b.name?.toLowerCase() ? 1 : -1
|
||||
|
||||
export let sourceId
|
||||
export let selectTable
|
||||
|
||||
|
@ -18,6 +15,17 @@
|
|||
table => table.sourceId === sourceId && table._id !== TableNames.USERS
|
||||
)
|
||||
.sort(alphabetical)
|
||||
|
||||
const alphabetical = (a, b) => {
|
||||
return a.name?.toLowerCase() > b.name?.toLowerCase() ? 1 : -1
|
||||
}
|
||||
|
||||
const isViewActive = (view, isActive, views, viewsV2) => {
|
||||
return (
|
||||
(isActive("./view/v1") && views.selected?.name === view.name) ||
|
||||
(isActive("./view/v2") && viewsV2.selected?.id === view.id)
|
||||
)
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if $database?._id}
|
||||
|
@ -37,18 +45,23 @@
|
|||
<EditTablePopover {table} />
|
||||
{/if}
|
||||
</NavItem>
|
||||
{#each [...Object.keys(table.views || {})].sort() as viewName, idx (idx)}
|
||||
{#each [...Object.entries(table.views || {})].sort() as [name, view], idx (idx)}
|
||||
<NavItem
|
||||
indentLevel={2}
|
||||
icon="Remove"
|
||||
text={viewName}
|
||||
selected={$isActive("./view") && $views.selected?.name === viewName}
|
||||
on:click={() => $goto(`./view/${encodeURIComponent(viewName)}`)}
|
||||
selectedBy={$userSelectedResourceMap[viewName]}
|
||||
text={name}
|
||||
selected={isViewActive(view, $isActive, $views, $viewsV2)}
|
||||
on:click={() => {
|
||||
if (view.version === 2) {
|
||||
$goto(`./view/v2/${view.id}`)
|
||||
} else {
|
||||
$goto(`./view/v1/${encodeURIComponent(name)}`)
|
||||
}
|
||||
}}
|
||||
selectedBy={$userSelectedResourceMap[name] ||
|
||||
$userSelectedResourceMap[view.id]}
|
||||
>
|
||||
<EditViewPopover
|
||||
view={{ name: viewName, ...table.views[viewName] }}
|
||||
/>
|
||||
<EditViewPopover {view} />
|
||||
</NavItem>
|
||||
{/each}
|
||||
{/each}
|
||||
|
|
|
@ -35,7 +35,7 @@
|
|||
screen => screen.autoTableId === table._id
|
||||
)
|
||||
willBeDeleted = ["All table data"].concat(
|
||||
templateScreens.map(screen => `Screen ${screen.props._instanceName}`)
|
||||
templateScreens.map(screen => `Screen ${screen.routing?.route || ""}`)
|
||||
)
|
||||
confirmDeleteDialog.show()
|
||||
}
|
||||
|
@ -44,7 +44,10 @@
|
|||
const isSelected = $params.tableId === table._id
|
||||
try {
|
||||
await tables.delete(table)
|
||||
await store.actions.screens.delete(templateScreens)
|
||||
// Screens need deleted one at a time because of undo/redo
|
||||
for (let screen of templateScreens) {
|
||||
await store.actions.screens.delete(screen)
|
||||
}
|
||||
if (table.type === "external") {
|
||||
await datasources.fetch()
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
<script>
|
||||
import { goto, params } from "@roxi/routify"
|
||||
import { views } from "stores/backend"
|
||||
import { views, viewsV2 } from "stores/backend"
|
||||
import { cloneDeep } from "lodash/fp"
|
||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||
import {
|
||||
|
@ -24,23 +23,29 @@
|
|||
const updatedView = cloneDeep(view)
|
||||
updatedView.name = updatedName
|
||||
|
||||
await views.save({
|
||||
originalName,
|
||||
...updatedView,
|
||||
})
|
||||
if (view.version === 2) {
|
||||
await viewsV2.save({
|
||||
originalName,
|
||||
...updatedView,
|
||||
})
|
||||
} else {
|
||||
await views.save({
|
||||
originalName,
|
||||
...updatedView,
|
||||
})
|
||||
}
|
||||
|
||||
notifications.success("View renamed successfully")
|
||||
}
|
||||
|
||||
async function deleteView() {
|
||||
try {
|
||||
const isSelected =
|
||||
decodeURIComponent($params.viewName) === $views.selectedViewName
|
||||
const id = view.tableId
|
||||
await views.delete(view)
|
||||
notifications.success("View deleted")
|
||||
if (isSelected) {
|
||||
$goto(`./table/${id}`)
|
||||
if (view.version === 2) {
|
||||
await viewsV2.delete(view)
|
||||
} else {
|
||||
await views.delete(view)
|
||||
}
|
||||
notifications.success("View deleted")
|
||||
} catch (error) {
|
||||
notifications.error("Error deleting view")
|
||||
}
|
||||
|
|
|
@ -109,7 +109,13 @@
|
|||
type: "View",
|
||||
name: view.name,
|
||||
icon: "Remove",
|
||||
action: () => $goto(`./data/view/${view.name}`),
|
||||
action: () => {
|
||||
if (view.version === 2) {
|
||||
$goto(`./data/view/v2/${view.id}`)
|
||||
} else {
|
||||
$goto(`./data/view/${view.name}`)
|
||||
}
|
||||
},
|
||||
})) ?? []),
|
||||
...($queries?.list?.map(query => ({
|
||||
type: "Query",
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
<script>
|
||||
import { Select } from "@budibase/bbui"
|
||||
import { Select, FancySelect } from "@budibase/bbui"
|
||||
import { roles } from "stores/backend"
|
||||
import { licensing } from "stores/portal"
|
||||
|
||||
import { Constants, RoleUtils } from "@budibase/frontend-core"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import { capitalise } from "helpers"
|
||||
|
||||
export let value
|
||||
export let error
|
||||
|
@ -15,17 +18,43 @@
|
|||
export let align
|
||||
export let footer = null
|
||||
export let allowedRoles = null
|
||||
export let allowCreator = false
|
||||
export let fancySelect = false
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const RemoveID = "remove"
|
||||
|
||||
$: options = getOptions($roles, allowPublic, allowRemove, allowedRoles)
|
||||
|
||||
const getOptions = (roles, allowPublic, allowRemove, allowedRoles) => {
|
||||
$: options = getOptions(
|
||||
$roles,
|
||||
allowPublic,
|
||||
allowRemove,
|
||||
allowedRoles,
|
||||
allowCreator
|
||||
)
|
||||
const getOptions = (
|
||||
roles,
|
||||
allowPublic,
|
||||
allowRemove,
|
||||
allowedRoles,
|
||||
allowCreator
|
||||
) => {
|
||||
if (allowedRoles?.length) {
|
||||
return roles.filter(role => allowedRoles.includes(role._id))
|
||||
}
|
||||
let newRoles = [...roles]
|
||||
|
||||
if (allowCreator) {
|
||||
newRoles = [
|
||||
{
|
||||
_id: Constants.Roles.CREATOR,
|
||||
name: "Creator",
|
||||
tag:
|
||||
!$licensing.perAppBuildersEnabled &&
|
||||
capitalise(Constants.PlanType.BUSINESS),
|
||||
},
|
||||
...newRoles,
|
||||
]
|
||||
}
|
||||
if (allowRemove) {
|
||||
newRoles = [
|
||||
...newRoles,
|
||||
|
@ -64,19 +93,45 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<Select
|
||||
{autoWidth}
|
||||
{quiet}
|
||||
{disabled}
|
||||
{align}
|
||||
{footer}
|
||||
bind:value
|
||||
on:change={onChange}
|
||||
{options}
|
||||
getOptionLabel={role => role.name}
|
||||
getOptionValue={role => role._id}
|
||||
getOptionColour={getColor}
|
||||
getOptionIcon={getIcon}
|
||||
{placeholder}
|
||||
{error}
|
||||
/>
|
||||
{#if fancySelect}
|
||||
<FancySelect
|
||||
{autoWidth}
|
||||
{quiet}
|
||||
{disabled}
|
||||
{align}
|
||||
{footer}
|
||||
bind:value
|
||||
on:change={onChange}
|
||||
{options}
|
||||
label="Access on this app"
|
||||
getOptionLabel={role => role.name}
|
||||
getOptionValue={role => role._id}
|
||||
getOptionColour={getColor}
|
||||
getOptionIcon={getIcon}
|
||||
isOptionEnabled={option =>
|
||||
option._id !== Constants.Roles.CREATOR ||
|
||||
$licensing.perAppBuildersEnabled}
|
||||
{placeholder}
|
||||
{error}
|
||||
/>
|
||||
{:else}
|
||||
<Select
|
||||
{autoWidth}
|
||||
{quiet}
|
||||
{disabled}
|
||||
{align}
|
||||
{footer}
|
||||
bind:value
|
||||
on:change={onChange}
|
||||
{options}
|
||||
getOptionLabel={role => role.name}
|
||||
getOptionValue={role => role._id}
|
||||
getOptionColour={getColor}
|
||||
getOptionIcon={getIcon}
|
||||
isOptionEnabled={option =>
|
||||
option._id !== Constants.Roles.CREATOR ||
|
||||
$licensing.perAppBuildersEnabled}
|
||||
{placeholder}
|
||||
{error}
|
||||
/>
|
||||
{/if}
|
||||
|
|
|
@ -1,12 +1,20 @@
|
|||
<script>
|
||||
import { Select, Label, Checkbox, Input, Body } from "@budibase/bbui"
|
||||
import { tables } from "stores/backend"
|
||||
import { tables, viewsV2 } from "stores/backend"
|
||||
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
|
||||
|
||||
export let parameters
|
||||
export let bindings = []
|
||||
|
||||
$: tableOptions = $tables.list || []
|
||||
$: tableOptions = $tables.list.map(table => ({
|
||||
label: table.name,
|
||||
resourceId: table._id,
|
||||
}))
|
||||
$: viewOptions = $viewsV2.list.map(view => ({
|
||||
label: view.name,
|
||||
resourceId: view.id,
|
||||
}))
|
||||
$: options = [...(tableOptions || []), ...(viewOptions || [])]
|
||||
</script>
|
||||
|
||||
<div class="root">
|
||||
|
@ -15,9 +23,9 @@
|
|||
<Label>Table</Label>
|
||||
<Select
|
||||
bind:value={parameters.tableId}
|
||||
options={tableOptions}
|
||||
getOptionLabel={table => table.name}
|
||||
getOptionValue={table => table._id}
|
||||
{options}
|
||||
getOptionLabel={x => x.label}
|
||||
getOptionValue={x => x.resourceId}
|
||||
/>
|
||||
|
||||
<Label small>Row IDs</Label>
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
<script>
|
||||
import { Select, Label, Body, Checkbox, Input } from "@budibase/bbui"
|
||||
import { store, currentAsset } from "builderStore"
|
||||
import { tables } from "stores/backend"
|
||||
import { tables, viewsV2 } from "stores/backend"
|
||||
import {
|
||||
getContextProviderComponents,
|
||||
getSchemaForTable,
|
||||
getSchemaForDatasourcePlus,
|
||||
} from "builderStore/dataBinding"
|
||||
import SaveFields from "./SaveFields.svelte"
|
||||
|
||||
|
@ -23,7 +23,15 @@
|
|||
)
|
||||
$: providerOptions = getProviderOptions(formComponents, schemaComponents)
|
||||
$: schemaFields = getSchemaFields($currentAsset, parameters?.tableId)
|
||||
$: tableOptions = $tables.list || []
|
||||
$: tableOptions = $tables.list.map(table => ({
|
||||
label: table.name,
|
||||
resourceId: table._id,
|
||||
}))
|
||||
$: viewOptions = $viewsV2.list.map(view => ({
|
||||
label: view.name,
|
||||
resourceId: view.id,
|
||||
}))
|
||||
$: options = [...(tableOptions || []), ...(viewOptions || [])]
|
||||
|
||||
// Gets a context definition of a certain type from a component definition
|
||||
const extractComponentContext = (component, contextType) => {
|
||||
|
@ -60,7 +68,7 @@
|
|||
}
|
||||
|
||||
const getSchemaFields = (asset, tableId) => {
|
||||
const { schema } = getSchemaForTable(tableId)
|
||||
const { schema } = getSchemaForDatasourcePlus(tableId)
|
||||
delete schema._id
|
||||
delete schema._rev
|
||||
return Object.values(schema || {})
|
||||
|
@ -89,9 +97,9 @@
|
|||
<Label small>Duplicate to Table</Label>
|
||||
<Select
|
||||
bind:value={parameters.tableId}
|
||||
options={tableOptions}
|
||||
getOptionLabel={option => option.name}
|
||||
getOptionValue={option => option._id}
|
||||
{options}
|
||||
getOptionLabel={option => option.label}
|
||||
getOptionValue={option => option.resourceId}
|
||||
/>
|
||||
|
||||
<Label small />
|
||||
|
|
|
@ -1,21 +1,29 @@
|
|||
<script>
|
||||
import { Select, Label } from "@budibase/bbui"
|
||||
import { tables } from "stores/backend"
|
||||
import { tables, viewsV2 } from "stores/backend"
|
||||
import DrawerBindableInput from "components/common/bindings/DrawerBindableInput.svelte"
|
||||
|
||||
export let parameters
|
||||
export let bindings = []
|
||||
|
||||
$: tableOptions = $tables.list || []
|
||||
$: tableOptions = $tables.list.map(table => ({
|
||||
label: table.name,
|
||||
resourceId: table._id,
|
||||
}))
|
||||
$: viewOptions = $viewsV2.list.map(view => ({
|
||||
label: view.name,
|
||||
resourceId: view.id,
|
||||
}))
|
||||
$: options = [...(tableOptions || []), ...(viewOptions || [])]
|
||||
</script>
|
||||
|
||||
<div class="root">
|
||||
<Label>Table</Label>
|
||||
<Select
|
||||
bind:value={parameters.tableId}
|
||||
options={tableOptions}
|
||||
getOptionLabel={table => table.name}
|
||||
getOptionValue={table => table._id}
|
||||
{options}
|
||||
getOptionLabel={table => table.label}
|
||||
getOptionValue={table => table.resourceId}
|
||||
/>
|
||||
|
||||
<Label small>Row ID</Label>
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
<script>
|
||||
import { Select, Label, Body, Checkbox, Input } from "@budibase/bbui"
|
||||
import { store, currentAsset } from "builderStore"
|
||||
import { tables } from "stores/backend"
|
||||
import { tables, viewsV2 } from "stores/backend"
|
||||
import {
|
||||
getContextProviderComponents,
|
||||
getSchemaForTable,
|
||||
getSchemaForDatasourcePlus,
|
||||
} from "builderStore/dataBinding"
|
||||
import SaveFields from "./SaveFields.svelte"
|
||||
|
||||
|
@ -24,8 +24,16 @@
|
|||
"schema"
|
||||
)
|
||||
$: providerOptions = getProviderOptions(formComponents, schemaComponents)
|
||||
$: schemaFields = getSchemaFields($currentAsset, parameters?.tableId)
|
||||
$: tableOptions = $tables.list || []
|
||||
$: schemaFields = getSchemaFields(parameters?.tableId)
|
||||
$: tableOptions = $tables.list.map(table => ({
|
||||
label: table.name,
|
||||
resourceId: table._id,
|
||||
}))
|
||||
$: viewOptions = $viewsV2.list.map(view => ({
|
||||
label: view.name,
|
||||
resourceId: view.id,
|
||||
}))
|
||||
$: options = [...(tableOptions || []), ...(viewOptions || [])]
|
||||
|
||||
// Gets a context definition of a certain type from a component definition
|
||||
const extractComponentContext = (component, contextType) => {
|
||||
|
@ -61,8 +69,8 @@
|
|||
})
|
||||
}
|
||||
|
||||
const getSchemaFields = (asset, tableId) => {
|
||||
const { schema } = getSchemaForTable(tableId)
|
||||
const getSchemaFields = resourceId => {
|
||||
const { schema } = getSchemaForDatasourcePlus(resourceId)
|
||||
return Object.values(schema || {})
|
||||
}
|
||||
|
||||
|
@ -89,9 +97,9 @@
|
|||
<Label small>Table</Label>
|
||||
<Select
|
||||
bind:value={parameters.tableId}
|
||||
options={tableOptions}
|
||||
getOptionLabel={option => option.name}
|
||||
getOptionValue={option => option._id}
|
||||
{options}
|
||||
getOptionLabel={option => option.label}
|
||||
getOptionValue={option => option.resourceId}
|
||||
/>
|
||||
|
||||
<Label small />
|
||||
|
|
|
@ -15,6 +15,8 @@
|
|||
import {
|
||||
tables as tablesStore,
|
||||
queries as queriesStore,
|
||||
viewsV2 as viewsV2Store,
|
||||
views as viewsStore,
|
||||
} from "stores/backend"
|
||||
import { datasources, integrations } from "stores/backend"
|
||||
import BindingBuilder from "components/integration/QueryBindingBuilder.svelte"
|
||||
|
@ -39,15 +41,17 @@
|
|||
tableId: m._id,
|
||||
type: "table",
|
||||
}))
|
||||
$: views = $tablesStore.list.reduce((acc, cur) => {
|
||||
let viewsArr = Object.entries(cur.views || {}).map(([key, value]) => ({
|
||||
label: key,
|
||||
name: key,
|
||||
...value,
|
||||
type: "view",
|
||||
}))
|
||||
return [...acc, ...viewsArr]
|
||||
}, [])
|
||||
$: viewsV1 = $viewsStore.list.map(view => ({
|
||||
...view,
|
||||
label: view.name,
|
||||
type: "view",
|
||||
}))
|
||||
$: viewsV2 = $viewsV2Store.list.map(view => ({
|
||||
...view,
|
||||
label: view.name,
|
||||
type: "viewV2",
|
||||
}))
|
||||
$: views = [...(viewsV1 || []), ...(viewsV2 || [])]
|
||||
$: queries = $queriesStore.list
|
||||
.filter(q => showAllQueries || q.queryVerb === "read" || q.readable)
|
||||
.map(query => ({
|
||||
|
|
|
@ -33,17 +33,19 @@
|
|||
let anchors = {}
|
||||
let draggableItems = []
|
||||
|
||||
const buildDragable = items => {
|
||||
return items.map(item => {
|
||||
return {
|
||||
id: listItemKey ? item[listItemKey] : generate(),
|
||||
item,
|
||||
}
|
||||
})
|
||||
const buildDraggable = items => {
|
||||
return items
|
||||
.map(item => {
|
||||
return {
|
||||
id: listItemKey ? item[listItemKey] : generate(),
|
||||
item,
|
||||
}
|
||||
})
|
||||
.filter(item => item.id)
|
||||
}
|
||||
|
||||
$: if (items) {
|
||||
draggableItems = buildDragable(items)
|
||||
draggableItems = buildDraggable(items)
|
||||
}
|
||||
|
||||
const updateRowOrder = e => {
|
||||
|
|
|
@ -21,6 +21,9 @@
|
|||
let fieldList
|
||||
let schema
|
||||
let cachedValue
|
||||
let options
|
||||
let sanitisedValue
|
||||
let unconfigured
|
||||
|
||||
$: bindings = getBindableProperties($selectedScreen, componentInstance._id)
|
||||
$: actionType = componentInstance.actionType
|
||||
|
@ -34,16 +37,24 @@
|
|||
}
|
||||
|
||||
$: datasource = getDatasourceForProvider($currentAsset, componentInstance)
|
||||
$: resourceId = datasource.resourceId || datasource.tableId
|
||||
|
||||
$: if (!isEqual(value, cachedValue)) {
|
||||
cachedValue = value
|
||||
schema = getSchema($currentAsset, datasource)
|
||||
cachedValue = cloneDeep(value)
|
||||
}
|
||||
|
||||
$: options = Object.keys(schema || {})
|
||||
$: sanitisedValue = getValidColumns(convertOldFieldFormat(value), options)
|
||||
$: updateSanitsedFields(sanitisedValue)
|
||||
$: unconfigured = buildUnconfiguredOptions(schema, sanitisedFields)
|
||||
const updateState = value => {
|
||||
schema = getSchema($currentAsset, datasource)
|
||||
options = Object.keys(schema || {})
|
||||
sanitisedValue = getValidColumns(convertOldFieldFormat(value), options)
|
||||
updateSanitsedFields(sanitisedValue)
|
||||
unconfigured = buildUnconfiguredOptions(schema, sanitisedFields)
|
||||
fieldList = [...sanitisedFields, ...unconfigured]
|
||||
.map(buildSudoInstance)
|
||||
.filter(x => x != null)
|
||||
}
|
||||
|
||||
$: updateState(cachedValue, resourceId)
|
||||
|
||||
// Builds unused ones only
|
||||
const buildUnconfiguredOptions = (schema, selected) => {
|
||||
|
@ -97,8 +108,10 @@
|
|||
if (instance._component) {
|
||||
return instance
|
||||
}
|
||||
|
||||
const type = getComponentForField(instance.field, schema)
|
||||
if (!type) {
|
||||
return null
|
||||
}
|
||||
instance._component = `@budibase/standard-components/${type}`
|
||||
|
||||
const pseudoComponentInstance = store.actions.components.createInstance(
|
||||
|
@ -115,10 +128,6 @@
|
|||
return { ...instance, ...pseudoComponentInstance }
|
||||
}
|
||||
|
||||
$: if (sanitisedFields) {
|
||||
fieldList = [...sanitisedFields, ...unconfigured].map(buildSudoInstance)
|
||||
}
|
||||
|
||||
const processItemUpdate = e => {
|
||||
const updatedField = e.detail
|
||||
const parentFieldsUpdated = fieldList ? cloneDeep(fieldList) : []
|
||||
|
|
|
@ -1,28 +1,48 @@
|
|||
<script>
|
||||
import { Select } from "@budibase/bbui"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import { tables as tablesStore } from "stores/backend"
|
||||
import { createEventDispatcher, onMount } from "svelte"
|
||||
import { tables as tablesStore, viewsV2 } from "stores/backend"
|
||||
|
||||
export let value
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
$: tables = $tablesStore.list.map(m => ({
|
||||
label: m.name,
|
||||
tableId: m._id,
|
||||
$: tables = $tablesStore.list.map(table => ({
|
||||
type: "table",
|
||||
label: table.name,
|
||||
tableId: table._id,
|
||||
resourceId: table._id,
|
||||
}))
|
||||
$: views = $viewsV2.list.map(view => ({
|
||||
type: "viewV2",
|
||||
id: view.id,
|
||||
label: view.name,
|
||||
tableId: view.tableId,
|
||||
resourceId: view.id,
|
||||
}))
|
||||
$: options = [...(tables || []), ...(views || [])]
|
||||
|
||||
const onChange = e => {
|
||||
const dataSource = tables?.find(x => x.tableId === e.detail)
|
||||
dispatch("change", dataSource)
|
||||
dispatch(
|
||||
"change",
|
||||
options.find(x => x.resourceId === e.detail)
|
||||
)
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
// Migrate old values before "resourceId" existed
|
||||
if (value && !value.resourceId) {
|
||||
const view = views.find(x => x.resourceId === value.id)
|
||||
const table = tables.find(x => x.resourceId === value.tableId)
|
||||
dispatch("change", view || table)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<Select
|
||||
on:change={onChange}
|
||||
value={value?.tableId}
|
||||
options={tables}
|
||||
getOptionValue={x => x.tableId}
|
||||
value={value?.resourceId}
|
||||
{options}
|
||||
getOptionValue={x => x.resourceId}
|
||||
getOptionLabel={x => x.label}
|
||||
/>
|
||||
|
|
|
@ -40,7 +40,7 @@
|
|||
return
|
||||
}
|
||||
try {
|
||||
roleId = (await permissions.forResource(queryToFetch._id))["read"]
|
||||
roleId = (await permissions.forResource(queryToFetch._id))["read"].role
|
||||
} catch (err) {
|
||||
roleId = Constants.Roles.BASIC
|
||||
}
|
||||
|
|
|
@ -287,3 +287,9 @@ export const DatasourceTypes = {
|
|||
GRAPH: "Graph",
|
||||
API: "API",
|
||||
}
|
||||
|
||||
export const ROW_EXPORT_FORMATS = {
|
||||
CSV: "csv",
|
||||
JSON: "json",
|
||||
JSON_WITH_SCHEMA: "jsonWithSchema",
|
||||
}
|
||||
|
|
|
@ -26,6 +26,9 @@ export const capitalise = s => {
|
|||
|
||||
export const lowercase = s => s.substring(0, 1).toLowerCase() + s.substring(1)
|
||||
|
||||
export const lowercaseExceptFirst = s =>
|
||||
s.charAt(0) + s.substring(1).toLowerCase()
|
||||
|
||||
export const get_name = s => (!s ? "" : last(s.split("/")))
|
||||
|
||||
export const get_capitalised_name = name => pipe(name, [get_name, capitalise])
|
||||
|
|
|
@ -1,18 +1,27 @@
|
|||
<script>
|
||||
import {
|
||||
Icon,
|
||||
Divider,
|
||||
Heading,
|
||||
Layout,
|
||||
Input,
|
||||
clickOutside,
|
||||
notifications,
|
||||
ActionButton,
|
||||
CopyInput,
|
||||
Modal,
|
||||
FancyForm,
|
||||
FancyInput,
|
||||
Button,
|
||||
FancySelect,
|
||||
} from "@budibase/bbui"
|
||||
import { store } from "builderStore"
|
||||
import { groups, licensing, apps, users, auth, admin } from "stores/portal"
|
||||
import { fetchData, Constants, Utils } from "@budibase/frontend-core"
|
||||
import {
|
||||
fetchData,
|
||||
Constants,
|
||||
Utils,
|
||||
RoleUtils,
|
||||
} from "@budibase/frontend-core"
|
||||
import { sdk } from "@budibase/shared-core"
|
||||
import { API } from "api"
|
||||
import GroupIcon from "../../../portal/users/groups/_components/GroupIcon.svelte"
|
||||
|
@ -26,10 +35,15 @@
|
|||
let loaded = false
|
||||
let inviting = false
|
||||
let searchFocus = false
|
||||
|
||||
let invitingFlow = false
|
||||
// Initially filter entities without app access
|
||||
// Show all when false
|
||||
let filterByAppAccess = true
|
||||
let filterByAppAccess = false
|
||||
let email
|
||||
let error
|
||||
let form
|
||||
let creationRoleType = Constants.BudibaseRoles.AppUser
|
||||
let creationAccessType = Constants.Roles.BASIC
|
||||
|
||||
let appInvites = []
|
||||
let filteredInvites = []
|
||||
|
@ -40,8 +54,7 @@
|
|||
let userLimitReachedModal
|
||||
|
||||
let inviteFailureResponse = ""
|
||||
|
||||
$: queryIsEmail = emailValidator(query) === true
|
||||
$: validEmail = emailValidator(email) === true
|
||||
$: prodAppId = apps.getProdAppID($store.appId)
|
||||
$: promptInvite = showInvite(
|
||||
filteredInvites,
|
||||
|
@ -50,7 +63,6 @@
|
|||
query
|
||||
)
|
||||
$: isOwner = $auth.accountPortalAccess && $admin.cloud
|
||||
|
||||
const showInvite = (invites, users, groups, query) => {
|
||||
return !invites?.length && !users?.length && !groups?.length && query
|
||||
}
|
||||
|
@ -66,9 +78,9 @@
|
|||
if (!filterByAppAccess && !query) {
|
||||
filteredInvites =
|
||||
appInvites.length > 100 ? appInvites.slice(0, 100) : [...appInvites]
|
||||
filteredInvites.sort(sortInviteRoles)
|
||||
return
|
||||
}
|
||||
|
||||
filteredInvites = appInvites.filter(invite => {
|
||||
const inviteInfo = invite.info?.apps
|
||||
if (!query && inviteInfo && prodAppId) {
|
||||
|
@ -76,8 +88,8 @@
|
|||
}
|
||||
return invite.email.includes(query)
|
||||
})
|
||||
filteredInvites.sort(sortInviteRoles)
|
||||
}
|
||||
|
||||
$: filterByAppAccess, prodAppId, filterInvites(query)
|
||||
$: if (searchFocus === true) {
|
||||
filterByAppAccess = false
|
||||
|
@ -107,24 +119,63 @@
|
|||
})
|
||||
await usersFetch.refresh()
|
||||
|
||||
filteredUsers = $usersFetch.rows.map(user => {
|
||||
const isAdminOrBuilder = sdk.users.isAdminOrBuilder(user, prodAppId)
|
||||
let role = undefined
|
||||
if (isAdminOrBuilder) {
|
||||
role = Constants.Roles.ADMIN
|
||||
} else {
|
||||
const appRole = Object.keys(user.roles).find(x => x === prodAppId)
|
||||
if (appRole) {
|
||||
role = user.roles[appRole]
|
||||
filteredUsers = $usersFetch.rows
|
||||
.filter(user => user.email !== $auth.user.email)
|
||||
.map(user => {
|
||||
const isAdminOrGlobalBuilder = sdk.users.isAdminOrGlobalBuilder(
|
||||
user,
|
||||
prodAppId
|
||||
)
|
||||
const isAppBuilder = sdk.users.hasAppBuilderPermissions(user, prodAppId)
|
||||
let role
|
||||
if (isAdminOrGlobalBuilder) {
|
||||
role = Constants.Roles.ADMIN
|
||||
} else if (isAppBuilder) {
|
||||
role = Constants.Roles.CREATOR
|
||||
} else {
|
||||
const appRole = user.roles[prodAppId]
|
||||
if (appRole) {
|
||||
role = appRole
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...user,
|
||||
role,
|
||||
isAdminOrBuilder,
|
||||
}
|
||||
})
|
||||
return {
|
||||
...user,
|
||||
role,
|
||||
isAdminOrGlobalBuilder,
|
||||
isAppBuilder,
|
||||
}
|
||||
})
|
||||
.sort(sortRoles)
|
||||
}
|
||||
|
||||
const sortInviteRoles = (a, b) => {
|
||||
const aAppsEmpty = !a.info?.apps?.length && !a.info?.builder?.apps?.length
|
||||
const bAppsEmpty = !b.info?.apps?.length && !b.info?.builder?.apps?.length
|
||||
|
||||
return aAppsEmpty && !bAppsEmpty ? 1 : !aAppsEmpty && bAppsEmpty ? -1 : 0
|
||||
}
|
||||
|
||||
const sortRoles = (a, b) => {
|
||||
const roleA = a.role
|
||||
const roleB = b.role
|
||||
|
||||
const priorityA = RoleUtils.getRolePriority(roleA)
|
||||
const priorityB = RoleUtils.getRolePriority(roleB)
|
||||
|
||||
if (roleA === undefined && roleB !== undefined) {
|
||||
return 1
|
||||
} else if (roleA !== undefined && roleB === undefined) {
|
||||
return -1
|
||||
}
|
||||
|
||||
if (priorityA < priorityB) {
|
||||
return 1
|
||||
} else if (priorityA > priorityB) {
|
||||
return -1
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
const debouncedUpdateFetch = Utils.debounce(searchUsers, 250)
|
||||
|
@ -160,6 +211,12 @@
|
|||
if (user.role === role) {
|
||||
return
|
||||
}
|
||||
if (user.isAppBuilder) {
|
||||
await removeAppBuilder(user._id, prodAppId)
|
||||
}
|
||||
if (role === Constants.Roles.CREATOR) {
|
||||
await removeAppBuilder(user._id, prodAppId)
|
||||
}
|
||||
await updateAppUser(user, role)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
|
@ -189,6 +246,9 @@
|
|||
return
|
||||
}
|
||||
try {
|
||||
if (group?.builder?.apps.includes(prodAppId)) {
|
||||
await removeGroupAppBuilder(group._id)
|
||||
}
|
||||
await updateAppGroup(group, role)
|
||||
} catch {
|
||||
notifications.error("Group update failed")
|
||||
|
@ -225,14 +285,17 @@
|
|||
return nameMatch
|
||||
})
|
||||
.map(enrichGroupRole)
|
||||
.sort(sortRoles)
|
||||
}
|
||||
|
||||
const enrichGroupRole = group => {
|
||||
return {
|
||||
...group,
|
||||
role: group.roles?.[
|
||||
groups.actions.getGroupAppIds(group).find(x => x === prodAppId)
|
||||
],
|
||||
role: group?.builder?.apps.includes(prodAppId)
|
||||
? Constants.Roles.CREATOR
|
||||
: group.roles?.[
|
||||
groups.actions.getGroupAppIds(group).find(x => x === prodAppId)
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -245,8 +308,7 @@
|
|||
$: filteredGroups = searchGroups(enrichedGroups, query)
|
||||
$: groupUsers = buildGroupUsers(filteredGroups, filteredUsers)
|
||||
$: allUsers = [...filteredUsers, ...groupUsers]
|
||||
|
||||
/*
|
||||
/*
|
||||
Create pseudo users from the "users" attribute on app groups.
|
||||
These users will appear muted in the UI and show the ROLE
|
||||
inherited from their parent group. The users allow assigning of user
|
||||
|
@ -291,21 +353,29 @@
|
|||
}
|
||||
|
||||
async function inviteUser() {
|
||||
if (!queryIsEmail) {
|
||||
if (!validEmail) {
|
||||
notifications.error("Email is not valid")
|
||||
return
|
||||
}
|
||||
const newUserEmail = query + ""
|
||||
const newUserEmail = email + ""
|
||||
inviting = true
|
||||
|
||||
const payload = [
|
||||
{
|
||||
email: newUserEmail,
|
||||
builder: false,
|
||||
admin: false,
|
||||
apps: { [prodAppId]: Constants.Roles.BASIC },
|
||||
builder: { global: creationRoleType === Constants.BudibaseRoles.Admin },
|
||||
admin: { global: creationRoleType === Constants.BudibaseRoles.Admin },
|
||||
},
|
||||
]
|
||||
|
||||
const notCreatingAdmin = creationRoleType !== Constants.BudibaseRoles.Admin
|
||||
const isCreator = creationAccessType === Constants.Roles.CREATOR
|
||||
if (notCreatingAdmin && isCreator) {
|
||||
payload[0].builder.apps = [prodAppId]
|
||||
} else if (notCreatingAdmin && !isCreator) {
|
||||
payload[0].apps = { [prodAppId]: creationAccessType }
|
||||
}
|
||||
|
||||
let userInviteResponse
|
||||
try {
|
||||
userInviteResponse = await users.onboard(payload)
|
||||
|
@ -317,16 +387,23 @@
|
|||
return userInviteResponse
|
||||
}
|
||||
|
||||
const openInviteFlow = () => {
|
||||
$licensing.userLimitReached
|
||||
? userLimitReachedModal.show()
|
||||
: (invitingFlow = true)
|
||||
}
|
||||
|
||||
const onInviteUser = async () => {
|
||||
form.validate()
|
||||
userOnboardResponse = await inviteUser()
|
||||
const originalQuery = query + ""
|
||||
query = null
|
||||
const originalQuery = email + ""
|
||||
email = null
|
||||
|
||||
const newUser = userOnboardResponse?.successful.find(
|
||||
user => user.email === originalQuery
|
||||
)
|
||||
if (newUser) {
|
||||
query = originalQuery
|
||||
email = originalQuery
|
||||
notifications.success(
|
||||
userOnboardResponse.created
|
||||
? "User created successfully"
|
||||
|
@ -344,16 +421,28 @@
|
|||
notifications.error(inviteFailureResponse)
|
||||
}
|
||||
userOnboardResponse = null
|
||||
invitingFlow = false
|
||||
// trigger reload of the users
|
||||
query = ""
|
||||
}
|
||||
|
||||
const onUpdateUserInvite = async (invite, role) => {
|
||||
await users.updateInvite({
|
||||
let updateBody = {
|
||||
code: invite.code,
|
||||
apps: {
|
||||
...invite.apps,
|
||||
[prodAppId]: role,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
if (role === Constants.Roles.CREATOR) {
|
||||
updateBody.builder = updateBody.builder || {}
|
||||
updateBody.builder.apps = [...(updateBody.builder.apps ?? []), prodAppId]
|
||||
delete updateBody?.apps?.[prodAppId]
|
||||
} else if (role !== Constants.Roles.CREATOR && invite?.builder?.apps) {
|
||||
invite.builder.apps = []
|
||||
}
|
||||
await users.updateInvite(updateBody)
|
||||
await filterInvites(query)
|
||||
}
|
||||
|
||||
|
@ -373,6 +462,22 @@
|
|||
})
|
||||
}
|
||||
|
||||
const addAppBuilder = async userId => {
|
||||
await users.addAppBuilder(userId, prodAppId)
|
||||
}
|
||||
|
||||
const removeAppBuilder = async userId => {
|
||||
await users.removeAppBuilder(userId, prodAppId)
|
||||
}
|
||||
|
||||
const addGroupAppBuilder = async groupId => {
|
||||
await groups.actions.addGroupAppBuilder(groupId, prodAppId)
|
||||
}
|
||||
|
||||
const removeGroupAppBuilder = async groupId => {
|
||||
await groups.actions.removeGroupAppBuilder(groupId, prodAppId)
|
||||
}
|
||||
|
||||
const initSidePanel = async sidePaneOpen => {
|
||||
if (sidePaneOpen === true) {
|
||||
await groups.actions.init()
|
||||
|
@ -383,19 +488,21 @@
|
|||
$: initSidePanel($store.builderSidePanel)
|
||||
|
||||
function handleKeyDown(evt) {
|
||||
if (evt.key === "Enter" && queryIsEmail && !inviting) {
|
||||
if (evt.key === "Enter" && validEmail && !inviting) {
|
||||
onInviteUser()
|
||||
}
|
||||
}
|
||||
|
||||
const userTitle = user => {
|
||||
if (sdk.users.isAdmin(user)) {
|
||||
return "Admin"
|
||||
} else if (sdk.users.isBuilder(user, prodAppId)) {
|
||||
return "Developer"
|
||||
} else {
|
||||
return "App user"
|
||||
const getInviteRoleValue = invite => {
|
||||
if (invite.info?.admin?.global && invite.info?.builder?.global) {
|
||||
return Constants.Roles.ADMIN
|
||||
}
|
||||
|
||||
if (invite.info?.builder?.apps?.includes(prodAppId)) {
|
||||
return Constants.Roles.CREATOR
|
||||
}
|
||||
|
||||
return invite.info.apps?.[prodAppId]
|
||||
}
|
||||
|
||||
const getRoleFooter = user => {
|
||||
|
@ -403,7 +510,7 @@
|
|||
const role = $roles.find(role => role._id === user.role)
|
||||
return `This user has been given ${role?.name} access from the ${user.group} group`
|
||||
}
|
||||
if (user.isAdminOrBuilder) {
|
||||
if (user.isAdminOrGlobalBuilder) {
|
||||
return "This user's role grants admin access to all apps"
|
||||
}
|
||||
return null
|
||||
|
@ -423,227 +530,309 @@
|
|||
}}
|
||||
>
|
||||
<div class="builder-side-panel-header">
|
||||
<Heading size="S">Users</Heading>
|
||||
<Icon
|
||||
color="var(--spectrum-global-color-gray-600)"
|
||||
name="RailRightClose"
|
||||
hoverable
|
||||
<div
|
||||
on:click={() => {
|
||||
store.update(state => {
|
||||
state.builderSidePanel = false
|
||||
return state
|
||||
})
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div class="search" class:focused={searchFocus}>
|
||||
<span class="search-input">
|
||||
<Input
|
||||
placeholder={"Add users and groups to your app"}
|
||||
autocomplete="off"
|
||||
disabled={inviting}
|
||||
value={query}
|
||||
on:input={e => {
|
||||
query = e.target.value.trim()
|
||||
}}
|
||||
on:focus={() => (searchFocus = true)}
|
||||
on:blur={() => (searchFocus = false)}
|
||||
/>
|
||||
</span>
|
||||
|
||||
<span
|
||||
class="search-input-icon"
|
||||
class:searching={query || !filterByAppAccess}
|
||||
on:click={() => {
|
||||
if (!filterByAppAccess) {
|
||||
filterByAppAccess = true
|
||||
}
|
||||
if (!query) {
|
||||
return
|
||||
}
|
||||
query = null
|
||||
userOnboardResponse = null
|
||||
filterByAppAccess = true
|
||||
invitingFlow = false
|
||||
}}
|
||||
class="header"
|
||||
>
|
||||
<Icon name={!filterByAppAccess || query ? "Close" : "Search"} />
|
||||
</span>
|
||||
{#if invitingFlow}
|
||||
<Icon name="BackAndroid" />
|
||||
{/if}
|
||||
<Heading size="S">{invitingFlow ? "Invite new user" : "Users"}</Heading>
|
||||
</div>
|
||||
<div class="header">
|
||||
{#if !invitingFlow}
|
||||
<Button on:click={openInviteFlow} size="S" cta>Invite user</Button>
|
||||
{/if}
|
||||
<Icon
|
||||
color="var(--spectrum-global-color-gray-600)"
|
||||
name="RailRightClose"
|
||||
hoverable
|
||||
on:click={() => {
|
||||
store.update(state => {
|
||||
state.builderSidePanel = false
|
||||
return state
|
||||
})
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{#if !invitingFlow}
|
||||
<div class="search" class:focused={searchFocus}>
|
||||
<span class="search-input">
|
||||
<Input
|
||||
placeholder={"Add users and groups to your app"}
|
||||
autocomplete="off"
|
||||
disabled={inviting}
|
||||
value={query}
|
||||
on:input={e => {
|
||||
query = e.target.value.trim()
|
||||
}}
|
||||
on:focus={() => (searchFocus = true)}
|
||||
on:blur={() => (searchFocus = false)}
|
||||
/>
|
||||
</span>
|
||||
|
||||
<div class="body">
|
||||
{#if promptInvite && !userOnboardResponse}
|
||||
<Layout gap="S" paddingX="XL">
|
||||
<div class="invite-header">
|
||||
<Heading size="XS">No user found</Heading>
|
||||
<div class="invite-directions">
|
||||
Add a valid email to invite a new user
|
||||
</div>
|
||||
</div>
|
||||
<div class="invite-form">
|
||||
<span>{query || ""}</span>
|
||||
<ActionButton
|
||||
icon="UserAdd"
|
||||
disabled={!queryIsEmail || inviting}
|
||||
on:click={$licensing.userLimitReached
|
||||
? userLimitReachedModal.show
|
||||
: onInviteUser}
|
||||
>
|
||||
Add user
|
||||
</ActionButton>
|
||||
</div>
|
||||
</Layout>
|
||||
{/if}
|
||||
<span
|
||||
class="search-input-icon"
|
||||
class:searching={query || !filterByAppAccess}
|
||||
on:click={() => {
|
||||
if (!query) {
|
||||
return
|
||||
}
|
||||
query = null
|
||||
userOnboardResponse = null
|
||||
}}
|
||||
>
|
||||
<Icon name={!filterByAppAccess || query ? "Close" : "Search"} />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{#if !promptInvite}
|
||||
<Layout gap="L" noPadding>
|
||||
{#if filteredInvites?.length}
|
||||
<Layout noPadding gap="XS">
|
||||
<div class="auth-entity-header">
|
||||
<div class="auth-entity-title">Pending invites</div>
|
||||
<div class="auth-entity-access-title">Access</div>
|
||||
</div>
|
||||
{#each filteredInvites as invite}
|
||||
<div class="auth-entity">
|
||||
<div class="details">
|
||||
<div class="user-email" title={invite.email}>
|
||||
{invite.email}
|
||||
</div>
|
||||
</div>
|
||||
<div class="auth-entity-access">
|
||||
<RoleSelect
|
||||
placeholder={false}
|
||||
value={invite.info.apps?.[prodAppId]}
|
||||
allowRemove={invite.info.apps?.[prodAppId]}
|
||||
allowPublic={false}
|
||||
quiet={true}
|
||||
on:change={e => {
|
||||
onUpdateUserInvite(invite, e.detail)
|
||||
}}
|
||||
on:remove={() => {
|
||||
onUninviteAppUser(invite)
|
||||
}}
|
||||
autoWidth
|
||||
align="right"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</Layout>
|
||||
{/if}
|
||||
|
||||
{#if $licensing.groupsEnabled && filteredGroups?.length}
|
||||
<Layout noPadding gap="XS">
|
||||
<div class="auth-entity-header">
|
||||
<div class="auth-entity-title">Groups</div>
|
||||
<div class="auth-entity-access-title">Access</div>
|
||||
</div>
|
||||
{#each filteredGroups as group}
|
||||
<div
|
||||
class="auth-entity group"
|
||||
on:click={() => {
|
||||
if (selectedGroup != group._id) {
|
||||
selectedGroup = group._id
|
||||
} else {
|
||||
selectedGroup = null
|
||||
}
|
||||
}}
|
||||
on:keydown={() => {}}
|
||||
<div class="body">
|
||||
{#if promptInvite && !userOnboardResponse}
|
||||
<Layout gap="S" paddingX="XL">
|
||||
<div class="invite-header">
|
||||
<Heading size="XS">No user found</Heading>
|
||||
<div class="invite-directions">
|
||||
Try searching a different email or <span
|
||||
class="underlined"
|
||||
on:click={openInviteFlow}>invite a new user</span
|
||||
>
|
||||
<div class="details">
|
||||
<GroupIcon {group} size="S" />
|
||||
<div>
|
||||
{group.name}
|
||||
</div>
|
||||
<div class="auth-entity-meta">
|
||||
{`${group.users?.length} user${
|
||||
group.users?.length != 1 ? "s" : ""
|
||||
}`}
|
||||
</div>
|
||||
</div>
|
||||
<div class="auth-entity-access">
|
||||
<RoleSelect
|
||||
placeholder={false}
|
||||
value={group.role}
|
||||
allowRemove={group.role}
|
||||
allowPublic={false}
|
||||
quiet={true}
|
||||
on:change={e => {
|
||||
onUpdateGroup(group, e.detail)
|
||||
}}
|
||||
on:remove={() => {
|
||||
onUpdateGroup(group)
|
||||
}}
|
||||
autoWidth
|
||||
align="right"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</Layout>
|
||||
{/if}
|
||||
|
||||
{#if filteredUsers?.length}
|
||||
<div class="auth-entity-section">
|
||||
<div class="auth-entity-header">
|
||||
<div class="auth-entity-title">Users</div>
|
||||
<div class="auth-entity-access-title">Access</div>
|
||||
</div>
|
||||
{#each allUsers as user}
|
||||
<div class="auth-entity">
|
||||
<div class="details">
|
||||
<div class="user-email" title={user.email}>
|
||||
{user.email}
|
||||
</div>
|
||||
<div class="auth-entity-meta">
|
||||
{userTitle(user)}
|
||||
</div>
|
||||
</div>
|
||||
<div class="auth-entity-access" class:muted={user.group}>
|
||||
<RoleSelect
|
||||
footer={getRoleFooter(user)}
|
||||
placeholder={false}
|
||||
value={user.role}
|
||||
allowRemove={user.role && !user.group}
|
||||
allowPublic={false}
|
||||
quiet={true}
|
||||
on:change={e => {
|
||||
onUpdateUser(user, e.detail)
|
||||
}}
|
||||
on:remove={() => {
|
||||
onUpdateUser(user)
|
||||
}}
|
||||
autoWidth
|
||||
align="right"
|
||||
allowedRoles={user.isAdminOrBuilder
|
||||
? [Constants.Roles.ADMIN]
|
||||
: null}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</Layout>
|
||||
{/if}
|
||||
</Layout>
|
||||
{/if}
|
||||
|
||||
{#if userOnboardResponse?.created}
|
||||
<Layout gap="S" paddingX="XL">
|
||||
<div class="invite-header">
|
||||
<Heading size="XS">User added!</Heading>
|
||||
<div class="invite-directions">
|
||||
Email invites are not available without SMTP configuration. Here is
|
||||
the password that has been generated for this user.
|
||||
{#if !promptInvite}
|
||||
<Layout gap="L" noPadding>
|
||||
{#if filteredInvites?.length}
|
||||
<Layout noPadding gap="XS">
|
||||
<div class="auth-entity-header">
|
||||
<div class="auth-entity-title">Pending invites</div>
|
||||
<div class="auth-entity-access-title">Access</div>
|
||||
</div>
|
||||
{#each filteredInvites as invite}
|
||||
{@const user = {
|
||||
isAdminOrGlobalBuilder:
|
||||
invite.info?.admin?.global && invite.info?.builder?.global,
|
||||
}}
|
||||
|
||||
<div class="auth-entity">
|
||||
<div class="details">
|
||||
<div class="user-email" title={invite.email}>
|
||||
{invite.email}
|
||||
</div>
|
||||
</div>
|
||||
<div class="auth-entity-access">
|
||||
<RoleSelect
|
||||
footer={getRoleFooter(user)}
|
||||
placeholder={false}
|
||||
value={getInviteRoleValue(invite)}
|
||||
allowRemove={invite.info.apps?.[prodAppId]}
|
||||
allowPublic={false}
|
||||
allowCreator={true}
|
||||
quiet={true}
|
||||
on:change={e => {
|
||||
onUpdateUserInvite(invite, e.detail)
|
||||
}}
|
||||
on:remove={() => {
|
||||
onUninviteAppUser(invite)
|
||||
}}
|
||||
autoWidth
|
||||
align="right"
|
||||
allowedRoles={user.isAdminOrGlobalBuilder
|
||||
? [Constants.Roles.ADMIN]
|
||||
: null}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</Layout>
|
||||
{/if}
|
||||
|
||||
{#if $licensing.groupsEnabled && filteredGroups?.length}
|
||||
<Layout noPadding gap="XS">
|
||||
<div class="auth-entity-header">
|
||||
<div class="auth-entity-title">Groups</div>
|
||||
<div class="auth-entity-access-title">Access</div>
|
||||
</div>
|
||||
{#each filteredGroups as group}
|
||||
<div
|
||||
class="auth-entity group"
|
||||
on:click={() => {
|
||||
if (selectedGroup != group._id) {
|
||||
selectedGroup = group._id
|
||||
} else {
|
||||
selectedGroup = null
|
||||
}
|
||||
}}
|
||||
on:keydown={() => {}}
|
||||
>
|
||||
<div class="details">
|
||||
<GroupIcon {group} size="S" />
|
||||
<div>
|
||||
{group.name}
|
||||
</div>
|
||||
<div class="auth-entity-meta">
|
||||
{`${group.users?.length} user${
|
||||
group.users?.length != 1 ? "s" : ""
|
||||
}`}
|
||||
</div>
|
||||
</div>
|
||||
<div class="auth-entity-access">
|
||||
<RoleSelect
|
||||
placeholder={false}
|
||||
value={group.role}
|
||||
allowRemove={group.role}
|
||||
allowPublic={false}
|
||||
quiet={true}
|
||||
allowCreator={true}
|
||||
on:change={e => {
|
||||
if (e.detail === Constants.Roles.CREATOR) {
|
||||
addGroupAppBuilder(group._id)
|
||||
} else {
|
||||
onUpdateGroup(group, e.detail)
|
||||
}
|
||||
}}
|
||||
on:remove={() => {
|
||||
onUpdateGroup(group)
|
||||
}}
|
||||
autoWidth
|
||||
align="right"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</Layout>
|
||||
{/if}
|
||||
|
||||
{#if filteredUsers?.length}
|
||||
<div class="auth-entity-section">
|
||||
<div class="auth-entity-header">
|
||||
<div class="auth-entity-title">Users</div>
|
||||
<div class="auth-entity-access-title">Access</div>
|
||||
</div>
|
||||
{#each allUsers as user}
|
||||
<div class="auth-entity">
|
||||
<div class="details">
|
||||
<div class="user-email" title={user.email}>
|
||||
{user.email}
|
||||
</div>
|
||||
</div>
|
||||
<div class="auth-entity-access" class:muted={user.group}>
|
||||
<RoleSelect
|
||||
footer={getRoleFooter(user)}
|
||||
placeholder={false}
|
||||
value={user.role}
|
||||
allowRemove={user.role && !user.group}
|
||||
allowPublic={false}
|
||||
allowCreator={true}
|
||||
quiet={true}
|
||||
on:addcreator={() => {}}
|
||||
on:change={e => {
|
||||
if (e.detail === Constants.Roles.CREATOR) {
|
||||
addAppBuilder(user._id)
|
||||
} else {
|
||||
onUpdateUser(user, e.detail)
|
||||
}
|
||||
}}
|
||||
on:remove={() => {
|
||||
onUpdateUser(user)
|
||||
}}
|
||||
autoWidth
|
||||
align="right"
|
||||
allowedRoles={user.isAdminOrGlobalBuilder
|
||||
? [Constants.Roles.ADMIN]
|
||||
: null}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</Layout>
|
||||
{/if}
|
||||
|
||||
{#if userOnboardResponse?.created}
|
||||
<Layout gap="S" paddingX="XL">
|
||||
<div class="invite-header">
|
||||
<Heading size="XS">User added!</Heading>
|
||||
<div class="invite-directions">
|
||||
Email invites are not available without SMTP configuration. Here
|
||||
is the password that has been generated for this user.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<CopyInput
|
||||
value={userOnboardResponse.successful[0]?.password}
|
||||
label="Password"
|
||||
/>
|
||||
<div>
|
||||
<CopyInput
|
||||
value={userOnboardResponse.successful[0]?.password}
|
||||
label="Password"
|
||||
/>
|
||||
</div>
|
||||
</Layout>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<Divider />
|
||||
<div class="body">
|
||||
<Layout gap="L" noPadding>
|
||||
<div class="user-invite-form">
|
||||
<FancyForm bind:this={form}>
|
||||
<FancyInput
|
||||
disabled={false}
|
||||
label="Email"
|
||||
value={email}
|
||||
on:change={e => {
|
||||
email = e.detail
|
||||
}}
|
||||
validate={() => {
|
||||
if (!email) {
|
||||
return "Please enter an email"
|
||||
}
|
||||
return null
|
||||
}}
|
||||
{error}
|
||||
/>
|
||||
<FancySelect
|
||||
bind:value={creationRoleType}
|
||||
options={sdk.users.isAdmin($auth.user)
|
||||
? Constants.BudibaseRoleOptionsNew
|
||||
: Constants.BudibaseRoleOptionsNew.filter(
|
||||
option => option.value !== Constants.BudibaseRoles.Admin
|
||||
)}
|
||||
label="Role"
|
||||
/>
|
||||
{#if creationRoleType !== Constants.BudibaseRoles.Admin}
|
||||
<RoleSelect
|
||||
placeholder={false}
|
||||
bind:value={creationAccessType}
|
||||
allowPublic={false}
|
||||
allowCreator={true}
|
||||
quiet={true}
|
||||
autoWidth
|
||||
align="right"
|
||||
fancySelect
|
||||
/>
|
||||
{/if}
|
||||
</FancyForm>
|
||||
{#if creationRoleType === Constants.BudibaseRoles.Admin}
|
||||
<div class="admin-info">
|
||||
<Icon name="Info" />
|
||||
Admins will get full access to all apps and settings
|
||||
</div>
|
||||
{/if}
|
||||
<span class="add-user">
|
||||
<Button
|
||||
newStyles
|
||||
cta
|
||||
disabled={!email?.length}
|
||||
on:click={onInviteUser}>Add user</Button
|
||||
>
|
||||
</span>
|
||||
</div>
|
||||
</Layout>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<Modal bind:this={userLimitReachedModal}>
|
||||
<UpgradeModal {isOwner} />
|
||||
</Modal>
|
||||
|
@ -659,6 +848,27 @@
|
|||
align-items: center;
|
||||
}
|
||||
|
||||
.add-user {
|
||||
padding-top: var(--spacing-xl);
|
||||
width: 100%;
|
||||
display: grid;
|
||||
}
|
||||
|
||||
.admin-info {
|
||||
margin-top: var(--spacing-xl);
|
||||
padding: var(--spacing-l) var(--spacing-l) var(--spacing-l) var(--spacing-l);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-xl);
|
||||
height: 30px;
|
||||
background-color: var(--background-alt);
|
||||
}
|
||||
|
||||
.underlined {
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
flex: 1;
|
||||
}
|
||||
|
@ -746,12 +956,6 @@
|
|||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.invite-form {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
#builder-side-panel-container .search {
|
||||
padding-top: var(--spacing-m);
|
||||
padding-bottom: var(--spacing-m);
|
||||
|
@ -798,6 +1002,16 @@
|
|||
flex-direction: column;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-l);
|
||||
}
|
||||
|
||||
.user-invite-form {
|
||||
padding: 0 var(--spacing-xl) var(--spacing-xl) var(--spacing-xl);
|
||||
}
|
||||
|
||||
.body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import TableDataTable from "components/backend/DataTable/DataTable.svelte"
|
||||
import TableDataTable from "components/backend/DataTable/TableDataTable.svelte"
|
||||
import { tables, database } from "stores/backend"
|
||||
import { Banner } from "@budibase/bbui"
|
||||
|
||||
|
|
|
@ -1,14 +1,17 @@
|
|||
<script>
|
||||
import { onMount } from "svelte"
|
||||
import { views } from "stores/backend"
|
||||
import { views, viewsV2 } from "stores/backend"
|
||||
import { redirect } from "@roxi/routify"
|
||||
|
||||
onMount(async () => {
|
||||
const { list, selected } = $views
|
||||
if (selected) {
|
||||
$redirect(`./${encodeURIComponent(selected?.name)}`)
|
||||
} else if (list?.length) {
|
||||
$redirect(`./${encodeURIComponent(list[0].name)}`)
|
||||
if ($viewsV2.selected) {
|
||||
$redirect(`./v2/${$viewsV2.selected.id}`)
|
||||
} else if ($viewsV2.list?.length) {
|
||||
$redirect(`./v2/${$viewsV2.list[0].id}`)
|
||||
} else if ($views.selected) {
|
||||
$redirect(`./${encodeURIComponent($views.selected?.name)}`)
|
||||
} else if ($views.list?.length) {
|
||||
$redirect(`./${encodeURIComponent($views.list[0].name)}`)
|
||||
} else {
|
||||
$redirect("../")
|
||||
}
|
||||
|
|
|
@ -5,15 +5,15 @@
|
|||
import { onDestroy } from "svelte"
|
||||
import { store } from "builderStore"
|
||||
|
||||
$: viewName = $views.selectedViewName
|
||||
$: store.actions.websocket.selectResource(viewName)
|
||||
$: name = $views.selectedViewName
|
||||
$: store.actions.websocket.selectResource(name)
|
||||
|
||||
const stopSyncing = syncURLToState({
|
||||
urlParam: "viewName",
|
||||
stateKey: "selectedViewName",
|
||||
validate: name => $views.list?.some(view => view.name === name),
|
||||
update: views.select,
|
||||
fallbackUrl: "../",
|
||||
fallbackUrl: "../../",
|
||||
store: views,
|
||||
routify,
|
||||
decode: decodeURIComponent,
|
|
@ -0,0 +1,5 @@
|
|||
<script>
|
||||
import { redirect } from "@roxi/routify"
|
||||
|
||||
$redirect("../")
|
||||
</script>
|
|
@ -0,0 +1,25 @@
|
|||
<script>
|
||||
import { viewsV2 } from "stores/backend"
|
||||
import { syncURLToState } from "helpers/urlStateSync"
|
||||
import * as routify from "@roxi/routify"
|
||||
import { onDestroy } from "svelte"
|
||||
import { store } from "builderStore"
|
||||
|
||||
$: id = $viewsV2.selectedViewId
|
||||
$: store.actions.websocket.selectResource(id)
|
||||
|
||||
const stopSyncing = syncURLToState({
|
||||
urlParam: "viewId",
|
||||
stateKey: "selectedViewId",
|
||||
validate: id => $viewsV2.list?.some(view => view.id === id),
|
||||
update: viewsV2.select,
|
||||
fallbackUrl: "../../",
|
||||
store: viewsV2,
|
||||
routify,
|
||||
decode: decodeURIComponent,
|
||||
})
|
||||
|
||||
onDestroy(stopSyncing)
|
||||
</script>
|
||||
|
||||
<slot />
|
|
@ -0,0 +1,5 @@
|
|||
<script>
|
||||
import ViewV2DataTable from "components/backend/DataTable/ViewV2DataTable.svelte"
|
||||
</script>
|
||||
|
||||
<ViewV2DataTable />
|
|
@ -0,0 +1,5 @@
|
|||
<script>
|
||||
import { redirect } from "@roxi/routify"
|
||||
|
||||
$redirect("../")
|
||||
</script>
|
|
@ -297,8 +297,12 @@
|
|||
width: 100%;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
transition: background 130ms ease-out;
|
||||
}
|
||||
.divider:hover {
|
||||
cursor: row-resize;
|
||||
}
|
||||
.divider:hover:after {
|
||||
background: var(--spectrum-global-color-gray-300);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -110,7 +110,7 @@
|
|||
if (mode === "table") {
|
||||
datasourceModal.show()
|
||||
} else if (mode === "blank") {
|
||||
let templates = getTemplates($store, $tables.list)
|
||||
let templates = getTemplates($tables.list)
|
||||
const blankScreenTemplate = templates.find(
|
||||
t => t.id === "createFromScratch"
|
||||
)
|
||||
|
@ -131,8 +131,7 @@
|
|||
const completeDatasourceScreenCreation = async () => {
|
||||
const screens = selectedTemplates.map(template => {
|
||||
let screenTemplate = template.create()
|
||||
screenTemplate.datasource = template.datasource
|
||||
screenTemplate.autoTableId = template.table
|
||||
screenTemplate.autoTableId = template.resourceId
|
||||
return screenTemplate
|
||||
})
|
||||
await createScreens({ screens, screenAccessRole })
|
||||
|
@ -176,10 +175,10 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<Modal bind:this={datasourceModal}>
|
||||
<Modal bind:this={datasourceModal} autoFocus={false}>
|
||||
<DatasourceModal
|
||||
onConfirm={confirmScreenDatasources}
|
||||
initalScreens={!selectedTemplates ? [] : [...selectedTemplates]}
|
||||
initialScreens={!selectedTemplates ? [] : [...selectedTemplates]}
|
||||
/>
|
||||
</Modal>
|
||||
|
||||
|
|
|
@ -1,41 +1,30 @@
|
|||
<script>
|
||||
import { store } from "builderStore"
|
||||
import {
|
||||
ModalContent,
|
||||
Layout,
|
||||
notifications,
|
||||
Icon,
|
||||
Body,
|
||||
} from "@budibase/bbui"
|
||||
import { tables, datasources } from "stores/backend"
|
||||
import getTemplates from "builderStore/store/screenTemplates"
|
||||
import { ModalContent, Layout, notifications, Body } from "@budibase/bbui"
|
||||
import { datasources } from "stores/backend"
|
||||
import ICONS from "components/backend/DatasourceNavigator/icons"
|
||||
import { IntegrationNames } from "constants"
|
||||
import { onMount } from "svelte"
|
||||
import rowListScreen from "builderStore/store/screenTemplates/rowListScreen"
|
||||
import DatasourceTemplateRow from "./DatasourceTemplateRow.svelte"
|
||||
|
||||
export let onCancel
|
||||
export let onConfirm
|
||||
export let initalScreens = []
|
||||
export let initialScreens = []
|
||||
|
||||
let selectedScreens = [...initalScreens]
|
||||
let selectedScreens = [...initialScreens]
|
||||
|
||||
const toggleScreenSelection = (table, datasource) => {
|
||||
if (selectedScreens.find(s => s.table === table._id)) {
|
||||
$: filteredSources = $datasources.list?.filter(datasource => {
|
||||
return datasource.source !== IntegrationNames.REST && datasource["entities"]
|
||||
})
|
||||
|
||||
const toggleSelection = datasource => {
|
||||
const { resourceId } = datasource
|
||||
if (selectedScreens.find(s => s.resourceId === resourceId)) {
|
||||
selectedScreens = selectedScreens.filter(
|
||||
screen => screen.table !== table._id
|
||||
screen => screen.resourceId !== resourceId
|
||||
)
|
||||
} else {
|
||||
let partialTemplates = getTemplates($store, $tables.list).reduce(
|
||||
(acc, template) => {
|
||||
if (template.table === table._id) {
|
||||
template.datasource = datasource.name
|
||||
acc.push(template)
|
||||
}
|
||||
return acc
|
||||
},
|
||||
[]
|
||||
)
|
||||
selectedScreens = [...partialTemplates, ...selectedScreens]
|
||||
selectedScreens = [...selectedScreens, rowListScreen([datasource])[0]]
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -45,18 +34,6 @@
|
|||
})
|
||||
}
|
||||
|
||||
$: filteredSources = Array.isArray($datasources.list)
|
||||
? $datasources.list.reduce((acc, datasource) => {
|
||||
if (
|
||||
datasource.source !== IntegrationNames.REST &&
|
||||
datasource["entities"]
|
||||
) {
|
||||
acc.push(datasource)
|
||||
}
|
||||
return acc
|
||||
}, [])
|
||||
: []
|
||||
|
||||
onMount(async () => {
|
||||
try {
|
||||
await datasources.fetch()
|
||||
|
@ -81,6 +58,9 @@
|
|||
</Body>
|
||||
<Layout noPadding gap="S">
|
||||
{#each filteredSources as datasource}
|
||||
{@const entities = Array.isArray(datasource.entities)
|
||||
? datasource.entities
|
||||
: Object.values(datasource.entities || {})}
|
||||
<div class="data-source-wrap">
|
||||
<div class="data-source-header">
|
||||
<svelte:component
|
||||
|
@ -90,64 +70,45 @@
|
|||
/>
|
||||
<div class="data-source-name">{datasource.name}</div>
|
||||
</div>
|
||||
{#if Array.isArray(datasource.entities)}
|
||||
{#each datasource.entities.filter(table => table._id !== "ta_users") as table}
|
||||
<div
|
||||
class="data-source-entry"
|
||||
class:selected={selectedScreens.find(
|
||||
x => x.table === table._id
|
||||
)}
|
||||
on:click={() => toggleScreenSelection(table, datasource)}
|
||||
>
|
||||
<svg
|
||||
width="16px"
|
||||
height="16px"
|
||||
class="spectrum-Icon"
|
||||
style="color: white"
|
||||
focusable="false"
|
||||
>
|
||||
<use xlink:href="#spectrum-icon-18-Table" />
|
||||
</svg>
|
||||
{table.name}
|
||||
{#if selectedScreens.find(x => x.table === table._id)}
|
||||
<span class="data-source-check">
|
||||
<Icon size="S" name="CheckmarkCircle" />
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
<!-- List all tables -->
|
||||
{#each entities.filter(table => table._id !== "ta_users") as table}
|
||||
{@const views = Object.values(table.views || {}).filter(
|
||||
view => view.version === 2
|
||||
)}
|
||||
{@const tableDS = {
|
||||
tableId: table._id,
|
||||
label: table.name,
|
||||
resourceId: table._id,
|
||||
type: "table",
|
||||
}}
|
||||
{@const selected = selectedScreens.find(
|
||||
screen => screen.resourceId === tableDS.resourceId
|
||||
)}
|
||||
<DatasourceTemplateRow
|
||||
on:click={() => toggleSelection(tableDS)}
|
||||
{selected}
|
||||
datasource={tableDS}
|
||||
/>
|
||||
|
||||
<!-- List all views inside this table -->
|
||||
{#each views as view}
|
||||
{@const viewDS = {
|
||||
label: view.name,
|
||||
id: view.id,
|
||||
resourceId: view.id,
|
||||
tableId: view.tableId,
|
||||
type: "viewV2",
|
||||
}}
|
||||
{@const selected = selectedScreens.find(
|
||||
x => x.resourceId === viewDS.resourceId
|
||||
)}
|
||||
<DatasourceTemplateRow
|
||||
on:click={() => toggleSelection(viewDS)}
|
||||
{selected}
|
||||
datasource={viewDS}
|
||||
/>
|
||||
{/each}
|
||||
{/if}
|
||||
{#if datasource["entities"] && !Array.isArray(datasource.entities)}
|
||||
{#each Object.keys(datasource.entities).filter(table => table._id !== "ta_users") as table_key}
|
||||
<div
|
||||
class="data-source-entry"
|
||||
class:selected={selectedScreens.find(
|
||||
x => x.table === datasource.entities[table_key]._id
|
||||
)}
|
||||
on:click={() =>
|
||||
toggleScreenSelection(
|
||||
datasource.entities[table_key],
|
||||
datasource
|
||||
)}
|
||||
>
|
||||
<svg
|
||||
width="16px"
|
||||
height="16px"
|
||||
class="spectrum-Icon"
|
||||
style="color: white"
|
||||
focusable="false"
|
||||
>
|
||||
<use xlink:href="#spectrum-icon-18-Table" />
|
||||
</svg>
|
||||
{datasource.entities[table_key].name}
|
||||
{#if selectedScreens.find(x => x.table === datasource.entities[table_key]._id)}
|
||||
<span class="data-source-check">
|
||||
<Icon size="S" name="CheckmarkCircle" />
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
{/each}
|
||||
</Layout>
|
||||
|
@ -160,42 +121,10 @@
|
|||
display: grid;
|
||||
grid-gap: var(--spacing-s);
|
||||
}
|
||||
|
||||
.data-source-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-m);
|
||||
padding-bottom: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.data-source-entry {
|
||||
cursor: pointer;
|
||||
grid-gap: var(--spectrum-alias-grid-margin-xsmall);
|
||||
padding: var(--spectrum-alias-item-padding-s);
|
||||
background: var(--spectrum-alias-background-color-secondary);
|
||||
transition: 0.3s all;
|
||||
border: 1px solid var(--spectrum-global-color-gray-300);
|
||||
border-radius: 4px;
|
||||
border-width: 1px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.data-source-entry:hover,
|
||||
.selected {
|
||||
background: var(--spectrum-alias-background-color-tertiary);
|
||||
}
|
||||
|
||||
.data-source-entry .data-source-check {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.data-source-entry :global(.spectrum-Icon) {
|
||||
min-width: 16px;
|
||||
}
|
||||
|
||||
.data-source-entry .data-source-check :global(.spectrum-Icon) {
|
||||
color: var(--spectrum-global-color-green-600);
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
<script>
|
||||
import { Icon } from "@budibase/bbui"
|
||||
export let datasource
|
||||
export let selected = false
|
||||
|
||||
$: icon = datasource.type === "viewV2" ? "Remove" : "Table"
|
||||
</script>
|
||||
|
||||
<div class="data-source-entry" class:selected on:click>
|
||||
<Icon name={icon} color="var(--spectrum-global-color-gray-600)" />
|
||||
{datasource.label}
|
||||
{#if selected}
|
||||
<span class="data-source-check">
|
||||
<Icon size="S" name="CheckmarkCircle" />
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.data-source-entry {
|
||||
cursor: pointer;
|
||||
grid-gap: var(--spacing-m);
|
||||
padding: var(--spectrum-alias-item-padding-s);
|
||||
background: var(--spectrum-alias-background-color-secondary);
|
||||
transition: 0.3s all;
|
||||
border: 1px solid var(--spectrum-global-color-gray-300);
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.data-source-entry:hover,
|
||||
.selected {
|
||||
background: var(--spectrum-alias-background-color-tertiary);
|
||||
}
|
||||
|
||||
.data-source-check {
|
||||
margin-left: auto;
|
||||
}
|
||||
.data-source-check :global(.spectrum-Icon) {
|
||||
color: var(--spectrum-global-color-green-600);
|
||||
}
|
||||
</style>
|
|
@ -39,7 +39,7 @@
|
|||
return publishedApps
|
||||
}
|
||||
return publishedApps.filter(app => {
|
||||
if (sdk.users.isBuilder(user, app.appId)) {
|
||||
if (sdk.users.isBuilder(user, app.prodId)) {
|
||||
return true
|
||||
}
|
||||
if (!Object.keys(user?.roles).length && user?.userGroups) {
|
||||
|
@ -142,7 +142,12 @@
|
|||
<div class="group">
|
||||
<Layout gap="S" noPadding>
|
||||
{#each userApps as app (app.appId)}
|
||||
<a class="app" target="_blank" href={getUrl(app)}>
|
||||
<a
|
||||
class="app"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
href={getUrl(app)}
|
||||
>
|
||||
<div class="preview" use:gradient={{ seed: app.name }} />
|
||||
<div class="app-info">
|
||||
<Heading size="XS">{app.name}</Heading>
|
||||
|
|
|
@ -21,6 +21,7 @@
|
|||
import GroupIcon from "./_components/GroupIcon.svelte"
|
||||
import GroupUsers from "./_components/GroupUsers.svelte"
|
||||
import { sdk } from "@budibase/shared-core"
|
||||
import { Constants } from "@budibase/frontend-core"
|
||||
|
||||
export let groupId
|
||||
|
||||
|
@ -57,8 +58,11 @@
|
|||
)
|
||||
.map(app => ({
|
||||
...app,
|
||||
role: group?.roles?.[apps.getProdAppID(app.devId)],
|
||||
role: group?.builder?.apps.includes(apps.getProdAppID(app.devId))
|
||||
? Constants.Roles.CREATOR
|
||||
: group?.roles?.[apps.getProdAppID(app.devId)],
|
||||
}))
|
||||
|
||||
$: {
|
||||
if (loaded && !group?._id) {
|
||||
$goto("./")
|
||||
|
|
|
@ -1,9 +1,19 @@
|
|||
<script>
|
||||
import { Icon } from "@budibase/bbui"
|
||||
import { sdk } from "@budibase/shared-core"
|
||||
|
||||
export let value
|
||||
export let row
|
||||
$: count = getCount(Object.keys(value || {}).length)
|
||||
|
||||
$: count = Object.keys(value || {}).length
|
||||
const getCount = () => {
|
||||
return sdk.users.hasAppBuilderPermissions(row)
|
||||
? row.builder.apps.length +
|
||||
Object.keys(row.roles || {}).filter(appId =>
|
||||
row.builder.apps.includes(appId)
|
||||
).length
|
||||
: value?.length || 0
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="align">
|
||||
|
|
|
@ -89,7 +89,7 @@
|
|||
$: scimEnabled = $features.isScimEnabled
|
||||
$: isSSO = !!user?.provider
|
||||
$: readonly = !sdk.users.isAdmin($auth.user) || scimEnabled
|
||||
$: privileged = sdk.users.isAdminOrBuilder(user)
|
||||
$: privileged = sdk.users.isAdminOrGlobalBuilder(user)
|
||||
$: nameLabel = getNameLabel(user)
|
||||
$: filteredGroups = getFilteredGroups($groups, searchTerm)
|
||||
$: availableApps = getAvailableApps($apps, privileged, user?.roles)
|
||||
|
@ -98,17 +98,14 @@
|
|||
return y._id === userId
|
||||
})
|
||||
})
|
||||
$: globalRole = sdk.users.isAdmin(user)
|
||||
? "admin"
|
||||
: sdk.users.isBuilder(user)
|
||||
? "developer"
|
||||
: "appUser"
|
||||
$: globalRole = sdk.users.isAdmin(user) ? "admin" : "appUser"
|
||||
|
||||
const getAvailableApps = (appList, privileged, roles) => {
|
||||
let availableApps = appList.slice()
|
||||
if (!privileged) {
|
||||
availableApps = availableApps.filter(x => {
|
||||
return Object.keys(roles || {}).find(y => {
|
||||
let roleKeys = Object.keys(roles || {})
|
||||
return roleKeys.concat(user?.builder?.apps).find(y => {
|
||||
return x.appId === apps.extractAppId(y)
|
||||
})
|
||||
})
|
||||
|
@ -119,7 +116,7 @@
|
|||
name: app.name,
|
||||
devId: app.devId,
|
||||
icon: app.icon,
|
||||
role: privileged ? Constants.Roles.ADMIN : roles[prodAppId],
|
||||
role: getRole(prodAppId, roles),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -132,6 +129,18 @@
|
|||
return groups.filter(group => group.name?.toLowerCase().includes(search))
|
||||
}
|
||||
|
||||
const getRole = (prodAppId, roles) => {
|
||||
if (privileged) {
|
||||
return Constants.Roles.ADMIN
|
||||
}
|
||||
|
||||
if (user?.builder?.apps?.includes(prodAppId)) {
|
||||
return Constants.Roles.CREATOR
|
||||
}
|
||||
|
||||
return roles[prodAppId]
|
||||
}
|
||||
|
||||
const getNameLabel = user => {
|
||||
const { firstName, lastName, email } = user || {}
|
||||
if (!firstName && !lastName) {
|
||||
|
|
|
@ -2,12 +2,16 @@
|
|||
import { StatusLight } from "@budibase/bbui"
|
||||
import { RoleUtils } from "@budibase/frontend-core"
|
||||
import { roles } from "stores/backend"
|
||||
import { Constants } from "@budibase/frontend-core"
|
||||
import { capitalise } from "helpers"
|
||||
|
||||
export let value
|
||||
|
||||
const getRoleLabel = roleId => {
|
||||
const role = $roles.find(x => x._id === roleId)
|
||||
return role?.name || "Custom role"
|
||||
return roleId === Constants.Roles.CREATOR
|
||||
? capitalise(Constants.Roles.CREATOR.toLowerCase())
|
||||
: role?.name || "Custom role"
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue