Browse Source

Merge branch 'admin/user-management-ui' of github.com:Budibase/budibase into admin/user-management-ui

pull/1491/head
mike12345567 5 years ago
parent
commit
638e2f5c8e
  1. 19
      .github/CONTRIBUTING.md
  2. 16
      hosting/envoy.dev.yaml.hbs
  3. 1
      hosting/envoy.yaml
  4. 61
      hosting/scripts/setup.js
  5. 3
      package.json
  6. 7
      packages/auth/package.json
  7. 3
      packages/auth/src/environment.js
  8. 4
      packages/auth/src/index.js
  9. 240
      packages/auth/src/objectStore/index.js
  10. 13
      packages/auth/src/objectStore/utils.js
  11. 223
      packages/auth/yarn.lock
  12. 2
      packages/bbui/package.json
  13. 3
      packages/bbui/src/ActionMenu/ActionMenu.svelte
  14. 97
      packages/bbui/src/Form/Core/Dropzone.svelte
  15. 2
      packages/bbui/src/Form/Core/Picker.svelte
  16. 2
      packages/bbui/src/Form/Core/Select.svelte
  17. 1
      packages/bbui/src/Form/Core/TextField.svelte
  18. 2
      packages/bbui/src/Form/Dropzone.svelte
  19. 1
      packages/bbui/src/Form/Input.svelte
  20. 2
      packages/bbui/src/Form/Select.svelte
  21. 60
      packages/bbui/src/Label/Label.svelte
  22. 5
      packages/bbui/src/Layout/Layout.svelte
  23. 9
      packages/bbui/src/Layout/Page.svelte
  24. 8
      packages/bbui/src/Menu/Item.svelte
  25. 21
      packages/bbui/src/Modal/Modal.svelte
  26. 2
      packages/bbui/src/Modal/ModalContent.svelte
  27. 2
      packages/bbui/src/Table/CellRenderer.svelte
  28. 77
      packages/bbui/src/Table/Table.svelte
  29. 11
      packages/bbui/src/Typography/Body.svelte
  30. 8
      packages/bbui/yarn.lock
  31. 6
      packages/builder/package.json
  32. 90
      packages/builder/src/actions.js
  33. 2
      packages/builder/src/components/backend/DataTable/RowFieldControl.svelte
  34. 2
      packages/builder/src/components/backend/DataTable/Table.svelte
  35. 2
      packages/builder/src/components/backend/DataTable/modals/ManageAccessModal.svelte
  36. 2
      packages/builder/src/components/backend/DatasourceNavigator/TableIntegrationMenu/IntegrationConfigForm.svelte
  37. 12
      packages/builder/src/components/common/ConfigChecklist.svelte
  38. 9
      packages/builder/src/components/common/ConfirmDialog.svelte
  39. 2
      packages/builder/src/components/common/bindings/BindingPanel.svelte
  40. 2
      packages/builder/src/components/common/bindings/ServerBindingPanel.svelte
  41. 5
      packages/builder/src/components/design/AppPreview/CurrentItemPreview.svelte
  42. 2
      packages/builder/src/components/design/PropertiesPanel/PropertyControls/PropertyControl.svelte
  43. 18
      packages/builder/src/components/login/LoginForm.svelte
  44. 2
      packages/builder/src/components/settings/tabs/DangerZone.svelte
  45. 122
      packages/builder/src/components/start/AppCard.svelte
  46. 50
      packages/builder/src/components/start/AppList.svelte
  47. 80
      packages/builder/src/components/start/AppRow.svelte
  48. 15
      packages/builder/src/components/start/BuilderSettingsButton.svelte
  49. 239
      packages/builder/src/components/start/CreateAppModal.svelte
  50. 83
      packages/builder/src/components/start/Indicator.svelte
  51. 6
      packages/builder/src/components/start/LogoutButton.svelte
  52. 121
      packages/builder/src/components/start/Steps/Info.svelte
  53. 29
      packages/builder/src/components/start/Steps/User.svelte
  54. 2
      packages/builder/src/components/start/Steps/index.js
  55. 20
      packages/builder/src/helperStores.js
  56. 29
      packages/builder/src/pages/_layout.svelte
  57. 69
      packages/builder/src/pages/admin/index.svelte
  58. 131
      packages/builder/src/pages/builder/_layout.svelte
  59. 78
      packages/builder/src/pages/builder/admin/index.svelte
  60. 6
      packages/builder/src/pages/builder/app/[application]/_layout.svelte
  61. 0
      packages/builder/src/pages/builder/app/[application]/automate/[automation]/_layout.svelte
  62. 0
      packages/builder/src/pages/builder/app/[application]/automate/[automation]/index.svelte
  63. 0
      packages/builder/src/pages/builder/app/[application]/automate/_layout.svelte
  64. 0
      packages/builder/src/pages/builder/app/[application]/automate/index.svelte
  65. 0
      packages/builder/src/pages/builder/app/[application]/data/_layout.svelte
  66. 0
      packages/builder/src/pages/builder/app/[application]/data/datasource/[selectedDatasource]/[query]/_layout.svelte
  67. 0
      packages/builder/src/pages/builder/app/[application]/data/datasource/[selectedDatasource]/[query]/index.svelte
  68. 0
      packages/builder/src/pages/builder/app/[application]/data/datasource/[selectedDatasource]/_layout.svelte
  69. 2
      packages/builder/src/pages/builder/app/[application]/data/datasource/[selectedDatasource]/index.svelte
  70. 0
      packages/builder/src/pages/builder/app/[application]/data/datasource/_layout.svelte
  71. 0
      packages/builder/src/pages/builder/app/[application]/data/datasource/index.svelte
  72. 0
      packages/builder/src/pages/builder/app/[application]/data/index.svelte
  73. 0
      packages/builder/src/pages/builder/app/[application]/data/table/[selectedTable]/_layout.svelte
  74. 0
      packages/builder/src/pages/builder/app/[application]/data/table/[selectedTable]/index.svelte
  75. 0
      packages/builder/src/pages/builder/app/[application]/data/table/[selectedTable]/relationship/[selectedRow]/[selectedField]/index.svelte
  76. 0
      packages/builder/src/pages/builder/app/[application]/data/table/[selectedTable]/relationship/[selectedRow]/index.svelte
  77. 0
      packages/builder/src/pages/builder/app/[application]/data/table/[selectedTable]/relationship/index.svelte
  78. 0
      packages/builder/src/pages/builder/app/[application]/data/table/_layout.svelte
  79. 0
      packages/builder/src/pages/builder/app/[application]/data/table/index.svelte
  80. 0
      packages/builder/src/pages/builder/app/[application]/data/view/[selectedView]/_layout.svelte
  81. 0
      packages/builder/src/pages/builder/app/[application]/data/view/[selectedView]/index.svelte
  82. 0
      packages/builder/src/pages/builder/app/[application]/deploy/_layout.svelte
  83. 0
      packages/builder/src/pages/builder/app/[application]/deploy/index.svelte
  84. 0
      packages/builder/src/pages/builder/app/[application]/design/[assetType]/[asset]/_fallback.svelte
  85. 0
      packages/builder/src/pages/builder/app/[application]/design/[assetType]/_layout.svelte
  86. 0
      packages/builder/src/pages/builder/app/[application]/design/[assetType]/index.svelte
  87. 0
      packages/builder/src/pages/builder/app/[application]/design/_layout.svelte
  88. 0
      packages/builder/src/pages/builder/app/[application]/design/index.svelte
  89. 0
      packages/builder/src/pages/builder/app/[application]/index.svelte
  90. 0
      packages/builder/src/pages/builder/app/[application]/settings/_layout.svelte
  91. 0
      packages/builder/src/pages/builder/app/[application]/settings/index.svelte
  92. 4
      packages/builder/src/pages/builder/app/index.svelte
  93. 4
      packages/builder/src/pages/builder/auth/index.svelte
  94. 5
      packages/builder/src/pages/builder/auth/login.svelte
  95. 123
      packages/builder/src/pages/builder/index.svelte
  96. 81
      packages/builder/src/pages/builder/portal/_layout.svelte
  97. 231
      packages/builder/src/pages/builder/portal/apps/index.svelte
  98. 0
      packages/builder/src/pages/builder/portal/index.svelte
  99. 0
      packages/builder/src/pages/builder/portal/manage/_layout.svelte
  100. 0
      packages/builder/src/pages/builder/portal/manage/auth/_logos/Google.svelte

19
.github/CONTRIBUTING.md

@ -92,6 +92,16 @@ then `cd ` into your local copy.
### 3. Install and Build
To develop the Budibase platform you'll need [Docker](https://www.docker.com/) and [Docker Compose](https://docs.docker.com/compose/) installed.
#### Quick method
`yarn setup` will check that all necessary components are installed and setup the repo for usage.
#### Manual method
The following commands can be executed to manually get Budibase up and running (assuming Docker/Docker Compose has been installed).
`yarn` to install project dependencies
`yarn bootstrap` will install all budibase modules and symlink them together using lerna.
@ -112,10 +122,17 @@ To run the budibase server and builder in dev mode (i.e. with live reloading):
1. Open a new console
2. `yarn dev` (from root)
3. Access the builder on http://localhost:4001/_builder/
3. Access the builder on http://localhost:10000/builder
This will enable watch mode for both the builder app, server, client library and any component libraries.
### 5. Cleanup
If you wish to delete all the apps created in development and reset the environment then run the following:
1. `yarn nuke:docker` will wipe all the Budibase services
2. `yarn dev` will restart all the services
## Data Storage
When you are running locally, budibase stores data on disk using [PouchDB](https://pouchdb.com/), as well as some JSON on local files. After setting up budibase, you can find all of this data in the `~/.budibase` directory.

16
hosting/envoy.dev.yaml.hbs

@ -16,6 +16,11 @@ static_resources:
- name: local_services
domains: ["*"]
routes:
# special case to redirect specifically the route path
# to the builder, if this were a prefix then it would break minio
- match: { path: "/" }
redirect: { path_redirect: "/builder/" }
- match: { prefix: "/db/" }
route:
cluster: couchdb-service
@ -33,7 +38,14 @@ static_resources:
route:
cluster: server-dev
- match: { prefix: "/" }
# the below three cases are needed to make sure
# all traffic prefixed for the builder is passed through
# correctly.
- match: { path: "/" }
route:
cluster: builder-dev
- match: { prefix: "/builder/" }
route:
cluster: builder-dev
@ -41,7 +53,7 @@ static_resources:
route:
cluster: builder-dev
prefix_rewrite: "/builder/"
# minio is on the default route because this works
# best, minio + AWS SDK doesn't handle path proxy
- match: { prefix: "/" }

1
hosting/envoy.yaml

@ -21,7 +21,6 @@ static_resources:
cluster: app-service
prefix_rewrite: "/"
# special case for presenting our static self hosting page
- match: { path: "/" }
route:
cluster: app-service

61
hosting/scripts/setup.js

@ -0,0 +1,61 @@
#!/usr/bin/env node
const os = require("os")
const exec = require("child_process").exec
const fs = require("fs")
const platform = os.platform()
const windows = platform === "win32"
const mac = platform === "darwin"
const linux = platform === "linux"
function execute(command) {
return new Promise(resolve => {
exec(command, (err, stdout) => resolve(linux ? !!stdout : true))
})
}
async function commandExistsUnix(command) {
const unixCmd = `command -v ${command} 2>/dev/null && { echo >&1 ${command}; exit 0; }`
return execute(command)
}
async function commandExistsWindows(command) {
if (/[\x00-\x1f<>:"|?*]/.test(command)) {
return false
}
return execute(`where ${command}`)
}
function commandExists(command) {
return windows ? commandExistsWindows(command) : commandExistsUnix(command)
}
async function init() {
const docker = commandExists("docker")
const dockerCompose = commandExists("docker-compose")
if (docker && dockerCompose) {
console.log("Docker installed - continuing.")
return
}
if (mac) {
console.log(
"Please install docker by visiting: https://docs.docker.com/docker-for-mac/install/"
)
} else if (windows) {
console.log(
"Please install docker by visiting: https://docs.docker.com/docker-for-windows/install/"
)
} else if (linux) {
console.log("Beginning automated linux installation.")
await execute(`./hosting/scripts/linux/get-docker.sh`)
await execute(`./hosting/scripts/linux/get-docker-compose.sh`)
} else {
console.error(
"Platform unknown - please look online for information about installing docker for our OS."
)
}
console.log("Once installation complete please re-run the setup script.")
process.exit(-1)
}
init()

3
package.json

@ -14,9 +14,10 @@
"prettier-plugin-svelte": "^2.2.0",
"rimraf": "^3.0.2",
"rollup-plugin-replace": "^2.2.0",
"svelte": "^3.37.0"
"svelte": "^3.38.2"
},
"scripts": {
"setup": "./hosting/scripts/setup.js && yarn && yarn bootstrap && yarn build && yarn dev",
"bootstrap": "lerna link && lerna bootstrap",
"build": "lerna run build",
"initialise": "lerna run initialise",

7
packages/auth/package.json

@ -6,15 +6,20 @@
"author": "Budibase",
"license": "AGPL-3.0",
"dependencies": {
"aws-sdk": "^2.901.0",
"bcryptjs": "^2.4.3",
"ioredis": "^4.27.1",
"jsonwebtoken": "^8.5.1",
"koa-passport": "^4.1.4",
"node-fetch": "^2.6.1",
"passport-google-auth": "^1.0.2",
"passport-google-oauth": "^2.0.0",
"passport-jwt": "^4.0.0",
"passport-local": "^1.0.0",
"uuid": "^8.3.2"
"sanitize-s3-objectkey": "^0.0.1",
"tar-fs": "^2.1.1",
"uuid": "^8.3.2",
"zlib": "^1.0.5"
},
"devDependencies": {
"ioredis-mock": "^5.5.5"

3
packages/auth/src/environment.js

@ -12,5 +12,8 @@ module.exports = {
SALT_ROUNDS: process.env.SALT_ROUNDS,
REDIS_URL: process.env.REDIS_URL,
REDIS_PASSWORD: process.env.REDIS_PASSWORD,
MINIO_ACCESS_KEY: process.env.MINIO_ACCESS_KEY,
MINIO_SECRET_KEY: process.env.MINIO_SECRET_KEY,
MINIO_URL: process.env.MINIO_URL,
isTest,
}

4
packages/auth/src/index.js

@ -32,6 +32,10 @@ module.exports = {
Client: require("./redis"),
utils: require("./redis/utils"),
},
objectStore: {
...require("./objectStore"),
...require("./objectStore/utils"),
},
utils: {
...require("./utils"),
...require("./hashing"),

240
packages/auth/src/objectStore/index.js

@ -0,0 +1,240 @@
const sanitize = require("sanitize-s3-objectkey")
const AWS = require("aws-sdk")
const stream = require("stream")
const fetch = require("node-fetch")
const tar = require("tar-fs")
const zlib = require("zlib")
const { promisify } = require("util")
const { join } = require("path")
const fs = require("fs")
const env = require("../environment")
const { budibaseTempDir, ObjectStoreBuckets } = require("./utils")
const { v4 } = require("uuid")
const streamPipeline = promisify(stream.pipeline)
// use this as a temporary store of buckets that are being created
const STATE = {
bucketCreationPromises: {},
}
const CONTENT_TYPE_MAP = {
html: "text/html",
css: "text/css",
js: "application/javascript",
}
const STRING_CONTENT_TYPES = [
CONTENT_TYPE_MAP.html,
CONTENT_TYPE_MAP.css,
CONTENT_TYPE_MAP.js,
]
function publicPolicy(bucketName) {
return {
Version: "2012-10-17",
Statement: [
{
Effect: "Allow",
Principal: {
AWS: ["*"],
},
Action: "s3:GetObject",
Resource: [`arn:aws:s3:::${bucketName}/*`],
},
],
}
}
const PUBLIC_BUCKETS = [ObjectStoreBuckets.APPS, ObjectStoreBuckets.GLOBAL]
/**
* Gets a connection to the object store using the S3 SDK.
* @param {string} bucket the name of the bucket which blobs will be uploaded/retrieved from.
* @return {Object} an S3 object store object, check S3 Nodejs SDK for usage.
* @constructor
*/
exports.ObjectStore = bucket => {
AWS.config.update({
accessKeyId: env.MINIO_ACCESS_KEY,
secretAccessKey: env.MINIO_SECRET_KEY,
})
const config = {
s3ForcePathStyle: true,
signatureVersion: "v4",
params: {
Bucket: bucket,
},
}
if (env.MINIO_URL) {
config.endpoint = env.MINIO_URL
}
return new AWS.S3(config)
}
/**
* Given an object store and a bucket name this will make sure the bucket exists,
* if it does not exist then it will create it.
*/
exports.makeSureBucketExists = async (client, bucketName) => {
try {
await client
.headBucket({
Bucket: bucketName,
})
.promise()
} catch (err) {
const promises = STATE.bucketCreationPromises
if (promises[bucketName]) {
await promises[bucketName]
} else if (err.statusCode === 404) {
// bucket doesn't exist create it
promises[bucketName] = client
.createBucket({
Bucket: bucketName,
})
.promise()
await promises[bucketName]
delete promises[bucketName]
// public buckets are quite hidden in the system, make sure
// no bucket is set accidentally
if (PUBLIC_BUCKETS.includes(bucketName)) {
await client
.putBucketPolicy({
Bucket: bucketName,
Policy: JSON.stringify(publicPolicy(bucketName)),
})
.promise()
}
} else {
throw err
}
}
}
/**
* Uploads the contents of a file given the required parameters, useful when
* temp files in use (for example file uploaded as an attachment).
*/
exports.upload = async ({ bucket, filename, path, type, metadata }) => {
const extension = [...filename.split(".")].pop()
const fileBytes = fs.readFileSync(path)
const objectStore = exports.ObjectStore(bucket)
await exports.makeSureBucketExists(objectStore, bucket)
const config = {
// windows file paths need to be converted to forward slashes for s3
Key: sanitize(filename).replace(/\\/g, "/"),
Body: fileBytes,
ContentType: type || CONTENT_TYPE_MAP[extension.toLowerCase()],
}
if (metadata) {
config.Metadata = metadata
}
return objectStore.upload(config).promise()
}
/**
* Similar to the upload function but can be used to send a file stream
* through to the object store.
*/
exports.streamUpload = async (bucket, filename, stream) => {
const objectStore = exports.ObjectStore(bucket)
await exports.makeSureBucketExists(objectStore, bucket)
const params = {
Bucket: bucket,
Key: sanitize(filename).replace(/\\/g, "/"),
Body: stream,
}
return objectStore.upload(params).promise()
}
/**
* retrieves the contents of a file from the object store, if it is a known content type it
* will be converted, otherwise it will be returned as a buffer stream.
*/
exports.retrieve = async (bucket, filepath) => {
const objectStore = exports.ObjectStore(bucket)
const params = {
Bucket: bucket,
Key: sanitize(filepath).replace(/\\/g, "/"),
}
const response = await objectStore.getObject(params).promise()
// currently these are all strings
if (STRING_CONTENT_TYPES.includes(response.ContentType)) {
return response.Body.toString("utf8")
} else {
return response.Body
}
}
/**
* Same as retrieval function but puts to a temporary file.
*/
exports.retrieveToTmp = async (bucket, filepath) => {
const data = await exports.retrieve(bucket, filepath)
const outputPath = join(budibaseTempDir(), v4())
fs.writeFileSync(outputPath, data)
return outputPath
}
exports.deleteFolder = async (bucket, folder) => {
const client = exports.ObjectStore(bucket)
const listParams = {
Bucket: bucket,
Prefix: folder,
}
let response = await client.listObjects(listParams).promise()
if (response.Contents.length === 0) {
return
}
const deleteParams = {
Bucket: bucket,
Delete: {
Objects: [],
},
}
response.Contents.forEach(content => {
deleteParams.Delete.Objects.push({ Key: content.Key })
})
response = await client.deleteObjects(deleteParams).promise()
// can only empty 1000 items at once
if (response.Deleted.length === 1000) {
return exports.deleteFolder(bucket, folder)
}
}
exports.uploadDirectory = async (bucket, localPath, bucketPath) => {
let uploads = []
const files = fs.readdirSync(localPath, { withFileTypes: true })
for (let file of files) {
const path = join(bucketPath, file.name)
const local = join(localPath, file.name)
if (file.isDirectory()) {
uploads.push(exports.uploadDirectory(bucket, local, path))
} else {
uploads.push(
exports.streamUpload(bucket, path, fs.createReadStream(local))
)
}
}
await Promise.all(uploads)
}
exports.downloadTarball = async (url, bucket, path) => {
const response = await fetch(url)
if (!response.ok) {
throw new Error(`unexpected response ${response.statusText}`)
}
const tmpPath = join(budibaseTempDir(), path)
await streamPipeline(response.body, zlib.Unzip(), tar.extract(tmpPath))
if (!env.isTest()) {
await exports.uploadDirectory(bucket, tmpPath, path)
}
// return the temporary path incase there is a use for it
return tmpPath
}

13
packages/auth/src/objectStore/utils.js

@ -0,0 +1,13 @@
const { join } = require("path")
const { tmpdir } = require("os")
exports.ObjectStoreBuckets = {
BACKUPS: "backups",
APPS: "prod-budi-app-assets",
TEMPLATES: "templates",
GLOBAL: "global",
}
exports.budibaseTempDir = function () {
return join(tmpdir(), ".budibase")
}

223
packages/auth/yarn.lock

@ -36,6 +36,21 @@ asynckit@^0.4.0:
resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
integrity sha1-x57Zf380y48robyXkLzDZkdLS3k=
aws-sdk@^2.901.0:
version "2.901.0"
resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.901.0.tgz#96b387778cf2b3537383fba04994e815f1fab4d4"
integrity sha512-prmUjg4mjguamnwaXMdm/g1xtnT9515cjSaV/MhBsMUVzhe66EX7dLwiA7Jo8qUlwFMyCVIcp/2T+6KwJ9sQgQ==
dependencies:
buffer "4.9.2"
events "1.1.1"
ieee754 "1.1.13"
jmespath "0.15.0"
querystring "0.2.0"
sax "1.2.1"
url "0.10.3"
uuid "3.3.2"
xml2js "0.4.19"
aws-sign2@~0.7.0:
version "0.7.0"
resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8"
@ -51,6 +66,11 @@ balanced-match@^1.0.0:
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
base64-js@^1.0.2, base64-js@^1.3.1:
version "1.5.1"
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
base64url@3.x.x:
version "3.0.1"
resolved "https://registry.yarnpkg.com/base64url/-/base64url-3.0.1.tgz#6399d572e2bc3f90a9a8b22d5dbb0a32d33f788d"
@ -68,6 +88,15 @@ bcryptjs@^2.4.3:
resolved "https://registry.yarnpkg.com/bcryptjs/-/bcryptjs-2.4.3.tgz#9ab5627b93e60621ff7cdac5da9733027df1d0cb"
integrity sha1-mrVie5PmBiH/fNrF2pczAn3x0Ms=
bl@^4.0.3:
version "4.1.0"
resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a"
integrity sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==
dependencies:
buffer "^5.5.0"
inherits "^2.0.4"
readable-stream "^3.4.0"
brace-expansion@^1.1.7:
version "1.1.11"
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
@ -81,11 +110,33 @@ buffer-equal-constant-time@1.0.1:
resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819"
integrity sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=
buffer@4.9.2:
version "4.9.2"
resolved "https://registry.yarnpkg.com/buffer/-/buffer-4.9.2.tgz#230ead344002988644841ab0244af8c44bbe3ef8"
integrity sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==
dependencies:
base64-js "^1.0.2"
ieee754 "^1.1.4"
isarray "^1.0.0"
buffer@^5.5.0:
version "5.7.1"
resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0"
integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==
dependencies:
base64-js "^1.3.1"
ieee754 "^1.1.13"
caseless@~0.12.0:
version "0.12.0"
resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc"
integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=
chownr@^1.1.1:
version "1.1.4"
resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b"
integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==
cluster-key-slot@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/cluster-key-slot/-/cluster-key-slot-1.1.0.tgz#30474b2a981fb12172695833052bc0d01336d10d"
@ -147,6 +198,18 @@ ecdsa-sig-formatter@1.0.11:
dependencies:
safe-buffer "^5.0.1"
end-of-stream@^1.1.0, end-of-stream@^1.4.1:
version "1.4.4"
resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0"
integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==
dependencies:
once "^1.4.0"
events@1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924"
integrity sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=
extend@~3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa"
@ -200,6 +263,11 @@ form-data@~2.3.2:
combined-stream "^1.0.6"
mime-types "^2.1.12"
fs-constants@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad"
integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==
getpass@^0.1.1:
version "0.1.7"
resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa"
@ -265,6 +333,21 @@ http-signature@~1.2.0:
jsprim "^1.2.2"
sshpk "^1.7.0"
ieee754@1.1.13:
version "1.1.13"
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.13.tgz#ec168558e95aa181fd87d37f55c32bbcb6708b84"
integrity sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==
ieee754@^1.1.13, ieee754@^1.1.4:
version "1.2.1"
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==
inherits@^2.0.3, inherits@^2.0.4:
version "2.0.4"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
ioredis-mock@^5.5.5:
version "5.5.5"
resolved "https://registry.yarnpkg.com/ioredis-mock/-/ioredis-mock-5.5.5.tgz#dec9fedd238c6ab9f56c026fc366533144f8a256"
@ -297,11 +380,21 @@ is-typedarray@~1.0.0:
resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=
isarray@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=
isstream@~0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a"
integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=
jmespath@0.15.0:
version "0.15.0"
resolved "https://registry.yarnpkg.com/jmespath/-/jmespath-0.15.0.tgz#a3f222a9aae9f966f5d27c796510e28091764217"
integrity sha1-o/Iiqarp+Wb10nx5ZRDigJF2Qhc=
jsbn@~0.1.0:
version "0.1.1"
resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513"
@ -451,6 +544,11 @@ minimatch@^3.0.4:
dependencies:
brace-expansion "^1.1.7"
mkdirp-classic@^0.5.2:
version "0.5.3"
resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113"
integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==
ms@2.1.2:
version "2.1.2"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
@ -461,6 +559,11 @@ ms@^2.1.1:
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
node-fetch@^2.6.1:
version "2.6.1"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052"
integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==
node-forge@^0.7.1:
version "0.7.6"
resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.7.6.tgz#fdf3b418aee1f94f0ef642cd63486c77ca9724ac"
@ -476,6 +579,13 @@ oauth@0.9.x:
resolved "https://registry.yarnpkg.com/oauth/-/oauth-0.9.15.tgz#bd1fefaf686c96b75475aed5196412ff60cfb9c1"
integrity sha1-vR/vr2hslrdUda7VGWQS/2DPucE=
once@^1.3.1, once@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E=
dependencies:
wrappy "1"
os-tmpdir@~1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274"
@ -579,6 +689,19 @@ psl@^1.1.28:
resolved "https://registry.yarnpkg.com/psl/-/psl-1.8.0.tgz#9326f8bcfb013adcc005fdff056acce020e51c24"
integrity sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==
pump@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64"
integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==
dependencies:
end-of-stream "^1.1.0"
once "^1.3.1"
punycode@1.3.2:
version "1.3.2"
resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d"
integrity sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=
punycode@^2.1.0, punycode@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
@ -589,6 +712,20 @@ qs@~6.5.2:
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"
integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==
querystring@0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620"
integrity sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=
readable-stream@^3.1.1, readable-stream@^3.4.0:
version "3.6.0"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198"
integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==
dependencies:
inherits "^2.0.3"
string_decoder "^1.1.1"
util-deprecate "^1.0.1"
readline-sync@^1.4.9:
version "1.4.10"
resolved "https://registry.yarnpkg.com/readline-sync/-/readline-sync-1.4.10.tgz#41df7fbb4b6312d673011594145705bf56d8873b"
@ -637,7 +774,7 @@ request@^2.72.0, request@^2.74.0:
tunnel-agent "^0.6.0"
uuid "^3.3.2"
safe-buffer@^5.0.1, safe-buffer@^5.1.2:
safe-buffer@^5.0.1, safe-buffer@^5.1.2, safe-buffer@~5.2.0:
version "5.2.1"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
@ -647,6 +784,21 @@ safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0:
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
sanitize-s3-objectkey@^0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/sanitize-s3-objectkey/-/sanitize-s3-objectkey-0.0.1.tgz#efa9887cd45275b40234fb4bb12fc5754fe64e7e"
integrity sha512-ZTk7aqLxy4sD40GWcYWoLfbe05XLmkKvh6vGKe13ADlei24xlezcvjgKy1qRArlaIbIMYaqK7PCalvZtulZlaQ==
sax@1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.1.tgz#7b8e656190b228e81a66aea748480d828cd2d37a"
integrity sha1-e45lYZCyKOgaZq6nSEgNgozS03o=
sax@>=0.6.0:
version "1.2.4"
resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==
semver@^5.6.0:
version "5.7.1"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
@ -682,6 +834,34 @@ string-template@~1.0.0:
resolved "https://registry.yarnpkg.com/string-template/-/string-template-1.0.0.tgz#9e9f2233dc00f218718ec379a28a5673ecca8b96"
integrity sha1-np8iM9wA8hhxjsN5oopWc+zKi5Y=
string_decoder@^1.1.1:
version "1.3.0"
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e"
integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==
dependencies:
safe-buffer "~5.2.0"
tar-fs@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.1.tgz#489a15ab85f1f0befabb370b7de4f9eb5cbe8784"
integrity sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==
dependencies:
chownr "^1.1.1"
mkdirp-classic "^0.5.2"
pump "^3.0.0"
tar-stream "^2.1.4"
tar-stream@^2.1.4:
version "2.2.0"
resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.2.0.tgz#acad84c284136b060dc3faa64474aa9aebd77287"
integrity sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==
dependencies:
bl "^4.0.3"
end-of-stream "^1.4.1"
fs-constants "^1.0.0"
inherits "^2.0.3"
readable-stream "^3.1.1"
tmp@^0.0.33:
version "0.0.33"
resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9"
@ -721,11 +901,29 @@ uri-js@^4.2.2:
dependencies:
punycode "^2.1.0"
url@0.10.3:
version "0.10.3"
resolved "https://registry.yarnpkg.com/url/-/url-0.10.3.tgz#021e4d9c7705f21bbf37d03ceb58767402774c64"
integrity sha1-Ah5NnHcF8hu/N9A861h2dAJ3TGQ=
dependencies:
punycode "1.3.2"
querystring "0.2.0"
util-deprecate@^1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=
utils-merge@1.x.x:
version "1.0.1"
resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=
uuid@3.3.2:
version "3.3.2"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131"
integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==
uuid@^3.3.2:
version "3.4.0"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee"
@ -744,3 +942,26 @@ verror@1.10.0:
assert-plus "^1.0.0"
core-util-is "1.0.2"
extsprintf "^1.2.0"
wrappy@1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=
xml2js@0.4.19:
version "0.4.19"
resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.19.tgz#686c20f213209e94abf0d1bcf1efaa291c7827a7"
integrity sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==
dependencies:
sax ">=0.6.0"
xmlbuilder "~9.0.1"
xmlbuilder@~9.0.1:
version "9.0.7"
resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-9.0.7.tgz#132ee63d2ec5565c557e20f4c22df9aca686b10d"
integrity sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=
zlib@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/zlib/-/zlib-1.0.5.tgz#6e7c972fc371c645a6afb03ab14769def114fcc0"
integrity sha1-bnyXL8NxxkWmr7A6sUdp3vEU/MA=

2
packages/bbui/package.json

@ -27,7 +27,7 @@
"rollup-plugin-postcss": "^4.0.0",
"rollup-plugin-svelte": "^7.1.0",
"rollup-plugin-terser": "^7.0.2",
"svelte": "^3.37.0"
"svelte": "^3.38.2"
},
"keywords": [
"svelte"

3
packages/bbui/src/ActionMenu/ActionMenu.svelte

@ -4,6 +4,7 @@
import Menu from "../Menu/Menu.svelte"
export let disabled = false
export let align = "left"
let anchor
let dropdown
@ -31,7 +32,7 @@
<div use:getAnchor on:click={openMenu}>
<slot name="control" />
</div>
<Popover bind:this={dropdown} {anchor} align="left">
<Popover bind:this={dropdown} {anchor} {align}>
<Menu>
<slot />
</Menu>

97
packages/bbui/src/Form/Core/Dropzone.svelte

@ -15,6 +15,8 @@
export let fileSizeLimit = BYTES_IN_MB * 20
export let processFiles = null
export let handleFileTooLarge = null
export let gallery = true
export let error = null
const dispatch = createEventDispatcher()
const imageExtensions = [
@ -52,6 +54,8 @@
const newValue = [...value, ...processedFiles]
dispatch("change", newValue)
selectedImageIdx = newValue.length - 1
} else {
dispatch("change", fileList)
}
}
@ -94,47 +98,68 @@
<div class="container">
{#if selectedImage}
<div class="gallery">
<div class="title">
<div class="filename">{selectedImage.name}</div>
<div class="filesize">
{#if selectedImage.size <= BYTES_IN_MB}
{`${selectedImage.size / BYTES_IN_KB} KB`}
{:else}{`${selectedImage.size / BYTES_IN_MB} MB`}{/if}
{#if gallery}
<div class="gallery">
<div class="title">
<div class="filename">{selectedImage.name}</div>
<div class="filesize">
{#if selectedImage.size <= BYTES_IN_MB}
{`${selectedImage.size / BYTES_IN_KB} KB`}
{:else}{`${selectedImage.size / BYTES_IN_MB} MB`}{/if}
</div>
{#if !disabled}
<div class="delete-button" on:click={removeFile}>
<Icon name="Close" />
</div>
{/if}
</div>
{#if !disabled}
<div class="delete-button" on:click={removeFile}>
<Icon name="Close" />
{#if isImage}
<img alt="preview" src={selectedImage.url} />
{:else}
<div class="placeholder">
<div class="extension">{selectedImage.extension}</div>
<div>Preview not supported</div>
</div>
{/if}
</div>
{#if isImage}
<img alt="preview" src={selectedImage.url} />
{:else}
<div class="placeholder">
<div class="extension">{selectedImage.extension}</div>
<div>Preview not supported</div>
<div
class="nav left"
class:visible={selectedImageIdx > 0}
on:click={navigateLeft}
>
<Icon name="ChevronLeft" />
</div>
{/if}
<div
class="nav left"
class:visible={selectedImageIdx > 0}
on:click={navigateLeft}
>
<Icon name="ChevronLeft" />
</div>
<div
class="nav right"
class:visible={selectedImageIdx < fileCount - 1}
on:click={navigateRight}
>
<Icon name="ChevronRight" />
<div
class="nav right"
class:visible={selectedImageIdx < fileCount - 1}
on:click={navigateRight}
>
<Icon name="ChevronRight" />
</div>
<div class="footer">File {selectedImageIdx + 1} of {fileCount}</div>
</div>
<div class="footer">File {selectedImageIdx + 1} of {fileCount}</div>
</div>
{:else if value?.length}
{#each value as file}
<div class="gallery">
<div class="title">
<div class="filename">{file.name}</div>
<div class="filesize">
{#if file.size <= BYTES_IN_MB}
{`${file.size / BYTES_IN_KB} KB`}
{:else}{`${file.size / BYTES_IN_MB} MB`}{/if}
</div>
{#if !disabled}
<div class="delete-button" on:click={removeFile}>
<Icon name="Close" />
</div>
{/if}
</div>
</div>
{/each}
{/if}
{/if}
<div
class="spectrum-Dropzone"
class:is-invalid={!!error}
class:disabled
role="region"
tabindex="0"
@ -245,6 +270,9 @@
.spectrum-Dropzone {
user-select: none;
}
.spectrum-Dropzone.is-invalid {
border-color: var(--spectrum-global-color-red-400);
}
input[type="file"] {
display: none;
}
@ -276,7 +304,7 @@
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: stretch;
align-items: center;
}
.filename {
flex: 1 1 auto;
@ -331,6 +359,7 @@
.delete-button {
transition: all 0.3s;
margin-left: 10px;
display: flex;
}
.delete-button i {
font-size: 2em;

2
packages/bbui/src/Form/Core/Picker.svelte

@ -19,6 +19,7 @@
export let getOptionValue = option => option
export let open = false
export let readonly = false
export let quiet = false
const dispatch = createEventDispatcher()
const onClick = e => {
@ -33,6 +34,7 @@
<button
{id}
class="spectrum-Picker spectrum-Picker--sizeM"
class:spectrum-Picker--quiet={quiet}
{disabled}
class:is-invalid={!!error}
class:is-open={open}

2
packages/bbui/src/Form/Core/Select.svelte

@ -11,6 +11,7 @@
export let getOptionLabel = option => option
export let getOptionValue = option => option
export let readonly = false
export let quiet = false
const dispatch = createEventDispatcher()
let open = false
@ -43,6 +44,7 @@
<Picker
on:click
bind:open
{quiet}
{id}
{error}
{disabled}

1
packages/bbui/src/Form/Core/TextField.svelte

@ -37,6 +37,7 @@
}
focus = false
updateValue(event.target.value)
dispatch("blur")
}
const updateValueOnEnter = event => {

2
packages/bbui/src/Form/Dropzone.svelte

@ -11,6 +11,7 @@
export let fileSizeLimit = undefined
export let processFiles = undefined
export let handleFileTooLarge = undefined
export let gallery = true
const dispatch = createEventDispatcher()
const onChange = e => {
@ -27,6 +28,7 @@
{fileSizeLimit}
{processFiles}
{handleFileTooLarge}
{gallery}
on:change={onChange}
/>
</Field>

1
packages/bbui/src/Form/Input.svelte

@ -30,5 +30,6 @@
on:change={onChange}
on:click
on:input
on:blur
/>
</Field>

2
packages/bbui/src/Form/Select.svelte

@ -13,6 +13,7 @@
export let options = []
export let getOptionLabel = option => extractProperty(option, "label")
export let getOptionValue = option => extractProperty(option, "value")
export let quiet = false
const dispatch = createEventDispatcher()
const onChange = e => {
@ -29,6 +30,7 @@
<Field {label} {labelPosition} {disabled} {error}>
<Select
{quiet}
{error}
{disabled}
{readonly}

60
packages/bbui/src/Label/Label.svelte

@ -1,62 +1,16 @@
<script>
export let forAttr = "",
extraSmall = false,
small = false,
medium = false,
large = false,
extraLarge = false,
white = false,
grey = false,
black = false
import "@spectrum-css/fieldlabel/dist/index-vars.css"
export let size = "M"
</script>
<label
class="bb-label"
class:extraSmall
class:small
class:medium
class:large
class:extraLarge
class:white
class:grey
class:black
for={forAttr}
>
<label class={`spectrum-FieldLabel spectrum-FieldLabel--size${size}`}>
<slot />
</label>
<style>
.bb-label {
font-family: var(--font-sans);
font-weight: 500;
text-rendering: var(--text-render);
color: var(--ink);
font-size: var(--font-size-s);
margin-bottom: var(--spacing-s);
display: block;
}
.extraSmall {
font-size: var(--font-size-xs);
}
.small {
font-size: var(--font-size-s);
}
.medium {
font-size: var(--font-size-m);
}
.large {
font-size: var(--font-size-l);
}
.extraLarge {
font-size: var(--font-size-xl);
}
.white {
color: white;
}
.grey {
color: var(--grey-6);
}
.black {
color: var(--ink);
label {
padding: 0;
white-space: nowrap;
}
</style>

5
packages/bbui/src/Layout/Layout.svelte

@ -5,9 +5,11 @@
export let noPadding = false
export let gap = "M"
export let noGap = false
export let alignContent = "normal"
</script>
<div
style="align-content:{alignContent};"
class:horizontal
class="container paddingX-{!noPadding && paddingX} paddingY-{!noPadding &&
paddingY} gap-{!noGap && gap}"
@ -44,6 +46,9 @@
padding-top: var(--spacing-l);
padding-bottom: var(--spacing-l);
}
.gap-XS {
grid-gap: var(--spacing-s);
}
.gap-S {
grid-gap: var(--spectrum-alias-grid-gutter-xsmall);
}

9
packages/bbui/src/Layout/Page.svelte

@ -8,8 +8,10 @@
<style>
div {
display: grid;
grid-template-columns: 1fr;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: stretch;
max-width: 80ch;
margin: 0 auto;
padding: calc(var(--spacing-xl) * 2);
@ -18,6 +20,7 @@
.wide {
max-width: none;
margin: 0;
padding: var(--spacing-xl) calc(var(--spacing-xl) * 2);
padding: var(--spacing-xl) calc(var(--spacing-xl) * 2)
calc(var(--spacing-xl) * 2) calc(var(--spacing-xl) * 2);
}
</style>

8
packages/bbui/src/Menu/Item.svelte

@ -27,7 +27,7 @@
>
{#if icon}
<svg
class="spectrum-Icon spectrum-Icon--sizeM spectrum-Menu-itemIcon"
class="spectrum-Icon spectrum-Icon--sizeS spectrum-Menu-itemIcon"
focusable="false"
aria-hidden="true"
aria-label={icon}
@ -37,3 +37,9 @@
{/if}
<span class="spectrum-Menu-itemLabel"><slot /></span>
</li>
<style>
.spectrum-Menu-itemIcon {
align-self: center;
}
</style>

21
packages/bbui/src/Modal/Modal.svelte

@ -7,9 +7,10 @@
import Context from "../context"
export let fixed = false
export let inline = false
const dispatch = createEventDispatcher()
let visible = !!fixed
let visible = fixed || inline
$: dispatch(visible ? "show" : "hide")
export function show() {
@ -20,7 +21,7 @@
}
export function hide() {
if (!visible || fixed) {
if (!visible || fixed || inline) {
return
}
visible = false
@ -45,11 +46,17 @@
<svelte:window on:keydown={handleKey} />
{#if visible}
<!-- These svelte if statements need to be defined like this. -->
<!-- The modal transitions do not work if nested inside more than one "if" -->
{#if visible && inline}
<div use:focusFirstInput class="spectrum-Modal inline is-open">
<slot />
</div>
{:else if visible}
<Portal target=".modal-container">
<div
class="spectrum-Underlay is-open"
transition:fade={{ duration: 200 }}
transition:fade|local={{ duration: 200 }}
on:mousedown|self={hide}
>
<div class="modal-wrapper" on:mousedown|self={hide}>
@ -57,7 +64,7 @@
<div
use:focusFirstInput
class="spectrum-Modal is-open"
transition:fly={{ y: 30, duration: 200 }}
transition:fly|local={{ y: 30, duration: 200 }}
>
<slot />
</div>
@ -98,6 +105,7 @@
}
.spectrum-Modal {
background: var(--background);
overflow: visible;
max-height: none;
margin: 40px 0;
@ -106,4 +114,7 @@
--spectrum-global-dimension-size-100
);
}
:global(.spectrum--lightest .spectrum-Modal.inline) {
border: var(--border-light);
}
</style>

2
packages/bbui/src/Modal/ModalContent.svelte

@ -9,6 +9,7 @@
export let title = undefined
export let size = "S"
export let cancelText = "Cancel"
export let warning = false
export let confirmText = "Confirm"
export let showCancelButton = true
export let showConfirmButton = true
@ -60,6 +61,7 @@
{/if}
{#if showConfirmButton}
<Button
{warning}
group
cta
{...$$restProps}

2
packages/bbui/src/Table/CellRenderer.svelte

@ -23,7 +23,7 @@
}
$: type = schema?.type ?? "string"
$: customRenderer = customRenderers?.find(x => x.column === schema?.name)
$: renderer = customRenderer?.component ?? typeMap[type]
$: renderer = customRenderer?.component ?? typeMap[type] ?? StringRenderer
</script>
{#if renderer && (customRenderer || (value != null && value !== ""))}

77
packages/bbui/src/Table/Table.svelte

@ -214,7 +214,7 @@
>
<div style={contentStyle}>
<table class="spectrum-Table" class:spectrum-Table--quiet={quiet}>
{#if sortedRows?.length}
{#if fields.length}
<thead class="spectrum-Table-head">
<tr>
{#if showEditColumn}
@ -269,9 +269,10 @@
</thead>
{/if}
<tbody class="spectrum-Table-body">
{#if sortedRows?.length}
{#if sortedRows?.length && fields.length}
{#each sortedRows as row, idx}
<tr
on:click={() => dispatch("click", row)}
on:click={() => toggleSelectRow(row)}
class="spectrum-Table-row"
class:hidden={idx < firstVisibleRow || idx > lastVisibleRow}
@ -316,15 +317,25 @@
</tr>
{/each}
{:else}
<div class="placeholder">
<svg
class="spectrum-Icon spectrum-Icon--sizeXXL"
focusable="false"
>
<use xlink:href="#spectrum-icon-18-Table" />
</svg>
<div>No rows found</div>
</div>
<tr class="placeholder-row">
{#if showEditColumn}
<td class="placeholder-offset" />
{/if}
{#each fields as field}
<td />
{/each}
<div class="placeholder" class:has-fields={fields.length > 0}>
<div class="placeholder-content">
<svg
class="spectrum-Icon spectrum-Icon--sizeXXL"
focusable="false"
>
<use xlink:href="#spectrum-icon-18-Table" />
</svg>
<div>No rows found</div>
</div>
</div>
</tr>
{/if}
</tbody>
</table>
@ -347,7 +358,7 @@
overflow: auto;
}
.container.quiet {
border: none !important;
border: none;
}
table {
width: 100%;
@ -381,7 +392,7 @@
z-index: 2;
background-color: var(--spectrum-alias-background-color-secondary);
border-bottom: 1px solid
var(--spectrum-table-border-color, var(--spectrum-alias-border-color-mid)) !important;
var(--spectrum-table-border-color, var(--spectrum-alias-border-color-mid));
}
.spectrum-Table-headCell-content {
white-space: nowrap;
@ -396,7 +407,34 @@
text-overflow: ellipsis;
}
.placeholder-row {
position: relative;
height: 150px;
}
.placeholder-row td {
border-top: none !important;
border-bottom: none !important;
}
.placeholder-offset {
width: 1px;
}
.placeholder {
top: 0;
height: 100%;
left: 0;
width: 100%;
position: absolute;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
}
.placeholder.has-fields {
top: var(--header-height);
height: calc(100% - var(--header-height));
}
.placeholder-content {
padding: 20px;
display: flex;
flex-direction: column;
@ -407,12 +445,13 @@
var(--spectrum-alias-text-color)
);
}
.placeholder div {
.placeholder-content div {
margin-top: 10px;
font-size: var(
--spectrum-table-cell-text-size,
var(--spectrum-alias-font-size-default)
);
text-align: center;
}
tbody {
@ -431,17 +470,17 @@
td {
padding-top: 0;
padding-bottom: 0;
border-bottom: none !important;
border-bottom: none;
border-top: 1px solid
var(--spectrum-table-border-color, var(--spectrum-alias-border-color-mid)) !important;
border-radius: 0 !important;
var(--spectrum-table-border-color, var(--spectrum-alias-border-color-mid));
border-radius: 0;
}
tr:first-child td {
border-top: none !important;
border-top: none;
}
tr:last-child td {
border-bottom: 1px solid
var(--spectrum-table-border-color, var(--spectrum-alias-border-color-mid)) !important;
var(--spectrum-table-border-color, var(--spectrum-alias-border-color-mid));
}
td.spectrum-Table-cell--divider {
width: 1px;

11
packages/bbui/src/Typography/Body.svelte

@ -3,11 +3,20 @@
export let size = "M"
export let serif = false
export let noPadding = false
</script>
<p
class="spectrum-Body class:spectrum-Body--size{size}"
class:noPadding
class="spectrum-Body spectrum-Body--size{size}"
class:spectrum-Body--serif={serif}
>
<slot />
</p>
<style>
.noPadding {
padding: 0;
margin: 0;
}
</style>

8
packages/bbui/yarn.lock

@ -2407,10 +2407,10 @@ svelte-portal@^1.0.0:
resolved "https://registry.yarnpkg.com/svelte-portal/-/svelte-portal-1.0.0.tgz#36a47c5578b1a4d9b4dc60fa32a904640ec4cdd3"
integrity sha512-nHf+DS/jZ6jjnZSleBMSaZua9JlG5rZv9lOGKgJuaZStfevtjIlUJrkLc3vbV8QdBvPPVmvcjTlazAzfKu0v3Q==
svelte@^3.37.0:
version "3.37.0"
resolved "https://registry.yarnpkg.com/svelte/-/svelte-3.37.0.tgz#dc7cd24bcc275cdb3f8c684ada89e50489144ccd"
integrity sha512-TRF30F4W4+d+Jr2KzUUL1j8Mrpns/WM/WacxYlo5MMb2E5Qy2Pk1Guj6GylxsW9OnKQl1tnF8q3hG/hQ3h6VUA==
svelte@^3.38.2:
version "3.38.2"
resolved "https://registry.yarnpkg.com/svelte/-/svelte-3.38.2.tgz#55e5c681f793ae349b5cc2fe58e5782af4275ef5"
integrity sha512-q5Dq0/QHh4BLJyEVWGe7Cej5NWs040LWjMbicBGZ+3qpFWJ1YObRmUDZKbbovddLC9WW7THTj3kYbTOFmU9fbg==
svgo@^1.0.0:
version "1.3.2"

6
packages/builder/package.json

@ -74,7 +74,7 @@
"@spectrum-css/vars": "^3.0.1",
"codemirror": "^5.59.0",
"downloadjs": "1.4.7",
"lodash": "4.17.13",
"lodash": "4.17.21",
"posthog-js": "1.4.5",
"remixicon": "2.5.0",
"shortid": "2.2.15",
@ -90,7 +90,7 @@
"@babel/preset-env": "^7.13.12",
"@babel/runtime": "^7.13.10",
"@rollup/plugin-replace": "^2.4.2",
"@roxi/routify": "2.15.1",
"@roxi/routify": "2.18.0",
"@sveltejs/vite-plugin-svelte": "^1.0.0-next.5",
"@testing-library/jest-dom": "^5.11.10",
"@testing-library/svelte": "^3.0.0",
@ -106,7 +106,7 @@
"rollup": "^2.44.0",
"rollup-plugin-copy": "^3.4.0",
"start-server-and-test": "^1.12.1",
"svelte": "^3.37.0",
"svelte": "^3.38.2",
"svelte-jester": "^1.3.2",
"vite": "^2.1.5"
},

90
packages/builder/src/actions.js

@ -0,0 +1,90 @@
export const gradient = (node, config = {}) => {
const defaultConfig = {
points: 12,
saturation: 0.85,
lightness: 0.7,
softness: 0.9,
seed: null,
}
// Applies a gradient background
const createGradient = config => {
config = {
...defaultConfig,
...config,
}
const { saturation, lightness, softness, points } = config
const seed = config.seed || Math.random().toString(32).substring(2)
// Hash function which returns a fixed hash between specified limits
// for a given seed and a given version
const rangeHash = (seed, min = 0, max = 100, version = 0) => {
const range = max - min
let hash = range + version
for (let i = 0; i < seed.length * 2 + version; i++) {
hash = (hash << 5) - hash + seed.charCodeAt(i % seed.length)
hash = ((hash & hash) % range) + version
}
return min + (hash % range)
}
// Generates a random HSL colour using the options specified
const randomHSL = (seed, version, alpha = 1) => {
const lowerSaturation = Math.min(100, saturation * 100)
const upperSaturation = Math.min(100, (saturation + 0.2) * 100)
const lowerLightness = Math.min(100, lightness * 100)
const upperLightness = Math.min(100, (lightness + 0.2) * 100)
const hue = rangeHash(seed, 0, 360, version)
const sat = `${rangeHash(
seed,
lowerSaturation,
upperSaturation,
version
)}%`
const light = `${rangeHash(
seed,
lowerLightness,
upperLightness,
version
)}%`
return `hsla(${hue},${sat},${light},${alpha})`
}
// Generates a radial gradient stop point
const randomGradientPoint = (seed, version) => {
const lowerTransparency = Math.min(100, softness * 100)
const upperTransparency = Math.min(100, (softness + 0.2) * 100)
const transparency = rangeHash(
seed,
lowerTransparency,
upperTransparency,
version
)
return (
`radial-gradient(at ` +
`${rangeHash(seed, 0, 100, version)}% ` +
`${rangeHash(seed, 0, 100, version + 1)}%,` +
`${randomHSL(seed, version, saturation)} 0,` +
`transparent ${transparency}%)`
)
}
let css = `opacity:0.9;background:${randomHSL(seed, 0, 0.7)};`
css += "background-image:"
for (let i = 0; i < points - 1; i++) {
css += `${randomGradientPoint(seed, i)},`
}
css += `${randomGradientPoint(seed, points)};`
node.style = css
}
// Apply the initial gradient
createGradient(config)
return {
// Apply a new gradient
update: config => {
createGradient(config)
},
}
}

2
packages/builder/src/components/backend/DataTable/RowFieldControl.svelte

@ -1,7 +1,7 @@
<script>
import { Input, Select, DatePicker, Toggle, TextArea } from "@budibase/bbui"
import Dropzone from "components/common/Dropzone.svelte"
import { capitalise } from "../../../helpers"
import { capitalise } from "helpers"
import LinkedRowSelector from "components/common/LinkedRowSelector.svelte"
export let defaultValue

2
packages/builder/src/components/backend/DataTable/Table.svelte

@ -59,7 +59,7 @@
const selectRelationship = ({ tableId, rowId, fieldName }) => {
$goto(
`/builder/${$params.application}/data/table/${tableId}/relationship/${rowId}/${fieldName}`
`/builder/app/${$params.application}/data/table/${tableId}/relationship/${rowId}/${fieldName}`
)
}

2
packages/builder/src/components/backend/DataTable/modals/ManageAccessModal.svelte

@ -8,7 +8,7 @@
Body,
ModalContent,
} from "@budibase/bbui"
import { capitalise } from "../../../../helpers"
import { capitalise } from "helpers"
export let resourceId
export let permissions

2
packages/builder/src/components/backend/DatasourceNavigator/TableIntegrationMenu/IntegrationConfigForm.svelte

@ -1,7 +1,7 @@
<script>
import { Label, Input, Layout } from "@budibase/bbui"
import KeyValueBuilder from "components/integration/KeyValueBuilder.svelte"
import { capitalise } from "../../../../helpers"
import { capitalise } from "helpers"
export let integration
export let schema

12
packages/builder/src/components/common/ConfigChecklist.svelte

@ -1,22 +1,12 @@
<script>
import { isActive, url, goto } from "@roxi/routify"
import { onMount } from "svelte"
import {
ActionMenu,
Checkbox,
Body,
MenuItem,
Icon,
Heading,
Avatar,
Search,
Layout,
ProgressCircle,
SideNavigation as Navigation,
SideNavigationItem as Item,
} from "@budibase/bbui"
import api from "builderStore/api"
import { organisation, admin } from "stores/portal"
import { admin } from "stores/portal"
const MESSAGES = {
apps: "Create your first app",

9
packages/builder/src/components/common/ConfirmDialog.svelte

@ -7,6 +7,7 @@
export let cancelText = "Cancel"
export let onOk = undefined
export let onCancel = undefined
export let warning = true
let modal
@ -19,7 +20,13 @@
</script>
<Modal bind:this={modal} on:hide={onCancel}>
<ModalContent onConfirm={onOk} {title} confirmText={okText} {cancelText} red>
<ModalContent
onConfirm={onOk}
{title}
confirmText={okText}
{cancelText}
{warning}
>
<Body size="S">
{body}
<slot />

2
packages/builder/src/components/common/bindings/BindingPanel.svelte

@ -14,7 +14,7 @@
getBindableProperties,
readableToRuntimeBinding,
} from "builderStore/dataBinding"
import { currentAsset, store } from "../../../builderStore"
import { currentAsset, store } from "builderStore"
import { handlebarsCompletions } from "constants/completions"
import { addToText } from "./utils"

2
packages/builder/src/components/common/bindings/ServerBindingPanel.svelte

@ -10,7 +10,7 @@
import { createEventDispatcher } from "svelte"
import { isValid } from "@budibase/string-templates"
import { handlebarsCompletions } from "constants/completions"
import { readableToRuntimeBinding } from "../../../builderStore/dataBinding"
import { readableToRuntimeBinding } from "builderStore/dataBinding"
import { addToText } from "./utils"
const dispatch = createEventDispatcher()

5
packages/builder/src/components/design/AppPreview/CurrentItemPreview.svelte

@ -3,7 +3,7 @@
import { store, currentAsset, selectedComponent } from "builderStore"
import iframeTemplate from "./iframeTemplate"
import { Screen } from "builderStore/store/screenTemplates/utils/Screen"
import { FrontendTypes } from "../../../constants"
import { FrontendTypes } from "constants"
let iframe
let layout
@ -82,7 +82,8 @@
style="height: 100%; width: 100%"
title="componentPreview"
bind:this={iframe}
srcdoc={template} />
srcdoc={template}
/>
</div>
<style>

2
packages/builder/src/components/design/PropertiesPanel/PropertyControls/PropertyControl.svelte

@ -7,7 +7,7 @@
runtimeToReadableBinding,
} from "builderStore/dataBinding"
import BindingPanel from "components/common/bindings/BindingPanel.svelte"
import { capitalise } from "../../../../helpers"
import { capitalise } from "helpers"
export let label = ""
export let bindable = true

18
packages/builder/src/components/login/LoginForm.svelte

@ -1,7 +1,7 @@
<script>
import { goto } from "@roxi/routify"
import {
notifications,
Button,
Link,
Input,
Modal,
@ -18,27 +18,18 @@
username,
password,
})
notifications.success("Logged in successfully.")
notifications.success("Logged in successfully")
$goto("../portal")
} catch (err) {
console.error(err)
notifications.error("Invalid credentials")
}
}
async function createTestUser() {
try {
await auth.firstUser()
notifications.success("Test user created")
} catch (err) {
console.error(err)
notifications.error("Could not create test user")
}
}
</script>
<Modal fixed>
<ModalContent
size="L"
size="M"
title="Log In"
onConfirm={login}
confirmText="Log In"
@ -51,7 +42,6 @@
<Link target="_blank" href="/api/admin/auth/google">
Sign In With Google
</Link>
<Button secondary on:click={createTestUser}>Create Test User</Button>
</div>
</ModalContent>
</Modal>

2
packages/builder/src/components/settings/tabs/DangerZone.svelte

@ -11,7 +11,7 @@
const id = $params.application
await del(`/api/applications/${id}`)
loading = false
$goto("/builder/")
$goto("/builder")
}
</script>

122
packages/builder/src/components/start/AppCard.svelte

@ -1,72 +1,84 @@
<script>
import { goto } from "@roxi/routify"
import { ActionButton, Heading } from "@budibase/bbui"
import { notifications } from "@budibase/bbui"
import Spinner from "components/common/Spinner.svelte"
import download from "downloadjs"
import {
Heading,
Icon,
Body,
Layout,
ActionMenu,
MenuItem,
Link,
} from "@budibase/bbui"
import { gradient } from "actions"
import { url } from "@roxi/routify"
export let name, _id
let appExportLoading = false
async function exportApp() {
appExportLoading = true
try {
download(
`/api/backups/export?appId=${_id}&appname=${encodeURIComponent(name)}`
)
notifications.success("App Export Complete.")
} catch (err) {
console.error(err)
notifications.error("App Export Failed.")
} finally {
appExportLoading = false
}
}
export let app
export let exportApp
export let deleteApp
</script>
<div class="apps-card">
<Heading size="S">{name}</Heading>
<div class="card-footer" data-cy={`app-${name}`}>
<ActionButton on:click={() => $goto(`/builder/${_id}`)}>
Open
{name}
</ActionButton>
{#if appExportLoading}
<Spinner size="10" />
{:else}
<ActionButton icon="Download" quiet />
{/if}
</div>
<div class="wrapper">
<Layout noPadding gap="XS" alignContent="start">
<div class="preview" use:gradient={{ seed: app.name }} />
<div class="title">
<Link href={$url(`../../app/${app._id}`)}>
<Heading size="XS">
{app.name}
</Heading>
</Link>
<ActionMenu align="right">
<Icon slot="control" name="More" hoverable />
<MenuItem on:click={() => exportApp(app)} icon="Download">
Export
</MenuItem>
<MenuItem on:click={() => deleteApp(app)} icon="Delete">
Delete
</MenuItem>
</ActionMenu>
</div>
<div class="status">
<Body noPadding size="S">
Edited {Math.floor(1 + Math.random() * 10)} months ago
</Body>
{#if Math.random() > 0.5}
<Icon name="LockClosed" />
{/if}
</div>
</Layout>
</div>
<style>
.apps-card {
background-color: var(--background);
padding: var(--spacing-xl) var(--spacing-xl) var(--spacing-xl)
var(--spacing-xl);
max-width: 300px;
max-height: 150px;
border-radius: var(--border-radius-m);
border: var(--border-dark);
.wrapper {
overflow: hidden;
}
.preview {
height: 135px;
border-radius: var(--border-radius-s);
margin-bottom: var(--spacing-s);
}
.card-footer {
.title,
.status {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
margin-top: var(--spacing-m);
align-items: center;
}
i {
font-size: var(--font-size-l);
cursor: pointer;
transition: 0.2s all;
.title :global(a) {
text-decoration: none;
flex: 1 1 auto;
width: 0;
overflow: hidden;
text-overflow: ellipsis;
margin-right: var(--spacing-m);
}
i:hover {
color: var(--blue);
.title :global(h1) {
overflow: hidden;
text-overflow: ellipsis;
}
.title :global(h1:hover) {
color: var(--spectrum-global-color-blue-600);
cursor: pointer;
transition: color 130ms ease;
}
</style>

50
packages/builder/src/components/start/AppList.svelte

@ -1,50 +0,0 @@
<script>
import AppCard from "./AppCard.svelte"
import { Heading, Divider } from "@budibase/bbui"
import Spinner from "components/common/Spinner.svelte"
import { get } from "builderStore/api"
let promise = getApps()
async function getApps() {
const res = await get("/api/applications")
const json = await res.json()
if (res.ok) {
return json
} else {
throw new Error(json)
}
}
</script>
<div class="root">
<Heading size="M">Your Apps</Heading>
<Divider size="M" />
{#await promise}
<div class="spinner-container">
<Spinner size="30" />
</div>
{:then apps}
<div class="apps">
{#each apps as app}
<AppCard {...app} />
{/each}
</div>
{:catch err}
<h1 style="color:red">{err}</h1>
{/await}
</div>
<style>
.root {
margin-top: 10px;
}
.apps {
margin-top: var(--layout-m);
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
grid-gap: var(--layout-s);
justify-content: start;
}
</style>

80
packages/builder/src/components/start/AppRow.svelte

@ -0,0 +1,80 @@
<script>
import { gradient } from "actions"
import {
Heading,
Button,
Icon,
ActionMenu,
MenuItem,
Link,
} from "@budibase/bbui"
import { url } from "@roxi/routify"
export let app
export let openApp
export let exportApp
export let deleteApp
export let last
</script>
<div class="title" class:last>
<div class="preview" use:gradient={{ seed: app.name }} />
<Link href={$url(`../../app/${app._id}`)}>
<Heading size="XS">
{app.name}
</Heading>
</Link>
</div>
<div class:last>
Edited {Math.round(Math.random() * 10 + 1)} months ago
</div>
<div class:last>
{#if Math.random() < 0.33}
<div class="status status--open" />
Open
{:else if Math.random() < 0.33}
<div class="status status--locked-other" />
Locked by Will Wheaton
{:else}
<div class="status status--locked-you" />
Locked by you
{/if}
</div>
<div class:last>
<Button on:click={() => openApp(app)} size="S" secondary>Open</Button>
<ActionMenu align="right">
<Icon hoverable slot="control" name="More" />
<MenuItem on:click={() => exportApp(app)} icon="Download">Export</MenuItem>
<MenuItem on:click={() => deleteApp(app)} icon="Delete">Delete</MenuItem>
</ActionMenu>
</div>
<style>
.preview {
height: 40px;
width: 40px;
border-radius: var(--border-radius-s);
}
.title :global(a) {
text-decoration: none;
}
.title :global(h1:hover) {
color: var(--spectrum-global-color-blue-600);
cursor: pointer;
transition: color 130ms ease;
}
.status {
height: 10px;
width: 10px;
border-radius: 50%;
}
.status--locked-you {
background-color: var(--spectrum-global-color-orange-600);
}
.status--locked-other {
background-color: var(--spectrum-global-color-red-600);
}
.status--open {
background-color: var(--spectrum-global-color-green-600);
}
</style>

15
packages/builder/src/components/start/BuilderSettingsButton.svelte

@ -1,15 +0,0 @@
<script>
import { Button, Modal } from "@budibase/bbui"
import BuilderSettingsModal from "./BuilderSettingsModal.svelte"
let modal
</script>
<div>
<Button primary quiet icon="Settings" text on:click={modal.show}>
Settings
</Button>
</div>
<Modal bind:this={modal} width="30%">
<BuilderSettingsModal />
</Modal>

239
packages/builder/src/components/start/CreateAppModal.svelte

@ -1,58 +1,50 @@
<script>
import { writable, get as svelteGet } from "svelte/store"
import { notifications, Heading, Button } from "@budibase/bbui"
import {
notifications,
Input,
ModalContent,
Dropzone,
Body,
Checkbox,
} from "@budibase/bbui"
import { store, automationStore, hostingStore } from "builderStore"
import { string, object } from "yup"
import { string, mixed, object } from "yup"
import api, { get } from "builderStore/api"
import Spinner from "components/common/Spinner.svelte"
import { Info, User } from "./Steps"
import Indicator from "./Indicator.svelte"
import { goto } from "@roxi/routify"
import { fade } from "svelte/transition"
import { post } from "builderStore/api"
import analytics from "analytics"
import { onMount } from "svelte"
import Logo from "/assets/bb-logo.svg"
import { capitalise } from "../../helpers"
import { capitalise } from "helpers"
import { goto } from "@roxi/routify"
export let template
const currentStep = writable(0)
const values = writable({ roleId: "ADMIN" })
const values = writable({ name: null })
const errors = writable({})
const touched = writable({})
const steps = [Info, User]
let validators = [
{
applicationName: string().required("Your application must have a name"),
},
{
roleId: string()
.nullable()
.required("You need to select a role for this app"),
},
]
const validator = {
name: string().required("Your application must have a name"),
file: template ? mixed().required("Please choose a file to import") : null,
}
let submitting = false
let valid = false
$: checkValidity($values, validators[$currentStep])
$: checkValidity($values, validator)
onMount(async () => {
const hostingInfo = await hostingStore.actions.fetch()
if (hostingInfo.type === "self") {
await hostingStore.actions.fetchDeployedApps()
const existingAppNames = svelteGet(hostingStore).deployedAppNames
validators[0].applicationName = string()
.required("Your application must have a name.")
.test(
"non-existing-app-name",
"App with same name already exists. Please try another app name.",
value =>
!existingAppNames.some(
appName => appName.toLowerCase() === value.toLowerCase()
)
)
}
await hostingStore.actions.fetchDeployedApps()
const existingAppNames = svelteGet(hostingStore).deployedAppNames
validator.name = string()
.required("Your application must have a name")
.test(
"non-existing-app-name",
"Another app with the same name already exists",
value => {
return !existingAppNames.some(
appName => appName.toLowerCase() === value.toLowerCase()
)
}
)
})
const checkValidity = async (values, validator) => {
@ -70,15 +62,24 @@
async function createNewApp() {
submitting = true
// Check a template exists if we are important
if (template && !$values.file) {
$errors.file = "Please choose a file to import"
valid = false
submitting = false
return false
}
try {
// Create form data to create app
let data = new FormData()
data.append("name", $values.applicationName)
data.append("name", $values.name)
data.append("useTemplate", template != null)
if (template) {
data.append("templateName", template.name)
data.append("templateKey", template.key)
data.append("templateFile", template.file)
data.append("templateFile", $values.file)
}
// Create App
@ -89,7 +90,7 @@
}
analytics.captureEvent("App Created", {
name: $values.applicationName,
name: $values.name,
appId: appJson._id,
template,
})
@ -112,7 +113,7 @@
}
const userResp = await api.post(`/api/users/metadata/self`, user)
await userResp.json()
$goto(`./${appJson._id}`)
$goto(`/builder/app/${appJson._id}`)
} catch (error) {
console.error(error)
notifications.error(error)
@ -121,129 +122,33 @@
}
</script>
<div class="container">
<div class="sidebar">
<img src={Logo} alt="budibase icon" />
<div class="steps">
{#each steps as component, i}
<Indicator
active={$currentStep === i}
done={i < $currentStep}
step={i + 1}
/>
{/each}
</div>
</div>
<div class="body">
<div class="heading">
<Heading size="L">Get started with Budibase</Heading>
</div>
<div class="step">
{#each steps as component, i (i)}
<div class:hidden={$currentStep !== i}>
<svelte:component
this={component}
{template}
{values}
{errors}
{touched}
/>
</div>
{/each}
</div>
<div class="footer">
{#if $currentStep > 0}
<Button medium secondary on:click={() => $currentStep--}>Back</Button>
{/if}
{#if $currentStep < steps.length - 1}
<Button medium cta on:click={() => $currentStep++} disabled={!valid}>
Next
</Button>
{/if}
{#if $currentStep === steps.length - 1}
<Button
medium
cta
on:click={createNewApp}
disabled={!valid || submitting}
>
{submitting ? "Loading..." : "Submit"}
</Button>
{/if}
</div>
</div>
{#if submitting}
<div in:fade class="spinner-container">
<Spinner />
<span class="spinner-text">Creating your app...</span>
</div>
<ModalContent
title={template ? "Import app" : "Create new app"}
confirmText={template ? "Import app" : "Create app"}
onConfirm={createNewApp}
disabled={!valid}
>
{#if template}
<Dropzone
error={$touched.file && $errors.file}
gallery={false}
label="File to import"
value={[$values.file]}
on:change={e => {
$values.file = e.detail?.[0]
$touched.file = true
}}
/>
{/if}
</div>
<style>
.container {
min-height: 600px;
display: grid;
grid-template-columns: 80px 1fr;
position: relative;
}
.sidebar {
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: stretch;
padding: 40px 0;
background: var(--grey-1);
}
.steps {
flex: 1 1 auto;
display: grid;
border-bottom-left-radius: 0.5rem;
border-top-left-radius: 0.5rem;
grid-gap: 30px;
align-content: center;
}
.heading {
display: flex;
flex-direction: row;
align-items: center;
margin-bottom: 20px;
}
.body {
padding: 40px 60px 40px 60px;
display: grid;
align-items: center;
grid-template-rows: auto 1fr auto;
}
.footer {
display: flex;
flex-direction: row;
justify-content: flex-end;
align-items: center;
gap: 15px;
}
.spinner-container {
background: var(--background);
position: absolute;
border-radius: 5px;
left: 0;
top: 0;
right: 0;
bottom: 0;
display: grid;
justify-items: center;
align-content: center;
grid-gap: 50px;
}
.spinner-text {
font-size: 2em;
}
.hidden {
display: none;
}
img {
height: 40px;
margin-bottom: 20px;
}
</style>
<Body size="S">
Give your new app a name, and choose which groups have access (paid plans
only).
</Body>
<Input
bind:value={$values.name}
error={$touched.name && $errors.name}
on:blur={() => ($touched.name = true)}
label="Name"
/>
<Checkbox label="Group access" disabled value={true} text="All users" />
</ModalContent>

83
packages/builder/src/components/start/Indicator.svelte

@ -1,83 +0,0 @@
<script>
export let step, done, active
</script>
<div class="container" class:active class:done>
<div class="circle" class:active class:done>
{#if done}
<svg
width="12"
height="10"
viewBox="0 0 12 10"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M10.1212 0.319527C10.327 0.115582 10.6047 0.000803464 10.8944
4.20219e-06C11.1841 -0.00079506 11.4624 0.11245 11.6693
0.315256C11.8762 0.518062 11.9949 0.794134 11.9998 1.08379C12.0048
1.37344 11.8955 1.65339 11.6957 1.86313L5.82705 9.19893C5.72619
9.30757 5.60445 9.39475 5.46913 9.45527C5.3338 9.51578 5.18766 9.54839
5.03944 9.55113C4.89123 9.55388 4.74398 9.52671 4.60651
9.47124C4.46903 9.41578 4.34416 9.33316 4.23934 9.22833L0.350925
5.33845C0.242598 5.23751 0.155712 5.11578 0.0954499 4.98054C0.0351876
4.84529 0.00278364 4.69929 0.00017159 4.55124C-0.00244046 4.4032
0.024793 4.25615 0.0802466 4.11886C0.1357 3.98157 0.218238 3.85685
0.322937 3.75215C0.427636 3.64746 0.55235 3.56492 0.68964
3.50946C0.82693 3.45401 0.973983 3.42678 1.12203 3.42939C1.27007 3.432
1.41607 3.46441 1.55132 3.52467C1.68657 3.58493 1.80829 3.67182
1.90923 3.78014L4.98762 6.85706L10.0933 0.35187C10.1024 0.340482
10.1122 0.329679 10.1227 0.319527H10.1212Z"
fill="var(--background)"
/>
</svg>
{:else}{step}{/if}
</div>
</div>
<style>
.container::before {
content: "";
position: absolute;
top: -30px;
width: 1px;
height: 30px;
background: var(--grey-5);
}
.container:first-child::before {
display: none;
}
.container {
position: relative;
height: 45px;
display: grid;
place-items: center;
}
.container.active {
box-shadow: inset 3px 0 0 0 var(--blue);
}
.circle.active {
background: var(--blue);
color: white;
border: none;
}
.circle.done {
background: var(--grey-5);
color: white;
border: none;
}
.circle {
color: var(--grey-5);
font-size: 14px;
font-weight: 500;
display: grid;
place-items: center;
width: 30px;
height: 30px;
border-radius: 50%;
border: 1px solid var(--grey-5);
box-sizing: border-box;
}
</style>

6
packages/builder/src/components/start/LogoutButton.svelte

@ -1,6 +0,0 @@
<script>
import { Button } from "@budibase/bbui"
import { auth } from "stores/backend"
</script>
<Button primary quiet text icon="LogOut" on:click={auth.logout}>Log Out</Button>

121
packages/builder/src/components/start/Steps/Info.svelte

@ -1,121 +0,0 @@
<script>
import { Label, Heading, Input, notifications } from "@budibase/bbui"
const BYTES_IN_MB = 1000000
const FILE_SIZE_LIMIT = BYTES_IN_MB * 5
export let template
export let values
export let errors
export let touched
let blurred = { appName: false }
let file
function handleFile(evt) {
const fileArray = Array.from(evt.target.files)
if (fileArray.some(file => file.size >= FILE_SIZE_LIMIT)) {
notifications.error(
`Files cannot exceed ${
FILE_SIZE_LIMIT / BYTES_IN_MB
}MB. Please try again with smaller files.`
)
return
}
file = evt.target.files[0]
template.file = file
}
</script>
<div class="container">
{#if template?.fromFile}
<Heading size="L">Import your Web App</Heading>
{:else}
<Heading size="L">Create your Web App</Heading>
{/if}
{#if template?.fromFile}
<div class="template">
<Label extraSmall grey>Import File</Label>
<div class="dropzone">
<input
id="file-upload"
accept=".txt"
type="file"
on:change={handleFile}
/>
<label for="file-upload" class:uploaded={file}>
{#if file}{file.name}{:else}Import{/if}
</label>
</div>
</div>
{:else if template}
<div class="template">
<Label extraSmall grey>Selected Template</Label>
<Heading size="S">{template.name}</Heading>
</div>
{/if}
<Input
on:change={() => ($touched.applicationName = true)}
bind:value={$values.applicationName}
label="Web App Name"
placeholder="Enter name of your web application"
error={$touched.applicationName && $errors.applicationName}
/>
</div>
<style>
.container {
display: grid;
grid-gap: var(--spacing-xl);
margin-top: var(--spacing-xl);
}
.template :global(label) {
/* Fix layout due to LH 0 on heading */
margin-bottom: 16px;
}
.dropzone {
text-align: center;
display: flex;
align-items: center;
flex-direction: column;
border-radius: 10px;
transition: all 0.3s;
}
.uploaded {
color: var(--blue);
}
input[type="file"] {
display: none;
}
label {
font-family: var(--font-sans);
cursor: pointer;
font-weight: 500;
box-sizing: border-box;
overflow: hidden;
border-radius: var(--border-radius-s);
color: var(--ink);
padding: var(--spacing-m) var(--spacing-l);
transition: all 0.2s ease 0s;
display: inline-flex;
text-rendering: optimizeLegibility;
min-width: auto;
outline: none;
font-feature-settings: "case" 1, "rlig" 1, "calt" 0;
-webkit-box-align: center;
user-select: none;
flex-shrink: 0;
align-items: center;
justify-content: center;
width: 100%;
background-color: var(--grey-2);
font-size: var(--font-size-xs);
line-height: normal;
border: var(--border-transparent);
}
</style>

29
packages/builder/src/components/start/Steps/User.svelte

@ -1,29 +0,0 @@
<script>
import { Select, Heading } from "@budibase/bbui"
export let values
export let errors
export let touched
</script>
<div class="container">
<Heading size="L">What's your role for this app?</Heading>
<Select
bind:value={$values.roleId}
label="Role"
options={[
{ label: "Admin", value: "ADMIN" },
{ label: "Power User", value: "POWER_USER" },
]}
getOptionLabel={option => option.label}
getOptionValue={option => option.value}
error={$errors.roleId}
/>
</div>
<style>
.container {
display: grid;
grid-gap: var(--spacing-xl);
}
</style>

2
packages/builder/src/components/start/Steps/index.js

@ -1,2 +0,0 @@
export { default as Info } from "./Info.svelte"
export { default as User } from "./User.svelte"

20
packages/builder/src/helperStores.js

@ -0,0 +1,20 @@
import { writable } from 'svelte/store'
import api from "builderStore/api"
export function fetchData (url) {
const store = writable({status: 'LOADING', data: {}, error: {}})
async function get() {
store.update(u => ({...u, status: 'SUCCESS'}))
try {
const response = await api.get(url)
store.set({data: await response.json(), status: 'SUCCESS'})
} catch(e) {
store.set({data: {}, error: e, status: 'ERROR'})
}
}
get()
return {subscribe: store.subscribe, refresh: get}
}

29
packages/builder/src/pages/_layout.svelte

@ -1,29 +0,0 @@
<script>
import { onMount } from "svelte"
import { goto } from "@roxi/routify"
import {
SideNavigation as Navigation,
SideNavigationItem as Item,
} from "@budibase/bbui"
import { admin } from "stores/portal"
import LoginForm from "components/login/LoginForm.svelte"
import BuilderSettingsButton from "components/start/BuilderSettingsButton.svelte"
import LogoutButton from "components/start/LogoutButton.svelte"
import Logo from "/assets/budibase-logo.svg"
import api from "builderStore/api"
let checklist
onMount(async () => {
await admin.init()
if (!$admin?.checklist?.adminUser) {
$goto("./admin")
} else {
$goto("./portal")
}
})
</script>
{#if $admin.checklist}
<slot />
{/if}

69
packages/builder/src/pages/admin/index.svelte

@ -1,69 +0,0 @@
<script>
import {
Button,
Heading,
Label,
notifications,
Layout,
Input,
Body,
} from "@budibase/bbui"
import { goto } from "@roxi/routify"
import { onMount } from "svelte"
import api from "builderStore/api"
let adminUser = {}
async function save() {
try {
// Save the admin user
const response = await api.post(`/api/admin/users/init`, adminUser)
const json = await response.json()
if (response.status !== 200) throw new Error(json.message)
notifications.success(`Admin user created.`)
$goto("../portal")
} catch (err) {
notifications.error(`Failed to create admin user.`)
}
}
</script>
<section>
<div class="container">
<header>
<Heading size="M">Create an admin user</Heading>
<Body size="S">The admin user has access to everything in budibase.</Body>
</header>
<div class="config-form">
<Layout gap="S">
<Input label="email" bind:value={adminUser.email} />
<Input
label="password"
type="password"
bind:value={adminUser.password}
/>
<Button cta on:click={save}>Create super admin user</Button>
</Layout>
</div>
</div>
</section>
<style>
section {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
}
header {
text-align: center;
width: 80%;
margin: 0 auto;
}
.config-form {
margin-bottom: 42px;
}
</style>

131
packages/builder/src/pages/builder/_layout.svelte

@ -1,116 +1,33 @@
<script>
import {
SideNavigation as Navigation,
SideNavigationItem as Item,
} from "@budibase/bbui"
import { onMount } from "svelte"
import { goto } from "@roxi/routify"
import { auth } from "stores/backend"
import LoginForm from "components/login/LoginForm.svelte"
import BuilderSettingsButton from "components/start/BuilderSettingsButton.svelte"
import LogoutButton from "components/start/LogoutButton.svelte"
import Logo from "/assets/budibase-logo.svg"
import { admin } from "stores/portal"
let modal
</script>
{#if $auth}
{#if $auth.user}
<div class="root">
<div class="ui-nav">
<div class="home-logo">
<img src={Logo} alt="Budibase icon" />
</div>
<div class="nav-section">
<div class="nav-top">
<Navigation>
<Item href="/builder/" icon="Apps" selected>Apps</Item>
<Item external href="https://portal.budi.live/" icon="Servers">
Hosting
</Item>
<Item external href="https://docs.budibase.com/" icon="Book">
Documentation
</Item>
<Item
external
href="https://github.com/Budibase/budibase/discussions"
icon="PeopleGroup"
>
Community
</Item>
<Item
external
href="https://github.com/Budibase/budibase/issues/new/choose"
icon="Bug"
>
Raise an issue
</Item>
</Navigation>
</div>
<div class="nav-bottom">
<BuilderSettingsButton />
<LogoutButton />
</div>
</div>
</div>
<div class="main">
<slot />
</div>
</div>
{:else}
<section class="login">
<LoginForm />
</section>
{/if}
{/if}
<style>
.root {
display: grid;
grid-template-columns: 260px 1fr;
height: 100%;
width: 100%;
}
.login {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
width: 100%;
}
let loaded = false
$: hasAdminUser = !!$admin?.checklist?.adminUser
.main {
grid-column: 2;
overflow: auto;
}
onMount(async () => {
await admin.init()
await auth.checkAuth()
loaded = true
})
.ui-nav {
grid-column: 1;
background-color: var(--background);
padding: 20px;
display: flex;
flex-direction: column;
border-right: var(--border-light);
// Force creation of an admin user if one doesn't exist
$: {
if (loaded && !hasAdminUser) {
$goto("./admin")
}
}
.home-logo {
cursor: pointer;
height: 40px;
margin-bottom: 20px;
}
.home-logo img {
height: 40px;
}
.nav-section {
margin: 20px 0 0 0;
display: flex;
flex-direction: column;
justify-content: space-between;
height: 100%;
// Redirect to log in at any time if the user isn't authenticated
$: {
if (loaded && hasAdminUser && !$auth.user) {
$goto("./auth/login")
}
}
</script>
.nav-bottom :global(> *) {
margin-top: 5px;
}
</style>
{#if loaded}
<slot />
{/if}

78
packages/builder/src/pages/builder/admin/index.svelte

@ -0,0 +1,78 @@
<script>
import {
Button,
Heading,
notifications,
Layout,
Input,
Body,
} from "@budibase/bbui"
import { goto } from "@roxi/routify"
import api from "builderStore/api"
import { admin } from "stores/portal"
let adminUser = {}
async function save() {
try {
// Save the admin user
const response = await api.post(`/api/admin/users/init`, adminUser)
const json = await response.json()
if (response.status !== 200) {
throw new Error(json.message)
}
notifications.success(`Admin user created`)
await admin.init()
$goto("../portal")
} catch (err) {
notifications.error(`Failed to create admin user`)
}
}
</script>
<section>
<div class="container">
<Layout gap="XS">
<img src="https://i.imgur.com/ZKyklgF.png" />
</Layout>
<div class="center">
<Layout gap="XS">
<Heading size="M">Create an admin user</Heading>
<Body size="M"
>The admin user has access to everything in Budibase.</Body
>
</Layout>
</div>
<Layout gap="XS">
<Input label="Email" bind:value={adminUser.email} />
<Input label="Password" type="password" bind:value={adminUser.password} />
</Layout>
<Layout gap="S">
<Button cta on:click={save}>Create super admin user</Button>
</Layout>
</div>
</section>
<style>
section {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
}
.container {
margin: 0 auto;
width: 260px;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: stretch;
}
.center {
text-align: center;
}
img {
width: 40px;
margin: 0 auto;
}
</style>

6
packages/builder/src/pages/builder/[application]/_reset.svelte → packages/builder/src/pages/builder/app/[application]/_layout.svelte

@ -6,9 +6,9 @@
import ThemeEditorDropdown from "components/settings/ThemeEditorDropdown.svelte"
import FeedbackNavLink from "components/feedback/FeedbackNavLink.svelte"
import { get } from "builderStore/api"
import { isActive, goto, layout, params } from "@roxi/routify"
import { isActive, goto, layout } from "@roxi/routify"
import Logo from "/assets/bb-logo.svg"
import { capitalise } from "../../../helpers"
import { capitalise } from "helpers"
// Get Package and set store
export let application
@ -60,7 +60,7 @@
<img
src={Logo}
alt="budibase icon"
on:click={() => $goto(`/builder/`)}
on:click={() => $goto(`../../portal/`)}
/>
</button>

0
packages/builder/src/pages/builder/[application]/automate/[automation]/_layout.svelte → packages/builder/src/pages/builder/app/[application]/automate/[automation]/_layout.svelte

0
packages/builder/src/pages/builder/[application]/automate/[automation]/index.svelte → packages/builder/src/pages/builder/app/[application]/automate/[automation]/index.svelte

0
packages/builder/src/pages/builder/[application]/automate/_layout.svelte → packages/builder/src/pages/builder/app/[application]/automate/_layout.svelte

0
packages/builder/src/pages/builder/[application]/automate/index.svelte → packages/builder/src/pages/builder/app/[application]/automate/index.svelte

0
packages/builder/src/pages/builder/[application]/data/_layout.svelte → packages/builder/src/pages/builder/app/[application]/data/_layout.svelte

0
packages/builder/src/pages/builder/[application]/data/datasource/[selectedDatasource]/[query]/_layout.svelte → packages/builder/src/pages/builder/app/[application]/data/datasource/[selectedDatasource]/[query]/_layout.svelte

0
packages/builder/src/pages/builder/[application]/data/datasource/[selectedDatasource]/[query]/index.svelte → packages/builder/src/pages/builder/app/[application]/data/datasource/[selectedDatasource]/[query]/index.svelte

0
packages/builder/src/pages/builder/[application]/data/datasource/[selectedDatasource]/_layout.svelte → packages/builder/src/pages/builder/app/[application]/data/datasource/[selectedDatasource]/_layout.svelte

2
packages/builder/src/pages/builder/[application]/data/datasource/[selectedDatasource]/index.svelte → packages/builder/src/pages/builder/app/[application]/data/datasource/[selectedDatasource]/index.svelte

@ -5,7 +5,7 @@
import { notifications } from "@budibase/bbui"
import IntegrationConfigForm from "components/backend/DatasourceNavigator/TableIntegrationMenu/IntegrationConfigForm.svelte"
import ICONS from "components/backend/DatasourceNavigator/icons"
import { capitalise } from "../../../../../../helpers"
import { capitalise } from "helpers"
let unsaved = false

0
packages/builder/src/pages/builder/[application]/data/datasource/_layout.svelte → packages/builder/src/pages/builder/app/[application]/data/datasource/_layout.svelte

0
packages/builder/src/pages/builder/[application]/data/datasource/index.svelte → packages/builder/src/pages/builder/app/[application]/data/datasource/index.svelte

0
packages/builder/src/pages/builder/[application]/data/index.svelte → packages/builder/src/pages/builder/app/[application]/data/index.svelte

0
packages/builder/src/pages/builder/[application]/data/table/[selectedTable]/_layout.svelte → packages/builder/src/pages/builder/app/[application]/data/table/[selectedTable]/_layout.svelte

0
packages/builder/src/pages/builder/[application]/data/table/[selectedTable]/index.svelte → packages/builder/src/pages/builder/app/[application]/data/table/[selectedTable]/index.svelte

0
packages/builder/src/pages/builder/[application]/data/table/[selectedTable]/relationship/[selectedRow]/[selectedField]/index.svelte → packages/builder/src/pages/builder/app/[application]/data/table/[selectedTable]/relationship/[selectedRow]/[selectedField]/index.svelte

0
packages/builder/src/pages/builder/[application]/data/table/[selectedTable]/relationship/[selectedRow]/index.svelte → packages/builder/src/pages/builder/app/[application]/data/table/[selectedTable]/relationship/[selectedRow]/index.svelte

0
packages/builder/src/pages/builder/[application]/data/table/[selectedTable]/relationship/index.svelte → packages/builder/src/pages/builder/app/[application]/data/table/[selectedTable]/relationship/index.svelte

0
packages/builder/src/pages/builder/[application]/data/table/_layout.svelte → packages/builder/src/pages/builder/app/[application]/data/table/_layout.svelte

0
packages/builder/src/pages/builder/[application]/data/table/index.svelte → packages/builder/src/pages/builder/app/[application]/data/table/index.svelte

0
packages/builder/src/pages/builder/[application]/data/view/[selectedView]/_layout.svelte → packages/builder/src/pages/builder/app/[application]/data/view/[selectedView]/_layout.svelte

0
packages/builder/src/pages/builder/[application]/data/view/[selectedView]/index.svelte → packages/builder/src/pages/builder/app/[application]/data/view/[selectedView]/index.svelte

0
packages/builder/src/pages/builder/[application]/deploy/_layout.svelte → packages/builder/src/pages/builder/app/[application]/deploy/_layout.svelte

0
packages/builder/src/pages/builder/[application]/deploy/index.svelte → packages/builder/src/pages/builder/app/[application]/deploy/index.svelte

0
packages/builder/src/pages/builder/[application]/design/[assetType]/[asset]/_fallback.svelte → packages/builder/src/pages/builder/app/[application]/design/[assetType]/[asset]/_fallback.svelte

0
packages/builder/src/pages/builder/[application]/design/[assetType]/_layout.svelte → packages/builder/src/pages/builder/app/[application]/design/[assetType]/_layout.svelte

0
packages/builder/src/pages/builder/[application]/design/[assetType]/index.svelte → packages/builder/src/pages/builder/app/[application]/design/[assetType]/index.svelte

0
packages/builder/src/pages/builder/[application]/design/_layout.svelte → packages/builder/src/pages/builder/app/[application]/design/_layout.svelte

0
packages/builder/src/pages/builder/[application]/design/index.svelte → packages/builder/src/pages/builder/app/[application]/design/index.svelte

0
packages/builder/src/pages/builder/[application]/index.svelte → packages/builder/src/pages/builder/app/[application]/index.svelte

0
packages/builder/src/pages/builder/[application]/settings/_layout.svelte → packages/builder/src/pages/builder/app/[application]/settings/_layout.svelte

0
packages/builder/src/pages/builder/[application]/settings/index.svelte → packages/builder/src/pages/builder/app/[application]/settings/index.svelte

4
packages/builder/src/pages/builder/app/index.svelte

@ -0,0 +1,4 @@
<script>
import { goto } from "@roxi/routify"
$goto("../portal")
</script>

4
packages/builder/src/pages/builder/auth/index.svelte

@ -0,0 +1,4 @@
<script>
import { goto } from "@roxi/routify"
$goto("./login")
</script>

5
packages/builder/src/pages/builder/auth/login.svelte

@ -0,0 +1,5 @@
<script>
import LoginForm from "components/login/LoginForm.svelte"
</script>
<LoginForm />

123
packages/builder/src/pages/builder/index.svelte

@ -1,123 +1,4 @@
<script>
import api from "builderStore/api"
import AppList from "components/start/AppList.svelte"
import { get } from "builderStore/api"
import CreateAppModal from "components/start/CreateAppModal.svelte"
import { Button, Heading, Modal, ButtonGroup } from "@budibase/bbui"
import TemplateList from "components/start/TemplateList.svelte"
import analytics from "analytics"
import Banner from "/assets/orange-landscape.png"
let hasKey
let template
let modal
async function getApps() {
const res = await get("/api/applications")
const json = await res.json()
if (res.ok) {
return json
} else {
throw new Error(json)
}
}
async function fetchKeys() {
const response = await api.get(`/api/keys/`)
return await response.json()
}
async function checkIfKeysAndApps() {
const keys = await fetchKeys()
const apps = await getApps()
if (keys.userId) {
hasKey = true
analytics.identify(keys.userId)
}
}
function selectTemplate(newTemplate) {
template = newTemplate
modal.show()
}
function initiateAppImport() {
template = { fromFile: true }
modal.show()
}
function closeModal() {
template = null
modal.hide()
}
checkIfKeysAndApps()
import { goto } from "@roxi/routify"
$goto("./portal")
</script>
<div class="container">
<div class="header">
<Heading size="M">Welcome to the Budibase Beta</Heading>
<ButtonGroup>
<Button secondary on:click={initiateAppImport}>Import Web App</Button>
<Button cta on:click={modal.show}>Create New Web App</Button>
</ButtonGroup>
</div>
<div class="banner">
<img src={Banner} alt="rocket" />
<div class="banner-content">
Every accomplishment starts with a decision to try.
</div>
</div>
<!-- <TemplateList onSelect={selectTemplate} /> -->
<AppList />
</div>
<Modal bind:this={modal} padding={false} width="600px" on:hide={closeModal}>
<CreateAppModal {hasKey} {template} />
</Modal>
<style>
.container {
display: grid;
gap: var(--spacing-xl);
margin: 40px 80px;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
}
.banner {
display: flex;
align-items: center;
justify-content: center;
position: relative;
text-align: center;
color: white;
border-radius: 16px;
}
.banner img {
height: 250px;
width: 100%;
border-radius: 5px;
}
.banner-content {
position: absolute;
font-size: 24px;
color: white;
font-weight: 500;
}
.button-group {
display: flex;
flex-direction: row;
}
</style>

81
packages/builder/src/pages/portal/_layout.svelte → packages/builder/src/pages/builder/portal/_layout.svelte

@ -1,49 +1,53 @@
<script>
import { isActive, url, goto } from "@roxi/routify"
import { isActive, goto } from "@roxi/routify"
import { onMount } from "svelte"
import {
ActionMenu,
Checkbox,
MenuItem,
Icon,
Heading,
Avatar,
Search,
Layout,
ProgressCircle,
SideNavigation as Navigation,
SideNavigationItem as Item,
ActionMenu,
MenuItem,
Modal,
} from "@budibase/bbui"
import api from "builderStore/api"
import ConfigChecklist from "components/common/ConfigChecklist.svelte"
import { organisation, admin } from "stores/portal"
organisation.init()
import { organisation, apps } from "stores/portal"
import { auth } from "stores/backend"
import BuilderSettingsModal from "components/start/BuilderSettingsModal.svelte"
let orgName
let orgLogo
let user
let oldSettingsModal
async function getInfo() {
// fetch orgInfo
orgName = "ACME Inc."
orgLogo = "https://via.placeholder.com/150"
user = { name: "John Doe" }
}
onMount(getInfo)
onMount(() => {
organisation.init()
getInfo()
})
let menu = [
{ title: "Apps", href: "/portal/apps" },
{ title: "Drafts", href: "/portal/drafts" },
{ title: "Users", href: "/portal/manage/users", heading: "Manage" },
{ title: "Groups", href: "/portal/manage/groups" },
{ title: "Auth", href: "/portal/manage/auth" },
{ title: "Email", href: "/portal/manage/email" },
{ title: "General", href: "/portal/settings/general", heading: "Settings" },
{ title: "Theming", href: "/portal/theming" },
{ title: "Account", href: "/portal/account" },
{ title: "Apps", href: "/builder/portal/apps" },
{ title: "Drafts", href: "/builder/portal/drafts" },
{ title: "Users", href: "/builder/portal/manage/users", heading: "Manage" },
{ title: "Groups", href: "/builder/portal/manage/groups" },
{ title: "Auth", href: "/builder/portal/manage/auth" },
{ title: "Email", href: "/builder/portal/manage/email" },
{
title: "General",
href: "/builder/portal/settings/general",
heading: "Settings",
},
{ title: "Theming", href: "/builder/portal/theming" },
{ title: "Account", href: "/builder/portal/account" },
]
</script>
@ -51,7 +55,7 @@
<div class="nav">
<Layout paddingX="L" paddingY="L">
<div class="branding">
<div class="name">
<div class="name" on:click={() => $goto("./apps")}>
<img
src={$organisation?.logoUrl || "https://i.imgur.com/ZKyklgF.png"}
alt="Logotype"
@ -74,30 +78,42 @@
<div class="main">
<div class="toolbar">
<Search placeholder="Global search" />
<div class="avatar">
<Avatar size="M" name="John Doe" />
<Icon size="XL" name="ChevronDown" />
</div>
<ActionMenu align="right">
<div slot="control" class="avatar">
<Avatar size="M" name="John Doe" />
<Icon size="XL" name="ChevronDown" />
</div>
<MenuItem icon="Settings" on:click={oldSettingsModal.show}>
Old settings
</MenuItem>
<MenuItem icon="LogOut" on:click={auth.logout}>Log out</MenuItem>
</ActionMenu>
</div>
<div>
<div class="content">
<slot />
</div>
</div>
</div>
<Modal bind:this={oldSettingsModal} width="30%">
<BuilderSettingsModal />
</Modal>
<style>
.container {
min-height: 100vh;
height: 100%;
display: grid;
grid-template-columns: 250px 1fr;
align-items: stretch;
}
.nav {
background: var(--background);
border-right: var(--border-light);
overflow: auto;
}
.main {
display: grid;
grid-template-rows: auto 1fr;
overflow: hidden;
}
.branding {
display: grid;
@ -112,6 +128,9 @@
grid-gap: var(--spacing-m);
align-items: center;
}
.name:hover {
cursor: pointer;
}
.avatar {
display: grid;
grid-template-columns: auto auto;
@ -129,6 +148,7 @@
grid-template-columns: 250px auto;
justify-content: space-between;
padding: var(--spacing-m) calc(var(--spacing-xl) * 2);
align-items: center;
}
img {
width: 28px;
@ -139,4 +159,9 @@
text-overflow: ellipsis;
font-weight: 500;
}
.content {
overflow: auto;
display: grid;
grid-template-rows: 1fr;
}
</style>

231
packages/builder/src/pages/builder/portal/apps/index.svelte

@ -0,0 +1,231 @@
<script>
import {
Heading,
Layout,
Button,
ActionButton,
ActionGroup,
ButtonGroup,
Select,
Modal,
ModalContent,
Page,
notifications,
Body,
} from "@budibase/bbui"
import CreateAppModal from "components/start/CreateAppModal.svelte"
import api, { del } from "builderStore/api"
import analytics from "analytics"
import { onMount } from "svelte"
import { apps } from "stores/portal"
import download from "downloadjs"
import { goto } from "@roxi/routify"
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
import AppCard from "components/start/AppCard.svelte"
import AppRow from "components/start/AppRow.svelte"
let layout = "grid"
let template
let appToDelete
let creationModal
let deletionModal
let creatingApp = false
let loaded = false
const checkKeys = async () => {
const response = await api.get(`/api/keys/`)
const keys = await response.json()
if (keys.userId) {
analytics.identify(keys.userId)
}
}
const initiateAppCreation = () => {
creationModal.show()
creatingApp = true
}
const initiateAppImport = () => {
template = { fromFile: true }
creationModal.show()
creatingApp = true
}
const stopAppCreation = () => {
template = null
creatingApp = false
}
const openApp = app => {
$goto(`../../app/${app._id}`)
}
const exportApp = app => {
try {
download(
`/api/backups/export?appId=${app._id}&appname=${encodeURIComponent(
app.name
)}`
)
notifications.success("App export complete")
} catch (err) {
console.error(err)
notifications.error("App export failed")
}
}
const deleteApp = app => {
appToDelete = app
deletionModal.show()
}
const confirmDeleteApp = async () => {
if (!appToDelete) {
return
}
await del(`/api/applications/${appToDelete?._id}`)
await apps.load()
appToDelete = null
}
onMount(async () => {
checkKeys()
await apps.load()
loaded = true
})
</script>
<Page wide>
{#if $apps.length}
<Layout noPadding>
<div class="title">
<Heading>Apps</Heading>
<ButtonGroup>
<Button secondary on:click={initiateAppImport}>Import app</Button>
<Button cta on:click={initiateAppCreation}>Create new app</Button>
</ButtonGroup>
</div>
<div class="filter">
<div class="select">
<Select quiet placeholder="Filter by groups" />
</div>
<ActionGroup>
<ActionButton
on:click={() => (layout = "grid")}
selected={layout === "grid"}
quiet
icon="ClassicGridView"
/>
<ActionButton
on:click={() => (layout = "table")}
selected={layout === "table"}
quiet
icon="ViewRow"
/>
</ActionGroup>
</div>
<div
class:appGrid={layout === "grid"}
class:appTable={layout === "table"}
>
{#each $apps as app, idx (app._id)}
<svelte:component
this={layout === "grid" ? AppCard : AppRow}
{app}
{openApp}
{exportApp}
{deleteApp}
last={idx === $apps.length - 1}
/>
{/each}
</div>
</Layout>
{/if}
{#if !$apps.length && !creatingApp && loaded}
<div class="empty-wrapper">
<Modal inline>
<ModalContent
title="Create your first app"
confirmText="Create app"
showCancelButton={false}
showCloseIcon={false}
onConfirm={initiateAppCreation}
size="M"
>
<div slot="footer">
<Button on:click={initiateAppImport} secondary>Import app</Button>
</div>
<Body size="S">
The purpose of the Budibase builder is to help you build beautiful,
powerful applications quickly and easily.
</Body>
</ModalContent>
</Modal>
</div>
{/if}
</Page>
<Modal
bind:this={creationModal}
padding={false}
width="600px"
on:hide={stopAppCreation}
>
<CreateAppModal {template} />
</Modal>
<ConfirmDialog
bind:this={deletionModal}
title="Confirm deletion"
okText="Delete app"
onOk={confirmDeleteApp}
>
Are you sure you want to delete the app <b>{appToDelete?.name}</b>?
</ConfirmDialog>
<style>
.title,
.filter {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
}
.select {
width: 120px;
}
.appGrid {
display: grid;
grid-gap: 50px;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
}
.appTable {
display: grid;
grid-template-rows: auto;
grid-template-columns: 1fr 1fr 1fr auto;
align-items: center;
}
.appTable :global(> div) {
height: 70px;
display: grid;
align-items: center;
gap: var(--spacing-xl);
grid-template-columns: auto 1fr;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
padding: 0 var(--spacing-s);
}
.appTable :global(> div:not(.last)) {
border-bottom: var(--border-light);
}
.empty-wrapper {
flex: 1 1 auto;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
</style>

0
packages/builder/src/pages/portal/index.svelte → packages/builder/src/pages/builder/portal/index.svelte

0
packages/builder/src/pages/portal/manage/_layout.svelte → packages/builder/src/pages/builder/portal/manage/_layout.svelte

0
packages/builder/src/pages/portal/manage/auth/logos/Google.svelte → packages/builder/src/pages/builder/portal/manage/auth/_logos/Google.svelte

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

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

Loading…
Cancel
Save