Merge branch 'master' into builder/consolidating-missing-code
This commit is contained in:
commit
50ecfd1e25
|
@ -166,6 +166,18 @@ rm -rf ~/.budibase
|
||||||
```
|
```
|
||||||
Follow from **Step 3. Install and Build** in the setup guide above. You should have a fresh Budibase installation.
|
Follow from **Step 3. Install and Build** in the setup guide above. You should have a fresh Budibase installation.
|
||||||
|
|
||||||
|
### Running tests
|
||||||
|
|
||||||
|
#### End-to-end Tests
|
||||||
|
|
||||||
|
Budibase uses Cypress to run a number of E2E tests. To run the tests execute the following command in the root folder:
|
||||||
|
|
||||||
|
```
|
||||||
|
yarn test:e2e
|
||||||
|
```
|
||||||
|
|
||||||
|
Or if you are in the builder you can run `yarn cy:test`.
|
||||||
|
|
||||||
|
|
||||||
### Other Useful Information
|
### Other Useful Information
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
"@rollup/plugin-json": "^4.0.2",
|
"@rollup/plugin-json": "^4.0.2",
|
||||||
"babel-eslint": "^10.0.3",
|
"babel-eslint": "^10.0.3",
|
||||||
"eslint": "^6.8.0",
|
"eslint": "^6.8.0",
|
||||||
|
"eslint-plugin-cypress": "^2.11.1",
|
||||||
"eslint-plugin-prettier": "^3.1.2",
|
"eslint-plugin-prettier": "^3.1.2",
|
||||||
"eslint-plugin-svelte3": "^2.7.3",
|
"eslint-plugin-svelte3": "^2.7.3",
|
||||||
"lerna": "3.14.1",
|
"lerna": "3.14.1",
|
||||||
|
@ -24,10 +25,11 @@
|
||||||
"test": "lerna run test",
|
"test": "lerna run test",
|
||||||
"lint": "eslint packages",
|
"lint": "eslint packages",
|
||||||
"lint:fix": "eslint --fix packages",
|
"lint:fix": "eslint --fix packages",
|
||||||
"format": "prettier --write \"{,!(node_modules)/**/}*.{js,jsx,svelte}\""
|
"format": "prettier --write \"{,!(node_modules)/**/}*.{js,jsx,svelte}\"",
|
||||||
|
"test:e2e": "lerna run cy:test"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@material/icon-button": "4.0.0",
|
"@material/icon-button": "4.0.0",
|
||||||
"date-fns": "^2.10.0"
|
"date-fns": "^2.10.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,4 +5,5 @@ package-lock.json
|
||||||
yarn.lock
|
yarn.lock
|
||||||
release/
|
release/
|
||||||
dist/
|
dist/
|
||||||
|
cypress/screenshots
|
||||||
routify
|
routify
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"baseUrl": "http://localhost:4001/_builder/",
|
||||||
|
"video": false
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"name": "Using fixtures to represent data",
|
||||||
|
"email": "hello@cypress.io",
|
||||||
|
"body": "Fixtures are a great way to mock data for responses to routes"
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"id": 8739,
|
||||||
|
"name": "Jane",
|
||||||
|
"email": "jane@example.com"
|
||||||
|
}
|
|
@ -0,0 +1,232 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"name": "Leanne Graham",
|
||||||
|
"username": "Bret",
|
||||||
|
"email": "Sincere@april.biz",
|
||||||
|
"address": {
|
||||||
|
"street": "Kulas Light",
|
||||||
|
"suite": "Apt. 556",
|
||||||
|
"city": "Gwenborough",
|
||||||
|
"zipcode": "92998-3874",
|
||||||
|
"geo": {
|
||||||
|
"lat": "-37.3159",
|
||||||
|
"lng": "81.1496"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"phone": "1-770-736-8031 x56442",
|
||||||
|
"website": "hildegard.org",
|
||||||
|
"company": {
|
||||||
|
"name": "Romaguera-Crona",
|
||||||
|
"catchPhrase": "Multi-layered client-server neural-net",
|
||||||
|
"bs": "harness real-time e-markets"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"name": "Ervin Howell",
|
||||||
|
"username": "Antonette",
|
||||||
|
"email": "Shanna@melissa.tv",
|
||||||
|
"address": {
|
||||||
|
"street": "Victor Plains",
|
||||||
|
"suite": "Suite 879",
|
||||||
|
"city": "Wisokyburgh",
|
||||||
|
"zipcode": "90566-7771",
|
||||||
|
"geo": {
|
||||||
|
"lat": "-43.9509",
|
||||||
|
"lng": "-34.4618"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"phone": "010-692-6593 x09125",
|
||||||
|
"website": "anastasia.net",
|
||||||
|
"company": {
|
||||||
|
"name": "Deckow-Crist",
|
||||||
|
"catchPhrase": "Proactive didactic contingency",
|
||||||
|
"bs": "synergize scalable supply-chains"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"name": "Clementine Bauch",
|
||||||
|
"username": "Samantha",
|
||||||
|
"email": "Nathan@yesenia.net",
|
||||||
|
"address": {
|
||||||
|
"street": "Douglas Extension",
|
||||||
|
"suite": "Suite 847",
|
||||||
|
"city": "McKenziehaven",
|
||||||
|
"zipcode": "59590-4157",
|
||||||
|
"geo": {
|
||||||
|
"lat": "-68.6102",
|
||||||
|
"lng": "-47.0653"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"phone": "1-463-123-4447",
|
||||||
|
"website": "ramiro.info",
|
||||||
|
"company": {
|
||||||
|
"name": "Romaguera-Jacobson",
|
||||||
|
"catchPhrase": "Face to face bifurcated interface",
|
||||||
|
"bs": "e-enable strategic applications"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 4,
|
||||||
|
"name": "Patricia Lebsack",
|
||||||
|
"username": "Karianne",
|
||||||
|
"email": "Julianne.OConner@kory.org",
|
||||||
|
"address": {
|
||||||
|
"street": "Hoeger Mall",
|
||||||
|
"suite": "Apt. 692",
|
||||||
|
"city": "South Elvis",
|
||||||
|
"zipcode": "53919-4257",
|
||||||
|
"geo": {
|
||||||
|
"lat": "29.4572",
|
||||||
|
"lng": "-164.2990"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"phone": "493-170-9623 x156",
|
||||||
|
"website": "kale.biz",
|
||||||
|
"company": {
|
||||||
|
"name": "Robel-Corkery",
|
||||||
|
"catchPhrase": "Multi-tiered zero tolerance productivity",
|
||||||
|
"bs": "transition cutting-edge web services"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 5,
|
||||||
|
"name": "Chelsey Dietrich",
|
||||||
|
"username": "Kamren",
|
||||||
|
"email": "Lucio_Hettinger@annie.ca",
|
||||||
|
"address": {
|
||||||
|
"street": "Skiles Walks",
|
||||||
|
"suite": "Suite 351",
|
||||||
|
"city": "Roscoeview",
|
||||||
|
"zipcode": "33263",
|
||||||
|
"geo": {
|
||||||
|
"lat": "-31.8129",
|
||||||
|
"lng": "62.5342"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"phone": "(254)954-1289",
|
||||||
|
"website": "demarco.info",
|
||||||
|
"company": {
|
||||||
|
"name": "Keebler LLC",
|
||||||
|
"catchPhrase": "User-centric fault-tolerant solution",
|
||||||
|
"bs": "revolutionize end-to-end systems"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 6,
|
||||||
|
"name": "Mrs. Dennis Schulist",
|
||||||
|
"username": "Leopoldo_Corkery",
|
||||||
|
"email": "Karley_Dach@jasper.info",
|
||||||
|
"address": {
|
||||||
|
"street": "Norberto Crossing",
|
||||||
|
"suite": "Apt. 950",
|
||||||
|
"city": "South Christy",
|
||||||
|
"zipcode": "23505-1337",
|
||||||
|
"geo": {
|
||||||
|
"lat": "-71.4197",
|
||||||
|
"lng": "71.7478"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"phone": "1-477-935-8478 x6430",
|
||||||
|
"website": "ola.org",
|
||||||
|
"company": {
|
||||||
|
"name": "Considine-Lockman",
|
||||||
|
"catchPhrase": "Synchronised bottom-line interface",
|
||||||
|
"bs": "e-enable innovative applications"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 7,
|
||||||
|
"name": "Kurtis Weissnat",
|
||||||
|
"username": "Elwyn.Skiles",
|
||||||
|
"email": "Telly.Hoeger@billy.biz",
|
||||||
|
"address": {
|
||||||
|
"street": "Rex Trail",
|
||||||
|
"suite": "Suite 280",
|
||||||
|
"city": "Howemouth",
|
||||||
|
"zipcode": "58804-1099",
|
||||||
|
"geo": {
|
||||||
|
"lat": "24.8918",
|
||||||
|
"lng": "21.8984"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"phone": "210.067.6132",
|
||||||
|
"website": "elvis.io",
|
||||||
|
"company": {
|
||||||
|
"name": "Johns Group",
|
||||||
|
"catchPhrase": "Configurable multimedia task-force",
|
||||||
|
"bs": "generate enterprise e-tailers"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 8,
|
||||||
|
"name": "Nicholas Runolfsdottir V",
|
||||||
|
"username": "Maxime_Nienow",
|
||||||
|
"email": "Sherwood@rosamond.me",
|
||||||
|
"address": {
|
||||||
|
"street": "Ellsworth Summit",
|
||||||
|
"suite": "Suite 729",
|
||||||
|
"city": "Aliyaview",
|
||||||
|
"zipcode": "45169",
|
||||||
|
"geo": {
|
||||||
|
"lat": "-14.3990",
|
||||||
|
"lng": "-120.7677"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"phone": "586.493.6943 x140",
|
||||||
|
"website": "jacynthe.com",
|
||||||
|
"company": {
|
||||||
|
"name": "Abernathy Group",
|
||||||
|
"catchPhrase": "Implemented secondary concept",
|
||||||
|
"bs": "e-enable extensible e-tailers"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 9,
|
||||||
|
"name": "Glenna Reichert",
|
||||||
|
"username": "Delphine",
|
||||||
|
"email": "Chaim_McDermott@dana.io",
|
||||||
|
"address": {
|
||||||
|
"street": "Dayna Park",
|
||||||
|
"suite": "Suite 449",
|
||||||
|
"city": "Bartholomebury",
|
||||||
|
"zipcode": "76495-3109",
|
||||||
|
"geo": {
|
||||||
|
"lat": "24.6463",
|
||||||
|
"lng": "-168.8889"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"phone": "(775)976-6794 x41206",
|
||||||
|
"website": "conrad.com",
|
||||||
|
"company": {
|
||||||
|
"name": "Yost and Sons",
|
||||||
|
"catchPhrase": "Switchable contextually-based project",
|
||||||
|
"bs": "aggregate real-time technologies"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 10,
|
||||||
|
"name": "Clementina DuBuque",
|
||||||
|
"username": "Moriah.Stanton",
|
||||||
|
"email": "Rey.Padberg@karina.biz",
|
||||||
|
"address": {
|
||||||
|
"street": "Kattie Turnpike",
|
||||||
|
"suite": "Suite 198",
|
||||||
|
"city": "Lebsackbury",
|
||||||
|
"zipcode": "31428-2261",
|
||||||
|
"geo": {
|
||||||
|
"lat": "-38.2386",
|
||||||
|
"lng": "57.2232"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"phone": "024-648-3804",
|
||||||
|
"website": "ambrose.net",
|
||||||
|
"company": {
|
||||||
|
"name": "Hoeger LLC",
|
||||||
|
"catchPhrase": "Centralized empowering task-force",
|
||||||
|
"bs": "target end-to-end models"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
|
@ -0,0 +1,17 @@
|
||||||
|
context('Create an Application', () => {
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.visit('localhost:4001/_builder')
|
||||||
|
})
|
||||||
|
|
||||||
|
// https://on.cypress.io/interacting-with-elements
|
||||||
|
|
||||||
|
it('should create a new application', () => {
|
||||||
|
// https://on.cypress.io/type
|
||||||
|
cy.createApp('My Cool App', 'This is a description')
|
||||||
|
|
||||||
|
cy.visit('localhost:4001/_builder')
|
||||||
|
|
||||||
|
cy.contains('My Cool App').should('exist')
|
||||||
|
})
|
||||||
|
})
|
|
@ -0,0 +1,53 @@
|
||||||
|
context('Create Components', () => {
|
||||||
|
|
||||||
|
before(() => {
|
||||||
|
cy.visit('localhost:4001/_builder')
|
||||||
|
// https://on.cypress.io/type
|
||||||
|
cy.createApp('Model App', 'Model App Description')
|
||||||
|
cy.createModel('dog', 'name', 'age')
|
||||||
|
cy.addRecord('bob', '15')
|
||||||
|
})
|
||||||
|
|
||||||
|
// https://on.cypress.io/interacting-with-elements
|
||||||
|
it('should add a container', () => {
|
||||||
|
cy.contains('frontend').click()
|
||||||
|
cy.get('.switcher > :nth-child(2)').click()
|
||||||
|
|
||||||
|
cy.contains('Container').click()
|
||||||
|
})
|
||||||
|
it('should add a headline', () => {
|
||||||
|
cy.addHeadlineComponent('An Amazing headline!')
|
||||||
|
|
||||||
|
getIframeBody().contains('An Amazing headline!')
|
||||||
|
})
|
||||||
|
it('change the font size of the headline', () => {
|
||||||
|
cy.contains('Typography').click()
|
||||||
|
cy.get('input[name="font-size"]')
|
||||||
|
.type('60px')
|
||||||
|
cy.contains('Design').click()
|
||||||
|
|
||||||
|
getIframeBody().contains('An Amazing headline!').should('have.css', 'font-size', '60px')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const getIframeDocument = () => {
|
||||||
|
return cy
|
||||||
|
.get('iframe')
|
||||||
|
// Cypress yields jQuery element, which has the real
|
||||||
|
// DOM element under property "0".
|
||||||
|
// From the real DOM iframe element we can get
|
||||||
|
// the "document" element, it is stored in "contentDocument" property
|
||||||
|
// Cypress "its" command can access deep properties using dot notation
|
||||||
|
// https://on.cypress.io/its
|
||||||
|
.its('0.contentDocument').should('exist')
|
||||||
|
}
|
||||||
|
|
||||||
|
const getIframeBody = () => {
|
||||||
|
// get the document
|
||||||
|
return getIframeDocument()
|
||||||
|
// automatically retries until body is loaded
|
||||||
|
.its('body').should('not.be.undefined')
|
||||||
|
// wraps "body" DOM element to allow
|
||||||
|
// chaining more Cypress commands, like ".find(...)"
|
||||||
|
.then(cy.wrap)
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
context('Create a Model', () => {
|
||||||
|
|
||||||
|
before(() => {
|
||||||
|
cy.visit('localhost:4001/_builder')
|
||||||
|
// https://on.cypress.io/type
|
||||||
|
cy.createApp('Model App', 'Model App Description')
|
||||||
|
})
|
||||||
|
|
||||||
|
// https://on.cypress.io/interacting-with-elements
|
||||||
|
it('should create a new model', () => {
|
||||||
|
|
||||||
|
cy.createModel('dog', 'name', 'age')
|
||||||
|
|
||||||
|
// Check if model exists
|
||||||
|
cy.get('.title').should('have.text', 'dog')
|
||||||
|
})
|
||||||
|
it('should add a record', () => {
|
||||||
|
cy.addRecord('bob', '15')
|
||||||
|
|
||||||
|
cy.contains('bob').should('have.text', 'bob')
|
||||||
|
})
|
||||||
|
})
|
|
@ -0,0 +1,19 @@
|
||||||
|
context('Create a User', () => {
|
||||||
|
|
||||||
|
before(() => {
|
||||||
|
cy.visit('localhost:4001/_builder')
|
||||||
|
// https://on.cypress.io/type
|
||||||
|
cy.createApp('User App', 'This app is used to test user creation')
|
||||||
|
})
|
||||||
|
|
||||||
|
// https://on.cypress.io/interacting-with-elements
|
||||||
|
it('should create a user', () => {
|
||||||
|
// Close Model modal that shows up after creating an app
|
||||||
|
cy.get('.close').click()
|
||||||
|
|
||||||
|
cy.createUser('bbuser', 'test', 'ADMIN')
|
||||||
|
|
||||||
|
// Check to make sure user was created!
|
||||||
|
cy.contains('bbuser').should('have.text', 'bbuser')
|
||||||
|
})
|
||||||
|
})
|
|
@ -0,0 +1,46 @@
|
||||||
|
context('Create a workflow', () => {
|
||||||
|
|
||||||
|
before(() => {
|
||||||
|
cy.visit('localhost:4001/_builder')
|
||||||
|
|
||||||
|
cy.createApp('Workflow Test App', 'This app is used to test that workflows do in fact work!')
|
||||||
|
})
|
||||||
|
|
||||||
|
// https://on.cypress.io/interacting-with-elements
|
||||||
|
it('should create a workflow', () => {
|
||||||
|
cy.createModel('dog', 'name', 'age')
|
||||||
|
cy.createUser('bbuser', 'test', 'ADMIN')
|
||||||
|
|
||||||
|
|
||||||
|
cy.contains('workflow').click()
|
||||||
|
cy.get('.new-workflow-button').click()
|
||||||
|
cy.get('input').type('Add Record')
|
||||||
|
cy.contains('Save').click()
|
||||||
|
|
||||||
|
// Add trigger
|
||||||
|
cy.get('[data-cy=add-workflow-component]').click()
|
||||||
|
cy.get('[data-cy=RECORD_SAVED]').click()
|
||||||
|
cy.get('.budibase__input').select('dog')
|
||||||
|
|
||||||
|
// Create action
|
||||||
|
cy.get('[data-cy=SAVE_RECORD]').click()
|
||||||
|
cy.get(':nth-child(2) > .budibase__input').type('goodboy')
|
||||||
|
cy.get(':nth-child(3) > .budibase__input').type('11')
|
||||||
|
|
||||||
|
// Save
|
||||||
|
cy.get('[data-cy=save-workflow-setup]').click()
|
||||||
|
cy.get('.workflow-button').click()
|
||||||
|
|
||||||
|
// Activate Workflow
|
||||||
|
cy.get('[data-cy=activate-workflow]').click()
|
||||||
|
|
||||||
|
})
|
||||||
|
it('should add record when a new record is added', () => {
|
||||||
|
cy.contains('backend').click()
|
||||||
|
|
||||||
|
cy.addRecord('bob', '15')
|
||||||
|
|
||||||
|
cy.contains('goodboy').should('have.text', 'goodboy')
|
||||||
|
|
||||||
|
})
|
||||||
|
})
|
|
@ -0,0 +1,22 @@
|
||||||
|
/// <reference types="cypress" />
|
||||||
|
// ***********************************************************
|
||||||
|
// This example plugins/index.js can be used to load plugins
|
||||||
|
//
|
||||||
|
// You can change the location of this file or turn off loading
|
||||||
|
// the plugins file with the 'pluginsFile' configuration option.
|
||||||
|
//
|
||||||
|
// You can read more here:
|
||||||
|
// https://on.cypress.io/plugins-guide
|
||||||
|
// ***********************************************************
|
||||||
|
|
||||||
|
// This function is called when a project is opened or re-opened (e.g. due to
|
||||||
|
// the project's config changing)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {Cypress.PluginConfig}
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
module.exports = (on, config) => {
|
||||||
|
// `on` is used to hook into various events Cypress emits
|
||||||
|
// `config` is the resolved Cypress config
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
// What this script does:
|
||||||
|
// 1. Removes the old test folder if it exists (.budibase-cypress)
|
||||||
|
// 2. Initialises using `.budibase-cypress`
|
||||||
|
// 3. Runs the server using said folder
|
||||||
|
|
||||||
|
const rimraf = require("rimraf")
|
||||||
|
const { join } = require("path")
|
||||||
|
const homedir = join(require("os").homedir(), ".budibase-cypress")
|
||||||
|
const init = require("../../cli/src/commands/init/initHandler")
|
||||||
|
const run = require("../../cli/src/commands/run/runHandler")
|
||||||
|
|
||||||
|
rimraf.sync(homedir)
|
||||||
|
|
||||||
|
init({ dir: homedir, clientId: "cypress-test" }).then(() => {
|
||||||
|
delete require.cache[require.resolve("../../server/src/environment")]
|
||||||
|
run({ dir: homedir })
|
||||||
|
})
|
|
@ -0,0 +1,105 @@
|
||||||
|
// ***********************************************
|
||||||
|
// This example commands.js shows you how to
|
||||||
|
// create various custom commands and overwrite
|
||||||
|
// existing commands.
|
||||||
|
//
|
||||||
|
// For more comprehensive examples of custom
|
||||||
|
// commands please read more here:
|
||||||
|
// https://on.cypress.io/custom-commands
|
||||||
|
// ***********************************************
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// -- This is a parent command --
|
||||||
|
// Cypress.Commands.add("login", (email, password) => { ... })
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// -- This is a child command --
|
||||||
|
// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... })
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// -- This is a dual command --
|
||||||
|
// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... })
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// -- This will overwrite an existing command --
|
||||||
|
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
|
||||||
|
|
||||||
|
Cypress.Commands.add("createApp", (name, description) => {
|
||||||
|
cy.get(".banner-button")
|
||||||
|
.click()
|
||||||
|
.get('input[name="name"]')
|
||||||
|
.type(name)
|
||||||
|
.should("have.value", name)
|
||||||
|
|
||||||
|
cy.get('textarea[name="description"]')
|
||||||
|
.type(description)
|
||||||
|
.should("have.value", description)
|
||||||
|
|
||||||
|
cy.contains("Save").click()
|
||||||
|
})
|
||||||
|
Cypress.Commands.add("createModel", (modelName, firstField, secondField) => {
|
||||||
|
// Enter model name
|
||||||
|
cy.get("[data-cy=Name]")
|
||||||
|
.click()
|
||||||
|
.type(modelName)
|
||||||
|
|
||||||
|
// Add 'name' field
|
||||||
|
cy.get("[data-cy=add-new-model-field]").click()
|
||||||
|
cy.get("[data-cy=Name]")
|
||||||
|
.click()
|
||||||
|
.type(firstField)
|
||||||
|
cy.contains("Save").click()
|
||||||
|
|
||||||
|
// Add 'age' field
|
||||||
|
cy.get("[data-cy=add-new-model-field]").click()
|
||||||
|
|
||||||
|
cy.get("[data-cy=Name]")
|
||||||
|
.click()
|
||||||
|
.type(secondField)
|
||||||
|
cy.get("select").select("number")
|
||||||
|
cy.contains("Save").click()
|
||||||
|
cy.contains(secondField).should("exist")
|
||||||
|
|
||||||
|
// Save model
|
||||||
|
cy.contains("Save").click()
|
||||||
|
})
|
||||||
|
Cypress.Commands.add("addRecord", (firstField, secondField) => {
|
||||||
|
cy.contains("Create new record").click()
|
||||||
|
|
||||||
|
cy.get("[data-cy=name-input]")
|
||||||
|
.click()
|
||||||
|
.type(firstField)
|
||||||
|
cy.get("[data-cy=age-input]")
|
||||||
|
.click()
|
||||||
|
.type(secondField)
|
||||||
|
|
||||||
|
// Save
|
||||||
|
cy.contains("Save").click()
|
||||||
|
})
|
||||||
|
|
||||||
|
Cypress.Commands.add("createUser", (username, password, level) => {
|
||||||
|
// Create User
|
||||||
|
cy.get(".nav-group-header > .ri-add-line").click()
|
||||||
|
|
||||||
|
cy.get("[data-cy=username]").type(username)
|
||||||
|
cy.get("[data-cy=password]").type(password)
|
||||||
|
cy.get("[data-cy=accessLevel]").select(level)
|
||||||
|
|
||||||
|
// Save
|
||||||
|
cy.contains("Save").click()
|
||||||
|
})
|
||||||
|
|
||||||
|
Cypress.Commands.add("addHeadlineComponent", text => {
|
||||||
|
cy.get(".switcher > :nth-child(2)").click()
|
||||||
|
|
||||||
|
cy.get("[data-cy=Text]").click()
|
||||||
|
cy.get("[data-cy=Headline]").click()
|
||||||
|
cy.get(".tabs > :nth-child(2)").click()
|
||||||
|
cy.get('input[type="text"]').type(text)
|
||||||
|
cy.contains("Design").click()
|
||||||
|
})
|
||||||
|
Cypress.Commands.add("addButtonComponent", () => {
|
||||||
|
cy.get(".switcher > :nth-child(2)").click()
|
||||||
|
|
||||||
|
cy.get("[data-cy=Button]").click()
|
||||||
|
})
|
|
@ -0,0 +1,3 @@
|
||||||
|
Cypress.Cookies.defaults({
|
||||||
|
whitelist: "builder:token",
|
||||||
|
})
|
|
@ -0,0 +1,21 @@
|
||||||
|
// ***********************************************************
|
||||||
|
// This example support/index.js is processed and
|
||||||
|
// loaded automatically before your test files.
|
||||||
|
//
|
||||||
|
// This is a great place to put global configuration and
|
||||||
|
// behavior that modifies Cypress.
|
||||||
|
//
|
||||||
|
// You can change the location of this file or turn off
|
||||||
|
// automatically serving support files with the
|
||||||
|
// 'supportFile' configuration option.
|
||||||
|
//
|
||||||
|
// You can read more here:
|
||||||
|
// https://on.cypress.io/configuration
|
||||||
|
// ***********************************************************
|
||||||
|
|
||||||
|
// Import commands.js using ES2015 syntax:
|
||||||
|
import "./cookies"
|
||||||
|
import "./commands"
|
||||||
|
|
||||||
|
// Alternatively you can use CommonJS syntax:
|
||||||
|
// require('./commands')
|
|
@ -9,7 +9,11 @@
|
||||||
"test": "jest",
|
"test": "jest",
|
||||||
"test:watch": "jest --watchAll",
|
"test:watch": "jest --watchAll",
|
||||||
"dev:builder": "routify --routify-dir routify -c rollup",
|
"dev:builder": "routify --routify-dir routify -c rollup",
|
||||||
"rollup": "rollup -c -w"
|
"rollup": "rollup -c -w",
|
||||||
|
"cy:setup": "node ./cypress/setup.js",
|
||||||
|
"cy:run": "cypress run",
|
||||||
|
"cy:open": "cypress open",
|
||||||
|
"cy:test": "start-server-and-test cy:setup http://localhost:4001/_builder cy:run"
|
||||||
},
|
},
|
||||||
"jest": {
|
"jest": {
|
||||||
"globals": {
|
"globals": {
|
||||||
|
@ -35,11 +39,19 @@
|
||||||
},
|
},
|
||||||
"transformIgnorePatterns": [
|
"transformIgnorePatterns": [
|
||||||
"/node_modules/(?!svelte).+\\.js$"
|
"/node_modules/(?!svelte).+\\.js$"
|
||||||
|
],
|
||||||
|
"modulePathIgnorePatterns": [
|
||||||
|
"<rootDir>/cypress/"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"eslintConfig": {
|
||||||
|
"extends": [
|
||||||
|
"plugin:cypress/recommended"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@beyonk/svelte-notifications": "^2.0.3",
|
"@beyonk/svelte-notifications": "^2.0.3",
|
||||||
"@budibase/bbui": "^1.1.1",
|
"@budibase/bbui": "^1.8.0",
|
||||||
"@budibase/client": "^0.0.32",
|
"@budibase/client": "^0.0.32",
|
||||||
"@nx-js/compiler-util": "^2.0.0",
|
"@nx-js/compiler-util": "^2.0.0",
|
||||||
"codemirror": "^5.51.0",
|
"codemirror": "^5.51.0",
|
||||||
|
@ -67,10 +79,12 @@
|
||||||
"@sveltech/routify": "1.7.11",
|
"@sveltech/routify": "1.7.11",
|
||||||
"babel-jest": "^24.8.0",
|
"babel-jest": "^24.8.0",
|
||||||
"browser-sync": "^2.26.7",
|
"browser-sync": "^2.26.7",
|
||||||
|
"cypress": "^4.8.0",
|
||||||
"http-proxy-middleware": "^0.19.1",
|
"http-proxy-middleware": "^0.19.1",
|
||||||
"jest": "^24.8.0",
|
"jest": "^24.8.0",
|
||||||
"ncp": "^2.0.0",
|
"ncp": "^2.0.0",
|
||||||
"npm-run-all": "^4.1.5",
|
"npm-run-all": "^4.1.5",
|
||||||
|
"rimraf": "^3.0.2",
|
||||||
"rollup": "^1.12.0",
|
"rollup": "^1.12.0",
|
||||||
"rollup-plugin-alias": "^1.5.2",
|
"rollup-plugin-alias": "^1.5.2",
|
||||||
"rollup-plugin-browsersync": "^1.0.0",
|
"rollup-plugin-browsersync": "^1.0.0",
|
||||||
|
@ -83,6 +97,7 @@
|
||||||
"rollup-plugin-svelte": "^5.0.3",
|
"rollup-plugin-svelte": "^5.0.3",
|
||||||
"rollup-plugin-terser": "^4.0.4",
|
"rollup-plugin-terser": "^4.0.4",
|
||||||
"rollup-plugin-url": "^2.2.2",
|
"rollup-plugin-url": "^2.2.2",
|
||||||
|
"start-server-and-test": "^1.11.0",
|
||||||
"svelte": "3.23.x"
|
"svelte": "3.23.x"
|
||||||
},
|
},
|
||||||
"gitHead": "115189f72a850bfb52b65ec61d932531bf327072"
|
"gitHead": "115189f72a850bfb52b65ec61d932531bf327072"
|
||||||
|
|
|
@ -165,6 +165,10 @@ export default {
|
||||||
src: "node_modules/@budibase/client/dist/budibase-client.esm.mjs",
|
src: "node_modules/@budibase/client/dist/budibase-client.esm.mjs",
|
||||||
dest: outputpath,
|
dest: outputpath,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
src: "node_modules/@budibase/bbui/dist/bbui.css",
|
||||||
|
dest: outputpath,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
|
|
@ -1,17 +1,13 @@
|
||||||
const apiCall = method => async (url, body) => {
|
const apiCall = method => async (url, body) => {
|
||||||
|
const headers = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
}
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
method: method,
|
method: method,
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
"x-user-agent": "Budibase Builder",
|
|
||||||
},
|
|
||||||
body: body && JSON.stringify(body),
|
body: body && JSON.stringify(body),
|
||||||
|
headers,
|
||||||
})
|
})
|
||||||
|
|
||||||
// if (response.status === 500) {
|
|
||||||
// throw new Error("Server Error");
|
|
||||||
// }
|
|
||||||
|
|
||||||
return response
|
return response
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -22,9 +18,9 @@ export const del = apiCall("DELETE")
|
||||||
export const put = apiCall("PUT")
|
export const put = apiCall("PUT")
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
post,
|
post: apiCall("POST"),
|
||||||
get,
|
get: apiCall("GET"),
|
||||||
patch,
|
patch: apiCall("PATCH"),
|
||||||
delete: del,
|
delete: apiCall("DELETE"),
|
||||||
put,
|
put: apiCall("PUT"),
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,8 +24,8 @@ export const getBackendUiStore = () => {
|
||||||
store.actions = {
|
store.actions = {
|
||||||
database: {
|
database: {
|
||||||
select: async db => {
|
select: async db => {
|
||||||
const modelsResponse = await api.get(`/api/${db._id}/models`)
|
const modelsResponse = await api.get(`/api/models`)
|
||||||
const viewsResponse = await api.get(`/api/${db._id}/views`)
|
const viewsResponse = await api.get(`/api/views`)
|
||||||
const models = await modelsResponse.json()
|
const models = await modelsResponse.json()
|
||||||
const views = await viewsResponse.json()
|
const views = await viewsResponse.json()
|
||||||
store.update(state => {
|
store.update(state => {
|
||||||
|
|
|
@ -3,8 +3,8 @@ import api from "../../api"
|
||||||
import Workflow from "./Workflow"
|
import Workflow from "./Workflow"
|
||||||
|
|
||||||
const workflowActions = store => ({
|
const workflowActions = store => ({
|
||||||
fetch: async instanceId => {
|
fetch: async () => {
|
||||||
const WORKFLOWS_URL = `/api/${instanceId}/workflows`
|
const WORKFLOWS_URL = `/api/workflows`
|
||||||
const workflowResponse = await api.get(WORKFLOWS_URL)
|
const workflowResponse = await api.get(WORKFLOWS_URL)
|
||||||
const json = await workflowResponse.json()
|
const json = await workflowResponse.json()
|
||||||
store.update(state => {
|
store.update(state => {
|
||||||
|
@ -12,14 +12,14 @@ const workflowActions = store => ({
|
||||||
return state
|
return state
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
create: async ({ instanceId, name }) => {
|
create: async ({ name }) => {
|
||||||
const workflow = {
|
const workflow = {
|
||||||
name,
|
name,
|
||||||
definition: {
|
definition: {
|
||||||
steps: [],
|
steps: [],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
const CREATE_WORKFLOW_URL = `/api/${instanceId}/workflows`
|
const CREATE_WORKFLOW_URL = `/api/workflows`
|
||||||
const response = await api.post(CREATE_WORKFLOW_URL, workflow)
|
const response = await api.post(CREATE_WORKFLOW_URL, workflow)
|
||||||
const json = await response.json()
|
const json = await response.json()
|
||||||
store.update(state => {
|
store.update(state => {
|
||||||
|
@ -28,8 +28,8 @@ const workflowActions = store => ({
|
||||||
return state
|
return state
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
save: async ({ instanceId, workflow }) => {
|
save: async ({ workflow }) => {
|
||||||
const UPDATE_WORKFLOW_URL = `/api/${instanceId}/workflows`
|
const UPDATE_WORKFLOW_URL = `/api/workflows`
|
||||||
const response = await api.put(UPDATE_WORKFLOW_URL, workflow)
|
const response = await api.put(UPDATE_WORKFLOW_URL, workflow)
|
||||||
const json = await response.json()
|
const json = await response.json()
|
||||||
store.update(state => {
|
store.update(state => {
|
||||||
|
@ -42,8 +42,8 @@ const workflowActions = store => ({
|
||||||
return state
|
return state
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
update: async ({ instanceId, workflow }) => {
|
update: async ({ workflow }) => {
|
||||||
const UPDATE_WORKFLOW_URL = `/api/${instanceId}/workflows`
|
const UPDATE_WORKFLOW_URL = `/api/workflows`
|
||||||
const response = await api.put(UPDATE_WORKFLOW_URL, workflow)
|
const response = await api.put(UPDATE_WORKFLOW_URL, workflow)
|
||||||
const json = await response.json()
|
const json = await response.json()
|
||||||
store.update(state => {
|
store.update(state => {
|
||||||
|
@ -55,9 +55,9 @@ const workflowActions = store => ({
|
||||||
return state
|
return state
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
delete: async ({ instanceId, workflow }) => {
|
delete: async ({ workflow }) => {
|
||||||
const { _id, _rev } = workflow
|
const { _id, _rev } = workflow
|
||||||
const DELETE_WORKFLOW_URL = `/api/${instanceId}/workflows/${_id}/${_rev}`
|
const DELETE_WORKFLOW_URL = `/api/workflows/${_id}/${_rev}`
|
||||||
await api.delete(DELETE_WORKFLOW_URL)
|
await api.delete(DELETE_WORKFLOW_URL)
|
||||||
|
|
||||||
store.update(state => {
|
store.update(state => {
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
import { buildStyle } from "../../helpers.js"
|
import { buildStyle } from "../../helpers.js"
|
||||||
export let value = ""
|
export let value = ""
|
||||||
|
export let name = ""
|
||||||
export let textAlign = "left"
|
export let textAlign = "left"
|
||||||
export let width = "160px"
|
export let width = "160px"
|
||||||
export let placeholder = ""
|
export let placeholder = ""
|
||||||
|
@ -25,6 +26,7 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
|
{name}
|
||||||
class:centerPlaceholder
|
class:centerPlaceholder
|
||||||
type="text"
|
type="text"
|
||||||
value={displayValue}
|
value={displayValue}
|
||||||
|
|
|
@ -13,6 +13,7 @@
|
||||||
<label class="uk-form-label">{label}</label>
|
<label class="uk-form-label">{label}</label>
|
||||||
<div class="uk-form-controls">
|
<div class="uk-form-controls">
|
||||||
<input
|
<input
|
||||||
|
data-cy={label}
|
||||||
class="budibase__input"
|
class="budibase__input"
|
||||||
class:uk-form-danger={hasError}
|
class:uk-form-danger={hasError}
|
||||||
on:change
|
on:change
|
||||||
|
|
|
@ -53,12 +53,10 @@
|
||||||
let views = []
|
let views = []
|
||||||
let currentPage = 0
|
let currentPage = 0
|
||||||
|
|
||||||
$: instanceId = $backendUiStore.selectedDatabase._id
|
|
||||||
|
|
||||||
$: {
|
$: {
|
||||||
if ($backendUiStore.selectedView) {
|
if ($backendUiStore.selectedView) {
|
||||||
api
|
api
|
||||||
.fetchDataForView($backendUiStore.selectedView, instanceId)
|
.fetchDataForView($backendUiStore.selectedView)
|
||||||
.then(records => {
|
.then(records => {
|
||||||
data = records || []
|
data = records || []
|
||||||
headers = Object.keys($backendUiStore.selectedModel.schema).filter(
|
headers = Object.keys($backendUiStore.selectedModel.schema).filter(
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import api from "builderStore/api"
|
import api from "builderStore/api"
|
||||||
|
|
||||||
export async function createUser(user, instanceId) {
|
export async function createUser(user) {
|
||||||
const CREATE_USER_URL = `/api/${instanceId}/users`
|
const CREATE_USER_URL = `/api/users`
|
||||||
const response = await api.post(CREATE_USER_URL, user)
|
const response = await api.post(CREATE_USER_URL, user)
|
||||||
return await response.json()
|
return await response.json()
|
||||||
}
|
}
|
||||||
|
@ -14,21 +14,21 @@ export async function createDatabase(appname, instanceName) {
|
||||||
return await response.json()
|
return await response.json()
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteRecord(record, instanceId) {
|
export async function deleteRecord(record) {
|
||||||
const DELETE_RECORDS_URL = `/api/${instanceId}/${record._modelId}/records/${record._id}/${record._rev}`
|
const DELETE_RECORDS_URL = `/api/${record._modelId}/records/${record._id}/${record._rev}`
|
||||||
const response = await api.delete(DELETE_RECORDS_URL)
|
const response = await api.delete(DELETE_RECORDS_URL)
|
||||||
return response
|
return response
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function saveRecord(record, instanceId, modelId) {
|
export async function saveRecord(record, modelId) {
|
||||||
const SAVE_RECORDS_URL = `/api/${instanceId}/${modelId}/records`
|
const SAVE_RECORDS_URL = `/api/${modelId}/records`
|
||||||
const response = await api.post(SAVE_RECORDS_URL, record)
|
const response = await api.post(SAVE_RECORDS_URL, record)
|
||||||
|
|
||||||
return await response.json()
|
return await response.json()
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchDataForView(viewName, instanceId) {
|
export async function fetchDataForView(viewName) {
|
||||||
const FETCH_RECORDS_URL = `/api/${instanceId}/views/${viewName}`
|
const FETCH_RECORDS_URL = `/api/views/${viewName}`
|
||||||
|
|
||||||
const response = await api.get(FETCH_RECORDS_URL)
|
const response = await api.get(FETCH_RECORDS_URL)
|
||||||
return await response.json()
|
return await response.json()
|
||||||
|
|
|
@ -18,7 +18,6 @@
|
||||||
let fieldToEdit
|
let fieldToEdit
|
||||||
|
|
||||||
$: modelFields = model.schema ? Object.entries(model.schema) : []
|
$: modelFields = model.schema ? Object.entries(model.schema) : []
|
||||||
$: instanceId = $backendUiStore.selectedDatabase._id
|
|
||||||
|
|
||||||
function editField() {}
|
function editField() {}
|
||||||
|
|
||||||
|
@ -27,7 +26,7 @@
|
||||||
function onFinishedFieldEdit() {}
|
function onFinishedFieldEdit() {}
|
||||||
|
|
||||||
async function saveModel() {
|
async function saveModel() {
|
||||||
const SAVE_MODEL_URL = `/api/${instanceId}/models`
|
const SAVE_MODEL_URL = `/api/models`
|
||||||
const response = await api.post(SAVE_MODEL_URL, model)
|
const response = await api.post(SAVE_MODEL_URL, model)
|
||||||
const newModel = await response.json()
|
const newModel = await response.json()
|
||||||
backendUiStore.actions.models.create(newModel)
|
backendUiStore.actions.models.create(newModel)
|
||||||
|
@ -54,7 +53,10 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="table-controls">
|
<div class="table-controls">
|
||||||
<span class="label">Fields</span>
|
<span class="label">Fields</span>
|
||||||
<div class="hoverable new-field" on:click={() => (showFieldView = true)}>
|
<div
|
||||||
|
data-cy="add-new-model-field"
|
||||||
|
class="hoverable new-field"
|
||||||
|
on:click={() => (showFieldView = true)}>
|
||||||
Add new field
|
Add new field
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -14,8 +14,6 @@
|
||||||
let errors = []
|
let errors = []
|
||||||
let selectedModel
|
let selectedModel
|
||||||
|
|
||||||
$: instanceId = $backendUiStore.selectedDatabase._id
|
|
||||||
|
|
||||||
$: modelSchema = $backendUiStore.selectedModel
|
$: modelSchema = $backendUiStore.selectedModel
|
||||||
? Object.entries($backendUiStore.selectedModel.schema)
|
? Object.entries($backendUiStore.selectedModel.schema)
|
||||||
: []
|
: []
|
||||||
|
@ -49,7 +47,6 @@
|
||||||
...record,
|
...record,
|
||||||
modelId: $backendUiStore.selectedModel._id,
|
modelId: $backendUiStore.selectedModel._id,
|
||||||
},
|
},
|
||||||
instanceId,
|
|
||||||
$backendUiStore.selectedModel._id
|
$backendUiStore.selectedModel._id
|
||||||
)
|
)
|
||||||
if (recordResponse.errors) {
|
if (recordResponse.errors) {
|
||||||
|
|
|
@ -29,7 +29,7 @@
|
||||||
function deleteView() {}
|
function deleteView() {}
|
||||||
|
|
||||||
async function saveView() {
|
async function saveView() {
|
||||||
const SAVE_VIEW_URL = `/api/${instanceId}/views`
|
const SAVE_VIEW_URL = `/api/views`
|
||||||
const response = await api.post(SAVE_VIEW_URL, view)
|
const response = await api.post(SAVE_VIEW_URL, view)
|
||||||
backendUiStore.update(state => {
|
backendUiStore.update(state => {
|
||||||
state.views = [...state.views, response.view]
|
state.views = [...state.views, response.view]
|
||||||
|
|
|
@ -10,12 +10,11 @@
|
||||||
let accessLevelId
|
let accessLevelId
|
||||||
|
|
||||||
$: valid = username && password && accessLevelId
|
$: valid = username && password && accessLevelId
|
||||||
$: instanceId = $backendUiStore.selectedDatabase._id
|
|
||||||
$: appId = $store.appId
|
$: appId = $store.appId
|
||||||
|
|
||||||
async function createUser() {
|
async function createUser() {
|
||||||
const user = { name: username, username, password, accessLevelId }
|
const user = { name: username, username, password, accessLevelId }
|
||||||
const response = await api.createUser(user, instanceId)
|
const response = await api.createUser(user)
|
||||||
backendUiStore.actions.users.create(response)
|
backendUiStore.actions.users.create(response)
|
||||||
onClosed()
|
onClosed()
|
||||||
}
|
}
|
||||||
|
@ -29,15 +28,26 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="uk-margin">
|
<div class="uk-margin">
|
||||||
<label class="uk-form-label" for="form-stacked-text">Username</label>
|
<label class="uk-form-label" for="form-stacked-text">Username</label>
|
||||||
<input class="uk-input" type="text" bind:value={username} />
|
<input
|
||||||
|
data-cy="username"
|
||||||
|
class="uk-input"
|
||||||
|
type="text"
|
||||||
|
bind:value={username} />
|
||||||
</div>
|
</div>
|
||||||
<div class="uk-margin">
|
<div class="uk-margin">
|
||||||
<label class="uk-form-label" for="form-stacked-text">Password</label>
|
<label class="uk-form-label" for="form-stacked-text">Password</label>
|
||||||
<input class="uk-input" type="password" bind:value={password} />
|
<input
|
||||||
|
data-cy="password"
|
||||||
|
class="uk-input"
|
||||||
|
type="password"
|
||||||
|
bind:value={password} />
|
||||||
</div>
|
</div>
|
||||||
<div class="uk-margin">
|
<div class="uk-margin">
|
||||||
<label class="uk-form-label" for="form-stacked-text">Access Level</label>
|
<label class="uk-form-label" for="form-stacked-text">Access Level</label>
|
||||||
<select class="uk-select" bind:value={accessLevelId}>
|
<select
|
||||||
|
data-cy="accessLevel"
|
||||||
|
class="uk-select"
|
||||||
|
bind:value={accessLevelId}>
|
||||||
<option value="" />
|
<option value="" />
|
||||||
<option value="POWER_USER">Power User</option>
|
<option value="POWER_USER">Power User</option>
|
||||||
<option value="ADMIN">Admin</option>
|
<option value="ADMIN">Admin</option>
|
||||||
|
|
|
@ -6,7 +6,6 @@
|
||||||
export let record
|
export let record
|
||||||
export let onClosed
|
export let onClosed
|
||||||
|
|
||||||
$: instanceId = $backendUiStore.selectedDatabase._id
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
|
@ -25,7 +24,7 @@
|
||||||
<ActionButton
|
<ActionButton
|
||||||
alert
|
alert
|
||||||
on:click={async () => {
|
on:click={async () => {
|
||||||
await api.deleteRecord(record, instanceId)
|
await api.deleteRecord(record)
|
||||||
backendUiStore.actions.records.delete(record)
|
backendUiStore.actions.records.delete(record)
|
||||||
onClosed()
|
onClosed()
|
||||||
}}>
|
}}>
|
||||||
|
|
|
@ -32,6 +32,7 @@
|
||||||
|
|
||||||
{#if type === 'select'}
|
{#if type === 'select'}
|
||||||
<select
|
<select
|
||||||
|
data-cy="{label}-select"
|
||||||
class={determineClassName(type)}
|
class={determineClassName(type)}
|
||||||
bind:value
|
bind:value
|
||||||
class:uk-form-danger={errors.length > 0}>
|
class:uk-form-danger={errors.length > 0}>
|
||||||
|
@ -41,6 +42,7 @@
|
||||||
</select>
|
</select>
|
||||||
{:else}
|
{:else}
|
||||||
<input
|
<input
|
||||||
|
data-cy="{label}-input"
|
||||||
class={determineClassName(type)}
|
class={determineClassName(type)}
|
||||||
class:uk-form-danger={errors.length > 0}
|
class:uk-form-danger={errors.length > 0}
|
||||||
{checked}
|
{checked}
|
||||||
|
|
|
@ -7,7 +7,6 @@
|
||||||
CreateEditModelModal,
|
CreateEditModelModal,
|
||||||
CreateEditViewModal,
|
CreateEditViewModal,
|
||||||
} from "components/database/ModelDataTable/modals"
|
} from "components/database/ModelDataTable/modals"
|
||||||
import api from "builderStore/api"
|
|
||||||
|
|
||||||
const { open, close } = getContext("simple-modal")
|
const { open, close } = getContext("simple-modal")
|
||||||
|
|
||||||
|
|
|
@ -52,7 +52,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteModel(modelToDelete) {
|
async function deleteModel(modelToDelete) {
|
||||||
const DELETE_MODEL_URL = `/api/${instanceId}/models/${node._id}/${node._rev}`
|
const DELETE_MODEL_URL = `/api/models/${node._id}/${node._rev}`
|
||||||
const response = await api.delete(DELETE_MODEL_URL)
|
const response = await api.delete(DELETE_MODEL_URL)
|
||||||
backendUiStore.update(state => {
|
backendUiStore.update(state => {
|
||||||
state.models = state.models.filter(
|
state.models = state.models.filter(
|
||||||
|
|
|
@ -12,11 +12,10 @@
|
||||||
|
|
||||||
$: currentAppInfo = {
|
$: currentAppInfo = {
|
||||||
appname: $store.appname,
|
appname: $store.appname,
|
||||||
instanceId: $backendUiStore.selectedDatabase._id,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchUsers() {
|
async function fetchUsers() {
|
||||||
const FETCH_USERS_URL = `/api/${currentAppInfo.instanceId}/users`
|
const FETCH_USERS_URL = `/api/users`
|
||||||
const response = await api.get(FETCH_USERS_URL)
|
const response = await api.get(FETCH_USERS_URL)
|
||||||
const users = await response.json()
|
const users = await response.json()
|
||||||
backendUiStore.update(state => {
|
backendUiStore.update(state => {
|
||||||
|
|
|
@ -91,7 +91,6 @@
|
||||||
? screenPlaceholder
|
? screenPlaceholder
|
||||||
: $store.currentPreviewItem,
|
: $store.currentPreviewItem,
|
||||||
],
|
],
|
||||||
appRootPath: "",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$: selectedComponentType = getComponentTypeName($store.currentComponentInfo)
|
$: selectedComponentType = getComponentTypeName($store.currentComponentInfo)
|
||||||
|
@ -108,6 +107,8 @@
|
||||||
selectedComponentType,
|
selectedComponentType,
|
||||||
selectedComponentId,
|
selectedComponentId,
|
||||||
frontendDefinition,
|
frontendDefinition,
|
||||||
|
appId: $store.appId,
|
||||||
|
instanceId: $backendUiStore.selectedDatabase._id,
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,6 +20,7 @@ export default `<html>
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
<script src='/assets/budibase-client.js'></script>
|
||||||
<script>
|
<script>
|
||||||
function receiveMessage(event) {
|
function receiveMessage(event) {
|
||||||
|
|
||||||
|
@ -45,11 +46,10 @@ export default `<html>
|
||||||
styles.appendChild(document.createTextNode(data.styles))
|
styles.appendChild(document.createTextNode(data.styles))
|
||||||
|
|
||||||
window["##BUDIBASE_FRONTEND_DEFINITION##"] = data.frontendDefinition;
|
window["##BUDIBASE_FRONTEND_DEFINITION##"] = data.frontendDefinition;
|
||||||
if (clientModule) {
|
if (window.loadBudibase) {
|
||||||
clientModule.loadBudibase({ window, localStorage })
|
loadBudibase({ window, localStorage })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let clientModule
|
|
||||||
let styles
|
let styles
|
||||||
let selectedComponentStyle
|
let selectedComponentStyle
|
||||||
|
|
||||||
|
@ -59,12 +59,9 @@ export default `<html>
|
||||||
return false;
|
return false;
|
||||||
}, true)
|
}, true)
|
||||||
|
|
||||||
import('/_builder/budibase-client.esm.mjs')
|
window.addEventListener('message', receiveMessage)
|
||||||
.then(module => {
|
window.dispatchEvent(new Event('bb-ready'))
|
||||||
clientModule = module
|
|
||||||
window.addEventListener('message', receiveMessage)
|
|
||||||
window.dispatchEvent(new Event('bb-ready'))
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
<div class="tabs">
|
<div class="tabs">
|
||||||
{#each categories as category}
|
{#each categories as category}
|
||||||
<li
|
<li
|
||||||
|
data-cy={category.name}
|
||||||
on:click={() => onClick(category)}
|
on:click={() => onClick(category)}
|
||||||
class:active={selectedCategory === category}>
|
class:active={selectedCategory === category}>
|
||||||
{category.name}
|
{category.name}
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
<script>
|
||||||
|
import FlatButton from "./FlatButton.svelte";
|
||||||
|
|
||||||
|
export let format = "hex";
|
||||||
|
export let onclick = format => {};
|
||||||
|
|
||||||
|
let colorFormats = ["hex", "rgb", "hsl"];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.flatbutton-group {
|
||||||
|
font-weight: 500;
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row nowrap;
|
||||||
|
justify-content: center;
|
||||||
|
width: 170px;
|
||||||
|
height: 30px;
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="flatbutton-group">
|
||||||
|
{#each colorFormats as text}
|
||||||
|
<FlatButton
|
||||||
|
selected={format === text}
|
||||||
|
{text}
|
||||||
|
on:click={() => onclick(text)} />
|
||||||
|
{/each}
|
||||||
|
</div>
|
|
@ -0,0 +1,25 @@
|
||||||
|
<script>
|
||||||
|
import {buildStyle} from "./helpers.js"
|
||||||
|
import {fade} from "svelte/transition"
|
||||||
|
|
||||||
|
export let backgroundSize = "10px"
|
||||||
|
export let borderRadius = ""
|
||||||
|
export let height = ""
|
||||||
|
export let width = ""
|
||||||
|
export let margin = ""
|
||||||
|
|
||||||
|
$: style = buildStyle({backgroundSize, borderRadius, height, width, margin})
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
div {
|
||||||
|
background-image: url('data:image/svg+xml;utf8, <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2 2"><path fill="white" d="M1,0H2V1H1V0ZM0,1H1V2H0V1Z"/><path fill="gray" d="M0,0H1V1H0V0ZM1,1H2V2H1V1Z"/></svg>');
|
||||||
|
height: fit-content;
|
||||||
|
width: fit-content;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div in:fade {style}>
|
||||||
|
<slot />
|
||||||
|
</div>
|
|
@ -0,0 +1,281 @@
|
||||||
|
<script>
|
||||||
|
import { onMount, createEventDispatcher } from "svelte";
|
||||||
|
import { fade } from 'svelte/transition';
|
||||||
|
import Swatch from "./Swatch.svelte";
|
||||||
|
import CheckedBackground from "./CheckedBackground.svelte"
|
||||||
|
import {buildStyle} from "./helpers.js"
|
||||||
|
import {
|
||||||
|
getColorFormat,
|
||||||
|
convertToHSVA,
|
||||||
|
convertHsvaToFormat
|
||||||
|
} from "./utils.js";
|
||||||
|
import Slider from "./Slider.svelte";
|
||||||
|
import Palette from "./Palette.svelte";
|
||||||
|
import ButtonGroup from "./ButtonGroup.svelte";
|
||||||
|
import Input from "./Input.svelte";
|
||||||
|
|
||||||
|
export let value = "#3ec1d3ff";
|
||||||
|
export let swatches = [] //TODO: Safe swatches - limit to 12. warn in console
|
||||||
|
export let disableSwatches = false
|
||||||
|
export let format = "hexa";
|
||||||
|
export let open = false;
|
||||||
|
|
||||||
|
export let pickerHeight = 0;
|
||||||
|
export let pickerWidth = 0;
|
||||||
|
|
||||||
|
let adder = null;
|
||||||
|
|
||||||
|
let h = null;
|
||||||
|
let s = null;
|
||||||
|
let v = null;
|
||||||
|
let a = null;
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
if(!swatches.length > 0) {
|
||||||
|
//Don't use locally stored recent colors if swatches have been passed as props
|
||||||
|
getRecentColors()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (format) {
|
||||||
|
convertAndSetHSVA()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function getRecentColors() {
|
||||||
|
let colorStore = localStorage.getItem("cp:recent-colors")
|
||||||
|
if (colorStore) {
|
||||||
|
swatches = JSON.parse(colorStore)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setRecentColor(color) {
|
||||||
|
if (swatches.length === 12) {
|
||||||
|
swatches.splice(0, 1)
|
||||||
|
}
|
||||||
|
if (!swatches.includes(color)) {
|
||||||
|
swatches = [...swatches, color]
|
||||||
|
localStorage.setItem("cp:recent-colors", JSON.stringify(swatches))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function convertAndSetHSVA() {
|
||||||
|
let hsva = convertToHSVA(value, format);
|
||||||
|
setHSVA(hsva);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setHSVA([hue, sat, val, alpha]) {
|
||||||
|
h = hue;
|
||||||
|
s = sat;
|
||||||
|
v = val;
|
||||||
|
a = alpha;
|
||||||
|
}
|
||||||
|
|
||||||
|
//fired by choosing a color from the palette
|
||||||
|
function setSaturationAndValue({ detail }) {
|
||||||
|
s = detail.s;
|
||||||
|
v = detail.v;
|
||||||
|
value = convertHsvaToFormat([h, s, v, a], format);
|
||||||
|
dispatchValue()
|
||||||
|
}
|
||||||
|
|
||||||
|
function setHue({color, isDrag}) {
|
||||||
|
h = color;
|
||||||
|
value = convertHsvaToFormat([h, s, v, a], format);
|
||||||
|
if(!isDrag) {
|
||||||
|
dispatchValue()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setAlpha({color, isDrag}) {
|
||||||
|
a = color === "1.00" ? "1" : color;
|
||||||
|
value = convertHsvaToFormat([h, s, v, a], format);
|
||||||
|
if(!isDrag) {
|
||||||
|
dispatchValue()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function dispatchValue() {
|
||||||
|
dispatch("change", value)
|
||||||
|
}
|
||||||
|
|
||||||
|
function changeFormatAndConvert(f) {
|
||||||
|
format = f;
|
||||||
|
value = convertHsvaToFormat([h, s, v, a], format);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleColorInput(text) {
|
||||||
|
let format = getColorFormat(text)
|
||||||
|
if(format) {
|
||||||
|
value = text
|
||||||
|
convertAndSetHSVA()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function dispatchInputChange() {
|
||||||
|
if(format) {
|
||||||
|
dispatchValue()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function addSwatch() {
|
||||||
|
if(format) {
|
||||||
|
dispatch("addswatch", value)
|
||||||
|
setRecentColor(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeSwatch(idx) {
|
||||||
|
let removedSwatch = swatches.splice(idx, 1);
|
||||||
|
swatches = swatches
|
||||||
|
dispatch("removeswatch", removedSwatch)
|
||||||
|
localStorage.setItem("cp:recent-colors", JSON.stringify(swatches))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function applySwatch(color) {
|
||||||
|
if(value !== color) {
|
||||||
|
format = getColorFormat(color)
|
||||||
|
if(format) {
|
||||||
|
value = color
|
||||||
|
convertAndSetHSVA()
|
||||||
|
dispatchValue()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$: border = (v > 90 && s < 5) ? "1px dashed #dedada" : ""
|
||||||
|
$: style = buildStyle({background: value, border})
|
||||||
|
$: shrink = swatches.length > 0
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.colorpicker-container {
|
||||||
|
display: flex;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 400;
|
||||||
|
flex-direction: column;
|
||||||
|
/* height: 265px; */
|
||||||
|
height: auto;
|
||||||
|
width: 220px;
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: 2px;
|
||||||
|
box-shadow: 0 0.15em 1.5em 0 rgba(0,0,0,.1), 0 0 1em 0 rgba(0,0,0,.03);
|
||||||
|
}
|
||||||
|
|
||||||
|
.palette-panel {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-panel {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 8px;
|
||||||
|
background: white;
|
||||||
|
border: 1px solid #d2d2d2;
|
||||||
|
color: #777373;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alpha-hue-panel {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 25px 1fr;
|
||||||
|
grid-gap: 15px;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-color {
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.swatch-panel {
|
||||||
|
flex: 0 0 15px;
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row wrap;
|
||||||
|
justify-content: flex-start;
|
||||||
|
padding: 0 5px;
|
||||||
|
max-height: 56px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.adder {
|
||||||
|
flex: 1;
|
||||||
|
height: 20px;
|
||||||
|
display: flex;
|
||||||
|
transition: flex 0.5s;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
background: #f1f3f4;
|
||||||
|
cursor: pointer;
|
||||||
|
border: 1px solid #d4d4d4;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-left: 5px;
|
||||||
|
margin-top: 3px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shrink {
|
||||||
|
flex: 0 0 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.format-input-panel {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
padding-top: 3px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="colorpicker-container" bind:clientHeight={pickerHeight} bind:clientWidth={pickerWidth}>
|
||||||
|
|
||||||
|
<div class="palette-panel">
|
||||||
|
<Palette on:change={setSaturationAndValue} {h} {s} {v} {a} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="control-panel">
|
||||||
|
<div class="alpha-hue-panel">
|
||||||
|
<div>
|
||||||
|
<CheckedBackground borderRadius="50%" backgroundSize="8px">
|
||||||
|
<div class="selected-color" {style} />
|
||||||
|
</CheckedBackground>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Slider type="hue" value={h} on:change={(hue) => setHue(hue.detail)} on:dragend={dispatchValue} />
|
||||||
|
|
||||||
|
<CheckedBackground borderRadius="10px" backgroundSize="7px">
|
||||||
|
<Slider
|
||||||
|
type="alpha"
|
||||||
|
value={a}
|
||||||
|
on:change={(alpha, isDrag) => setAlpha(alpha.detail, isDrag)}
|
||||||
|
on:dragend={dispatchValue}
|
||||||
|
/>
|
||||||
|
</CheckedBackground>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if !disableSwatches}
|
||||||
|
<div transition:fade class="swatch-panel">
|
||||||
|
{#if swatches.length > 0}
|
||||||
|
{#each swatches as color, idx}
|
||||||
|
<Swatch {color} on:click={() => applySwatch(color)} on:removeswatch={() => removeSwatch(idx)} />
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
{#if swatches.length !== 12}
|
||||||
|
<div bind:this={adder} transition:fade class="adder" on:click={addSwatch} class:shrink>
|
||||||
|
<span>+</span>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="format-input-panel">
|
||||||
|
<ButtonGroup {format} onclick={changeFormatAndConvert} />
|
||||||
|
<Input {value} on:input={event => handleColorInput(event.target.value)} on:change={dispatchInputChange} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
|
@ -0,0 +1,152 @@
|
||||||
|
<script>
|
||||||
|
import Colorpicker from "./Colorpicker.svelte"
|
||||||
|
import CheckedBackground from "./CheckedBackground.svelte"
|
||||||
|
import {createEventDispatcher, afterUpdate, beforeUpdate} from "svelte"
|
||||||
|
|
||||||
|
import {buildStyle} from "./helpers.js"
|
||||||
|
import { fade } from 'svelte/transition';
|
||||||
|
import {getColorFormat} from "./utils.js"
|
||||||
|
|
||||||
|
export let value = "#3ec1d3ff"
|
||||||
|
export let swatches = []
|
||||||
|
export let disableSwatches = false
|
||||||
|
export let open = false;
|
||||||
|
export let width = "25px"
|
||||||
|
export let height = "25px"
|
||||||
|
|
||||||
|
let format = "hexa";
|
||||||
|
let dimensions = {top: 0, left: 0}
|
||||||
|
let colorPreview = null
|
||||||
|
|
||||||
|
let previewHeight = null
|
||||||
|
let previewWidth = null
|
||||||
|
let pickerWidth = 0
|
||||||
|
let pickerHeight = 0
|
||||||
|
|
||||||
|
let anchorEl = null
|
||||||
|
let parentNodes = [];
|
||||||
|
let errorMsg = null
|
||||||
|
|
||||||
|
$: previewStyle = buildStyle({width, height, background: value})
|
||||||
|
$: errorPreviewStyle = buildStyle({width, height})
|
||||||
|
$: pickerStyle = buildStyle({top: `${dimensions.top}px`, left: `${dimensions.left}px`})
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
|
beforeUpdate(() => {
|
||||||
|
format = getColorFormat(value)
|
||||||
|
if(!format) {
|
||||||
|
errorMsg = `Colorpicker - ${value} is an unknown color format. Please use a hex, rgb or hsl value`
|
||||||
|
console.error(errorMsg)
|
||||||
|
}else{
|
||||||
|
errorMsg = null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
afterUpdate(() => {
|
||||||
|
if(colorPreview && colorPreview.offsetParent && !anchorEl) {
|
||||||
|
//Anchor relative to closest positioned ancestor element. If none, then anchor to body
|
||||||
|
anchorEl = colorPreview.offsetParent
|
||||||
|
let curEl = colorPreview
|
||||||
|
let els = []
|
||||||
|
//Travel up dom tree from preview element to find parent elements that scroll
|
||||||
|
while(!anchorEl.isSameNode(curEl)) {
|
||||||
|
curEl = curEl.parentNode
|
||||||
|
let elOverflow = window.getComputedStyle(curEl).getPropertyValue("overflow")
|
||||||
|
if(/scroll|auto/.test(elOverflow)) {
|
||||||
|
els.push(curEl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
parentNodes = els
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
function openColorpicker(event) {
|
||||||
|
if(colorPreview) {
|
||||||
|
open = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$: if(open && colorPreview) {
|
||||||
|
const {top: spaceAbove, width, bottom, right, left: spaceLeft} = colorPreview.getBoundingClientRect()
|
||||||
|
const {innerHeight, innerWidth} = window
|
||||||
|
|
||||||
|
const {offsetLeft, offsetTop} = colorPreview
|
||||||
|
//get the scrollTop value for all scrollable parent elements
|
||||||
|
let scrollTop = parentNodes.reduce((scrollAcc, el) => scrollAcc += el.scrollTop, 0);
|
||||||
|
|
||||||
|
const spaceBelow = (innerHeight - spaceAbove) - previewHeight
|
||||||
|
const top = spaceAbove > spaceBelow ? (offsetTop - pickerHeight) - scrollTop : (offsetTop + previewHeight) - scrollTop
|
||||||
|
|
||||||
|
//TOO: Testing and Scroll Awareness for x Scroll
|
||||||
|
const spaceRight = (innerWidth - spaceLeft) + previewWidth
|
||||||
|
const left = spaceRight > spaceLeft ? (offsetLeft + previewWidth) : offsetLeft - pickerWidth
|
||||||
|
|
||||||
|
dimensions = {top, left}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onColorChange(color) {
|
||||||
|
value = color.detail;
|
||||||
|
dispatch("change", color.detail)
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="color-preview-container">
|
||||||
|
{#if !errorMsg}
|
||||||
|
<CheckedBackground borderRadius="3px" backgroundSize="8px">
|
||||||
|
<div bind:this={colorPreview} bind:clientHeight={previewHeight} bind:clientWidth={previewWidth} class="color-preview" style={previewStyle} on:click={openColorpicker} />
|
||||||
|
</CheckedBackground>
|
||||||
|
|
||||||
|
{#if open}
|
||||||
|
<div transition:fade class="picker-container" style={pickerStyle}>
|
||||||
|
<Colorpicker on:change={onColorChange} on:addswatch on:removeswatch bind:format bind:value bind:pickerHeight bind:pickerWidth {swatches} {disableSwatches} {open} />
|
||||||
|
</div>
|
||||||
|
<div on:click|self={() => open = false} class="overlay"></div>
|
||||||
|
{/if}
|
||||||
|
{:else}
|
||||||
|
<div class="color-preview preview-error" style={errorPreviewStyle}>
|
||||||
|
<span>×</span>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.color-preview-container{
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row nowrap;
|
||||||
|
height: fit-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-preview {
|
||||||
|
border-radius: 3px;
|
||||||
|
border: 1px solid #dedada;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-error {
|
||||||
|
background: #cccccc;
|
||||||
|
color: #808080;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 18px;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.picker-container {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 3;
|
||||||
|
width: fit-content;
|
||||||
|
height: fit-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlay{
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
<script>
|
||||||
|
export let text = "";
|
||||||
|
export let selected = false;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.flatbutton {
|
||||||
|
cursor: pointer;
|
||||||
|
border: 1px solid #d4d4d4;
|
||||||
|
border-radius: 8px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
margin: 5px;
|
||||||
|
transition: all 0.3s;
|
||||||
|
font-size: 10px;
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
background: #f1f3f4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected {
|
||||||
|
color: #ffffff;
|
||||||
|
background-color: #003cb0;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="flatbutton" class:selected on:click>{text}</div>
|
|
@ -0,0 +1,28 @@
|
||||||
|
<script>
|
||||||
|
export let value = "";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
div {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin: 5px 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
width: 175px;
|
||||||
|
font-size: 13px;
|
||||||
|
background: #f1f3f4;
|
||||||
|
border-radius: 8px;
|
||||||
|
height: 20px;
|
||||||
|
outline-color: #003cb0;
|
||||||
|
color: inherit;
|
||||||
|
text-align: center;
|
||||||
|
border: 1px solid #dadada;
|
||||||
|
font-weight: 550;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<input on:input on:change type="text" {value} maxlength="25" />
|
||||||
|
</div>
|
|
@ -0,0 +1,63 @@
|
||||||
|
<script>
|
||||||
|
import { onMount, createEventDispatcher } from "svelte";
|
||||||
|
import CheckedBackground from "./CheckedBackground.svelte"
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
|
export let h = 0;
|
||||||
|
export let s = 0;
|
||||||
|
export let v = 0;
|
||||||
|
export let a = 1;
|
||||||
|
|
||||||
|
let palette;
|
||||||
|
|
||||||
|
let paletteHeight, paletteWidth = 0;
|
||||||
|
|
||||||
|
|
||||||
|
function handleClick(event) {
|
||||||
|
const { left, top } = palette.getBoundingClientRect();
|
||||||
|
let clickX = (event.clientX - left)
|
||||||
|
let clickY = (event.clientY - top)
|
||||||
|
if((clickX > 0 && clickY > 0) && (clickX < paletteWidth && clickY < paletteHeight)) {
|
||||||
|
let s = (clickX / paletteWidth) * 100
|
||||||
|
let v = 100 - ((clickY / paletteHeight) * 100)
|
||||||
|
dispatch("change", {s, v})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
$: pickerX = (s * paletteWidth) / 100;
|
||||||
|
$: pickerY = paletteHeight * ((100 - v) / 100)
|
||||||
|
|
||||||
|
$: paletteGradient = `linear-gradient(to top, rgba(0, 0, 0, 1), transparent),
|
||||||
|
linear-gradient(to left, hsla(${h}, 100%, 50%, ${a}), rgba(255, 255, 255, ${a}))
|
||||||
|
`;
|
||||||
|
$: style = `background: ${paletteGradient};`;
|
||||||
|
|
||||||
|
$: pickerStyle = `transform: translate(${pickerX - 8}px, ${pickerY - 8}px);`
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.palette {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 140px;
|
||||||
|
cursor: crosshair;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.picker {
|
||||||
|
position: absolute;
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
background: transparent;
|
||||||
|
border: 2px solid white;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<CheckedBackground width="100%">
|
||||||
|
<div bind:this={palette} bind:clientHeight={paletteHeight} bind:clientWidth={paletteWidth} on:click={handleClick} class="palette" {style}>
|
||||||
|
<div class="picker" style={pickerStyle} />
|
||||||
|
</div>
|
||||||
|
</CheckedBackground>
|
|
@ -0,0 +1,89 @@
|
||||||
|
<script>
|
||||||
|
import { onMount, createEventDispatcher } from "svelte";
|
||||||
|
import dragable from "./drag.js";
|
||||||
|
|
||||||
|
export let value = 1;
|
||||||
|
export let type = "hue";
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
|
let slider;
|
||||||
|
let sliderWidth = 0;
|
||||||
|
|
||||||
|
function onSliderChange(mouseX, isDrag = false) {
|
||||||
|
const { left, width } = slider.getBoundingClientRect();
|
||||||
|
let clickPosition = mouseX - left;
|
||||||
|
|
||||||
|
let percentageClick = (clickPosition / sliderWidth).toFixed(2)
|
||||||
|
|
||||||
|
if (percentageClick >= 0 && percentageClick <= 1) {
|
||||||
|
let value =
|
||||||
|
type === "hue"
|
||||||
|
? 360 * percentageClick
|
||||||
|
: percentageClick;
|
||||||
|
|
||||||
|
dispatch("change", {color: value, isDrag});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$: thumbPosition =
|
||||||
|
type === "hue" ? sliderWidth * (value / 360) : sliderWidth * value;
|
||||||
|
|
||||||
|
$: style = `transform: translateX(${thumbPosition - 6}px);`;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.color-format-slider {
|
||||||
|
position: relative;
|
||||||
|
align-self: center;
|
||||||
|
height: 8px;
|
||||||
|
width: 150px;
|
||||||
|
border-radius: 10px;
|
||||||
|
margin: 10px 0px;
|
||||||
|
border: 1px solid #e8e8ef;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hue {
|
||||||
|
background: linear-gradient(
|
||||||
|
to right,
|
||||||
|
hsl(0, 100%, 50%),
|
||||||
|
hsl(60, 100%, 50%),
|
||||||
|
hsl(120, 100%, 50%),
|
||||||
|
hsl(180, 100%, 50%),
|
||||||
|
hsl(240, 100%, 50%),
|
||||||
|
hsl(300, 100%, 50%),
|
||||||
|
hsl(360, 100%, 50%)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
.alpha {
|
||||||
|
background: linear-gradient(to right, transparent, rgb(0 0 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider-thumb {
|
||||||
|
position: absolute;
|
||||||
|
bottom: -3px;
|
||||||
|
height: 12px;
|
||||||
|
width: 12px;
|
||||||
|
border: 1px solid #777676;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: #ffffff;
|
||||||
|
cursor:grab;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div
|
||||||
|
bind:this={slider}
|
||||||
|
bind:clientWidth={sliderWidth}
|
||||||
|
on:click={event => onSliderChange(event.clientX)}
|
||||||
|
class="color-format-slider"
|
||||||
|
class:hue={type === 'hue'}
|
||||||
|
class:alpha={type === 'alpha'}>
|
||||||
|
<div
|
||||||
|
use:dragable
|
||||||
|
on:drag={e => onSliderChange(e.detail, true)}
|
||||||
|
on:dragend
|
||||||
|
class="slider-thumb"
|
||||||
|
{style} />
|
||||||
|
</div>
|
|
@ -0,0 +1,53 @@
|
||||||
|
<script>
|
||||||
|
import {createEventDispatcher} from "svelte"
|
||||||
|
import { fade } from 'svelte/transition';
|
||||||
|
import CheckedBackground from "./CheckedBackground.svelte"
|
||||||
|
|
||||||
|
export let hovered = false
|
||||||
|
export let color = "#fff"
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.swatch {
|
||||||
|
position: relative;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid #dedada;
|
||||||
|
height: 20px;
|
||||||
|
width: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.space {
|
||||||
|
padding: 3px 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.remove-icon {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
top: -5px;
|
||||||
|
right: -4px;
|
||||||
|
width:10px;
|
||||||
|
height: 10px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: #800000;
|
||||||
|
color: #fff;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="space">
|
||||||
|
<CheckedBackground borderRadius="6px">
|
||||||
|
<div in:fade class="swatch" style={`background: ${color};`} on:click|self on:mouseover={() => hovered = true} on:mouseleave={() => hovered = false}>
|
||||||
|
{#if hovered}
|
||||||
|
<div in:fade class="remove-icon" on:click|self={()=> dispatch("removeswatch")}>
|
||||||
|
<span on:click|self={()=> dispatch("removeswatch")}>×</span>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</CheckedBackground>
|
||||||
|
</div>
|
|
@ -0,0 +1,23 @@
|
||||||
|
export default function(node) {
|
||||||
|
function handleMouseDown() {
|
||||||
|
window.addEventListener("mousemove", handleMouseMove)
|
||||||
|
window.addEventListener("mouseup", handleMouseUp)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMouseMove(event) {
|
||||||
|
let mouseX = event.clientX
|
||||||
|
node.dispatchEvent(
|
||||||
|
new CustomEvent("drag", {
|
||||||
|
detail: mouseX,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMouseUp() {
|
||||||
|
window.removeEventListener("mousedown", handleMouseDown)
|
||||||
|
window.removeEventListener("mousemove", handleMouseMove)
|
||||||
|
node.dispatchEvent(new CustomEvent("dragend"))
|
||||||
|
}
|
||||||
|
|
||||||
|
node.addEventListener("mousedown", handleMouseDown)
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
export const buildStyle = styles => {
|
||||||
|
let str = ""
|
||||||
|
for (let s in styles) {
|
||||||
|
if (styles[s]) {
|
||||||
|
let key = convertCamel(s)
|
||||||
|
str += `${key}: ${styles[s]}; `
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
|
||||||
|
export const convertCamel = str => {
|
||||||
|
return str.replace(/[A-Z]/g, match => `-${match.toLowerCase()}`)
|
||||||
|
}
|
|
@ -0,0 +1,2 @@
|
||||||
|
import Colorpreview from "./Colorpreview.svelte"
|
||||||
|
export default Colorpreview
|
|
@ -0,0 +1,279 @@
|
||||||
|
export const isValidHex = str =>
|
||||||
|
/^#(?:[A-F0-9]{3}$|[A-F0-9]{4}$|[A-F0-9]{6}$|[A-F0-9]{8})$/gi.test(str)
|
||||||
|
|
||||||
|
const getHexaValues = hexString => {
|
||||||
|
if (hexString.length <= 5) {
|
||||||
|
let hexArr = hexString.match(/[A-F0-9]/gi)
|
||||||
|
let t = hexArr.map(c => (c += c))
|
||||||
|
return t
|
||||||
|
} else {
|
||||||
|
return hexString.match(/[A-F0-9]{2}/gi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const isValidRgb = str => {
|
||||||
|
const hasValidStructure = /^(?:rgba\(|rgb\()(?:[0-9,\s]|\.(?=\d))*\)$/gi.test(
|
||||||
|
str
|
||||||
|
)
|
||||||
|
if (hasValidStructure) {
|
||||||
|
return testRgbaValues(str.toLowerCase())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const findNonNumericChars = /[a-z()\s]/gi
|
||||||
|
|
||||||
|
export const getNumericValues = str =>
|
||||||
|
str
|
||||||
|
.replace(findNonNumericChars, "")
|
||||||
|
.split(",")
|
||||||
|
.map(v => (v !== "" ? v : undefined))
|
||||||
|
|
||||||
|
export const testRgbaValues = str => {
|
||||||
|
const rgba = getNumericValues(str)
|
||||||
|
const [r, g, b, a] = rgba
|
||||||
|
|
||||||
|
let isValidLengthRange =
|
||||||
|
(str.startsWith("rgb(") && rgba.length === 3) ||
|
||||||
|
(str.startsWith("rgba(") && rgba.length === 4)
|
||||||
|
let isValidColorRange = [r, g, b].every(v => v >= 0 && v <= 255)
|
||||||
|
let isValidAlphaRange = str.startsWith("rgba(")
|
||||||
|
? `${a}`.length <= 4 && a >= 0 && a <= 1
|
||||||
|
: true
|
||||||
|
|
||||||
|
return isValidLengthRange && isValidColorRange && isValidAlphaRange
|
||||||
|
}
|
||||||
|
|
||||||
|
export const isValidHsl = str => {
|
||||||
|
const hasValidStructure = /^(?:hsl\(|hsla\()(?:[0-9,%\s]|\.(?=\d))*\)$/gi.test(
|
||||||
|
str
|
||||||
|
)
|
||||||
|
if (hasValidStructure) {
|
||||||
|
return testHslaValues(str.toLowerCase())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const testHslaValues = str => {
|
||||||
|
const hsla = getNumericValues(str)
|
||||||
|
const [h, s, l, a] = hsla
|
||||||
|
const isUndefined = [h, s, l].some(v => v === undefined)
|
||||||
|
|
||||||
|
if (isUndefined) return false
|
||||||
|
|
||||||
|
let isValidLengthRange =
|
||||||
|
(str.startsWith("hsl(") && hsla.length === 3) ||
|
||||||
|
(str.startsWith("hsla(") && hsla.length === 4)
|
||||||
|
let isValidHue = h >= 0 && h <= 360
|
||||||
|
let isValidSatLum = [s, l].every(
|
||||||
|
v => v.endsWith("%") && parseInt(v) >= 0 && parseInt(v) <= 100
|
||||||
|
)
|
||||||
|
let isValidAlphaRange = str.startsWith("hsla(")
|
||||||
|
? `${a}`.length <= 4 && a >= 0 && a <= 1
|
||||||
|
: true
|
||||||
|
|
||||||
|
return isValidLengthRange && isValidHue && isValidSatLum && isValidAlphaRange
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getColorFormat = color => {
|
||||||
|
if (typeof color === "string") {
|
||||||
|
if (isValidHex(color)) {
|
||||||
|
return "hex"
|
||||||
|
} else if (isValidRgb(color)) {
|
||||||
|
return "rgb"
|
||||||
|
} else if (isValidHsl(color)) {
|
||||||
|
return "hsl"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const convertToHSVA = (value, format) => {
|
||||||
|
switch (format) {
|
||||||
|
case "hex":
|
||||||
|
return getAndConvertHexa(value)
|
||||||
|
case "rgb":
|
||||||
|
return getAndConvertRgba(value)
|
||||||
|
case "hsl":
|
||||||
|
return getAndConvertHsla(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const convertHsvaToFormat = (hsva, format) => {
|
||||||
|
switch (format) {
|
||||||
|
case "hex":
|
||||||
|
return hsvaToHexa(hsva, true)
|
||||||
|
case "rgb":
|
||||||
|
return hsvaToRgba(hsva, true)
|
||||||
|
case "hsl":
|
||||||
|
return hsvaToHsla(hsva)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getAndConvertHexa = color => {
|
||||||
|
let [rHex, gHex, bHex, aHex] = getHexaValues(color)
|
||||||
|
return hexaToHSVA([rHex, gHex, bHex], aHex)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getAndConvertRgba = color => {
|
||||||
|
let rgba = getNumericValues(color)
|
||||||
|
return rgbaToHSVA(rgba)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getAndConvertHsla = color => {
|
||||||
|
let hsla = getNumericValues(color)
|
||||||
|
return hslaToHSVA(hsla)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const hexaToHSVA = (hex, alpha = "FF") => {
|
||||||
|
const rgba = hex
|
||||||
|
.map(v => parseInt(v, 16))
|
||||||
|
.concat(Number((parseInt(alpha, 16) / 255).toFixed(2)))
|
||||||
|
return rgbaToHSVA(rgba)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const rgbaToHSVA = rgba => {
|
||||||
|
const [r, g, b, a = 1] = rgba
|
||||||
|
let hsv = _rgbToHSV([r, g, b])
|
||||||
|
return [...hsv, a].map(x => parseFloat(x))
|
||||||
|
}
|
||||||
|
|
||||||
|
export const hslaToHSVA = ([h, s, l, a = 1]) => {
|
||||||
|
let sat = s.replace(/%/, "")
|
||||||
|
let lum = l.replace(/%/, "")
|
||||||
|
let hsv = _hslToHSV([h, sat, lum])
|
||||||
|
return [...hsv, a].map(x => parseFloat(x))
|
||||||
|
}
|
||||||
|
|
||||||
|
export const hsvaToHexa = (hsva, asString = false) => {
|
||||||
|
const [r, g, b, a] = hsvaToRgba(hsva)
|
||||||
|
|
||||||
|
const hexa = [r, g, b]
|
||||||
|
.map(v => {
|
||||||
|
let hex = Math.round(v).toString(16)
|
||||||
|
return hex.length === 1 ? `0${hex}` : hex
|
||||||
|
})
|
||||||
|
.concat(Math.round(a * 255).toString(16))
|
||||||
|
return asString ? `#${hexa.join("")}` : hexa
|
||||||
|
}
|
||||||
|
|
||||||
|
export const hsvaToRgba = ([h, s, v, a = 1], asString = false) => {
|
||||||
|
let rgb = _hsvToRgb([h, s, v]).map(x => Math.round(x))
|
||||||
|
let rgba = [...rgb, a < 1 ? _fixNum(a, 2) : a]
|
||||||
|
return asString ? `rgba(${rgba.join(",")})` : rgba
|
||||||
|
}
|
||||||
|
|
||||||
|
export const hsvaToHsla = ([h, s, v, a = 1]) => {
|
||||||
|
let [hue, sat, lum] = _hsvToHSL([h, s, v])
|
||||||
|
let hsla = [hue, sat + "%", lum + "%", a < 1 ? _fixNum(a, 2) : a]
|
||||||
|
return `hsla(${hsla.join(",")})`
|
||||||
|
}
|
||||||
|
|
||||||
|
export const _hslToHSV = hsl => {
|
||||||
|
const h = hsl[0]
|
||||||
|
let s = hsl[1] / 100
|
||||||
|
let l = hsl[2] / 100
|
||||||
|
let smin = s
|
||||||
|
const lmin = Math.max(l, 0.01)
|
||||||
|
|
||||||
|
l *= 2
|
||||||
|
s *= l <= 1 ? l : 2 - l
|
||||||
|
smin *= lmin <= 1 ? lmin : 2 - lmin
|
||||||
|
const v = (l + s) / 2
|
||||||
|
const sv = l === 0 ? (2 * smin) / (lmin + smin) : (2 * s) / (l + s)
|
||||||
|
|
||||||
|
return [h, sv * 100, v * 100]
|
||||||
|
}
|
||||||
|
|
||||||
|
//Credit : https://github.com/Qix-/color-convert
|
||||||
|
export const _rgbToHSV = rgb => {
|
||||||
|
let rdif
|
||||||
|
let gdif
|
||||||
|
let bdif
|
||||||
|
let h
|
||||||
|
let s
|
||||||
|
|
||||||
|
const r = rgb[0] / 255
|
||||||
|
const g = rgb[1] / 255
|
||||||
|
const b = rgb[2] / 255
|
||||||
|
const v = Math.max(r, g, b)
|
||||||
|
const diff = v - Math.min(r, g, b)
|
||||||
|
const diffc = function(c) {
|
||||||
|
return (v - c) / 6 / diff + 1 / 2
|
||||||
|
}
|
||||||
|
|
||||||
|
if (diff === 0) {
|
||||||
|
h = 0
|
||||||
|
s = 0
|
||||||
|
} else {
|
||||||
|
s = diff / v
|
||||||
|
rdif = diffc(r)
|
||||||
|
gdif = diffc(g)
|
||||||
|
bdif = diffc(b)
|
||||||
|
|
||||||
|
if (r === v) {
|
||||||
|
h = bdif - gdif
|
||||||
|
} else if (g === v) {
|
||||||
|
h = 1 / 3 + rdif - bdif
|
||||||
|
} else if (b === v) {
|
||||||
|
h = 2 / 3 + gdif - rdif
|
||||||
|
}
|
||||||
|
|
||||||
|
if (h < 0) {
|
||||||
|
h += 1
|
||||||
|
} else if (h > 1) {
|
||||||
|
h -= 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const hsvResult = [h * 360, s * 100, v * 100].map(v => Math.round(v))
|
||||||
|
return hsvResult
|
||||||
|
}
|
||||||
|
|
||||||
|
//Credit : https://github.com/Qix-/color-convert
|
||||||
|
export const _hsvToRgb = hsv => {
|
||||||
|
const h = hsv[0] / 60
|
||||||
|
const s = hsv[1] / 100
|
||||||
|
let v = hsv[2] / 100
|
||||||
|
const hi = Math.floor(h) % 6
|
||||||
|
|
||||||
|
const f = h - Math.floor(h)
|
||||||
|
const p = 255 * v * (1 - s)
|
||||||
|
const q = 255 * v * (1 - s * f)
|
||||||
|
const t = 255 * v * (1 - s * (1 - f))
|
||||||
|
v *= 255
|
||||||
|
|
||||||
|
switch (hi) {
|
||||||
|
case 0:
|
||||||
|
return [v, t, p]
|
||||||
|
case 1:
|
||||||
|
return [q, v, p]
|
||||||
|
case 2:
|
||||||
|
return [p, v, t]
|
||||||
|
case 3:
|
||||||
|
return [p, q, v]
|
||||||
|
case 4:
|
||||||
|
return [t, p, v]
|
||||||
|
case 5:
|
||||||
|
return [v, p, q]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//Credit : https://github.com/Qix-/color-convert
|
||||||
|
export const _hsvToHSL = hsv => {
|
||||||
|
const h = hsv[0]
|
||||||
|
const s = hsv[1] / 100
|
||||||
|
const v = hsv[2] / 100
|
||||||
|
const vmin = Math.max(v, 0.01)
|
||||||
|
let sl
|
||||||
|
let l
|
||||||
|
|
||||||
|
l = (2 - s) * v
|
||||||
|
const lmin = (2 - s) * vmin
|
||||||
|
sl = s * vmin
|
||||||
|
sl /= lmin <= 1 ? lmin : 2 - lmin
|
||||||
|
sl = sl || 0
|
||||||
|
l /= 2
|
||||||
|
|
||||||
|
return [_fixNum(h, 0), _fixNum(sl * 100, 0), _fixNum(l * 100, 0)]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const _fixNum = (value, decimalPlaces) =>
|
||||||
|
Number(parseFloat(value).toFixed(decimalPlaces))
|
|
@ -0,0 +1,106 @@
|
||||||
|
import { getColorFormat, convertToHSVA, convertHsvaToFormat } from "./utils"
|
||||||
|
|
||||||
|
describe("convertToHSVA - convert to hsva from format", () => {
|
||||||
|
test("convert from hexa", () => {
|
||||||
|
expect(convertToHSVA("#f222d382", "hex")).toEqual([309, 86, 95, 0.51])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("convert from hex", () => {
|
||||||
|
expect(convertToHSVA("#f222d3", "hex")).toEqual([309, 86, 95, 1])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("convert from rgba", () => {
|
||||||
|
expect(convertToHSVA("rgba(242, 34, 211, 1)", "rgb")).toEqual([
|
||||||
|
309,
|
||||||
|
86,
|
||||||
|
95,
|
||||||
|
1,
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("convert from rgb", () => {
|
||||||
|
expect(convertToHSVA("rgb(150, 80, 255)", "rgb")).toEqual([264, 69, 100, 1])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("convert from from hsl", () => {
|
||||||
|
expect(convertToHSVA("hsl(264, 100%, 65.7%)", "hsl")).toEqual([
|
||||||
|
264,
|
||||||
|
68.6,
|
||||||
|
100,
|
||||||
|
1,
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("convert from from hsla", () => {
|
||||||
|
expect(convertToHSVA("hsla(264, 100%, 65.7%, 0.51)", "hsl")).toEqual([
|
||||||
|
264,
|
||||||
|
68.6,
|
||||||
|
100,
|
||||||
|
0.51,
|
||||||
|
])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("convertHsvaToFormat - convert from hsva to format", () => {
|
||||||
|
test("Convert to hexa", () => {
|
||||||
|
expect(convertHsvaToFormat([264, 68.63, 100, 0.5], "hex")).toBe("#9650ff80")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("Convert to rgba", () => {
|
||||||
|
expect(convertHsvaToFormat([264, 68.63, 100, 0.75], "rgb")).toBe(
|
||||||
|
"rgba(150,80,255,0.75)"
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("Convert to hsla", () => {
|
||||||
|
expect(convertHsvaToFormat([264, 68.63, 100, 1], "hsl")).toBe(
|
||||||
|
"hsla(264,100%,66%,1)"
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("Get Color Format", () => {
|
||||||
|
test("Testing valid hex string", () => {
|
||||||
|
expect(getColorFormat("#FFF")).toBe("hex")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("Testing invalid hex string", () => {
|
||||||
|
expect(getColorFormat("#FFZ")).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
test("Testing valid hex with alpha", () => {
|
||||||
|
expect(getColorFormat("#FF00BB80")).toBe("hex")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("Test valid rgb value", () => {
|
||||||
|
expect(getColorFormat("RGB(255, 20, 50)")).toBe("rgb")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("Testing invalid rgb value", () => {
|
||||||
|
expect(getColorFormat("rgb(255, 0)")).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
test("Testing rgb value with alpha", () => {
|
||||||
|
expect(getColorFormat("rgba(255, 0, 50, 0.5)")).toBe("rgb")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("Testing rgb value with incorrectly provided alpha", () => {
|
||||||
|
expect(getColorFormat("rgb(255, 0, 50, 0.5)")).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
test("Testing invalid hsl value", () => {
|
||||||
|
expect(getColorFormat("hsla(255, 0)")).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
test("Testing hsla value with alpha", () => {
|
||||||
|
expect(getColorFormat("hsla(150, 60%, 50%, 0.5)")).toBe("hsl")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("Testing hsl value with incorrectly provided alpha", () => {
|
||||||
|
expect(getColorFormat("hsl(150, 0, 50, 0.5)")).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
test("Testing out of bounds hsl", () => {
|
||||||
|
expect(getColorFormat("hsl(375, 0, 50)")).toBeUndefined()
|
||||||
|
})
|
||||||
|
})
|
|
@ -120,8 +120,6 @@
|
||||||
height: 100%;
|
height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
overflow-x: hidden;
|
|
||||||
overflow-y: hidden;
|
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
@ -139,7 +137,6 @@
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
overflow-y: auto;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.instance-name {
|
.instance-name {
|
||||||
|
|
|
@ -31,6 +31,7 @@
|
||||||
<FlatButtonGroup value={selectedCategory} {buttonProps} {onChange} />
|
<FlatButtonGroup value={selectedCategory} {buttonProps} {onChange} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="positioned-wrapper">
|
||||||
<div class="design-view-property-groups">
|
<div class="design-view-property-groups">
|
||||||
{#if propertyGroupNames.length > 0}
|
{#if propertyGroupNames.length > 0}
|
||||||
{#each propertyGroupNames as groupName}
|
{#each propertyGroupNames as groupName}
|
||||||
|
@ -49,6 +50,7 @@
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.design-view-container {
|
.design-view-container {
|
||||||
|
@ -62,10 +64,15 @@
|
||||||
flex: 0 0 50px;
|
flex: 0 0 50px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.positioned-wrapper{
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.design-view-property-groups {
|
.design-view-property-groups {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
overflow-x: hidden;
|
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,11 @@
|
||||||
export let item
|
export let item
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="item-item" in:fly={{ y: 100, duration: 1000 }} on:click>
|
<div
|
||||||
|
data-cy={item.name}
|
||||||
|
class="item-item"
|
||||||
|
in:fly={{ y: 100, duration: 1000 }}
|
||||||
|
on:click>
|
||||||
<div class="item-icon">
|
<div class="item-icon">
|
||||||
<i class={item.icon} />
|
<i class={item.icon} />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -12,6 +12,8 @@
|
||||||
if (v.target) {
|
if (v.target) {
|
||||||
let val = props.valueKey ? v.target[props.valueKey] : v.target.value
|
let val = props.valueKey ? v.target[props.valueKey] : v.target.value
|
||||||
onChange(key, val)
|
onChange(key, val)
|
||||||
|
}else if(v.detail) {
|
||||||
|
onChange(key, v.detail)
|
||||||
} else {
|
} else {
|
||||||
onChange(key, v)
|
onChange(key, v)
|
||||||
}
|
}
|
||||||
|
@ -36,7 +38,8 @@
|
||||||
{...handlevalueKey(value)}
|
{...handlevalueKey(value)}
|
||||||
on:change={val => handleChange(key, val)}
|
on:change={val => handleChange(key, val)}
|
||||||
onChange={val => handleChange(key, val)}
|
onChange={val => handleChange(key, val)}
|
||||||
{...props} />
|
{...props}
|
||||||
|
name={key} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@ import Input from "../common/Input.svelte"
|
||||||
import OptionSelect from "./OptionSelect.svelte"
|
import OptionSelect from "./OptionSelect.svelte"
|
||||||
import InputGroup from "../common/Inputs/InputGroup.svelte"
|
import InputGroup from "../common/Inputs/InputGroup.svelte"
|
||||||
import FlatButtonGroup from "./FlatButtonGroup.svelte"
|
import FlatButtonGroup from "./FlatButtonGroup.svelte"
|
||||||
// import Colorpicker from "../common/Colorpicker.svelte"
|
import Colorpicker from "./Colorpicker"
|
||||||
/*
|
/*
|
||||||
TODO: Allow for default values for all properties
|
TODO: Allow for default values for all properties
|
||||||
*/
|
*/
|
||||||
|
@ -256,8 +256,8 @@ export const typography = [
|
||||||
{
|
{
|
||||||
label: "Color",
|
label: "Color",
|
||||||
key: "color",
|
key: "color",
|
||||||
control: Input,
|
control: Colorpicker,
|
||||||
placeholder: "hex",
|
defaultValue: "#000",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "align",
|
label: "align",
|
||||||
|
@ -305,7 +305,8 @@ export const background = [
|
||||||
{
|
{
|
||||||
label: "Color",
|
label: "Color",
|
||||||
key: "background",
|
key: "background",
|
||||||
control: Input,
|
control: Colorpicker,
|
||||||
|
defaultValue: "#000",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Image",
|
label: "Image",
|
||||||
|
@ -347,7 +348,8 @@ export const border = [
|
||||||
{
|
{
|
||||||
label: "Color",
|
label: "Color",
|
||||||
key: "border-color",
|
key: "border-color",
|
||||||
control: Input,
|
control: Colorpicker,
|
||||||
|
defaultValue: "#000",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Style",
|
label: "Style",
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import { store, backendUiStore, workflowStore } from "builderStore"
|
import { store, backendUiStore, workflowStore } from "builderStore"
|
||||||
import { notifier } from "@beyonk/svelte-notifications"
|
import { notifier } from "@beyonk/svelte-notifications"
|
||||||
import api from "builderStore/api"
|
|
||||||
import ActionButton from "components/common/ActionButton.svelte"
|
import ActionButton from "components/common/ActionButton.svelte"
|
||||||
|
|
||||||
export let onClosed
|
export let onClosed
|
||||||
|
|
|
@ -3,7 +3,6 @@
|
||||||
import { onMount, getContext } from "svelte"
|
import { onMount, getContext } from "svelte"
|
||||||
import { backendUiStore, workflowStore } from "builderStore"
|
import { backendUiStore, workflowStore } from "builderStore"
|
||||||
import { notifier } from "@beyonk/svelte-notifications"
|
import { notifier } from "@beyonk/svelte-notifications"
|
||||||
import api from "builderStore/api"
|
|
||||||
import WorkflowBlockSetup from "./WorkflowBlockSetup.svelte"
|
import WorkflowBlockSetup from "./WorkflowBlockSetup.svelte"
|
||||||
import DeleteWorkflowModal from "./DeleteWorkflowModal.svelte"
|
import DeleteWorkflowModal from "./DeleteWorkflowModal.svelte"
|
||||||
|
|
||||||
|
@ -96,7 +95,10 @@
|
||||||
{#if workflowBlock}
|
{#if workflowBlock}
|
||||||
<WorkflowBlockSetup {workflowBlock} />
|
<WorkflowBlockSetup {workflowBlock} />
|
||||||
<div class="buttons">
|
<div class="buttons">
|
||||||
<button class="workflow-button hoverable" on:click={saveWorkflow}>
|
<button
|
||||||
|
data-cy="save-workflow-setup"
|
||||||
|
class="workflow-button hoverable"
|
||||||
|
on:click={saveWorkflow}>
|
||||||
Save Workflow
|
Save Workflow
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
|
|
|
@ -3,7 +3,6 @@
|
||||||
import { workflowStore, backendUiStore } from "builderStore"
|
import { workflowStore, backendUiStore } from "builderStore"
|
||||||
import { notifier } from "@beyonk/svelte-notifications"
|
import { notifier } from "@beyonk/svelte-notifications"
|
||||||
import Flowchart from "./flowchart/FlowChart.svelte"
|
import Flowchart from "./flowchart/FlowChart.svelte"
|
||||||
import api from "builderStore/api"
|
|
||||||
|
|
||||||
let selectedWorkflow
|
let selectedWorkflow
|
||||||
let uiTree
|
let uiTree
|
||||||
|
@ -50,6 +49,7 @@
|
||||||
class:highlighted={!workflowLive}
|
class:highlighted={!workflowLive}
|
||||||
class:hoverable={!workflowLive}
|
class:hoverable={!workflowLive}
|
||||||
class="play-button hoverable"
|
class="play-button hoverable"
|
||||||
|
data-cy="activate-workflow"
|
||||||
on:click={() => setWorkflowLive(true)}>
|
on:click={() => setWorkflowLive(true)}>
|
||||||
<i class="ri-play-fill" />
|
<i class="ri-play-fill" />
|
||||||
</button>
|
</button>
|
||||||
|
|
|
@ -3,7 +3,6 @@
|
||||||
import { backendUiStore, workflowStore } from "builderStore"
|
import { backendUiStore, workflowStore } from "builderStore"
|
||||||
import { WorkflowList } from "../"
|
import { WorkflowList } from "../"
|
||||||
import WorkflowBlock from "./WorkflowBlock.svelte"
|
import WorkflowBlock from "./WorkflowBlock.svelte"
|
||||||
import api from "builderStore/api"
|
|
||||||
import blockDefinitions from "../blockDefinitions"
|
import blockDefinitions from "../blockDefinitions"
|
||||||
|
|
||||||
let selectedTab = "TRIGGER"
|
let selectedTab = "TRIGGER"
|
||||||
|
|
|
@ -15,7 +15,10 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="workflow-block hoverable" on:click={addBlockToWorkflow}>
|
<div
|
||||||
|
class="workflow-block hoverable"
|
||||||
|
on:click={addBlockToWorkflow}
|
||||||
|
data-cy={actionId}>
|
||||||
<div>
|
<div>
|
||||||
<i class={blockDefinition.icon} />
|
<i class={blockDefinition.icon} />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import { store, backendUiStore, workflowStore } from "builderStore"
|
import { store, backendUiStore, workflowStore } from "builderStore"
|
||||||
import { notifier } from "@beyonk/svelte-notifications"
|
import { notifier } from "@beyonk/svelte-notifications"
|
||||||
import api from "builderStore/api"
|
|
||||||
import ActionButton from "components/common/ActionButton.svelte"
|
import ActionButton from "components/common/ActionButton.svelte"
|
||||||
|
|
||||||
export let onClosed
|
export let onClosed
|
||||||
|
|
|
@ -3,7 +3,6 @@
|
||||||
import { notifier } from "@beyonk/svelte-notifications"
|
import { notifier } from "@beyonk/svelte-notifications"
|
||||||
import { onMount, getContext } from "svelte"
|
import { onMount, getContext } from "svelte"
|
||||||
import { backendUiStore, workflowStore } from "builderStore"
|
import { backendUiStore, workflowStore } from "builderStore"
|
||||||
import api from "builderStore/api"
|
|
||||||
import CreateWorkflowModal from "./CreateWorkflowModal.svelte"
|
import CreateWorkflowModal from "./CreateWorkflowModal.svelte"
|
||||||
|
|
||||||
const { open, close } = getContext("simple-modal")
|
const { open, close } = getContext("simple-modal")
|
||||||
|
@ -23,7 +22,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
workflowStore.actions.fetch($backendUiStore.selectedDatabase._id)
|
workflowStore.actions.fetch()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
import { backendUiStore, workflowStore } from "builderStore"
|
import { backendUiStore, workflowStore } from "builderStore"
|
||||||
import { WorkflowList, BlockList } from "./"
|
import { WorkflowList, BlockList } from "./"
|
||||||
import api from "builderStore/api"
|
|
||||||
import blockDefinitions from "./blockDefinitions"
|
import blockDefinitions from "./blockDefinitions"
|
||||||
|
|
||||||
let selectedTab = "WORKFLOWS"
|
let selectedTab = "WORKFLOWS"
|
||||||
|
@ -11,6 +10,7 @@
|
||||||
|
|
||||||
<header>
|
<header>
|
||||||
<span
|
<span
|
||||||
|
data-cy="workflow-list"
|
||||||
class="hoverable workflow-header"
|
class="hoverable workflow-header"
|
||||||
class:selected={selectedTab === 'WORKFLOWS'}
|
class:selected={selectedTab === 'WORKFLOWS'}
|
||||||
on:click={() => (selectedTab = 'WORKFLOWS')}>
|
on:click={() => (selectedTab = 'WORKFLOWS')}>
|
||||||
|
@ -18,6 +18,7 @@
|
||||||
</span>
|
</span>
|
||||||
{#if $workflowStore.currentWorkflow}
|
{#if $workflowStore.currentWorkflow}
|
||||||
<span
|
<span
|
||||||
|
data-cy="add-workflow-component"
|
||||||
class="hoverable"
|
class="hoverable"
|
||||||
class:selected={selectedTab === 'ADD'}
|
class:selected={selectedTab === 'ADD'}
|
||||||
on:click={() => (selectedTab = 'ADD')}>
|
on:click={() => (selectedTab = 'ADD')}>
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html>
|
<html>
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset='utf8'>
|
<meta charset='utf8'>
|
||||||
<meta name='viewport' content='width=device-width'>
|
<meta name='viewport' content='width=device-width'>
|
||||||
|
@ -14,6 +15,7 @@
|
||||||
<link rel='stylesheet' href='/_builder/budibase.css'>
|
<link rel='stylesheet' href='/_builder/budibase.css'>
|
||||||
<link rel='stylesheet' href='/_builder/monokai.css'>
|
<link rel='stylesheet' href='/_builder/monokai.css'>
|
||||||
<link rel='stylesheet' href='/_builder/bundle.css'>
|
<link rel='stylesheet' href='/_builder/bundle.css'>
|
||||||
|
<link rel='stylesheet' href='/_builder/bbui.css'>
|
||||||
<link rel='stylesheet' href='/_builder/fonts.css'>
|
<link rel='stylesheet' href='/_builder/fonts.css'>
|
||||||
<link rel='stylesheet' href="/_builder/uikit.min.css">
|
<link rel='stylesheet' href="/_builder/uikit.min.css">
|
||||||
</head>
|
</head>
|
||||||
|
@ -21,4 +23,5 @@
|
||||||
<body id="app">
|
<body id="app">
|
||||||
<script src='/_builder/bundle.js'></script>
|
<script src='/_builder/bundle.js'></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
|
@ -103,7 +103,6 @@
|
||||||
background-color: var(--white);
|
background-color: var(--white);
|
||||||
min-height: 0px;
|
min-height: 0px;
|
||||||
height: calc(100vh - 69px);
|
height: calc(100vh - 69px);
|
||||||
overflow-y: hidden;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-group-header > div:nth-child(1) {
|
.nav-group-header > div:nth-child(1) {
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
<script>
|
<script>
|
||||||
|
import { params } from "@sveltech/routify"
|
||||||
store.setCurrentPage($params.page)
|
store.setCurrentPage($params.page)
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -7,7 +7,7 @@ const Sqrl = require("squirrelly")
|
||||||
const uuid = require("uuid")
|
const uuid = require("uuid")
|
||||||
|
|
||||||
module.exports = opts => {
|
module.exports = opts => {
|
||||||
run(opts)
|
return run(opts)
|
||||||
}
|
}
|
||||||
|
|
||||||
const run = async opts => {
|
const run = async opts => {
|
||||||
|
|
|
@ -17,9 +17,7 @@ const run = async opts => {
|
||||||
exec(`cd ${join(opts.dir, opts.applicationId)} && npm install`)
|
exec(`cd ${join(opts.dir, opts.applicationId)} && npm install`)
|
||||||
console.log(chalk.green(`Budibase app ${opts.name} created!`))
|
console.log(chalk.green(`Budibase app ${opts.name} created!`))
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(
|
console.error(chalk.red("Error creating new app", error))
|
||||||
chalk.red("Error creating new app", JSON.stringify(error, { space: 2 }))
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -53,7 +51,9 @@ const createAppInstance = async opts => {
|
||||||
const createInstCtx = {
|
const createInstCtx = {
|
||||||
params: {
|
params: {
|
||||||
clientId: process.env.CLIENT_ID,
|
clientId: process.env.CLIENT_ID,
|
||||||
applicationId: opts.applicationId,
|
},
|
||||||
|
user: {
|
||||||
|
appId: opts.applicationId,
|
||||||
},
|
},
|
||||||
request: {
|
request: {
|
||||||
body: { name: `dev-${process.env.CLIENT_ID}` },
|
body: { name: `dev-${process.env.CLIENT_ID}` },
|
||||||
|
|
|
@ -5,7 +5,6 @@ module.exports = ({ dir }) => {
|
||||||
dir = xPlatHomeDir(dir)
|
dir = xPlatHomeDir(dir)
|
||||||
process.env.BUDIBASE_DIR = resolve(dir)
|
process.env.BUDIBASE_DIR = resolve(dir)
|
||||||
require("dotenv").config({ path: resolve(dir, ".env") })
|
require("dotenv").config({ path: resolve(dir, ".env") })
|
||||||
console.log("dotenv loaded")
|
|
||||||
|
|
||||||
// dont make this a variable or top level require
|
// dont make this a variable or top level require
|
||||||
// ti will cause environment module to be loaded prematurely
|
// ti will cause environment module to be loaded prematurely
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
"client": "web"
|
"client": "web"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"testURL": "http://jest-breaks-if-this-does-not-exist",
|
"testURL": "http://test.com",
|
||||||
"moduleNameMapper": {
|
"moduleNameMapper": {
|
||||||
"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "<rootDir>/internals/mocks/fileMock.js",
|
"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "<rootDir>/internals/mocks/fileMock.js",
|
||||||
"\\.(css|less|sass|scss)$": "identity-obj-proxy"
|
"\\.(css|less|sass|scss)$": "identity-obj-proxy"
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import { authenticate } from "./authenticate"
|
import { authenticate } from "./authenticate"
|
||||||
import { triggerWorkflow } from "./workflow"
|
import { triggerWorkflow } from "./workflow"
|
||||||
|
|
||||||
export const createApi = ({ rootPath = "", setState, getState }) => {
|
export const createApi = ({ setState, getState }) => {
|
||||||
const apiCall = method => async ({ url, body }) => {
|
const apiCall = method => async ({ url, body }) => {
|
||||||
const response = await fetch(`${rootPath}${url}`, {
|
const response = await fetch(url, {
|
||||||
method: method,
|
method: method,
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
|
@ -45,7 +45,6 @@ export const createApi = ({ rootPath = "", setState, getState }) => {
|
||||||
const isSuccess = obj => !obj || !obj[ERROR_MEMBER]
|
const isSuccess = obj => !obj || !obj[ERROR_MEMBER]
|
||||||
|
|
||||||
const apiOpts = {
|
const apiOpts = {
|
||||||
rootPath,
|
|
||||||
setState,
|
setState,
|
||||||
getState,
|
getState,
|
||||||
isSuccess,
|
isSuccess,
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { attachChildren } from "./render/attachChildren"
|
||||||
import { createTreeNode } from "./render/prepareRenderComponent"
|
import { createTreeNode } from "./render/prepareRenderComponent"
|
||||||
import { screenRouter } from "./render/screenRouter"
|
import { screenRouter } from "./render/screenRouter"
|
||||||
import { createStateManager } from "./state/stateManager"
|
import { createStateManager } from "./state/stateManager"
|
||||||
|
import { getAppId } from "./render/getAppId"
|
||||||
|
|
||||||
export const createApp = ({
|
export const createApp = ({
|
||||||
componentLibraries,
|
componentLibraries,
|
||||||
|
@ -15,11 +16,9 @@ export const createApp = ({
|
||||||
const onScreenSlotRendered = screenSlotNode => {
|
const onScreenSlotRendered = screenSlotNode => {
|
||||||
const onScreenSelected = (screen, url) => {
|
const onScreenSelected = (screen, url) => {
|
||||||
const stateManager = createStateManager({
|
const stateManager = createStateManager({
|
||||||
frontendDefinition,
|
|
||||||
componentLibraries,
|
componentLibraries,
|
||||||
onScreenSlotRendered: () => {},
|
onScreenSlotRendered: () => {},
|
||||||
routeTo,
|
routeTo,
|
||||||
appRootPath: frontendDefinition.appRootPath,
|
|
||||||
})
|
})
|
||||||
const getAttachChildrenParams = attachChildrenParams(stateManager)
|
const getAttachChildrenParams = attachChildrenParams(stateManager)
|
||||||
screenSlotNode.props._children = [screen.props]
|
screenSlotNode.props._children = [screen.props]
|
||||||
|
@ -36,10 +35,10 @@ export const createApp = ({
|
||||||
routeTo = screenRouter({
|
routeTo = screenRouter({
|
||||||
screens: frontendDefinition.screens,
|
screens: frontendDefinition.screens,
|
||||||
onScreenSelected,
|
onScreenSelected,
|
||||||
appRootPath: frontendDefinition.appRootPath,
|
window,
|
||||||
})
|
})
|
||||||
const fallbackPath = window.location.pathname.replace(
|
const fallbackPath = window.location.pathname.replace(
|
||||||
frontendDefinition.appRootPath,
|
getAppId(window.document.cookie),
|
||||||
""
|
""
|
||||||
)
|
)
|
||||||
routeTo(currentUrl || fallbackPath)
|
routeTo(currentUrl || fallbackPath)
|
||||||
|
@ -59,10 +58,8 @@ export const createApp = ({
|
||||||
|
|
||||||
let rootTreeNode
|
let rootTreeNode
|
||||||
const pageStateManager = createStateManager({
|
const pageStateManager = createStateManager({
|
||||||
frontendDefinition,
|
|
||||||
componentLibraries,
|
componentLibraries,
|
||||||
onScreenSlotRendered,
|
onScreenSlotRendered,
|
||||||
appRootPath: frontendDefinition.appRootPath,
|
|
||||||
// seems weird, but the routeTo variable may not be available at this point
|
// seems weird, but the routeTo variable may not be available at this point
|
||||||
routeTo: url => routeTo(url),
|
routeTo: url => routeTo(url),
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { createApp } from "./createApp"
|
import { createApp } from "./createApp"
|
||||||
import { builtins, builtinLibName } from "./render/builtinComponents"
|
import { builtins, builtinLibName } from "./render/builtinComponents"
|
||||||
|
import { getAppId } from "./render/getAppId"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* create a web application from static budibase definition files.
|
* create a web application from static budibase definition files.
|
||||||
|
@ -8,7 +9,7 @@ import { builtins, builtinLibName } from "./render/builtinComponents"
|
||||||
export const loadBudibase = async opts => {
|
export const loadBudibase = async opts => {
|
||||||
const _window = (opts && opts.window) || window
|
const _window = (opts && opts.window) || window
|
||||||
// const _localStorage = (opts && opts.localStorage) || localStorage
|
// const _localStorage = (opts && opts.localStorage) || localStorage
|
||||||
|
const appId = getAppId(_window.document.cookie)
|
||||||
const frontendDefinition = _window["##BUDIBASE_FRONTEND_DEFINITION##"]
|
const frontendDefinition = _window["##BUDIBASE_FRONTEND_DEFINITION##"]
|
||||||
|
|
||||||
const user = {}
|
const user = {}
|
||||||
|
@ -20,9 +21,7 @@ export const loadBudibase = async opts => {
|
||||||
for (let library of libraries) {
|
for (let library of libraries) {
|
||||||
// fetch the JavaScript for the component libraries from the server
|
// fetch the JavaScript for the component libraries from the server
|
||||||
componentLibraryModules[library] = await import(
|
componentLibraryModules[library] = await import(
|
||||||
`/${frontendDefinition.appId}/componentlibrary?library=${encodeURI(
|
`/componentlibrary?library=${encodeURI(library)}`
|
||||||
library
|
|
||||||
)}`
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -38,11 +37,11 @@ export const loadBudibase = async opts => {
|
||||||
componentLibraries: componentLibraryModules,
|
componentLibraries: componentLibraryModules,
|
||||||
frontendDefinition,
|
frontendDefinition,
|
||||||
user,
|
user,
|
||||||
window,
|
window: _window,
|
||||||
})
|
})
|
||||||
|
|
||||||
const route = _window.location
|
const route = _window.location
|
||||||
? _window.location.pathname.replace(frontendDefinition.appRootPath, "")
|
? _window.location.pathname.replace(`${appId}/`, "").replace(appId, "")
|
||||||
: ""
|
: ""
|
||||||
|
|
||||||
initialisePage(frontendDefinition.page, _window.document.body, route)
|
initialisePage(frontendDefinition.page, _window.document.body, route)
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
export const getAppId = docCookie => {
|
||||||
|
const cookie =
|
||||||
|
docCookie.split(";").find(c => c.trim().startsWith("budibase:token")) ||
|
||||||
|
docCookie.split(";").find(c => c.trim().startsWith("builder:token"))
|
||||||
|
|
||||||
|
const base64Token = cookie.substring(lengthOfKey)
|
||||||
|
|
||||||
|
const user = JSON.parse(atob(base64Token.split(".")[1]))
|
||||||
|
return user.appId
|
||||||
|
}
|
||||||
|
|
||||||
|
const lengthOfKey = "budibase:token=".length
|
|
@ -1,11 +1,20 @@
|
||||||
import regexparam from "regexparam"
|
import regexparam from "regexparam"
|
||||||
import { routerStore } from "../state/store"
|
import { routerStore } from "../state/store"
|
||||||
|
import { getAppId } from "./getAppId"
|
||||||
|
|
||||||
export const screenRouter = ({ screens, onScreenSelected, appRootPath }) => {
|
export const screenRouter = ({ screens, onScreenSelected, window }) => {
|
||||||
const makeRootedPath = url => {
|
const makeRootedPath = url => {
|
||||||
if (appRootPath) {
|
if (
|
||||||
if (url) return `${appRootPath}${url.startsWith("/") ? "" : "/"}${url}`
|
window.location &&
|
||||||
return appRootPath
|
(window.location.hostname === "localhost" ||
|
||||||
|
window.location.hostname === "127.0.0.1")
|
||||||
|
) {
|
||||||
|
const appId = getAppId(window.document.cookie)
|
||||||
|
if (url) {
|
||||||
|
if (url.startsWith(appId)) return url
|
||||||
|
return `/${appId}${url.startsWith("/") ? "" : "/"}${url}`
|
||||||
|
}
|
||||||
|
return appId
|
||||||
}
|
}
|
||||||
return url
|
return url
|
||||||
}
|
}
|
||||||
|
@ -70,7 +79,7 @@ export const screenRouter = ({ screens, onScreenSelected, appRootPath }) => {
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
const target = x.target || "_self"
|
const target = (x && x.target) || "_self"
|
||||||
if (!y || target !== "_self" || x.host !== location.host) return
|
if (!y || target !== "_self" || x.host !== location.host) return
|
||||||
|
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
|
|
@ -6,31 +6,19 @@ export const trimSlash = str => str.replace(/^\/+|\/+$/g, "")
|
||||||
|
|
||||||
export const bbFactory = ({
|
export const bbFactory = ({
|
||||||
store,
|
store,
|
||||||
frontendDefinition,
|
|
||||||
componentLibraries,
|
componentLibraries,
|
||||||
onScreenSlotRendered,
|
onScreenSlotRendered,
|
||||||
}) => {
|
}) => {
|
||||||
const relativeUrl = url => {
|
const apiCall = method => (url, body) => {
|
||||||
if (!frontendDefinition.appRootPath) return url
|
return fetch(url, {
|
||||||
if (
|
|
||||||
url.startsWith("http:") ||
|
|
||||||
url.startsWith("https:") ||
|
|
||||||
url.startsWith("./")
|
|
||||||
)
|
|
||||||
return url
|
|
||||||
|
|
||||||
return frontendDefinition.appRootPath + "/" + trimSlash(url)
|
|
||||||
}
|
|
||||||
|
|
||||||
const apiCall = method => (url, body) =>
|
|
||||||
fetch(url, {
|
|
||||||
method: method,
|
method: method,
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
"x-user-agent": "Budibase Builder",
|
|
||||||
},
|
},
|
||||||
body: body && JSON.stringify(body),
|
body: body && JSON.stringify(body),
|
||||||
|
credentials: "same-origin",
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const api = {
|
const api = {
|
||||||
post: apiCall("POST"),
|
post: apiCall("POST"),
|
||||||
|
@ -63,7 +51,6 @@ export const bbFactory = ({
|
||||||
getContext: getContext(treeNode),
|
getContext: getContext(treeNode),
|
||||||
setContext: setContext(treeNode),
|
setContext: setContext(treeNode),
|
||||||
store: store,
|
store: store,
|
||||||
relativeUrl,
|
|
||||||
api,
|
api,
|
||||||
parent,
|
parent,
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,14 +6,13 @@ import { createApi } from "../api"
|
||||||
|
|
||||||
export const EVENT_TYPE_MEMBER_NAME = "##eventHandlerType"
|
export const EVENT_TYPE_MEMBER_NAME = "##eventHandlerType"
|
||||||
|
|
||||||
export const eventHandlers = (rootPath, routeTo) => {
|
export const eventHandlers = routeTo => {
|
||||||
const handler = (parameters, execute) => ({
|
const handler = (parameters, execute) => ({
|
||||||
execute,
|
execute,
|
||||||
parameters,
|
parameters,
|
||||||
})
|
})
|
||||||
|
|
||||||
const api = createApi({
|
const api = createApi({
|
||||||
rootPath,
|
|
||||||
setState,
|
setState,
|
||||||
getState: (path, fallback) => getState(path, fallback),
|
getState: (path, fallback) => getState(path, fallback),
|
||||||
})
|
})
|
||||||
|
|
|
@ -21,13 +21,11 @@ const isMetaProp = propName =>
|
||||||
propName === "_styles"
|
propName === "_styles"
|
||||||
|
|
||||||
export const createStateManager = ({
|
export const createStateManager = ({
|
||||||
appRootPath,
|
|
||||||
frontendDefinition,
|
|
||||||
componentLibraries,
|
componentLibraries,
|
||||||
onScreenSlotRendered,
|
onScreenSlotRendered,
|
||||||
routeTo,
|
routeTo,
|
||||||
}) => {
|
}) => {
|
||||||
let handlerTypes = eventHandlers(appRootPath, routeTo)
|
let handlerTypes = eventHandlers(routeTo)
|
||||||
let currentState
|
let currentState
|
||||||
|
|
||||||
const getCurrentState = () => currentState
|
const getCurrentState = () => currentState
|
||||||
|
@ -35,7 +33,6 @@ export const createStateManager = ({
|
||||||
const bb = bbFactory({
|
const bb = bbFactory({
|
||||||
store: appStore,
|
store: appStore,
|
||||||
getCurrentState,
|
getCurrentState,
|
||||||
frontendDefinition,
|
|
||||||
componentLibraries,
|
componentLibraries,
|
||||||
onScreenSlotRendered,
|
onScreenSlotRendered,
|
||||||
})
|
})
|
||||||
|
|
|
@ -18,7 +18,7 @@ describe("screenRouting", () => {
|
||||||
|
|
||||||
it("should load correct screen, for initial URL, when appRootPath is something", async () => {
|
it("should load correct screen, for initial URL, when appRootPath is something", async () => {
|
||||||
const { page, screens } = pageWith3Screens()
|
const { page, screens } = pageWith3Screens()
|
||||||
const { dom } = await load(page, screens, "/testApp/screen2", "/testApp")
|
const { dom } = await load(page, screens, "/TEST_APP_ID/screen2", "127.0.0.1")
|
||||||
|
|
||||||
const rootDiv = dom.window.document.body.children[0]
|
const rootDiv = dom.window.document.body.children[0]
|
||||||
expect(rootDiv.children.length).toBe(1)
|
expect(rootDiv.children.length).toBe(1)
|
||||||
|
@ -50,8 +50,8 @@ describe("screenRouting", () => {
|
||||||
const { dom, app } = await load(
|
const { dom, app } = await load(
|
||||||
page,
|
page,
|
||||||
screens,
|
screens,
|
||||||
"/testApp/screen2",
|
"/TEST_APP_ID/screen2",
|
||||||
"/testApp"
|
"127.0.0.1"
|
||||||
)
|
)
|
||||||
|
|
||||||
app.routeTo()("/screen3")
|
app.routeTo()("/screen3")
|
||||||
|
|
|
@ -1,19 +1,33 @@
|
||||||
import { JSDOM } from "jsdom"
|
import jsdom, { JSDOM } from "jsdom"
|
||||||
import { loadBudibase } from "../src/index"
|
import { loadBudibase } from "../src/index"
|
||||||
|
|
||||||
export const load = async (page, screens, url, appRootPath) => {
|
export const load = async (page, screens, url, host = "test.com") => {
|
||||||
screens = screens || []
|
screens = screens || []
|
||||||
url = url || "/"
|
url = url || "/"
|
||||||
appRootPath = appRootPath || ""
|
|
||||||
|
const fullUrl = `http://${host}${url}`
|
||||||
|
const cookieJar = new jsdom.CookieJar()
|
||||||
|
const cookie = `${btoa("{}")}.${btoa('{"appId":"TEST_APP_ID"}')}.signature`
|
||||||
|
cookieJar.setCookie(
|
||||||
|
`budibase:token=${cookie};domain=${host};path=/`,
|
||||||
|
fullUrl,
|
||||||
|
{
|
||||||
|
looseMode: false,
|
||||||
|
},
|
||||||
|
() => {}
|
||||||
|
)
|
||||||
|
|
||||||
const dom = new JSDOM("<!DOCTYPE html><html><body></body><html>", {
|
const dom = new JSDOM("<!DOCTYPE html><html><body></body><html>", {
|
||||||
url: `http://test${url}`,
|
url: fullUrl,
|
||||||
|
cookieJar,
|
||||||
})
|
})
|
||||||
|
|
||||||
autoAssignIds(page.props)
|
autoAssignIds(page.props)
|
||||||
for (let s of screens) {
|
for (let s of screens) {
|
||||||
autoAssignIds(s.props)
|
autoAssignIds(s.props)
|
||||||
}
|
}
|
||||||
setAppDef(dom.window, page, screens)
|
setAppDef(dom.window, page, screens)
|
||||||
addWindowGlobals(dom.window, page, screens, appRootPath, {
|
addWindowGlobals(dom.window, page, screens, {
|
||||||
hierarchy: {},
|
hierarchy: {},
|
||||||
actions: [],
|
actions: [],
|
||||||
triggers: [],
|
triggers: [],
|
||||||
|
@ -27,11 +41,10 @@ export const load = async (page, screens, url, appRootPath) => {
|
||||||
return { dom, app }
|
return { dom, app }
|
||||||
}
|
}
|
||||||
|
|
||||||
const addWindowGlobals = (window, page, screens, appRootPath) => {
|
const addWindowGlobals = (window, page, screens) => {
|
||||||
window["##BUDIBASE_FRONTEND_DEFINITION##"] = {
|
window["##BUDIBASE_FRONTEND_DEFINITION##"] = {
|
||||||
page,
|
page,
|
||||||
screens,
|
screens,
|
||||||
appRootPath,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -88,7 +101,6 @@ const setAppDef = (window, page, screens) => {
|
||||||
componentLibraries: [],
|
componentLibraries: [],
|
||||||
page,
|
page,
|
||||||
screens,
|
screens,
|
||||||
appRootPath: "",
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -14,12 +14,6 @@ export default async () => {
|
||||||
componentLibraries["@budibase/standard-components"] = standardcomponents
|
componentLibraries["@budibase/standard-components"] = standardcomponents
|
||||||
const appDef = { hierarchy: {}, actions: {} }
|
const appDef = { hierarchy: {}, actions: {} }
|
||||||
const user = { name: "yeo", permissions: [] }
|
const user = { name: "yeo", permissions: [] }
|
||||||
const { initialisePage } = createApp(
|
const { initialisePage } = createApp(componentLibraries, {}, appDef, user, {})
|
||||||
componentLibraries,
|
|
||||||
{ appRootPath: "" },
|
|
||||||
appDef,
|
|
||||||
user,
|
|
||||||
{}
|
|
||||||
)
|
|
||||||
return initialisePage
|
return initialisePage
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,9 +5,6 @@ COUCH_DB_URL={{couchDbUrl}}
|
||||||
# identifies a client database - i.e. group of apps
|
# identifies a client database - i.e. group of apps
|
||||||
CLIENT_ID={{clientId}}
|
CLIENT_ID={{clientId}}
|
||||||
|
|
||||||
# Full access API key for server
|
|
||||||
ADMIN_SECRET={{adminSecret}}
|
|
||||||
|
|
||||||
# used to create cookie hashes
|
# used to create cookie hashes
|
||||||
JWT_SECRET={{cookieKey1}}
|
JWT_SECRET={{cookieKey1}}
|
||||||
|
|
||||||
|
|
|
@ -4,5 +4,4 @@ process.env.NODE_ENV = "jest"
|
||||||
process.env.JWT_SECRET = "test-jwtsecret"
|
process.env.JWT_SECRET = "test-jwtsecret"
|
||||||
process.env.CLIENT_ID = "test-client-id"
|
process.env.CLIENT_ID = "test-client-id"
|
||||||
process.env.BUDIBASE_DIR = tmpdir("budibase-unittests")
|
process.env.BUDIBASE_DIR = tmpdir("budibase-unittests")
|
||||||
process.env.ADMIN_SECRET = "test-admin-secret"
|
|
||||||
process.env.LOG_LEVEL = "silent"
|
process.env.LOG_LEVEL = "silent"
|
||||||
|
|
|
@ -8,7 +8,7 @@ const {
|
||||||
} = require("../../utilities/accessLevels")
|
} = require("../../utilities/accessLevels")
|
||||||
|
|
||||||
exports.fetch = async function(ctx) {
|
exports.fetch = async function(ctx) {
|
||||||
const db = new CouchDB(ctx.params.instanceId)
|
const db = new CouchDB(ctx.user.instanceId)
|
||||||
const body = await db.query("database/by_type", {
|
const body = await db.query("database/by_type", {
|
||||||
include_docs: true,
|
include_docs: true,
|
||||||
key: ["accesslevel"],
|
key: ["accesslevel"],
|
||||||
|
@ -19,12 +19,12 @@ exports.fetch = async function(ctx) {
|
||||||
{
|
{
|
||||||
_id: ADMIN_LEVEL_ID,
|
_id: ADMIN_LEVEL_ID,
|
||||||
name: "Admin",
|
name: "Admin",
|
||||||
permissions: await generateAdminPermissions(ctx.params.instanceId),
|
permissions: await generateAdminPermissions(ctx.user.instanceId),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
_id: POWERUSER_LEVEL_ID,
|
_id: POWERUSER_LEVEL_ID,
|
||||||
name: "Power User",
|
name: "Power User",
|
||||||
permissions: await generatePowerUserPermissions(ctx.params.instanceId),
|
permissions: await generatePowerUserPermissions(ctx.user.instanceId),
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -32,12 +32,12 @@ exports.fetch = async function(ctx) {
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.find = async function(ctx) {
|
exports.find = async function(ctx) {
|
||||||
const db = new CouchDB(ctx.params.instanceId)
|
const db = new CouchDB(ctx.user.instanceId)
|
||||||
ctx.body = await db.get(ctx.params.levelId)
|
ctx.body = await db.get(ctx.params.levelId)
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.update = async function(ctx) {
|
exports.update = async function(ctx) {
|
||||||
const db = new CouchDB(ctx.params.instanceId)
|
const db = new CouchDB(ctx.user.instanceId)
|
||||||
const level = await db.get(ctx.params.levelId)
|
const level = await db.get(ctx.params.levelId)
|
||||||
level.name = ctx.body.name
|
level.name = ctx.body.name
|
||||||
level.permissions = ctx.request.body.permissions
|
level.permissions = ctx.request.body.permissions
|
||||||
|
@ -48,7 +48,7 @@ exports.update = async function(ctx) {
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.patch = async function(ctx) {
|
exports.patch = async function(ctx) {
|
||||||
const db = new CouchDB(ctx.params.instanceId)
|
const db = new CouchDB(ctx.user.instanceId)
|
||||||
const level = await db.get(ctx.params.levelId)
|
const level = await db.get(ctx.params.levelId)
|
||||||
const { removedPermissions, addedPermissions, _rev } = ctx.request.body
|
const { removedPermissions, addedPermissions, _rev } = ctx.request.body
|
||||||
|
|
||||||
|
@ -84,7 +84,7 @@ exports.patch = async function(ctx) {
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.create = async function(ctx) {
|
exports.create = async function(ctx) {
|
||||||
const db = new CouchDB(ctx.params.instanceId)
|
const db = new CouchDB(ctx.user.instanceId)
|
||||||
|
|
||||||
const level = {
|
const level = {
|
||||||
name: ctx.request.body.name,
|
name: ctx.request.body.name,
|
||||||
|
@ -101,7 +101,7 @@ exports.create = async function(ctx) {
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.destroy = async function(ctx) {
|
exports.destroy = async function(ctx) {
|
||||||
const db = new CouchDB(ctx.params.instanceId)
|
const db = new CouchDB(ctx.user.instanceId)
|
||||||
await db.remove(ctx.params.levelId, ctx.params.rev)
|
await db.remove(ctx.params.levelId, ctx.params.rev)
|
||||||
ctx.message = `Access Level ${ctx.params.id} deleted successfully`
|
ctx.message = `Access Level ${ctx.params.id} deleted successfully`
|
||||||
ctx.status = 200
|
ctx.status = 200
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
const CouchDB = require("../../db")
|
const CouchDB = require("../../db")
|
||||||
const ClientDb = require("../../db/clientDb")
|
const ClientDb = require("../../db/clientDb")
|
||||||
const { getPackageForBuilder } = require("../../utilities/builder")
|
const { getPackageForBuilder, buildPage } = require("../../utilities/builder")
|
||||||
const newid = require("../../db/newid")
|
const newid = require("../../db/newid")
|
||||||
const env = require("../../environment")
|
const env = require("../../environment")
|
||||||
const instanceController = require("./instance")
|
const instanceController = require("./instance")
|
||||||
|
@ -9,6 +9,7 @@ const { copy, exists, readFile, writeFile } = require("fs-extra")
|
||||||
const { budibaseAppsDir } = require("../../utilities/budibaseDir")
|
const { budibaseAppsDir } = require("../../utilities/budibaseDir")
|
||||||
const { exec } = require("child_process")
|
const { exec } = require("child_process")
|
||||||
const sqrl = require("squirrelly")
|
const sqrl = require("squirrelly")
|
||||||
|
const setBuilderToken = require("../../utilities/builder/setBuilderToken")
|
||||||
|
|
||||||
exports.fetch = async function(ctx) {
|
exports.fetch = async function(ctx) {
|
||||||
const db = new CouchDB(ClientDb.name(getClientId(ctx)))
|
const db = new CouchDB(ClientDb.name(getClientId(ctx)))
|
||||||
|
@ -25,6 +26,14 @@ exports.fetchAppPackage = async function(ctx) {
|
||||||
const db = new CouchDB(ClientDb.name(clientId))
|
const db = new CouchDB(ClientDb.name(clientId))
|
||||||
const application = await db.get(ctx.params.applicationId)
|
const application = await db.get(ctx.params.applicationId)
|
||||||
ctx.body = await getPackageForBuilder(ctx.config, application)
|
ctx.body = await getPackageForBuilder(ctx.config, application)
|
||||||
|
/*
|
||||||
|
instance is hardcoded now - this can only change when we move
|
||||||
|
pages and screens into the database
|
||||||
|
*/
|
||||||
|
const devInstance = application.instances.find(
|
||||||
|
i => i.name === `dev-${clientId}`
|
||||||
|
)
|
||||||
|
setBuilderToken(ctx, ctx.params.applicationId, devInstance._id)
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.create = async function(ctx) {
|
exports.create = async function(ctx) {
|
||||||
|
@ -37,10 +46,12 @@ exports.create = async function(ctx) {
|
||||||
const appId = newid()
|
const appId = newid()
|
||||||
// insert an appId -> clientId lookup
|
// insert an appId -> clientId lookup
|
||||||
const masterDb = new CouchDB("clientAppLookup")
|
const masterDb = new CouchDB("clientAppLookup")
|
||||||
|
|
||||||
await masterDb.put({
|
await masterDb.put({
|
||||||
_id: appId,
|
_id: appId,
|
||||||
clientId,
|
clientId,
|
||||||
})
|
})
|
||||||
|
|
||||||
const db = new CouchDB(ClientDb.name(clientId))
|
const db = new CouchDB(ClientDb.name(clientId))
|
||||||
|
|
||||||
const newApplication = {
|
const newApplication = {
|
||||||
|
@ -56,18 +67,18 @@ exports.create = async function(ctx) {
|
||||||
description: ctx.request.body.description,
|
description: ctx.request.body.description,
|
||||||
}
|
}
|
||||||
|
|
||||||
const { rev } = await db.post(newApplication)
|
const { rev } = await db.put(newApplication)
|
||||||
newApplication._rev = rev
|
newApplication._rev = rev
|
||||||
|
|
||||||
const createInstCtx = {
|
const createInstCtx = {
|
||||||
params: {
|
user: {
|
||||||
applicationId: newApplication._id,
|
appId: newApplication._id,
|
||||||
},
|
},
|
||||||
request: {
|
request: {
|
||||||
body: { name: `dev-${clientId}` },
|
body: { name: `dev-${clientId}` },
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
await instanceController.create(createInstCtx)
|
await instanceController.create(createInstCtx)
|
||||||
|
newApplication.instances.push(createInstCtx.body)
|
||||||
|
|
||||||
if (ctx.isDev) {
|
if (ctx.isDev) {
|
||||||
const newAppFolder = await createEmptyAppPackage(ctx, newApplication)
|
const newAppFolder = await createEmptyAppPackage(ctx, newApplication)
|
||||||
|
@ -100,15 +111,23 @@ const createEmptyAppPackage = async (ctx, app) => {
|
||||||
await updateJsonFile(join(appsFolder, app._id, "package.json"), {
|
await updateJsonFile(join(appsFolder, app._id, "package.json"), {
|
||||||
name: npmFriendlyAppName(app.name),
|
name: npmFriendlyAppName(app.name),
|
||||||
})
|
})
|
||||||
await updateJsonFile(
|
|
||||||
|
const mainJson = await updateJsonFile(
|
||||||
join(appsFolder, app._id, "pages", "main", "page.json"),
|
join(appsFolder, app._id, "pages", "main", "page.json"),
|
||||||
app
|
app
|
||||||
)
|
)
|
||||||
await updateJsonFile(
|
|
||||||
|
await buildPage(ctx.config, app._id, "main", { page: mainJson })
|
||||||
|
|
||||||
|
const unauthenticatedJson = await updateJsonFile(
|
||||||
join(appsFolder, app._id, "pages", "unauthenticated", "page.json"),
|
join(appsFolder, app._id, "pages", "unauthenticated", "page.json"),
|
||||||
app
|
app
|
||||||
)
|
)
|
||||||
|
|
||||||
|
await buildPage(ctx.config, app._id, "unauthenticated", {
|
||||||
|
page: unauthenticatedJson,
|
||||||
|
})
|
||||||
|
|
||||||
return newAppFolder
|
return newAppFolder
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -134,6 +153,7 @@ const updateJsonFile = async (filePath, app) => {
|
||||||
const json = await readFile(filePath, "utf8")
|
const json = await readFile(filePath, "utf8")
|
||||||
const newJson = sqrl.Render(json, app)
|
const newJson = sqrl.Render(json, app)
|
||||||
await writeFile(filePath, newJson, "utf8")
|
await writeFile(filePath, newJson, "utf8")
|
||||||
|
return JSON.parse(newJson)
|
||||||
}
|
}
|
||||||
|
|
||||||
const runNpmInstall = async newAppFolder => {
|
const runNpmInstall = async newAppFolder => {
|
||||||
|
|
|
@ -4,25 +4,31 @@ const ClientDb = require("../../db/clientDb")
|
||||||
const bcrypt = require("../../utilities/bcrypt")
|
const bcrypt = require("../../utilities/bcrypt")
|
||||||
|
|
||||||
exports.authenticate = async ctx => {
|
exports.authenticate = async ctx => {
|
||||||
|
if (!ctx.user.appId) ctx.throw(400, "No appId")
|
||||||
|
|
||||||
const { username, password } = ctx.request.body
|
const { username, password } = ctx.request.body
|
||||||
|
|
||||||
if (!username) ctx.throw(400, "Username Required.")
|
if (!username) ctx.throw(400, "Username Required.")
|
||||||
if (!password) ctx.throw(400, "Password Required")
|
if (!password) ctx.throw(400, "Password Required")
|
||||||
|
|
||||||
const masterDb = new CouchDB("clientAppLookup")
|
const masterDb = new CouchDB("clientAppLookup")
|
||||||
const { clientId } = await masterDb.get(ctx.params.appId)
|
|
||||||
|
const { clientId } = await masterDb.get(ctx.user.appId)
|
||||||
|
|
||||||
if (!clientId) {
|
if (!clientId) {
|
||||||
ctx.throw(400, "ClientId not suplied")
|
ctx.throw(400, "ClientId not suplied")
|
||||||
}
|
}
|
||||||
// find the instance that the user is associated with
|
// find the instance that the user is associated with
|
||||||
const db = new CouchDB(ClientDb.name(clientId))
|
const db = new CouchDB(ClientDb.name(clientId))
|
||||||
const appId = ctx.params.appId
|
const app = await db.get(ctx.user.appId)
|
||||||
const app = await db.get(appId)
|
|
||||||
const instanceId = app.userInstanceMap[username]
|
const instanceId = app.userInstanceMap[username]
|
||||||
|
|
||||||
if (!instanceId)
|
if (!instanceId)
|
||||||
ctx.throw(500, "User is not associated with an instance of app", appId)
|
ctx.throw(
|
||||||
|
500,
|
||||||
|
"User is not associated with an instance of app",
|
||||||
|
ctx.user.appId
|
||||||
|
)
|
||||||
|
|
||||||
// Check the user exists in the instance DB by username
|
// Check the user exists in the instance DB by username
|
||||||
const instanceDb = new CouchDB(instanceId)
|
const instanceDb = new CouchDB(instanceId)
|
||||||
|
@ -41,16 +47,22 @@ exports.authenticate = async ctx => {
|
||||||
const payload = {
|
const payload = {
|
||||||
userId: dbUser._id,
|
userId: dbUser._id,
|
||||||
accessLevelId: dbUser.accessLevelId,
|
accessLevelId: dbUser.accessLevelId,
|
||||||
instanceId: instanceId,
|
appId: ctx.user.appId,
|
||||||
|
instanceId,
|
||||||
}
|
}
|
||||||
|
|
||||||
const token = jwt.sign(payload, ctx.config.jwtSecret, {
|
const token = jwt.sign(payload, ctx.config.jwtSecret, {
|
||||||
expiresIn: "1 day",
|
expiresIn: "1 day",
|
||||||
})
|
})
|
||||||
|
|
||||||
const ONE_DAY_FROM_NOW = new Date(Date.now() + 24 * 3600)
|
const expires = new Date()
|
||||||
|
expires.setDate(expires.getDate() + 1)
|
||||||
|
|
||||||
ctx.cookies.set("budibase:token", token, { expires: ONE_DAY_FROM_NOW })
|
ctx.cookies.set("budibase:token", token, {
|
||||||
|
expires,
|
||||||
|
path: "/",
|
||||||
|
httpOnly: false,
|
||||||
|
})
|
||||||
|
|
||||||
ctx.body = {
|
ctx.body = {
|
||||||
token,
|
token,
|
||||||
|
|
|
@ -4,19 +4,19 @@ const newid = require("../../db/newid")
|
||||||
|
|
||||||
exports.create = async function(ctx) {
|
exports.create = async function(ctx) {
|
||||||
const instanceName = ctx.request.body.name
|
const instanceName = ctx.request.body.name
|
||||||
const appShortId = ctx.params.applicationId.substring(0, 7)
|
const { appId } = ctx.user
|
||||||
|
const appShortId = appId.substring(0, 7)
|
||||||
const instanceId = `inst_${appShortId}_${newid()}`
|
const instanceId = `inst_${appShortId}_${newid()}`
|
||||||
const { applicationId } = ctx.params
|
|
||||||
|
|
||||||
const masterDb = new CouchDB("clientAppLookup")
|
const masterDb = new CouchDB("clientAppLookup")
|
||||||
const { clientId } = await masterDb.get(applicationId)
|
const { clientId } = await masterDb.get(appId)
|
||||||
|
|
||||||
const db = new CouchDB(instanceId)
|
const db = new CouchDB(instanceId)
|
||||||
await db.put({
|
await db.put({
|
||||||
_id: "_design/database",
|
_id: "_design/database",
|
||||||
metadata: {
|
metadata: {
|
||||||
clientId,
|
clientId,
|
||||||
applicationId,
|
applicationId: appId,
|
||||||
},
|
},
|
||||||
views: {
|
views: {
|
||||||
by_username: {
|
by_username: {
|
||||||
|
@ -46,7 +46,7 @@ exports.create = async function(ctx) {
|
||||||
|
|
||||||
// Add the new instance under the app clientDB
|
// Add the new instance under the app clientDB
|
||||||
const clientDb = new CouchDB(client.name(clientId))
|
const clientDb = new CouchDB(client.name(clientId))
|
||||||
const budibaseApp = await clientDb.get(applicationId)
|
const budibaseApp = await clientDb.get(appId)
|
||||||
const instance = { _id: instanceId, name: instanceName }
|
const instance = { _id: instanceId, name: instanceName }
|
||||||
budibaseApp.instances.push(instance)
|
budibaseApp.instances.push(instance)
|
||||||
await clientDb.put(budibaseApp)
|
await clientDb.put(budibaseApp)
|
||||||
|
|
|
@ -2,7 +2,7 @@ const CouchDB = require("../../db")
|
||||||
const newid = require("../../db/newid")
|
const newid = require("../../db/newid")
|
||||||
|
|
||||||
exports.fetch = async function(ctx) {
|
exports.fetch = async function(ctx) {
|
||||||
const db = new CouchDB(ctx.params.instanceId)
|
const db = new CouchDB(ctx.user.instanceId)
|
||||||
const body = await db.query("database/by_type", {
|
const body = await db.query("database/by_type", {
|
||||||
include_docs: true,
|
include_docs: true,
|
||||||
key: ["model"],
|
key: ["model"],
|
||||||
|
@ -11,13 +11,13 @@ exports.fetch = async function(ctx) {
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.find = async function(ctx) {
|
exports.find = async function(ctx) {
|
||||||
const db = new CouchDB(ctx.params.instanceId)
|
const db = new CouchDB(ctx.user.instanceId)
|
||||||
const model = await db.get(ctx.params.id)
|
const model = await db.get(ctx.params.id)
|
||||||
ctx.body = model
|
ctx.body = model
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.create = async function(ctx) {
|
exports.create = async function(ctx) {
|
||||||
const db = new CouchDB(ctx.params.instanceId)
|
const db = new CouchDB(ctx.user.instanceId)
|
||||||
const newModel = {
|
const newModel = {
|
||||||
type: "model",
|
type: "model",
|
||||||
...ctx.request.body,
|
...ctx.request.body,
|
||||||
|
@ -65,7 +65,7 @@ exports.create = async function(ctx) {
|
||||||
exports.update = async function() {}
|
exports.update = async function() {}
|
||||||
|
|
||||||
exports.destroy = async function(ctx) {
|
exports.destroy = async function(ctx) {
|
||||||
const db = new CouchDB(ctx.params.instanceId)
|
const db = new CouchDB(ctx.user.instanceId)
|
||||||
|
|
||||||
const modelToDelete = await db.get(ctx.params.modelId)
|
const modelToDelete = await db.get(ctx.params.modelId)
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@ const validateJs = require("validate.js")
|
||||||
const newid = require("../../db/newid")
|
const newid = require("../../db/newid")
|
||||||
|
|
||||||
exports.save = async function(ctx) {
|
exports.save = async function(ctx) {
|
||||||
const db = new CouchDB(ctx.params.instanceId)
|
const db = new CouchDB(ctx.user.instanceId)
|
||||||
const record = ctx.request.body
|
const record = ctx.request.body
|
||||||
record.modelId = ctx.params.modelId
|
record.modelId = ctx.params.modelId
|
||||||
|
|
||||||
|
@ -46,7 +46,7 @@ exports.save = async function(ctx) {
|
||||||
ctx.eventEmitter &&
|
ctx.eventEmitter &&
|
||||||
ctx.eventEmitter.emit(`record:save`, {
|
ctx.eventEmitter.emit(`record:save`, {
|
||||||
record,
|
record,
|
||||||
instanceId: ctx.params.instanceId,
|
instanceId: ctx.user.instanceId,
|
||||||
})
|
})
|
||||||
ctx.body = record
|
ctx.body = record
|
||||||
ctx.status = 200
|
ctx.status = 200
|
||||||
|
@ -54,7 +54,7 @@ exports.save = async function(ctx) {
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.fetchView = async function(ctx) {
|
exports.fetchView = async function(ctx) {
|
||||||
const db = new CouchDB(ctx.params.instanceId)
|
const db = new CouchDB(ctx.user.instanceId)
|
||||||
const response = await db.query(`database/${ctx.params.viewName}`, {
|
const response = await db.query(`database/${ctx.params.viewName}`, {
|
||||||
include_docs: true,
|
include_docs: true,
|
||||||
})
|
})
|
||||||
|
@ -62,7 +62,7 @@ exports.fetchView = async function(ctx) {
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.fetchModelRecords = async function(ctx) {
|
exports.fetchModelRecords = async function(ctx) {
|
||||||
const db = new CouchDB(ctx.params.instanceId)
|
const db = new CouchDB(ctx.user.instanceId)
|
||||||
const response = await db.query(`database/all_${ctx.params.modelId}`, {
|
const response = await db.query(`database/all_${ctx.params.modelId}`, {
|
||||||
include_docs: true,
|
include_docs: true,
|
||||||
})
|
})
|
||||||
|
@ -70,7 +70,7 @@ exports.fetchModelRecords = async function(ctx) {
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.search = async function(ctx) {
|
exports.search = async function(ctx) {
|
||||||
const db = new CouchDB(ctx.params.instanceId)
|
const db = new CouchDB(ctx.user.instanceId)
|
||||||
const response = await db.allDocs({
|
const response = await db.allDocs({
|
||||||
include_docs: true,
|
include_docs: true,
|
||||||
...ctx.request.body,
|
...ctx.request.body,
|
||||||
|
@ -79,7 +79,7 @@ exports.search = async function(ctx) {
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.find = async function(ctx) {
|
exports.find = async function(ctx) {
|
||||||
const db = new CouchDB(ctx.params.instanceId)
|
const db = new CouchDB(ctx.user.instanceId)
|
||||||
const record = await db.get(ctx.params.recordId)
|
const record = await db.get(ctx.params.recordId)
|
||||||
if (record.modelId !== ctx.params.modelId) {
|
if (record.modelId !== ctx.params.modelId) {
|
||||||
ctx.throw(400, "Supplied modelId doe not match the record's modelId")
|
ctx.throw(400, "Supplied modelId doe not match the record's modelId")
|
||||||
|
@ -89,7 +89,7 @@ exports.find = async function(ctx) {
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.destroy = async function(ctx) {
|
exports.destroy = async function(ctx) {
|
||||||
const db = new CouchDB(ctx.params.instanceId)
|
const db = new CouchDB(ctx.user.instanceId)
|
||||||
const record = await db.get(ctx.params.recordId)
|
const record = await db.get(ctx.params.recordId)
|
||||||
if (record.modelId !== ctx.params.modelId) {
|
if (record.modelId !== ctx.params.modelId) {
|
||||||
ctx.throw(400, "Supplied modelId doe not match the record's modelId")
|
ctx.throw(400, "Supplied modelId doe not match the record's modelId")
|
||||||
|
@ -101,7 +101,7 @@ exports.destroy = async function(ctx) {
|
||||||
|
|
||||||
exports.validate = async function(ctx) {
|
exports.validate = async function(ctx) {
|
||||||
const errors = await validate({
|
const errors = await validate({
|
||||||
instanceId: ctx.params.instanceId,
|
instanceId: ctx.user.instanceId,
|
||||||
modelId: ctx.params.modelId,
|
modelId: ctx.params.modelId,
|
||||||
record: ctx.request.body,
|
record: ctx.request.body,
|
||||||
})
|
})
|
||||||
|
|
|
@ -4,11 +4,15 @@ const {
|
||||||
budibaseAppsDir,
|
budibaseAppsDir,
|
||||||
budibaseTempDir,
|
budibaseTempDir,
|
||||||
} = require("../../utilities/budibaseDir")
|
} = require("../../utilities/budibaseDir")
|
||||||
const env = require("../../environment")
|
const setBuilderToken = require("../../utilities/builder/setBuilderToken")
|
||||||
|
const { ANON_LEVEL_ID } = require("../../utilities/accessLevels")
|
||||||
|
const jwt = require("jsonwebtoken")
|
||||||
|
|
||||||
exports.serveBuilder = async function(ctx) {
|
exports.serveBuilder = async function(ctx) {
|
||||||
let builderPath = resolve(__dirname, "../../../builder")
|
let builderPath = resolve(__dirname, "../../../builder")
|
||||||
ctx.cookies.set("builder:token", env.ADMIN_SECRET)
|
if (ctx.file === "index.html") {
|
||||||
|
setBuilderToken(ctx)
|
||||||
|
}
|
||||||
await send(ctx, ctx.file, { root: ctx.devPath || builderPath })
|
await send(ctx, ctx.file, { root: ctx.devPath || builderPath })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -20,6 +24,33 @@ exports.serveApp = async function(ctx) {
|
||||||
"public",
|
"public",
|
||||||
ctx.isAuthenticated ? "main" : "unauthenticated"
|
ctx.isAuthenticated ? "main" : "unauthenticated"
|
||||||
)
|
)
|
||||||
|
// only set the appId cookie for /appId .. we COULD check for valid appIds
|
||||||
|
// but would like to avoid that DB hit
|
||||||
|
const looksLikeAppId = /^[0-9a-f]{32}$/.test(ctx.params.appId)
|
||||||
|
if (looksLikeAppId && !ctx.isAuthenticated) {
|
||||||
|
const anonUser = {
|
||||||
|
userId: "ANON",
|
||||||
|
accessLevelId: ANON_LEVEL_ID,
|
||||||
|
appId: ctx.params.appId,
|
||||||
|
}
|
||||||
|
const anonToken = jwt.sign(anonUser, ctx.config.jwtSecret)
|
||||||
|
ctx.cookies.set("budibase:token", anonToken, {
|
||||||
|
path: "/",
|
||||||
|
httpOnly: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
await send(ctx, ctx.file || "index.html", { root: ctx.devPath || appPath })
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.serveAppAsset = async function(ctx) {
|
||||||
|
// default to homedir
|
||||||
|
const appPath = resolve(
|
||||||
|
budibaseAppsDir(),
|
||||||
|
ctx.user.appId,
|
||||||
|
"public",
|
||||||
|
ctx.isAuthenticated ? "main" : "unauthenticated"
|
||||||
|
)
|
||||||
|
|
||||||
await send(ctx, ctx.file, { root: ctx.devPath || appPath })
|
await send(ctx, ctx.file, { root: ctx.devPath || appPath })
|
||||||
}
|
}
|
||||||
|
@ -28,7 +59,7 @@ exports.serveComponentLibrary = async function(ctx) {
|
||||||
// default to homedir
|
// default to homedir
|
||||||
let componentLibraryPath = resolve(
|
let componentLibraryPath = resolve(
|
||||||
budibaseAppsDir(),
|
budibaseAppsDir(),
|
||||||
ctx.params.appId,
|
ctx.user.appId,
|
||||||
"node_modules",
|
"node_modules",
|
||||||
decodeURI(ctx.query.library),
|
decodeURI(ctx.query.library),
|
||||||
"dist"
|
"dist"
|
||||||
|
|
|
@ -8,7 +8,7 @@ const {
|
||||||
} = require("../../utilities/accessLevels")
|
} = require("../../utilities/accessLevels")
|
||||||
|
|
||||||
exports.fetch = async function(ctx) {
|
exports.fetch = async function(ctx) {
|
||||||
const database = new CouchDB(ctx.params.instanceId)
|
const database = new CouchDB(ctx.user.instanceId)
|
||||||
const data = await database.query("database/by_type", {
|
const data = await database.query("database/by_type", {
|
||||||
include_docs: true,
|
include_docs: true,
|
||||||
key: ["user"],
|
key: ["user"],
|
||||||
|
@ -18,7 +18,7 @@ exports.fetch = async function(ctx) {
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.create = async function(ctx) {
|
exports.create = async function(ctx) {
|
||||||
const database = new CouchDB(ctx.params.instanceId)
|
const database = new CouchDB(ctx.user.instanceId)
|
||||||
const appId = (await database.get("_design/database")).metadata.applicationId
|
const appId = (await database.get("_design/database")).metadata.applicationId
|
||||||
const { username, password, name, accessLevelId } = ctx.request.body
|
const { username, password, name, accessLevelId } = ctx.request.body
|
||||||
|
|
||||||
|
@ -50,7 +50,7 @@ exports.create = async function(ctx) {
|
||||||
|
|
||||||
app.userInstanceMap = {
|
app.userInstanceMap = {
|
||||||
...app.userInstanceMap,
|
...app.userInstanceMap,
|
||||||
[username]: ctx.params.instanceId,
|
[username]: ctx.user.instanceId,
|
||||||
}
|
}
|
||||||
await db.put(app)
|
await db.put(app)
|
||||||
|
|
||||||
|
@ -66,14 +66,14 @@ exports.create = async function(ctx) {
|
||||||
exports.update = async function() {}
|
exports.update = async function() {}
|
||||||
|
|
||||||
exports.destroy = async function(ctx) {
|
exports.destroy = async function(ctx) {
|
||||||
const database = new CouchDB(ctx.params.instanceId)
|
const database = new CouchDB(ctx.user.instanceId)
|
||||||
await database.destroy(getUserId(ctx.params.username))
|
await database.destroy(getUserId(ctx.params.username))
|
||||||
ctx.message = `User ${ctx.params.username} deleted.`
|
ctx.message = `User ${ctx.params.username} deleted.`
|
||||||
ctx.status = 200
|
ctx.status = 200
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.find = async function(ctx) {
|
exports.find = async function(ctx) {
|
||||||
const database = new CouchDB(ctx.params.instanceId)
|
const database = new CouchDB(ctx.user.instanceId)
|
||||||
const user = await database.get(getUserId(ctx.params.username))
|
const user = await database.get(getUserId(ctx.params.username))
|
||||||
ctx.body = {
|
ctx.body = {
|
||||||
username: user.username,
|
username: user.username,
|
||||||
|
|
|
@ -3,7 +3,7 @@ const CouchDB = require("../../db")
|
||||||
const controller = {
|
const controller = {
|
||||||
query: async () => {},
|
query: async () => {},
|
||||||
fetch: async ctx => {
|
fetch: async ctx => {
|
||||||
const db = new CouchDB(ctx.params.instanceId)
|
const db = new CouchDB(ctx.user.instanceId)
|
||||||
const designDoc = await db.get("_design/database")
|
const designDoc = await db.get("_design/database")
|
||||||
const response = []
|
const response = []
|
||||||
|
|
||||||
|
@ -24,7 +24,7 @@ const controller = {
|
||||||
ctx.body = response
|
ctx.body = response
|
||||||
},
|
},
|
||||||
create: async ctx => {
|
create: async ctx => {
|
||||||
const db = new CouchDB(ctx.params.instanceId)
|
const db = new CouchDB(ctx.user.instanceId)
|
||||||
const newView = ctx.request.body
|
const newView = ctx.request.body
|
||||||
|
|
||||||
const designDoc = await db.get("_design/database")
|
const designDoc = await db.get("_design/database")
|
||||||
|
@ -38,7 +38,7 @@ const controller = {
|
||||||
ctx.message = `View ${newView.name} created successfully.`
|
ctx.message = `View ${newView.name} created successfully.`
|
||||||
},
|
},
|
||||||
destroy: async ctx => {
|
destroy: async ctx => {
|
||||||
const db = new CouchDB(ctx.params.instanceId)
|
const db = new CouchDB(ctx.user.instanceId)
|
||||||
ctx.body = await db.destroy(ctx.params.userId)
|
ctx.body = await db.destroy(ctx.params.userId)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@ const CouchDB = require("../../../db")
|
||||||
const newid = require("../../../db/newid")
|
const newid = require("../../../db/newid")
|
||||||
|
|
||||||
exports.create = async function(ctx) {
|
exports.create = async function(ctx) {
|
||||||
const db = new CouchDB(ctx.params.instanceId)
|
const db = new CouchDB(ctx.user.instanceId)
|
||||||
const workflow = ctx.request.body
|
const workflow = ctx.request.body
|
||||||
|
|
||||||
workflow._id = newid()
|
workflow._id = newid()
|
||||||
|
@ -22,7 +22,7 @@ exports.create = async function(ctx) {
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.update = async function(ctx) {
|
exports.update = async function(ctx) {
|
||||||
const db = new CouchDB(ctx.params.instanceId)
|
const db = new CouchDB(ctx.user.instanceId)
|
||||||
const workflow = ctx.request.body
|
const workflow = ctx.request.body
|
||||||
|
|
||||||
const response = await db.put(workflow)
|
const response = await db.put(workflow)
|
||||||
|
@ -40,7 +40,7 @@ exports.update = async function(ctx) {
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.fetch = async function(ctx) {
|
exports.fetch = async function(ctx) {
|
||||||
const db = new CouchDB(ctx.params.instanceId)
|
const db = new CouchDB(ctx.user.instanceId)
|
||||||
const response = await db.query(`database/by_type`, {
|
const response = await db.query(`database/by_type`, {
|
||||||
key: ["workflow"],
|
key: ["workflow"],
|
||||||
include_docs: true,
|
include_docs: true,
|
||||||
|
@ -69,6 +69,6 @@ exports.fetchActionScript = async function(ctx) {
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.destroy = async function(ctx) {
|
exports.destroy = async function(ctx) {
|
||||||
const db = new CouchDB(ctx.params.instanceId)
|
const db = new CouchDB(ctx.user.instanceId)
|
||||||
ctx.body = await db.remove(ctx.params.id, ctx.params.rev)
|
ctx.body = await db.remove(ctx.params.id, ctx.params.rev)
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,11 +4,11 @@ const controller = require("../controllers/accesslevel")
|
||||||
const router = Router()
|
const router = Router()
|
||||||
|
|
||||||
router
|
router
|
||||||
.post("/api/:instanceId/accesslevels", controller.create)
|
.post("/api/accesslevels", controller.create)
|
||||||
.put("/api/:instanceId/accesslevels", controller.update)
|
.put("/api/accesslevels", controller.update)
|
||||||
.get("/api/:instanceId/accesslevels", controller.fetch)
|
.get("/api/accesslevels", controller.fetch)
|
||||||
.get("/api/:instanceId/accesslevels/:levelId", controller.find)
|
.get("/api/accesslevels/:levelId", controller.find)
|
||||||
.delete("/api/:instanceId/accesslevels/:levelId/:rev", controller.destroy)
|
.delete("/api/accesslevels/:levelId/:rev", controller.destroy)
|
||||||
.patch("/api/:instanceId/accesslevels/:levelId", controller.patch)
|
.patch("/api/accesslevels/:levelId", controller.patch)
|
||||||
|
|
||||||
module.exports = router
|
module.exports = router
|
||||||
|
|
|
@ -3,6 +3,6 @@ const controller = require("../controllers/auth")
|
||||||
|
|
||||||
const router = Router()
|
const router = Router()
|
||||||
|
|
||||||
router.post("/:appId/api/authenticate", controller.authenticate)
|
router.post("/api/authenticate", controller.authenticate)
|
||||||
|
|
||||||
module.exports = router
|
module.exports = router
|
||||||
|
|
|
@ -6,7 +6,7 @@ const { BUILDER } = require("../../utilities/accessLevels")
|
||||||
const router = Router()
|
const router = Router()
|
||||||
|
|
||||||
router
|
router
|
||||||
.post("/api/:applicationId/instances", authorized(BUILDER), controller.create)
|
.post("/api/instances", authorized(BUILDER), controller.create)
|
||||||
.delete("/api/instances/:instanceId", authorized(BUILDER), controller.destroy)
|
.delete("/api/instances/:instanceId", authorized(BUILDER), controller.destroy)
|
||||||
|
|
||||||
module.exports = router
|
module.exports = router
|
||||||
|
|
|
@ -1,17 +1,21 @@
|
||||||
const Router = require("@koa/router")
|
const Router = require("@koa/router")
|
||||||
const modelController = require("../controllers/model")
|
const modelController = require("../controllers/model")
|
||||||
const authorized = require("../../middleware/authorized")
|
const authorized = require("../../middleware/authorized")
|
||||||
const { BUILDER } = require("../../utilities/accessLevels")
|
const { BUILDER, READ_MODEL } = require("../../utilities/accessLevels")
|
||||||
|
|
||||||
const router = Router()
|
const router = Router()
|
||||||
|
|
||||||
router
|
router
|
||||||
.get("/api/:instanceId/models", authorized(BUILDER), modelController.fetch)
|
.get("/api/models", authorized(BUILDER), modelController.fetch)
|
||||||
.get("/api/:instanceId/models/:id", authorized(BUILDER), modelController.find)
|
.get(
|
||||||
.post("/api/:instanceId/models", authorized(BUILDER), modelController.create)
|
"/api/models/:id",
|
||||||
|
authorized(READ_MODEL, ctx => ctx.params.id),
|
||||||
|
modelController.find
|
||||||
|
)
|
||||||
|
.post("/api/models", authorized(BUILDER), modelController.create)
|
||||||
// .patch("/api/:instanceId/models", controller.update)
|
// .patch("/api/:instanceId/models", controller.update)
|
||||||
.delete(
|
.delete(
|
||||||
"/api/:instanceId/models/:modelId/:revId",
|
"/api/models/:modelId/:revId",
|
||||||
authorized(BUILDER),
|
authorized(BUILDER),
|
||||||
modelController.destroy
|
modelController.destroy
|
||||||
)
|
)
|
||||||
|
|
|
@ -7,28 +7,28 @@ const router = Router()
|
||||||
|
|
||||||
router
|
router
|
||||||
.get(
|
.get(
|
||||||
"/api/:instanceId/:modelId/records",
|
"/api/:modelId/records",
|
||||||
authorized(READ_MODEL, ctx => ctx.params.modelId),
|
authorized(READ_MODEL, ctx => ctx.params.modelId),
|
||||||
recordController.fetchModelRecords
|
recordController.fetchModelRecords
|
||||||
)
|
)
|
||||||
.get(
|
.get(
|
||||||
"/api/:instanceId/:modelId/records/:recordId",
|
"/api/:modelId/records/:recordId",
|
||||||
authorized(READ_MODEL, ctx => ctx.params.modelId),
|
authorized(READ_MODEL, ctx => ctx.params.modelId),
|
||||||
recordController.find
|
recordController.find
|
||||||
)
|
)
|
||||||
.post("/api/:instanceId/records/search", recordController.search)
|
.post("/api/records/search", recordController.search)
|
||||||
.post(
|
.post(
|
||||||
"/api/:instanceId/:modelId/records",
|
"/api/:modelId/records",
|
||||||
authorized(WRITE_MODEL, ctx => ctx.params.modelId),
|
authorized(WRITE_MODEL, ctx => ctx.params.modelId),
|
||||||
recordController.save
|
recordController.save
|
||||||
)
|
)
|
||||||
.post(
|
.post(
|
||||||
"/api/:instanceId/:modelId/records/validate",
|
"/api/:modelId/records/validate",
|
||||||
authorized(WRITE_MODEL, ctx => ctx.params.modelId),
|
authorized(WRITE_MODEL, ctx => ctx.params.modelId),
|
||||||
recordController.validate
|
recordController.validate
|
||||||
)
|
)
|
||||||
.delete(
|
.delete(
|
||||||
"/api/:instanceId/:modelId/records/:recordId/:revId",
|
"/api/:modelId/records/:recordId/:revId",
|
||||||
authorized(WRITE_MODEL, ctx => ctx.params.modelId),
|
authorized(WRITE_MODEL, ctx => ctx.params.modelId),
|
||||||
recordController.destroy
|
recordController.destroy
|
||||||
)
|
)
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue