diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ab2e7d597b..9d473198b9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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. +### 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 diff --git a/package.json b/package.json index 443c8cccc3..8185d0bbb0 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "@rollup/plugin-json": "^4.0.2", "babel-eslint": "^10.0.3", "eslint": "^6.8.0", + "eslint-plugin-cypress": "^2.11.1", "eslint-plugin-prettier": "^3.1.2", "eslint-plugin-svelte3": "^2.7.3", "lerna": "3.14.1", @@ -24,10 +25,11 @@ "test": "lerna run test", "lint": "eslint 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": { "@material/icon-button": "4.0.0", "date-fns": "^2.10.0" } -} \ No newline at end of file +} diff --git a/packages/builder/.gitignore b/packages/builder/.gitignore index 94ba827047..49444a4fc0 100644 --- a/packages/builder/.gitignore +++ b/packages/builder/.gitignore @@ -5,4 +5,5 @@ package-lock.json yarn.lock release/ dist/ +cypress/screenshots routify diff --git a/packages/builder/cypress.json b/packages/builder/cypress.json new file mode 100644 index 0000000000..477d0a6dc4 --- /dev/null +++ b/packages/builder/cypress.json @@ -0,0 +1,4 @@ +{ + "baseUrl": "http://localhost:4001/_builder/", + "video": false +} \ No newline at end of file diff --git a/packages/builder/cypress/fixtures/example.json b/packages/builder/cypress/fixtures/example.json new file mode 100644 index 0000000000..da18d9352a --- /dev/null +++ b/packages/builder/cypress/fixtures/example.json @@ -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" +} \ No newline at end of file diff --git a/packages/builder/cypress/fixtures/profile.json b/packages/builder/cypress/fixtures/profile.json new file mode 100644 index 0000000000..b6c355ca5c --- /dev/null +++ b/packages/builder/cypress/fixtures/profile.json @@ -0,0 +1,5 @@ +{ + "id": 8739, + "name": "Jane", + "email": "jane@example.com" +} \ No newline at end of file diff --git a/packages/builder/cypress/fixtures/users.json b/packages/builder/cypress/fixtures/users.json new file mode 100644 index 0000000000..79b699aa77 --- /dev/null +++ b/packages/builder/cypress/fixtures/users.json @@ -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" + } + } +] \ No newline at end of file diff --git a/packages/builder/cypress/integration/createApp.spec.js b/packages/builder/cypress/integration/createApp.spec.js new file mode 100644 index 0000000000..07a8687ea8 --- /dev/null +++ b/packages/builder/cypress/integration/createApp.spec.js @@ -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') + }) +}) diff --git a/packages/builder/cypress/integration/createComponents.spec.js b/packages/builder/cypress/integration/createComponents.spec.js new file mode 100644 index 0000000000..cedd8fefe1 --- /dev/null +++ b/packages/builder/cypress/integration/createComponents.spec.js @@ -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) +} \ No newline at end of file diff --git a/packages/builder/cypress/integration/createModel.spec.js b/packages/builder/cypress/integration/createModel.spec.js new file mode 100644 index 0000000000..830f5e7a48 --- /dev/null +++ b/packages/builder/cypress/integration/createModel.spec.js @@ -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') + }) +}) diff --git a/packages/builder/cypress/integration/createUser.spec.js b/packages/builder/cypress/integration/createUser.spec.js new file mode 100644 index 0000000000..80d1a51d84 --- /dev/null +++ b/packages/builder/cypress/integration/createUser.spec.js @@ -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') + }) +}) diff --git a/packages/builder/cypress/integration/createWorkflow.spec.js b/packages/builder/cypress/integration/createWorkflow.spec.js new file mode 100644 index 0000000000..9188721988 --- /dev/null +++ b/packages/builder/cypress/integration/createWorkflow.spec.js @@ -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') + + }) +}) \ No newline at end of file diff --git a/packages/builder/cypress/plugins/index.js b/packages/builder/cypress/plugins/index.js new file mode 100644 index 0000000000..59b2bab6e4 --- /dev/null +++ b/packages/builder/cypress/plugins/index.js @@ -0,0 +1,22 @@ +/// +// *********************************************************** +// 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 +} diff --git a/packages/builder/cypress/setup.js b/packages/builder/cypress/setup.js new file mode 100644 index 0000000000..80516471da --- /dev/null +++ b/packages/builder/cypress/setup.js @@ -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 }) +}) diff --git a/packages/builder/cypress/support/commands.js b/packages/builder/cypress/support/commands.js new file mode 100644 index 0000000000..ed19ba8233 --- /dev/null +++ b/packages/builder/cypress/support/commands.js @@ -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() +}) diff --git a/packages/builder/cypress/support/cookies.js b/packages/builder/cypress/support/cookies.js new file mode 100644 index 0000000000..bfb470c57b --- /dev/null +++ b/packages/builder/cypress/support/cookies.js @@ -0,0 +1,3 @@ +Cypress.Cookies.defaults({ + whitelist: "builder:token", +}) diff --git a/packages/builder/cypress/support/index.js b/packages/builder/cypress/support/index.js new file mode 100644 index 0000000000..15c9d759fc --- /dev/null +++ b/packages/builder/cypress/support/index.js @@ -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') diff --git a/packages/builder/package.json b/packages/builder/package.json index 8fe9ffd494..1d7786a090 100644 --- a/packages/builder/package.json +++ b/packages/builder/package.json @@ -9,7 +9,11 @@ "test": "jest", "test:watch": "jest --watchAll", "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": { "globals": { @@ -35,11 +39,19 @@ }, "transformIgnorePatterns": [ "/node_modules/(?!svelte).+\\.js$" + ], + "modulePathIgnorePatterns": [ + "/cypress/" + ] + }, + "eslintConfig": { + "extends": [ + "plugin:cypress/recommended" ] }, "dependencies": { "@beyonk/svelte-notifications": "^2.0.3", - "@budibase/bbui": "^1.1.1", + "@budibase/bbui": "^1.8.0", "@budibase/client": "^0.0.32", "@nx-js/compiler-util": "^2.0.0", "codemirror": "^5.51.0", @@ -67,10 +79,12 @@ "@sveltech/routify": "1.7.11", "babel-jest": "^24.8.0", "browser-sync": "^2.26.7", + "cypress": "^4.8.0", "http-proxy-middleware": "^0.19.1", "jest": "^24.8.0", "ncp": "^2.0.0", "npm-run-all": "^4.1.5", + "rimraf": "^3.0.2", "rollup": "^1.12.0", "rollup-plugin-alias": "^1.5.2", "rollup-plugin-browsersync": "^1.0.0", @@ -83,6 +97,7 @@ "rollup-plugin-svelte": "^5.0.3", "rollup-plugin-terser": "^4.0.4", "rollup-plugin-url": "^2.2.2", + "start-server-and-test": "^1.11.0", "svelte": "3.23.x" }, "gitHead": "115189f72a850bfb52b65ec61d932531bf327072" diff --git a/packages/builder/rollup.config.js b/packages/builder/rollup.config.js index fd7c487c3f..0a4e016170 100644 --- a/packages/builder/rollup.config.js +++ b/packages/builder/rollup.config.js @@ -165,6 +165,10 @@ export default { src: "node_modules/@budibase/client/dist/budibase-client.esm.mjs", dest: outputpath, }, + { + src: "node_modules/@budibase/bbui/dist/bbui.css", + dest: outputpath, + }, ], }), diff --git a/packages/builder/src/builderStore/api.js b/packages/builder/src/builderStore/api.js index 3fcd35ce28..116222409f 100644 --- a/packages/builder/src/builderStore/api.js +++ b/packages/builder/src/builderStore/api.js @@ -1,17 +1,13 @@ const apiCall = method => async (url, body) => { + const headers = { + "Content-Type": "application/json", + } const response = await fetch(url, { method: method, - headers: { - "Content-Type": "application/json", - "x-user-agent": "Budibase Builder", - }, body: body && JSON.stringify(body), + headers, }) - // if (response.status === 500) { - // throw new Error("Server Error"); - // } - return response } @@ -22,9 +18,9 @@ export const del = apiCall("DELETE") export const put = apiCall("PUT") export default { - post, - get, - patch, - delete: del, - put, + post: apiCall("POST"), + get: apiCall("GET"), + patch: apiCall("PATCH"), + delete: apiCall("DELETE"), + put: apiCall("PUT"), } diff --git a/packages/builder/src/builderStore/store/backend.js b/packages/builder/src/builderStore/store/backend.js index 9a5a7b1fbc..53bdaa3360 100644 --- a/packages/builder/src/builderStore/store/backend.js +++ b/packages/builder/src/builderStore/store/backend.js @@ -24,8 +24,8 @@ export const getBackendUiStore = () => { store.actions = { database: { select: async db => { - const modelsResponse = await api.get(`/api/${db._id}/models`) - const viewsResponse = await api.get(`/api/${db._id}/views`) + const modelsResponse = await api.get(`/api/models`) + const viewsResponse = await api.get(`/api/views`) const models = await modelsResponse.json() const views = await viewsResponse.json() store.update(state => { diff --git a/packages/builder/src/builderStore/store/workflow/index.js b/packages/builder/src/builderStore/store/workflow/index.js index 8d5e63b197..9d50f85661 100644 --- a/packages/builder/src/builderStore/store/workflow/index.js +++ b/packages/builder/src/builderStore/store/workflow/index.js @@ -3,8 +3,8 @@ import api from "../../api" import Workflow from "./Workflow" const workflowActions = store => ({ - fetch: async instanceId => { - const WORKFLOWS_URL = `/api/${instanceId}/workflows` + fetch: async () => { + const WORKFLOWS_URL = `/api/workflows` const workflowResponse = await api.get(WORKFLOWS_URL) const json = await workflowResponse.json() store.update(state => { @@ -12,14 +12,14 @@ const workflowActions = store => ({ return state }) }, - create: async ({ instanceId, name }) => { + create: async ({ name }) => { const workflow = { name, definition: { steps: [], }, } - const CREATE_WORKFLOW_URL = `/api/${instanceId}/workflows` + const CREATE_WORKFLOW_URL = `/api/workflows` const response = await api.post(CREATE_WORKFLOW_URL, workflow) const json = await response.json() store.update(state => { @@ -28,8 +28,8 @@ const workflowActions = store => ({ return state }) }, - save: async ({ instanceId, workflow }) => { - const UPDATE_WORKFLOW_URL = `/api/${instanceId}/workflows` + save: async ({ workflow }) => { + const UPDATE_WORKFLOW_URL = `/api/workflows` const response = await api.put(UPDATE_WORKFLOW_URL, workflow) const json = await response.json() store.update(state => { @@ -42,8 +42,8 @@ const workflowActions = store => ({ return state }) }, - update: async ({ instanceId, workflow }) => { - const UPDATE_WORKFLOW_URL = `/api/${instanceId}/workflows` + update: async ({ workflow }) => { + const UPDATE_WORKFLOW_URL = `/api/workflows` const response = await api.put(UPDATE_WORKFLOW_URL, workflow) const json = await response.json() store.update(state => { @@ -55,9 +55,9 @@ const workflowActions = store => ({ return state }) }, - delete: async ({ instanceId, workflow }) => { + delete: async ({ 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) store.update(state => { diff --git a/packages/builder/src/components/common/Input.svelte b/packages/builder/src/components/common/Input.svelte index 82305be60a..5db342b2c8 100644 --- a/packages/builder/src/components/common/Input.svelte +++ b/packages/builder/src/components/common/Input.svelte @@ -2,6 +2,7 @@ import { onMount } from "svelte" import { buildStyle } from "../../helpers.js" export let value = "" + export let name = "" export let textAlign = "left" export let width = "160px" export let placeholder = "" @@ -25,6 +26,7 @@ {label}
{ data = records || [] headers = Object.keys($backendUiStore.selectedModel.schema).filter( diff --git a/packages/builder/src/components/database/ModelDataTable/api.js b/packages/builder/src/components/database/ModelDataTable/api.js index cb98879567..f50290b091 100644 --- a/packages/builder/src/components/database/ModelDataTable/api.js +++ b/packages/builder/src/components/database/ModelDataTable/api.js @@ -1,7 +1,7 @@ import api from "builderStore/api" -export async function createUser(user, instanceId) { - const CREATE_USER_URL = `/api/${instanceId}/users` +export async function createUser(user) { + const CREATE_USER_URL = `/api/users` const response = await api.post(CREATE_USER_URL, user) return await response.json() } @@ -14,21 +14,21 @@ export async function createDatabase(appname, instanceName) { return await response.json() } -export async function deleteRecord(record, instanceId) { - const DELETE_RECORDS_URL = `/api/${instanceId}/${record._modelId}/records/${record._id}/${record._rev}` +export async function deleteRecord(record) { + const DELETE_RECORDS_URL = `/api/${record._modelId}/records/${record._id}/${record._rev}` const response = await api.delete(DELETE_RECORDS_URL) return response } -export async function saveRecord(record, instanceId, modelId) { - const SAVE_RECORDS_URL = `/api/${instanceId}/${modelId}/records` +export async function saveRecord(record, modelId) { + const SAVE_RECORDS_URL = `/api/${modelId}/records` const response = await api.post(SAVE_RECORDS_URL, record) return await response.json() } -export async function fetchDataForView(viewName, instanceId) { - const FETCH_RECORDS_URL = `/api/${instanceId}/views/${viewName}` +export async function fetchDataForView(viewName) { + const FETCH_RECORDS_URL = `/api/views/${viewName}` const response = await api.get(FETCH_RECORDS_URL) return await response.json() diff --git a/packages/builder/src/components/database/ModelDataTable/modals/CreateEditModel/CreateEditModel.svelte b/packages/builder/src/components/database/ModelDataTable/modals/CreateEditModel/CreateEditModel.svelte index 6264b74e09..a01acdc68d 100644 --- a/packages/builder/src/components/database/ModelDataTable/modals/CreateEditModel/CreateEditModel.svelte +++ b/packages/builder/src/components/database/ModelDataTable/modals/CreateEditModel/CreateEditModel.svelte @@ -18,7 +18,6 @@ let fieldToEdit $: modelFields = model.schema ? Object.entries(model.schema) : [] - $: instanceId = $backendUiStore.selectedDatabase._id function editField() {} @@ -27,7 +26,7 @@ function onFinishedFieldEdit() {} 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 newModel = await response.json() backendUiStore.actions.models.create(newModel) @@ -54,7 +53,10 @@
Fields -
(showFieldView = true)}> +
(showFieldView = true)}> Add new field
diff --git a/packages/builder/src/components/database/ModelDataTable/modals/CreateEditRecord.svelte b/packages/builder/src/components/database/ModelDataTable/modals/CreateEditRecord.svelte index 62688d58e3..a97cdc0ca1 100644 --- a/packages/builder/src/components/database/ModelDataTable/modals/CreateEditRecord.svelte +++ b/packages/builder/src/components/database/ModelDataTable/modals/CreateEditRecord.svelte @@ -14,8 +14,6 @@ let errors = [] let selectedModel - $: instanceId = $backendUiStore.selectedDatabase._id - $: modelSchema = $backendUiStore.selectedModel ? Object.entries($backendUiStore.selectedModel.schema) : [] @@ -49,7 +47,6 @@ ...record, modelId: $backendUiStore.selectedModel._id, }, - instanceId, $backendUiStore.selectedModel._id ) if (recordResponse.errors) { diff --git a/packages/builder/src/components/database/ModelDataTable/modals/CreateEditView.svelte b/packages/builder/src/components/database/ModelDataTable/modals/CreateEditView.svelte index 4db445e54f..53cb3e2cc6 100644 --- a/packages/builder/src/components/database/ModelDataTable/modals/CreateEditView.svelte +++ b/packages/builder/src/components/database/ModelDataTable/modals/CreateEditView.svelte @@ -29,7 +29,7 @@ function deleteView() {} 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) backendUiStore.update(state => { state.views = [...state.views, response.view] diff --git a/packages/builder/src/components/database/ModelDataTable/modals/CreateUser.svelte b/packages/builder/src/components/database/ModelDataTable/modals/CreateUser.svelte index d5c924405f..2587f00ea8 100644 --- a/packages/builder/src/components/database/ModelDataTable/modals/CreateUser.svelte +++ b/packages/builder/src/components/database/ModelDataTable/modals/CreateUser.svelte @@ -10,12 +10,11 @@ let accessLevelId $: valid = username && password && accessLevelId - $: instanceId = $backendUiStore.selectedDatabase._id $: appId = $store.appId async function createUser() { 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) onClosed() } @@ -29,15 +28,26 @@
- +
- +
- diff --git a/packages/builder/src/components/database/ModelDataTable/modals/DeleteRecord.svelte b/packages/builder/src/components/database/ModelDataTable/modals/DeleteRecord.svelte index 05ecfcd85f..aa192b32eb 100644 --- a/packages/builder/src/components/database/ModelDataTable/modals/DeleteRecord.svelte +++ b/packages/builder/src/components/database/ModelDataTable/modals/DeleteRecord.svelte @@ -6,7 +6,6 @@ export let record export let onClosed - $: instanceId = $backendUiStore.selectedDatabase._id
@@ -25,7 +24,7 @@ { - await api.deleteRecord(record, instanceId) + await api.deleteRecord(record) backendUiStore.actions.records.delete(record) onClosed() }}> diff --git a/packages/builder/src/components/database/ModelDataTable/modals/RecordFieldControl.svelte b/packages/builder/src/components/database/ModelDataTable/modals/RecordFieldControl.svelte index 5c308a7abb..6d37246be9 100644 --- a/packages/builder/src/components/database/ModelDataTable/modals/RecordFieldControl.svelte +++ b/packages/builder/src/components/database/ModelDataTable/modals/RecordFieldControl.svelte @@ -32,6 +32,7 @@ {#if type === 'select'} {:else} 0} {checked} diff --git a/packages/builder/src/components/nav/HierarchyRow.svelte b/packages/builder/src/components/nav/HierarchyRow.svelte index 8921ab74de..7653e46fd0 100644 --- a/packages/builder/src/components/nav/HierarchyRow.svelte +++ b/packages/builder/src/components/nav/HierarchyRow.svelte @@ -7,7 +7,6 @@ CreateEditModelModal, CreateEditViewModal, } from "components/database/ModelDataTable/modals" - import api from "builderStore/api" const { open, close } = getContext("simple-modal") diff --git a/packages/builder/src/components/nav/SchemaManagementDrawer.svelte b/packages/builder/src/components/nav/SchemaManagementDrawer.svelte index cc9e5d86a7..9465c85140 100644 --- a/packages/builder/src/components/nav/SchemaManagementDrawer.svelte +++ b/packages/builder/src/components/nav/SchemaManagementDrawer.svelte @@ -52,7 +52,7 @@ } 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) backendUiStore.update(state => { state.models = state.models.filter( diff --git a/packages/builder/src/components/nav/UsersList.svelte b/packages/builder/src/components/nav/UsersList.svelte index faef8a4234..e213aa066c 100644 --- a/packages/builder/src/components/nav/UsersList.svelte +++ b/packages/builder/src/components/nav/UsersList.svelte @@ -12,11 +12,10 @@ $: currentAppInfo = { appname: $store.appname, - instanceId: $backendUiStore.selectedDatabase._id, } 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 users = await response.json() backendUiStore.update(state => { diff --git a/packages/builder/src/components/userInterface/AppPreview/CurrentItemPreview.svelte b/packages/builder/src/components/userInterface/AppPreview/CurrentItemPreview.svelte index 00d7e6c171..523ed25031 100644 --- a/packages/builder/src/components/userInterface/AppPreview/CurrentItemPreview.svelte +++ b/packages/builder/src/components/userInterface/AppPreview/CurrentItemPreview.svelte @@ -91,7 +91,6 @@ ? screenPlaceholder : $store.currentPreviewItem, ], - appRootPath: "", } $: selectedComponentType = getComponentTypeName($store.currentComponentInfo) @@ -108,6 +107,8 @@ selectedComponentType, selectedComponentId, frontendDefinition, + appId: $store.appId, + instanceId: $backendUiStore.selectedDatabase._id, }) ) } diff --git a/packages/builder/src/components/userInterface/AppPreview/iframeTemplate.js b/packages/builder/src/components/userInterface/AppPreview/iframeTemplate.js index 3d305f1c1d..c9272dddb2 100644 --- a/packages/builder/src/components/userInterface/AppPreview/iframeTemplate.js +++ b/packages/builder/src/components/userInterface/AppPreview/iframeTemplate.js @@ -20,6 +20,7 @@ export default ` font-weight: bold; } + diff --git a/packages/builder/src/components/userInterface/CategoryTab.svelte b/packages/builder/src/components/userInterface/CategoryTab.svelte index 074839eca2..5f7c3e584e 100644 --- a/packages/builder/src/components/userInterface/CategoryTab.svelte +++ b/packages/builder/src/components/userInterface/CategoryTab.svelte @@ -7,6 +7,7 @@
{#each categories as category}
  • onClick(category)} class:active={selectedCategory === category}> {category.name} diff --git a/packages/builder/src/components/userInterface/Colorpicker/ButtonGroup.svelte b/packages/builder/src/components/userInterface/Colorpicker/ButtonGroup.svelte new file mode 100644 index 0000000000..134dac772d --- /dev/null +++ b/packages/builder/src/components/userInterface/Colorpicker/ButtonGroup.svelte @@ -0,0 +1,29 @@ + + + + +
    + {#each colorFormats as text} + onclick(text)} /> + {/each} +
    diff --git a/packages/builder/src/components/userInterface/Colorpicker/CheckedBackground.svelte b/packages/builder/src/components/userInterface/Colorpicker/CheckedBackground.svelte new file mode 100644 index 0000000000..19d54523b2 --- /dev/null +++ b/packages/builder/src/components/userInterface/Colorpicker/CheckedBackground.svelte @@ -0,0 +1,25 @@ + + + + +
    + +
    \ No newline at end of file diff --git a/packages/builder/src/components/userInterface/Colorpicker/Colorpicker.svelte b/packages/builder/src/components/userInterface/Colorpicker/Colorpicker.svelte new file mode 100644 index 0000000000..5989e6b3ed --- /dev/null +++ b/packages/builder/src/components/userInterface/Colorpicker/Colorpicker.svelte @@ -0,0 +1,281 @@ + + + + +
    + +
    + +
    + +
    +
    +
    + +
    + +
    +
    + setHue(hue.detail)} on:dragend={dispatchValue} /> + + + setAlpha(alpha.detail, isDrag)} + on:dragend={dispatchValue} + /> + + +
    +
    + + {#if !disableSwatches} +
    + {#if swatches.length > 0} + {#each swatches as color, idx} + applySwatch(color)} on:removeswatch={() => removeSwatch(idx)} /> + {/each} + {/if} + {#if swatches.length !== 12} +
    + + +
    + {/if} +
    + {/if} + +
    + + handleColorInput(event.target.value)} on:change={dispatchInputChange} /> +
    +
    + +
    diff --git a/packages/builder/src/components/userInterface/Colorpicker/Colorpreview.svelte b/packages/builder/src/components/userInterface/Colorpicker/Colorpreview.svelte new file mode 100644 index 0000000000..32a5e63ff3 --- /dev/null +++ b/packages/builder/src/components/userInterface/Colorpicker/Colorpreview.svelte @@ -0,0 +1,152 @@ + + +
    + {#if !errorMsg} + +
    + + + {#if open} +
    + +
    +
    open = false} class="overlay">
    + {/if} + {:else} +
    + × +
    + {/if} +
    + + + + diff --git a/packages/builder/src/components/userInterface/Colorpicker/FlatButton.svelte b/packages/builder/src/components/userInterface/Colorpicker/FlatButton.svelte new file mode 100644 index 0000000000..881f73d134 --- /dev/null +++ b/packages/builder/src/components/userInterface/Colorpicker/FlatButton.svelte @@ -0,0 +1,29 @@ + + + + +
    {text}
    diff --git a/packages/builder/src/components/userInterface/Colorpicker/Input.svelte b/packages/builder/src/components/userInterface/Colorpicker/Input.svelte new file mode 100644 index 0000000000..811a51cea6 --- /dev/null +++ b/packages/builder/src/components/userInterface/Colorpicker/Input.svelte @@ -0,0 +1,28 @@ + + + + +
    + +
    diff --git a/packages/builder/src/components/userInterface/Colorpicker/Palette.svelte b/packages/builder/src/components/userInterface/Colorpicker/Palette.svelte new file mode 100644 index 0000000000..4763ee86cc --- /dev/null +++ b/packages/builder/src/components/userInterface/Colorpicker/Palette.svelte @@ -0,0 +1,63 @@ + + + + + +
    +
    +
    + diff --git a/packages/builder/src/components/userInterface/Colorpicker/Slider.svelte b/packages/builder/src/components/userInterface/Colorpicker/Slider.svelte new file mode 100644 index 0000000000..edf0888432 --- /dev/null +++ b/packages/builder/src/components/userInterface/Colorpicker/Slider.svelte @@ -0,0 +1,89 @@ + + + + +
    onSliderChange(event.clientX)} + class="color-format-slider" + class:hue={type === 'hue'} + class:alpha={type === 'alpha'}> +
    onSliderChange(e.detail, true)} + on:dragend + class="slider-thumb" + {style} /> +
    diff --git a/packages/builder/src/components/userInterface/Colorpicker/Swatch.svelte b/packages/builder/src/components/userInterface/Colorpicker/Swatch.svelte new file mode 100644 index 0000000000..e602b72d33 --- /dev/null +++ b/packages/builder/src/components/userInterface/Colorpicker/Swatch.svelte @@ -0,0 +1,53 @@ + + + + +
    + +
    hovered = true} on:mouseleave={() => hovered = false}> + {#if hovered} +
    dispatch("removeswatch")}> + dispatch("removeswatch")}>× +
    + {/if} +
    +
    +
    \ No newline at end of file diff --git a/packages/builder/src/components/userInterface/Colorpicker/drag.js b/packages/builder/src/components/userInterface/Colorpicker/drag.js new file mode 100644 index 0000000000..54fa5e924b --- /dev/null +++ b/packages/builder/src/components/userInterface/Colorpicker/drag.js @@ -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) +} diff --git a/packages/builder/src/components/userInterface/Colorpicker/helpers.js b/packages/builder/src/components/userInterface/Colorpicker/helpers.js new file mode 100644 index 0000000000..5d9ff750af --- /dev/null +++ b/packages/builder/src/components/userInterface/Colorpicker/helpers.js @@ -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()}`) +} diff --git a/packages/builder/src/components/userInterface/Colorpicker/index.js b/packages/builder/src/components/userInterface/Colorpicker/index.js new file mode 100644 index 0000000000..21e19acc9b --- /dev/null +++ b/packages/builder/src/components/userInterface/Colorpicker/index.js @@ -0,0 +1,2 @@ +import Colorpreview from "./Colorpreview.svelte" +export default Colorpreview diff --git a/packages/builder/src/components/userInterface/Colorpicker/utils.js b/packages/builder/src/components/userInterface/Colorpicker/utils.js new file mode 100644 index 0000000000..e14c8711ab --- /dev/null +++ b/packages/builder/src/components/userInterface/Colorpicker/utils.js @@ -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)) diff --git a/packages/builder/src/components/userInterface/Colorpicker/utils.test.js b/packages/builder/src/components/userInterface/Colorpicker/utils.test.js new file mode 100644 index 0000000000..c47d54ed1b --- /dev/null +++ b/packages/builder/src/components/userInterface/Colorpicker/utils.test.js @@ -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() + }) +}) diff --git a/packages/builder/src/components/userInterface/ComponentPropertiesPanel.svelte b/packages/builder/src/components/userInterface/ComponentPropertiesPanel.svelte index f7abefad4e..ad07f8458f 100644 --- a/packages/builder/src/components/userInterface/ComponentPropertiesPanel.svelte +++ b/packages/builder/src/components/userInterface/ComponentPropertiesPanel.svelte @@ -120,8 +120,6 @@ height: 100%; display: flex; flex-direction: column; - overflow-x: hidden; - overflow-y: hidden; padding: 20px; box-sizing: border-box; } @@ -139,7 +137,6 @@ margin-top: 10px; flex: 1 1 auto; min-height: 0; - overflow-y: auto; } .instance-name { diff --git a/packages/builder/src/components/userInterface/DesignView.svelte b/packages/builder/src/components/userInterface/DesignView.svelte index 070ba502e3..6826bddc5c 100644 --- a/packages/builder/src/components/userInterface/DesignView.svelte +++ b/packages/builder/src/components/userInterface/DesignView.svelte @@ -31,6 +31,7 @@
    +
    {#if propertyGroupNames.length > 0} {#each propertyGroupNames as groupName} @@ -49,6 +50,7 @@ {/if}
    +