Merge branch 'admin/user-management-ui' of github.com:Budibase/budibase into admin/user-management-ui
This commit is contained in:
commit
3e63c616d3
|
@ -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,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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
|
@ -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",
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -32,6 +32,10 @@ module.exports = {
|
|||
Client: require("./redis"),
|
||||
utils: require("./redis/utils"),
|
||||
},
|
||||
objectStore: {
|
||||
...require("./objectStore"),
|
||||
...require("./objectStore/utils"),
|
||||
},
|
||||
utils: {
|
||||
...require("./utils"),
|
||||
...require("./hashing"),
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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")
|
||||
}
|
|
@ -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=
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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,6 +98,7 @@
|
|||
|
||||
<div class="container">
|
||||
{#if selectedImage}
|
||||
{#if gallery}
|
||||
<div class="gallery">
|
||||
<div class="title">
|
||||
<div class="filename">{selectedImage.name}</div>
|
||||
|
@ -132,9 +137,29 @@
|
|||
</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;
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -37,6 +37,7 @@
|
|||
}
|
||||
focus = false
|
||||
updateValue(event.target.value)
|
||||
dispatch("blur")
|
||||
}
|
||||
|
||||
const updateValueOnEnter = event => {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -30,5 +30,6 @@
|
|||
on:change={onChange}
|
||||
on:click
|
||||
on:input
|
||||
on:blur
|
||||
/>
|
||||
</Field>
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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,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);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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 !== ""))}
|
||||
|
|
|
@ -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,7 +317,15 @@
|
|||
</tr>
|
||||
{/each}
|
||||
{:else}
|
||||
<div class="placeholder">
|
||||
<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"
|
||||
|
@ -325,6 +334,8 @@
|
|||
</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;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
},
|
||||
|
|
|
@ -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)
|
||||
},
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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}`
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
Body,
|
||||
ModalContent,
|
||||
} from "@budibase/bbui"
|
||||
import { capitalise } from "../../../../helpers"
|
||||
import { capitalise } from "helpers"
|
||||
|
||||
export let resourceId
|
||||
export let permissions
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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 />
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
const id = $params.application
|
||||
await del(`/api/applications/${id}`)
|
||||
loading = false
|
||||
$goto("/builder/")
|
||||
$goto("/builder")
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
|
@ -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 />
|
||||
<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);
|
||||
.title :global(a) {
|
||||
text-decoration: none;
|
||||
flex: 1 1 auto;
|
||||
width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
margin-right: var(--spacing-m);
|
||||
}
|
||||
.title :global(h1) {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.title :global(h1:hover) {
|
||||
color: var(--spectrum-global-color-blue-600);
|
||||
cursor: pointer;
|
||||
transition: 0.2s all;
|
||||
}
|
||||
|
||||
i:hover {
|
||||
color: var(--blue);
|
||||
transition: color 130ms ease;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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.")
|
||||
validator.name = 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(
|
||||
"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}
|
||||
<ModalContent
|
||||
title={template ? "Import app" : "Create new app"}
|
||||
confirmText={template ? "Import app" : "Create app"}
|
||||
onConfirm={createNewApp}
|
||||
disabled={!valid}
|
||||
>
|
||||
{submitting ? "Loading..." : "Submit"}
|
||||
</Button>
|
||||
{#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>
|
||||
</div>
|
||||
{#if submitting}
|
||||
<div in:fade class="spinner-container">
|
||||
<Spinner />
|
||||
<span class="spinner-text">Creating your app...</span>
|
||||
</div>
|
||||
{/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>
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -1,2 +0,0 @@
|
|||
export { default as Info } from "./Info.svelte"
|
||||
export { default as User } from "./User.svelte"
|
|
@ -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}
|
||||
}
|
|
@ -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}
|
|
@ -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>
|
|
@ -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
|
||||
let loaded = false
|
||||
$: hasAdminUser = !!$admin?.checklist?.adminUser
|
||||
|
||||
onMount(async () => {
|
||||
await admin.init()
|
||||
await auth.checkAuth()
|
||||
loaded = true
|
||||
})
|
||||
|
||||
// Force creation of an admin user if one doesn't exist
|
||||
$: {
|
||||
if (loaded && !hasAdminUser) {
|
||||
$goto("./admin")
|
||||
}
|
||||
}
|
||||
|
||||
// Redirect to log in at any time if the user isn't authenticated
|
||||
$: {
|
||||
if (loaded && hasAdminUser && !$auth.user) {
|
||||
$goto("./auth/login")
|
||||
}
|
||||
}
|
||||
</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">
|
||||
{#if loaded}
|
||||
<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%;
|
||||
}
|
||||
|
||||
.main {
|
||||
grid-column: 2;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.ui-nav {
|
||||
grid-column: 1;
|
||||
background-color: var(--background);
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-right: var(--border-light);
|
||||
}
|
||||
|
||||
.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%;
|
||||
}
|
||||
|
||||
.nav-bottom :global(> *) {
|
||||
margin-top: 5px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -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,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>
|
||||
|
|
@ -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,0 +1,4 @@
|
|||
<script>
|
||||
import { goto } from "@roxi/routify"
|
||||
$goto("../portal")
|
||||
</script>
|
|
@ -0,0 +1,4 @@
|
|||
<script>
|
||||
import { goto } from "@roxi/routify"
|
||||
$goto("./login")
|
||||
</script>
|
|
@ -0,0 +1,5 @@
|
|||
<script>
|
||||
import LoginForm from "components/login/LoginForm.svelte"
|
||||
</script>
|
||||
|
||||
<LoginForm />
|
|
@ -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>
|
||||
|
|
|
@ -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">
|
||||
<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>
|
|
@ -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>
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue