├── .nvmrc ├── Procfile ├── .npmrc ├── .dockerignore ├── app ├── react │ ├── .prettierrc │ ├── public │ │ └── locales │ │ │ ├── en │ │ │ ├── common.json │ │ │ └── login.json │ │ │ └── pt-BR │ │ │ ├── common.json │ │ │ └── login.json │ ├── containers │ │ └── Providers │ │ │ ├── index.ts │ │ │ └── Providers.tsx │ ├── utils │ │ └── enums │ │ │ └── localeNamespaces.ts │ ├── components │ │ └── Button │ │ │ ├── index.ts │ │ │ └── Button.tsx │ ├── tsconfig.json │ ├── i18n │ │ └── index.ts │ ├── theme │ │ └── index.ts │ └── .eslintrc ├── img │ ├── composto-01.png │ ├── loading-dots.gif │ ├── wave01.svg │ ├── wave03.svg │ ├── wave02.svg │ └── brmw-logo.svg ├── angular │ ├── components │ │ ├── statusBar │ │ │ ├── styles.scss │ │ │ ├── template.html │ │ │ └── index.js │ │ ├── icons │ │ │ ├── logic │ │ │ │ ├── index.scss │ │ │ │ ├── index.js │ │ │ │ └── index.html │ │ │ └── conceptual │ │ │ │ ├── index.scss │ │ │ │ ├── index.js │ │ │ │ └── index.html │ │ ├── sqlComparasionDropdown │ │ │ ├── template.html │ │ │ └── index.js │ │ ├── githubSponsorBanner │ │ │ ├── index.js │ │ │ └── index.scss │ │ ├── supportBannersList │ │ │ ├── index.js │ │ │ ├── index.html │ │ │ └── index.scss │ │ ├── confirmationModal.html │ │ ├── deleteModelModal.html │ │ ├── dropdown.html │ │ ├── sqlGeneratorModal.js │ │ ├── sqlGeneratorModal.html │ │ ├── deleteModelModal.js │ │ ├── noteEditor.js │ │ ├── conversionOptionModal.js │ │ ├── conversionAttributeModal.js │ │ ├── conversionOptionModal.html │ │ ├── renameModelModal.js │ │ ├── queryExpressionModal │ │ │ ├── styles.scss │ │ │ └── index.js │ │ ├── dropdown.js │ │ ├── dropdownIcon.js │ │ ├── confirmationModal.js │ │ ├── conversionAttributeModal.html │ │ ├── duplicateModelModal.html │ │ ├── renameModelModal.html │ │ ├── duplicateModelModal.js │ │ ├── dropdownIcon.html │ │ ├── createModelModal.html │ │ ├── createModelModal.js │ │ ├── importModelModal.html │ │ ├── importModelModal.js │ │ ├── shareModelModal.js │ │ ├── shareModelModal.html │ │ ├── noteEditor.html │ │ └── keyboardController.js │ ├── noaccess │ │ ├── noaccess.js │ │ └── noaccess.html │ ├── service │ │ ├── Column.js │ │ ├── queryExpressionService.js │ │ ├── toolsViewService.js │ │ ├── preventExitService.js │ │ ├── logicFactory.js │ │ ├── authService.js │ │ └── modelAPI.js │ ├── session.js │ ├── publicview │ │ ├── publicView.scss │ │ ├── publicview.html │ │ └── publicview.js │ ├── logic │ │ ├── checkConstraint.js │ │ ├── columnForm.js │ │ ├── checkConstraint.html │ │ ├── columnForm.html │ │ └── logic.html │ ├── conceptual │ │ ├── validator.js │ │ ├── factory.js │ │ ├── validator.test.js │ │ ├── entityExtensor.js │ │ ├── factory.test.js │ │ └── conceptual.html │ ├── recovery │ │ ├── recovery.html │ │ ├── recovery.js │ │ ├── reset.html │ │ └── reset.js │ ├── login │ │ ├── login.js │ │ └── login.html │ ├── signup │ │ ├── signup.js │ │ └── signup.html │ ├── preferences │ │ ├── preferences.html │ │ └── preferences.js │ ├── view │ │ ├── view.html │ │ └── view.js │ └── workspace │ │ └── workspace.html ├── sass │ ├── projectsList.scss │ ├── joint-custom.scss │ ├── forbidden-access.scss │ ├── app.scss │ ├── modelsList.scss │ ├── atomic.scss │ ├── print.scss │ ├── feedback.scss │ ├── modal.scss │ ├── table-preview.scss │ ├── sql.scss │ ├── tables.scss │ ├── note-color-list.scss │ ├── modelWorkspace.scss │ ├── buttons.scss │ ├── colors.scss │ ├── elementActions.scss │ ├── public.scss │ └── mainHeader.scss ├── index.html └── joint │ └── notes.js ├── server_app ├── helpers │ ├── index.js │ ├── hash.js │ ├── config.js │ ├── crypto.test.js │ └── crypto.js ├── user │ ├── model.js │ ├── validator.js │ ├── validator.test.js │ └── service.test.js ├── middleware │ └── index.js ├── model │ ├── model.js │ ├── validator.js │ ├── handler.test.js │ └── validator.test.js ├── mail │ └── sender.js └── app.js ├── cypress ├── support │ ├── e2e.js │ ├── commands.js │ └── commands.d.ts ├── .eslintrc.json ├── e2e │ ├── api │ │ └── model.cy.js │ ├── model.cy.js │ ├── login.cy.js │ └── models.cy.js └── fixtures │ └── conceptualModel.json ├── cypress.env.example.json ├── jest.config.js ├── postcss.config.js ├── .env.example ├── cypress.config.ts ├── .editorconfig ├── Dockerfile ├── .github ├── ISSUE_TEMPLATE │ ├── eu-preciso-de-ajuda-com---.md │ ├── requisitar-funcionalidade.md │ └── reportar-problema.md └── FUNDING.yml ├── docker-compose.yml ├── .babelrc ├── server.js ├── .gitignore ├── webpack.config.js └── package.json /.nvmrc: -------------------------------------------------------------------------------- 1 | 20 -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: node server.js -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmjs.com/ 2 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .git/ 3 | data/ 4 | -------------------------------------------------------------------------------- /app/react/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "endOfLine": "auto" 3 | } -------------------------------------------------------------------------------- /app/react/public/locales/en/common.json: -------------------------------------------------------------------------------- 1 | { 2 | 3 | } -------------------------------------------------------------------------------- /app/react/public/locales/pt-BR/common.json: -------------------------------------------------------------------------------- 1 | { 2 | 3 | } -------------------------------------------------------------------------------- /server_app/helpers/index.js: -------------------------------------------------------------------------------- 1 | exports.Crypto = require('./hash.js') 2 | -------------------------------------------------------------------------------- /app/react/public/locales/en/login.json: -------------------------------------------------------------------------------- 1 | { 2 | "Login": "Login in English" 3 | } -------------------------------------------------------------------------------- /app/react/public/locales/pt-BR/login.json: -------------------------------------------------------------------------------- 1 | { 2 | "Login": "Login em Português" 3 | } -------------------------------------------------------------------------------- /cypress/support/e2e.js: -------------------------------------------------------------------------------- 1 | import "@4tw/cypress-drag-drop"; 2 | import "./commands"; 3 | -------------------------------------------------------------------------------- /cypress/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "plugin:cypress/recommended" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /cypress.env.example.json: -------------------------------------------------------------------------------- 1 | { 2 | "user": "user@example.com", 3 | "password": "p@s5W0rD" 4 | } 5 | -------------------------------------------------------------------------------- /app/img/composto-01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brmodeloweb/brmodelo-app/HEAD/app/img/composto-01.png -------------------------------------------------------------------------------- /app/img/loading-dots.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brmodeloweb/brmodelo-app/HEAD/app/img/loading-dots.gif -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testEnvironment: 'node', 3 | roots: ["app", "server_app"] 4 | }; -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [require("autoprefixer"), require("cssnano")], 3 | }; 4 | -------------------------------------------------------------------------------- /app/react/containers/Providers/index.ts: -------------------------------------------------------------------------------- 1 | import Providers from "./Providers"; 2 | 3 | export default Providers; 4 | -------------------------------------------------------------------------------- /app/angular/components/statusBar/styles.scss: -------------------------------------------------------------------------------- 1 | .status-bar { 2 | font-size: 0.9em; 3 | font-weight: 300; 4 | color: var(--black); 5 | } -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | API_URL=http://localhost:3000 2 | PORT=3000 3 | MONGODB_LOCAL_URL="mongodb://mongo:27017/brmodeloDB" 4 | SECRET_TOKEN=talkischeapshowmethecode -------------------------------------------------------------------------------- /app/react/utils/enums/localeNamespaces.ts: -------------------------------------------------------------------------------- 1 | enum LocaleNamespaces { 2 | COMMON = "common", 3 | LOGIN = "login", 4 | } 5 | 6 | export default LocaleNamespaces; 7 | -------------------------------------------------------------------------------- /app/angular/components/statusBar/template.html: -------------------------------------------------------------------------------- 1 |

{{ 'Last save:' | translate }} {{ $ctrl.updatedAt | date:"dd/MM/yyyy" }} {{ 'at' | translate }} {{ $ctrl.updatedAt | date:"HH:mm" }}

-------------------------------------------------------------------------------- /app/angular/components/icons/logic/index.scss: -------------------------------------------------------------------------------- 1 | icon-logic { 2 | position: relative; 3 | display: inline-block; 4 | } 5 | 6 | icon-logic svg { 7 | display: block; 8 | position: relative; 9 | top: 5px; 10 | } -------------------------------------------------------------------------------- /app/angular/components/icons/conceptual/index.scss: -------------------------------------------------------------------------------- 1 | icon-conceptual { 2 | position: relative; 3 | display: inline-block; 4 | } 5 | 6 | icon-conceptual svg { 7 | display: block; 8 | position: relative; 9 | top: 5px; 10 | } -------------------------------------------------------------------------------- /server_app/helpers/hash.js: -------------------------------------------------------------------------------- 1 | const crypto = require("crypto") 2 | 3 | module.exports = function (pass, salt) { 4 | let hash = crypto.createHash("sha512") 5 | hash.update(pass, "utf8") 6 | hash.update(salt, "utf8") 7 | return hash.digest("base64") 8 | } -------------------------------------------------------------------------------- /cypress.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'cypress' 2 | 3 | export default defineConfig({ 4 | env: { 5 | apiUrl: 'http://localhost:3000', 6 | }, 7 | e2e: { 8 | setupNodeEvents(on, config) {}, 9 | baseUrl: 'http://localhost:9000', 10 | }, 11 | }) 12 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | trim_trailing_whitespace = true 7 | insert_final_newline = false 8 | indent_style = tab 9 | 10 | [{package.json}] 11 | indent_style = space 12 | indent_size = 2 13 | 14 | [*.{md, markdown}] 15 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /app/angular/components/icons/logic/index.js: -------------------------------------------------------------------------------- 1 | import angular from "angular"; 2 | import template from "./index.html"; 3 | import "./index.scss"; 4 | 5 | const iconLogic = function () {}; 6 | 7 | export default angular.module("app.iconLogic", []).component("iconLogic", { 8 | template, 9 | controller: iconLogic 10 | }).name; 11 | -------------------------------------------------------------------------------- /app/angular/components/sqlComparasionDropdown/template.html: -------------------------------------------------------------------------------- 1 | 5 | -------------------------------------------------------------------------------- /app/angular/noaccess/noaccess.js: -------------------------------------------------------------------------------- 1 | import angular from "angular"; 2 | import template from "./noaccess.html"; 3 | 4 | const controller = function(){ 5 | const ctrl = this; 6 | } 7 | 8 | export default angular 9 | .module("app.noaccess", []) 10 | .component("noAccess", { 11 | template, 12 | controller: controller, 13 | }).name; -------------------------------------------------------------------------------- /app/img/wave01.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/img/wave03.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/react/components/Button/index.ts: -------------------------------------------------------------------------------- 1 | import { react2angular } from "react2angular"; 2 | import angular from "angular"; 3 | import Button from "./Button"; 4 | 5 | export { Button }; 6 | export default angular 7 | .module("app.react.button", []) 8 | .component("reactButton", react2angular(Button, ["label", "onClick", "type"])) 9 | .name; 10 | -------------------------------------------------------------------------------- /app/angular/components/icons/conceptual/index.js: -------------------------------------------------------------------------------- 1 | import angular from "angular"; 2 | import template from "./index.html"; 3 | import "./index.scss"; 4 | 5 | const iconConceptual = function () {}; 6 | 7 | export default angular.module("app.iconConceptual", []).component("iconConceptual", { 8 | template, 9 | controller: iconConceptual 10 | }).name; 11 | -------------------------------------------------------------------------------- /app/sass/projectsList.scss: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////////////// 2 | // .projectsList 3 | //////////////////////////////////////////////////////////////////////////////// 4 | .projectsList .model-name { 5 | display: block; 6 | max-width: 500px; 7 | overflow: hidden; 8 | white-space: nowrap; 9 | text-overflow: ellipsis; 10 | } -------------------------------------------------------------------------------- /app/angular/components/githubSponsorBanner/index.js: -------------------------------------------------------------------------------- 1 | import angular from "angular"; 2 | import template from "./index.html"; 3 | import "./index.scss"; 4 | 5 | const githubSponsorBanner = function () {}; 6 | 7 | export default angular.module("app.githubSponsorBanner", []).component("githubSponsorBanner", { 8 | template, 9 | controller: githubSponsorBanner, 10 | }).name; 11 | -------------------------------------------------------------------------------- /app/angular/components/supportBannersList/index.js: -------------------------------------------------------------------------------- 1 | import angular from "angular"; 2 | import template from "./index.html"; 3 | import "./index.scss"; 4 | 5 | const supportBannersList = function () {}; 6 | 7 | export default angular 8 | .module("app.supportBannersList", []) 9 | .component("supportBannersList", { 10 | template, 11 | comtroller: supportBannersList, 12 | }).name -------------------------------------------------------------------------------- /app/img/wave02.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /server_app/user/model.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | const Schema = mongoose.Schema; 3 | 4 | const userSchema = new Schema({ 5 | login: { type: String }, 6 | name: { type: String }, 7 | password: { type: String }, 8 | recoveryCode: { type: String }, 9 | created: { type: Date, default: Date.now }, 10 | }); 11 | 12 | module.exports = mongoose.model("User", userSchema); -------------------------------------------------------------------------------- /app/angular/components/statusBar/index.js: -------------------------------------------------------------------------------- 1 | import angular from "angular"; 2 | import template from "./template.html"; 3 | import "./styles.scss"; 4 | 5 | const statusBarController = function () {}; 6 | 7 | export default angular.module("app.statusBar", []).component("statusBar", { 8 | template, 9 | controller: statusBarController, 10 | bindings: { 11 | updatedAt: "<", 12 | }, 13 | }).name; 14 | -------------------------------------------------------------------------------- /app/angular/components/githubSponsorBanner/index.scss: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////////////// 2 | // .alert-sponsor 3 | //////////////////////////////////////////////////////////////////////////////// 4 | .github-sponsor-banner { 5 | display: flex; 6 | gap: 10px; 7 | border: 1px solid #ddd; 8 | margin-top: 24px; 9 | } 10 | 11 | .github-sponsor-banner .github-sponsor-img { 12 | width: 128px; 13 | } -------------------------------------------------------------------------------- /server_app/helpers/config.js: -------------------------------------------------------------------------------- 1 | const SecretToken = process.env.SECRET_TOKEN; 2 | const MongoUrl = process.env.PROD_MONGODB || process.env.MONGODB_LOCAL_URL || process.env.MONGO_URI; 3 | const IsDevelopment = process.env.NODE_ENV === "development"; 4 | const Port = Number(process.env.PORT); 5 | const ApiUrl = `${process.env.API_URL}`; 6 | 7 | module.exports = { 8 | SecretToken, 9 | MongoUrl, 10 | IsDevelopment, 11 | Port, 12 | ApiUrl, 13 | } -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:20-alpine 2 | 3 | WORKDIR /usr/src/app 4 | 5 | COPY yarn.lock package.json . 6 | 7 | RUN --mount=type=cache,target=/root/.yarn --mount=type=cache,target=/root/.cache YARN_CACHE_FOLDER=/root/.yarn yarn install 8 | 9 | COPY . . 10 | 11 | # expose port 3000 for server and 9000 for webpack-dev-server 12 | EXPOSE 3000 9000 13 | 14 | # run start:frontend and start:dev in parallel 15 | CMD ["yarn", "start:frontend", "start:dev"] 16 | -------------------------------------------------------------------------------- /app/angular/components/supportBannersList/index.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/angular/components/confirmationModal.html: -------------------------------------------------------------------------------- 1 | 4 | 5 | 8 | 9 | -------------------------------------------------------------------------------- /app/angular/service/Column.js: -------------------------------------------------------------------------------- 1 | export default class Column { 2 | constructor({ name = "", PK = false, FK = false, type = "INT", idOrigin = "", idLink = "" } = {}) { 3 | this.FK = FK; 4 | this.PK = PK; 5 | this.NOT_NULL = false; 6 | this.UNIQUE = false; 7 | this.AUTO_INCREMENT = false; 8 | this.defaultValue = ""; 9 | this.name = name; 10 | this.tableOrigin = { 11 | idOrigin, 12 | idLink, 13 | idName: "", 14 | } 15 | this.type = type; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/sass/joint-custom.scss: -------------------------------------------------------------------------------- 1 | /////////////////////////////////////////////////////////////////////////////// 2 | // Joint customizations 3 | /////////////////////////////////////////////////////////////////////////////// 4 | .link-tools { 5 | display: none; 6 | } 7 | 8 | .marker-arrowheads { 9 | display: none; 10 | } 11 | 12 | .joint-link.joint-theme-default .marker-vertex { 13 | display: none; 14 | } 15 | 16 | .joint-link.joint-theme-default .connection-wrap:hover { 17 | display: none; 18 | } 19 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/eu-preciso-de-ajuda-com---.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Eu preciso de ajuda com... 3 | about: Para qualquer pergunta que você tenha 4 | title: '' 5 | labels: Question 6 | assignees: '' 7 | 8 | --- 9 | 10 | > Antes de fazer sua pergunta, por favor explore a documentação do BRModelo Web: 11 | > https://docs.brmodeloweb.com 12 | > 13 | > Se mesmo assim você não encontrou a resposta que precisa, adicione sua pergunta a seguir: 14 | 15 | --- 16 | 17 | ### Pergunta 18 | Adicione aqui sua pergunta 19 | -------------------------------------------------------------------------------- /app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | BRMW 6 | 7 | 8 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /app/angular/components/deleteModelModal.html: -------------------------------------------------------------------------------- 1 | 4 | 5 | 8 | 9 | -------------------------------------------------------------------------------- /app/angular/components/dropdown.html: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /app/angular/session.js: -------------------------------------------------------------------------------- 1 | angular.module('myapp').service('Session', function($cookies) { 2 | 3 | this.create = function(sessionId, userId, userName) { 4 | this.id = sessionId; 5 | this.userId = userId; 6 | this.userName = userName; 7 | }; 8 | 9 | this.destroy = function() { 10 | this.id = null; 11 | this.userId = null; 12 | this.userName = null; 13 | 14 | $cookies.remove('sessionId'); 15 | $cookies.remove('userId'); 16 | $cookies.remove('userName'); 17 | }; 18 | 19 | this.sessionDetails = { 20 | 21 | } 22 | 23 | }); -------------------------------------------------------------------------------- /app/sass/forbidden-access.scss: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////////////// 2 | // .forbidden-access 3 | //////////////////////////////////////////////////////////////////////////////// 4 | .forbidden-access { 5 | display: grid; 6 | height: 100vh; 7 | } 8 | 9 | .forbidden-access .wrapper { 10 | align-self: center; 11 | } 12 | 13 | .forbidden-access .wrapper { 14 | text-align: center; 15 | max-width: 600px; 16 | } 17 | 18 | .forbidden-icon { 19 | display: block; 20 | width: 80%; 21 | margin: 0 auto 24px auto; 22 | } -------------------------------------------------------------------------------- /app/angular/components/sqlGeneratorModal.js: -------------------------------------------------------------------------------- 1 | import angular from "angular"; 2 | import template from "./sqlGeneratorModal.html"; 3 | 4 | const app = angular.module("app.sqlGeneratorModal", []); 5 | 6 | function Controller() { 7 | const $ctrl = this; 8 | 9 | $ctrl.cancel = () => { 10 | $ctrl.dismiss({ 11 | reason: "cancel", 12 | }); 13 | }; 14 | } 15 | 16 | export default app.component("sqlGeneratorModal", { 17 | template: template, 18 | bindings: { 19 | close: "&", 20 | dismiss: "&", 21 | sql: "<", 22 | }, 23 | controller: Controller, 24 | }).name; 25 | -------------------------------------------------------------------------------- /app/angular/components/sqlGeneratorModal.html: -------------------------------------------------------------------------------- 1 | 4 | 5 | 17 | -------------------------------------------------------------------------------- /app/react/containers/Providers/Providers.tsx: -------------------------------------------------------------------------------- 1 | import i18n from "i18n"; 2 | import PropTypes from "prop-types"; 3 | import React from "react"; 4 | import { I18nextProvider } from "react-i18next"; 5 | import { ThemeProvider } from "styled-components"; 6 | import theme from "../../theme"; 7 | 8 | type ProviderProps = { 9 | children: JSX.Element; 10 | }; 11 | 12 | const Providers: React.FC = ({ 13 | children 14 | }) => { 15 | return ( 16 | 17 | {children} 18 | 19 | ); 20 | }; 21 | 22 | export default Providers; 23 | -------------------------------------------------------------------------------- /server_app/middleware/index.js: -------------------------------------------------------------------------------- 1 | const jwt = require('jsonwebtoken'); 2 | const { SecretToken } = require('../helpers/config'); 3 | 4 | exports.validateJWT = (req, res, next) => { 5 | const token = req.headers['brx-access-token']; 6 | if (!token) return res.status(401).json({ auth: false, message: 'No token provided.' }); 7 | 8 | jwt.verify(token, SecretToken, function(err, decoded) { 9 | if (err) return res.status(500).json({ auth: false, message: 'Failed to authenticate token.' }); 10 | 11 | // if everything ok, save to request for use in other routes 12 | req.userId = decoded.id; 13 | next(); 14 | }); 15 | } 16 | 17 | -------------------------------------------------------------------------------- /app/angular/components/deleteModelModal.js: -------------------------------------------------------------------------------- 1 | import angular from "angular"; 2 | import template from "./deleteModelModal.html"; 3 | 4 | const app = angular.module("app.modelDeleterModal", []); 5 | 6 | function Controller() { 7 | const $ctrl = this; 8 | 9 | $ctrl.delete = () => { 10 | $ctrl.close({ 11 | result: "delete", 12 | }); 13 | }; 14 | 15 | $ctrl.cancel = () => { 16 | $ctrl.dismiss({ 17 | reason: "cancel", 18 | }); 19 | }; 20 | } 21 | 22 | export default app.component("deleteModelModal", { 23 | template: template, 24 | bindings: { 25 | close: "&", 26 | dismiss: "&", 27 | }, 28 | controller: Controller, 29 | }).name; 30 | -------------------------------------------------------------------------------- /app/angular/components/noteEditor.js: -------------------------------------------------------------------------------- 1 | import angular from "angular"; 2 | import template from "./noteEditor.html"; 3 | 4 | const app = angular.module("app.noteEditor", []); 5 | 6 | const Controller = function () { 7 | const $ctrl = this; 8 | $ctrl.submitted = false; 9 | 10 | $ctrl.updateNoteText = (newText) => { 11 | $ctrl.selected.element.setText(newText); 12 | } 13 | 14 | $ctrl.updateNoteColor = (newColor) => { 15 | $ctrl.selected.element.setColor(newColor); 16 | } 17 | }; 18 | 19 | export default app.component("noteEditor", { 20 | template: template, 21 | bindings: { 22 | selected: "<", 23 | }, 24 | controller: Controller, 25 | }).name; -------------------------------------------------------------------------------- /server_app/model/model.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | 3 | let ShareOptions = mongoose.Schema({ 4 | active: {type: Boolean, required: true}, 5 | importAllowed: {type: Boolean, required: true} 6 | }); 7 | 8 | let model = mongoose.Schema({ 9 | who: { type: String, required: true }, 10 | name: { type: String, required: true }, 11 | created: { type: Date, default: Date.now }, 12 | updated: { type: Date, default: Date.now }, 13 | model: { type: Object, required: true }, 14 | type: { type: String, required: true }, 15 | shareOptions: { type: ShareOptions, required: false } 16 | }); 17 | 18 | module.exports = mongoose.model("Model", model); -------------------------------------------------------------------------------- /app/angular/components/conversionOptionModal.js: -------------------------------------------------------------------------------- 1 | import angular from "angular"; 2 | import template from "./conversionOptionModal.html"; 3 | 4 | const app = angular.module("app.ConversionOptionModal", []); 5 | 6 | const Controller = function () { 7 | const $ctrl = this; 8 | 9 | $ctrl.forward = function (selectedOption) { 10 | $ctrl.submitted = true; 11 | $ctrl.close({ result: selectedOption }); 12 | }; 13 | }; 14 | 15 | export default app.component("conversionOptionModal", { 16 | template: template, 17 | bindings: { 18 | options: "<", 19 | summary: "<", 20 | title: "<", 21 | close: "&", 22 | }, 23 | controller: Controller, 24 | }).name; 25 | -------------------------------------------------------------------------------- /app/sass/app.scss: -------------------------------------------------------------------------------- 1 | @import 'colors.scss'; 2 | @import 'public.scss'; 3 | @import 'mainHeader.scss'; 4 | @import 'structure.scss'; 5 | @import 'projectsList'; 6 | @import 'modelsList.scss'; 7 | @import 'modelWorkspace.scss'; 8 | @import 'form.scss'; 9 | @import 'buttons.scss'; 10 | @import 'tables.scss'; 11 | @import 'modal.scss'; 12 | @import 'feedback.scss'; 13 | @import 'atomic.scss'; 14 | @import 'selectOptions.scss'; 15 | @import 'sql.scss'; 16 | @import 'print.scss'; 17 | @import 'joint-custom.scss'; 18 | @import 'sidebarPannel'; 19 | @import 'table-preview.scss'; 20 | @import 'elementActions.scss'; 21 | @import 'forbidden-access.scss'; 22 | @import 'note-color-list.scss'; 23 | -------------------------------------------------------------------------------- /app/react/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "esModuleInterop": true, 8 | "strict": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "noFallthroughCasesInSwitch": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "noEmit": false, 16 | "jsx": "react-jsx", 17 | "baseUrl": ".", 18 | "paths": { 19 | "@components/*": ["components/*"], 20 | "@pages/*": ["pages/*"], 21 | "@containers/*": ["containers/*"], 22 | "@enums/*": ["utils/enums/*"] 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/react/components/Button/Button.tsx: -------------------------------------------------------------------------------- 1 | import React, { MouseEventHandler, ReactNode } from "react"; 2 | 3 | enum ButtonTypes { 4 | BUTTON = "button", 5 | SUBMIT = "submit", 6 | RESET = "reset", 7 | } 8 | 9 | type ButtonProps = { 10 | label?: string; 11 | type?: ButtonTypes; 12 | onClick: MouseEventHandler; 13 | children: ReactNode; 14 | }; 15 | 16 | const Button: React.FC = ({ 17 | label, 18 | onClick, 19 | type = ButtonTypes.BUTTON, 20 | children, 21 | }) => { 22 | return ( 23 | 26 | ); 27 | }; 28 | 29 | export default Button; 30 | -------------------------------------------------------------------------------- /app/angular/components/conversionAttributeModal.js: -------------------------------------------------------------------------------- 1 | import angular from "angular"; 2 | import template from "./conversionAttributeModal.html"; 3 | 4 | const app = angular.module("app.createModelModal", []); 5 | 6 | const Controller = function () { 7 | const $ctrl = this; 8 | $ctrl.selectedValue = "new_column" 9 | 10 | $ctrl.forward = (selectedOption) => { 11 | $ctrl.submitted = true; 12 | $ctrl.close({ result: {"value": selectedOption, "quantity": $ctrl.qt}}); 13 | }; 14 | }; 15 | 16 | export default app.component("conversionAttributeModal", { 17 | template: template, 18 | bindings: { 19 | options: "<", 20 | summary: "<", 21 | title: "<", 22 | close: "&", 23 | }, 24 | controller: Controller, 25 | }).name; 26 | -------------------------------------------------------------------------------- /app/angular/service/queryExpressionService.js: -------------------------------------------------------------------------------- 1 | export const comparasionOperators = { 2 | EQUAL_TO: (value) => `= ${value}`, 3 | NOT_EQUAL_TO: (value) => `<> ${value}`, 4 | GREATER_THAN: (value) => `> ${value}`, 5 | LESS_THAN: (value) => `< ${value}`, 6 | BETWEEN: (value, otherValue) => `BETWEEN ${value} AND ${otherValue}`, 7 | GREATER_THAN_OR_EQUAL_TO: (value) => `>= ${value}`, 8 | LESS_THAN_OR_EQUAL_TO: (value) => `<= ${value}`, 9 | IS: (value) => `= '${value}'`, 10 | IS_NOT: (value) => `<> '${value}'`, 11 | CONTAINS: (value) => `LIKE '%${value}%'`, 12 | DOES_NOT_CONTAIN: (value) => `NOT LIKE '%${value}%'`, 13 | STARTS_WITH: (value) => `LIKE '${value}%'`, 14 | ENDS_WITH: (value) => `LIKE '%${value}'`, 15 | } -------------------------------------------------------------------------------- /app/angular/components/icons/logic/index.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | mongo: 4 | container_name: mongo 5 | image: mongo 6 | volumes: 7 | - ./data:/data/db 8 | ports: 9 | - "27017:27017" 10 | app_server: 11 | container_name: app_server 12 | build: . 13 | command: yarn start:dev 14 | volumes: 15 | - .:/usr/src/app 16 | - /usr/src/app/node_modules 17 | ports: 18 | - "3000:3000" 19 | depends_on: 20 | - mongo 21 | app_frontend: 22 | container_name: app_frontend 23 | build: . 24 | command: yarn start:frontend 25 | volumes: 26 | - .:/usr/src/app 27 | - /usr/src/app/node_modules 28 | ports: 29 | - "9000:9000" 30 | depends_on: 31 | - app_server 32 | -------------------------------------------------------------------------------- /app/sass/modelsList.scss: -------------------------------------------------------------------------------- 1 | /////////////////////////////////////////////////////////////////////////////// 2 | // .modelsList header.container 3 | // Adjustments on Boostrap to accommodate .feedback block 4 | /////////////////////////////////////////////////////////////////////////////// 5 | .modelsList header.container { 6 | position: relative; 7 | } 8 | 9 | .modelsList header.container .page-header { 10 | margin-top: 60px 11 | } 12 | 13 | /////////////////////////////////////////////////////////////////////////////// 14 | // .modelsList .feedback 15 | /////////////////////////////////////////////////////////////////////////////// 16 | .modelsList .feedback { 17 | position: absolute; 18 | top: 15px; 19 | right: 15px; 20 | left: 15px; 21 | 22 | } -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | //"@babel/env", 4 | "@babel/react", 5 | [ 6 | "@babel/preset-env", 7 | { 8 | "modules": false 9 | }, 10 | "jest" 11 | ] 12 | ], 13 | "plugins": [ 14 | "@babel/plugin-proposal-class-properties", 15 | "@babel/plugin-syntax-dynamic-import", 16 | "angularjs-annotate", 17 | ["@babel/plugin-transform-runtime", 18 | { 19 | "regenerator": true 20 | } 21 | ] 22 | ], 23 | "env": { 24 | "test": { 25 | "presets": [ 26 | [ 27 | "@babel/preset-env", 28 | { 29 | "modules": "auto" 30 | }, 31 | "jest" 32 | ] 33 | ], 34 | "plugins": ["@babel/plugin-transform-runtime"] 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /app/angular/components/conversionOptionModal.html: -------------------------------------------------------------------------------- 1 | 4 | 5 | 17 | 18 | -------------------------------------------------------------------------------- /app/sass/atomic.scss: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////////////// 2 | // Hide Stuff 3 | //////////////////////////////////////////////////////////////////////////////// 4 | .hide {display: none !important;} 5 | 6 | .vhide { 7 | position: absolute !important; 8 | width: 1px !important; 9 | height: 1px !important; 10 | border: 0 !important; 11 | padding: 0 !important; 12 | margin: -1px !important; 13 | overflow: hidden !important; 14 | clip: rect(0 0 0 0) !important; 15 | } 16 | 17 | //////////////////////////////////////////////////////////////////////////////// 18 | // .separator 19 | //////////////////////////////////////////////////////////////////////////////// 20 | .separator { 21 | width: 100%; 22 | border-color: #cecece; 23 | } -------------------------------------------------------------------------------- /app/angular/components/renameModelModal.js: -------------------------------------------------------------------------------- 1 | import angular from "angular"; 2 | import template from "./renameModelModal.html"; 3 | 4 | const app = angular.module("app.renameModelModal", []); 5 | 6 | const Controller = function () { 7 | const $ctrl = this; 8 | $ctrl.submitted = false; 9 | 10 | $ctrl.rename = (newName) => { 11 | $ctrl.submitted = true; 12 | if (newName != null && newName != "") { 13 | $ctrl.close({ 14 | result: newName, 15 | }); 16 | } 17 | }; 18 | 19 | $ctrl.cancel = () => { 20 | $ctrl.dismiss({ 21 | reason: "cancel", 22 | }); 23 | }; 24 | }; 25 | 26 | export default app.component("renameModelModal", { 27 | template: template, 28 | bindings: { 29 | close: "&", 30 | dismiss: "&", 31 | }, 32 | controller: Controller, 33 | }).name; 34 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/requisitar-funcionalidade.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Requisitar funcionalidade 3 | about: Ideias e sugestões de como melhorar o BRMW 4 | title: '' 5 | labels: Feature request 6 | assignees: '' 7 | 8 | --- 9 | 10 | ### Sua sugestão de melhoria é relacionada a algum problema? 11 | 12 | Adicione aqui a descrição do problema 13 | _Ex: É muito complicado fazer..._ 14 | 15 | --- 16 | 17 | ### Descreva sua ideia de como resolver o problema 18 | Adicione aqui como você acha que deveríamos resolver esse problema. 19 | Fique a vontade para adicionar imagens, videos, desenhos ou qualquer material que te ajude a explicar sua proposta de solução 20 | 21 | --- 22 | 23 | ### Informações extras 24 | Aqui você pode adicionar qualquer informação extra que ache relevante pra nos ajudar a entender o problema. 25 | -------------------------------------------------------------------------------- /app/angular/publicview/publicView.scss: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////////////// 2 | // .publicView 3 | //////////////////////////////////////////////////////////////////////////////// 4 | .publicView > .navbar { 5 | height: 55px; 6 | background-color: #fff; 7 | } 8 | 9 | .publicView .navbar .br-loader { 10 | top: 63px; 11 | } 12 | 13 | .publicView .document-info { 14 | margin-left: 150px; 15 | } 16 | 17 | .publicView .document-info h2 { 18 | margin-top: 5px; 19 | margin-bottom: 15px; 20 | } 21 | 22 | .publicView .model-area { 23 | left: 0; 24 | } 25 | 26 | .publicView .workspace-header .nav li:first-child { 27 | margin-left: 149px; 28 | border-left: 1px solid var(--border-default); 29 | } 30 | 31 | .publicView .workspace-header .header-actions { 32 | position: relative; 33 | top: -3px; 34 | } -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | require("dotenv").config(); 3 | const app = require("./server_app/app"); 4 | const config = require("./server_app/helpers/config"); 5 | const { Port, MongoUrl, ApiUrl, IsDevelopment } = config; 6 | 7 | mongoose.connect( 8 | MongoUrl, 9 | { useNewUrlParser: true, useUnifiedTopology: true }, 10 | function (err) { 11 | if (err) throw err; 12 | 13 | const databaseInfo = IsDevelopment ? `Database: ${MongoUrl}\n` : ""; 14 | 15 | app.listen(Port, function () { 16 | console.log(` 17 | --------------------------------------------------- 18 | --------------- APPLICATION RUNNING --------------- 19 | --------------------------------------------------- 20 | 21 | App: ${ApiUrl} 22 | ${databaseInfo} 23 | --------------------------------------------------- 24 | `); 25 | }); 26 | } 27 | ); -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: brmodeloweb # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 14 | -------------------------------------------------------------------------------- /app/angular/components/queryExpressionModal/styles.scss: -------------------------------------------------------------------------------- 1 | .query-expression-modal { 2 | .labels-wrapper { 3 | margin-bottom: 12px; 4 | display: flex; 5 | flex-wrap: wrap; 6 | row-gap: 18px; 7 | 8 | .condition-label { 9 | padding: 6px; 10 | background: hsl(170, 100%, 20%); 11 | color: white; 12 | border-radius: 4px; 13 | margin-right: 8px; 14 | 15 | i { 16 | margin-left: 4px; 17 | cursor: pointer; 18 | } 19 | } 20 | } 21 | .form-content { 22 | display: flex; 23 | align-items: center; 24 | column-gap: 8px; 25 | margin-bottom: 12px; 26 | 27 | button { 28 | min-width: 180px; 29 | } 30 | 31 | .dropdown-tmp-list { 32 | max-height: 400px; 33 | overflow: auto; 34 | } 35 | } 36 | .form-content-joins { 37 | dropdown { 38 | width: 100%; 39 | } 40 | } 41 | .modal-message { 42 | margin-top: 16px; 43 | } 44 | } -------------------------------------------------------------------------------- /app/sass/print.scss: -------------------------------------------------------------------------------- 1 | @media print { 2 | 3 | //////////////////////////////////////////////////////////////////////////// 4 | // Hide not relevant interface when printing 5 | //////////////////////////////////////////////////////////////////////////// 6 | .elements-list, 7 | .main-header, 8 | .elements-holder, 9 | .workspace-header, 10 | .model-properties { 11 | display: none; 12 | } 13 | 14 | //////////////////////////////////////////////////////////////////////////// 15 | // Prepare printing area 16 | //////////////////////////////////////////////////////////////////////////// 17 | .model-area { 18 | position: static; 19 | } 20 | 21 | .mainContent, 22 | .editor-scroller { 23 | padding: 0 !important; 24 | margin: 0 !important; 25 | } 26 | 27 | .editor-scroller .joint-paper { 28 | position: static; 29 | height: 100vh; 30 | border-color: transparent; 31 | box-shadow: 0 0 0 transparent; 32 | } 33 | } -------------------------------------------------------------------------------- /app/angular/components/dropdown.js: -------------------------------------------------------------------------------- 1 | import angular from "angular"; 2 | import template from "./dropdown.html"; 3 | 4 | const dropDownController = function ($element, $timeout) { 5 | const ctrl = this; 6 | ctrl.open = false; 7 | 8 | ctrl.toggle = (command) => { 9 | ctrl.open = command; 10 | }; 11 | 12 | ctrl.select = (option) => { 13 | ctrl.selected = option; 14 | ctrl.toggle(false); 15 | ctrl.onSelect({ selected: option }); 16 | }; 17 | 18 | ctrl.$postLink = () => { 19 | $element.on("focusout", function () { 20 | if (ctrl.open) { 21 | $timeout(function () { 22 | ctrl.toggle(false); 23 | }, 300); 24 | } 25 | }); 26 | }; 27 | 28 | }; 29 | 30 | export default angular.module("app.dropdown", []).component("dropdown", { 31 | template, 32 | controller: dropDownController, 33 | bindings: { 34 | options: "<", 35 | selected: "<", 36 | onSelect: "&", 37 | disabled: '<' 38 | }, 39 | }).name; 40 | -------------------------------------------------------------------------------- /app/angular/components/supportBannersList/index.scss: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////////////// 2 | // Component base 3 | //////////////////////////////////////////////////////////////////////////////// 4 | support-banners-list { 5 | display: block; 6 | } 7 | 8 | .support-banner-list { 9 | list-style: none; 10 | padding: 0; 11 | } 12 | 13 | //////////////////////////////////////////////////////////////////////////////// 14 | // .small-banner 15 | //////////////////////////////////////////////////////////////////////////////// 16 | .small-banner a { 17 | display: block; 18 | border: 1px solid var(--gray-80); 19 | padding: 8px 6px; 20 | border-radius: 4px; 21 | margin-bottom: 12px; 22 | color: var(--gray-50); 23 | } 24 | 25 | .small-banner a:hover { 26 | text-decoration: none; 27 | background-color: #fff; 28 | } 29 | 30 | .small-banner p { 31 | margin: 0; 32 | font-size: 1.3rem; 33 | } -------------------------------------------------------------------------------- /server_app/helpers/crypto.test.js: -------------------------------------------------------------------------------- 1 | const { encrypt, decrypt } = require("./crypto"); 2 | 3 | describe("crypto test", () => { 4 | const OLD_ENV = process.env; 5 | 6 | beforeEach(() => { 7 | jest.resetModules(); 8 | process.env = { ...OLD_ENV }; 9 | }); 10 | 11 | afterAll(() => { 12 | process.env = OLD_ENV; 13 | }); 14 | 15 | test("Encrypt data", async () => { 16 | process.env.SECRET_TOKEN = "some_token"; 17 | const data = { name: "some_name" }; 18 | const json = JSON.stringify(data); 19 | 20 | const encrypted = encrypt(json); 21 | 22 | expect(encrypted).not.toBeNull(); 23 | }); 24 | 25 | test("Decrypt data", async () => { 26 | process.env.SECRET_TOKEN = "some_token"; 27 | const data = { name: "some_name" }; 28 | const json = JSON.stringify(data); 29 | 30 | const decrypted = decrypt(encrypt(json)); 31 | 32 | expect(decrypted).not.toBeNull(); 33 | expect(JSON.parse(decrypted)).toEqual(data); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /app/angular/components/dropdownIcon.js: -------------------------------------------------------------------------------- 1 | import angular from "angular"; 2 | import template from "./dropdownIcon.html"; 3 | 4 | const dropDownIconController = function ($element, $timeout) { 5 | const ctrl = this; 6 | ctrl.open = false; 7 | 8 | ctrl.toggle = (command) => { 9 | ctrl.open = command; 10 | }; 11 | 12 | ctrl.select = (option) => { 13 | ctrl.selected = option; 14 | ctrl.toggle(false); 15 | ctrl.onSelect({ selected: option }); 16 | }; 17 | 18 | ctrl.$postLink = () => { 19 | $element.on("focusout", function () { 20 | if (ctrl.open) { 21 | $timeout(function () { 22 | ctrl.toggle(false); 23 | }, 150); 24 | } 25 | }); 26 | }; 27 | }; 28 | 29 | export default angular.module("app.dropdownIcon", []).component("dropdownIcon", { 30 | template, 31 | controller: dropDownIconController, 32 | bindings: { 33 | options: "<", 34 | selected: "<", 35 | onSelect: "&", 36 | }, 37 | transclude: true 38 | }).name; 39 | -------------------------------------------------------------------------------- /app/angular/logic/checkConstraint.js: -------------------------------------------------------------------------------- 1 | import angular from "angular"; 2 | import template from "./checkConstraint.html"; 3 | 4 | const app = angular.module("app.checkConstraint", []); 5 | 6 | const Controller = function () { 7 | const $ctrl = this; 8 | 9 | $ctrl.isManual = true; 10 | 11 | $ctrl.toggleFormType = () => { 12 | $ctrl.isManual = !$ctrl.isManual; 13 | } 14 | 15 | $ctrl.selectCheckConstraint = (selected) => { 16 | $ctrl.column.checkConstraint = { 17 | type: selected.type, 18 | name: selected.name, 19 | }; 20 | } 21 | 22 | $ctrl.$onChanges = (changes) => { 23 | if (changes.column != null && changes.column.currentValue != null) { 24 | $ctrl.column = changes.column.currentValue; 25 | $ctrl.isManual = ($ctrl.column.checkConstraint || {}).checkExpression; 26 | } 27 | } 28 | } 29 | 30 | export default app.component("checkConstraint", { 31 | template: template, 32 | bindings: { 33 | column: "<", 34 | }, 35 | controller: Controller, 36 | }).name; -------------------------------------------------------------------------------- /app/sass/feedback.scss: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////////////// 2 | // .feedback 3 | //////////////////////////////////////////////////////////////////////////////// 4 | .workspace-header .feedback { 5 | position: absolute; 6 | top: calc(100% + 12px); 7 | left: 12px; 8 | width: calc(100% - 64px); 9 | z-index: 1; 10 | } 11 | 12 | //////////////////////////////////////////////////////////////////////////////// 13 | // .field.error 14 | //////////////////////////////////////////////////////////////////////////////// 15 | .field.error .fieldLabel { 16 | color: var (--danger-base); 17 | } 18 | 19 | .field.error .fieldInput { 20 | color: var(--danger-base); 21 | border-color: var(--danger-base); 22 | // background-color: rgba(red, 0.1); 23 | box-shadow: 1px 1px 1px var(--white) inset; 24 | } 25 | 26 | .field.error .fieldInput:focus {box-shadow: 1px 1px 1px var(--white) inset;} 27 | .field.error .fieldInput::placeholder {color: rgba(red, 0.6);} -------------------------------------------------------------------------------- /app/angular/components/confirmationModal.js: -------------------------------------------------------------------------------- 1 | import angular from "angular"; 2 | import template from "./confirmationModal.html"; 3 | 4 | const app = angular.module("app.confirmationModalController", []); 5 | 6 | function controller() { 7 | const $ctrl = this; 8 | 9 | $ctrl.$onInit = () => { 10 | const { 11 | title, 12 | content, 13 | cancelLabel = "Cancelar", 14 | confirmLabel = "Confirmar", 15 | } = $ctrl.resolve.modalData; 16 | 17 | $ctrl.title = title; 18 | $ctrl.content = content; 19 | $ctrl.cancelLabel = cancelLabel; 20 | $ctrl.confirmLabel = confirmLabel; 21 | }; 22 | 23 | $ctrl.confirm = () => { 24 | $ctrl.close({ 25 | result: true, 26 | }); 27 | }; 28 | 29 | $ctrl.cancel = () => { 30 | $ctrl.dismiss({ 31 | reason: "cancel", 32 | }); 33 | }; 34 | } 35 | 36 | export default app.component("confirmationModal", { 37 | template, 38 | bindings: { 39 | close: "&", 40 | dismiss: "&", 41 | resolve: "<", 42 | }, 43 | controller, 44 | }).name; 45 | -------------------------------------------------------------------------------- /app/angular/components/conversionAttributeModal.html: -------------------------------------------------------------------------------- 1 | 4 | 5 | 24 | 25 | -------------------------------------------------------------------------------- /app/angular/components/duplicateModelModal.html: -------------------------------------------------------------------------------- 1 | 4 | 5 | 24 | 25 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/reportar-problema.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Reportar Problema 3 | about: Reportar bugs no BRMW 4 | title: '' 5 | labels: Bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | ### Link da modelagem 11 | Adicione aqui o link da modelagem na qual você estava trabalhando quando encontrou o problema. 12 | 13 | --- 14 | 15 | ### Descreva o problema 16 | Adicione aqui uma breve descrição do problema. 17 | 18 | --- 19 | 20 | ### Passos para reproduzir 21 | 1. Na tela de... 22 | 1. Selecione a opção... 23 | 1. Clique em... 24 | 25 | --- 26 | 27 | ### Screenshots e/ou video 28 | Adicione aqui imagens ou videos que possam nos ajudar a entender o problema 29 | 30 | --- 31 | 32 | ### Sistema operacional e navegador 33 | - Adicione aqui seu sistema operacional (Windows, MacOS, Linux distro, etc...) 34 | - Adicione aqui seu navegador (Firefox, Chrome, Edge, etc) e a versão do mesmo 35 | 36 | --- 37 | 38 | ### Informações extras 39 | Aqui você pode adicionar qualquer informação extra que ache relevante pra nos ajudar a entender o problema. 40 | -------------------------------------------------------------------------------- /app/sass/modal.scss: -------------------------------------------------------------------------------- 1 | /////////////////////////////////////////////////////////////////////////////// 2 | // Modal customizations 3 | /////////////////////////////////////////////////////////////////////////////// 4 | .modal-content { 5 | background-color: #efefef; 6 | } 7 | 8 | .modal-title { 9 | color: var(--gray-20); 10 | } 11 | 12 | /////////////////////////////////////////////////////////////////////////////// 13 | // Loading indicator inside a modal 14 | /////////////////////////////////////////////////////////////////////////////// 15 | .modal-header { 16 | position: relative; 17 | } 18 | 19 | .modal-header .br-loader { 20 | position: absolute; 21 | top: unset; 22 | bottom: -9px; 23 | } 24 | 25 | /////////////////////////////////////////////////////////////////////////////// 26 | // For validation inside modal 27 | /////////////////////////////////////////////////////////////////////////////// 28 | .modal-body .feedback .alert { 29 | padding: 6px 10px; 30 | } 31 | 32 | .modal-body .feedback .alert p { 33 | font-size: 1.2rem; 34 | } -------------------------------------------------------------------------------- /app/angular/components/renameModelModal.html: -------------------------------------------------------------------------------- 1 | 4 | 5 | 23 | 24 | -------------------------------------------------------------------------------- /app/angular/conceptual/validator.js: -------------------------------------------------------------------------------- 1 | export default class Validator { 2 | 3 | constructor() {} 4 | 5 | isEntity = (element) => { 6 | return element.attributes.supertype === 'Entity'; 7 | }; 8 | 9 | isAttribute = (element) => { 10 | return element.attributes.supertype === 'Attribute'; 11 | }; 12 | 13 | isExtension = (element) => { 14 | return element.attributes.supertype === 'Inheritance'; 15 | }; 16 | 17 | isRelationship = (element) => { 18 | return element.attributes.supertype === 'Relationship' 19 | }; 20 | 21 | isAssociative = (element) => { 22 | return element.attributes.type === 'erd.Associative' 23 | }; 24 | 25 | isKey = (element) => { 26 | return element.attributes.supertype === 'Key'; 27 | }; 28 | 29 | isComposedAttribute = (element) => { 30 | return element.attributes.type === 'erd.ComposedAttribute'; 31 | }; 32 | 33 | isLink = (element) => { 34 | return element.attributes.type === 'erd.Link'; 35 | }; 36 | 37 | isNote = (element) => { 38 | return element.attributes.type === 'custom.Note'; 39 | }; 40 | 41 | } -------------------------------------------------------------------------------- /app/angular/service/toolsViewService.js: -------------------------------------------------------------------------------- 1 | import * as joint from "jointjs/dist/joint"; 2 | 3 | export default class ToolsViewService { 4 | constructor() { 5 | const defaultTools = this.#createDefaultTools(); 6 | this.toolsWithInfo = new joint.dia.ToolsView({ tools: [...defaultTools, new joint.shapes.erd.InfoButton(), new joint.linkTools.Remove()] }); 7 | this.tools = new joint.dia.ToolsView({ tools: [...defaultTools, new joint.linkTools.Remove()] }) 8 | this.toolsWithoutRemove = new joint.dia.ToolsView({ tools: defaultTools }); 9 | } 10 | 11 | getToolsView(conectionType = "") { 12 | if (conectionType === "Entity-Relationship") return this.toolsWithInfo; 13 | if (conectionType === "Table-View") return this.toolsWithoutRemove; 14 | return this.tools; 15 | } 16 | 17 | #createDefaultTools = () => { 18 | return [ 19 | new joint.linkTools.Vertices(), 20 | new joint.linkTools.Segments(), 21 | new joint.linkTools.SourceArrowhead(), 22 | new joint.linkTools.TargetArrowhead(), 23 | new joint.linkTools.Boundary(), 24 | ] 25 | } 26 | 27 | }; -------------------------------------------------------------------------------- /app/angular/components/icons/conceptual/index.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /cypress/e2e/api/model.cy.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | const conceptualModel = require("../../fixtures/conceptualModel.json"); 4 | 5 | describe("Models - Creation via API call", () => { 6 | beforeEach(() => { 7 | cy.intercept("GET", "/models?userId=*").as("getUserModels"); 8 | cy.loginViaGui(); 9 | cy.wait("@getUserModels").then((userModels) => { 10 | cy.cleanUpUserModels(userModels); 11 | }); 12 | cy.reload(); 13 | }); 14 | 15 | it("successfully creates a conceptual model via API", () => { 16 | cy.wait("@getUserModels").then((userModels) => { 17 | const userId = userModels.request.url.match(/userId=([^&]*)/)[1]; 18 | 19 | cy.createModelViaApi("conceptual", userId, conceptualModel); 20 | }); 21 | cy.reload(); 22 | cy.contains("td", "Conceptual").click(); 23 | 24 | cy.get(".editor-scroller [data-type='erd.Entity']").should("have.length", 2); 25 | cy.get(".editor-scroller [data-type='erd.Relationship']").should( 26 | "have.length", 27 | 1 28 | ); 29 | cy.get(".editor-scroller [data-type='erd.Link']").should("have.length", 2); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /app/react/i18n/index.ts: -------------------------------------------------------------------------------- 1 | import LocaleNamespaces from "@enums/localeNamespaces"; 2 | import i18n, { Resource } from "i18next"; 3 | import LanguageDetector from "i18next-browser-languagedetector"; 4 | import { initReactI18next } from "react-i18next"; 5 | import commonTranslationEN from "../public/locales/en/common.json"; 6 | import loginTranslationEN from "../public/locales/en/login.json"; 7 | import commonTranslationPTBR from "../public/locales/pt-BR/common.json"; 8 | import loginTranslationPTBR from "../public/locales/pt-BR/login.json"; 9 | 10 | const resources: Resource = { 11 | en: { 12 | common: commonTranslationEN, 13 | login: loginTranslationEN, 14 | }, 15 | "pt-BR": { 16 | common: commonTranslationPTBR, 17 | login: loginTranslationPTBR, 18 | }, 19 | }; 20 | 21 | i18n 22 | .use(LanguageDetector) 23 | .use(initReactI18next) 24 | .init({ 25 | resources, 26 | ns: Object.values(LocaleNamespaces), 27 | defaultNS: LocaleNamespaces.COMMON, 28 | debug: process.env.NODE_ENV !== "production", 29 | fallbackLng: "en", 30 | interpolation: { 31 | escapeValue: false, 32 | }, 33 | }); 34 | 35 | export default i18n; 36 | -------------------------------------------------------------------------------- /app/sass/table-preview.scss: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////////////// 2 | // .table-preview 3 | //////////////////////////////////////////////////////////////////////////////// 4 | .table-preview { 5 | border: 1px solid var(--border-default); 6 | border-radius: 2px 2px 0 0; 7 | margin-bottom: 12px; 8 | } 9 | 10 | .table-preview header { 11 | padding: 12px 8px; 12 | border-bottom: 1px solid var(--border-default); 13 | font-size: 1.3em; 14 | background-color: rgba(#000, 0.1); // Fallback color 15 | } 16 | 17 | .table-preview header h5 { 18 | font-family: monospace; 19 | font-weight: 500; 20 | margin: 0; 21 | } 22 | 23 | //////////////////////////////////////////////////////////////////////////////// 24 | // .table-preview-body 25 | //////////////////////////////////////////////////////////////////////////////// 26 | .table-preview-body .checkbox { 27 | padding: 6px 8px; 28 | margin: 0; 29 | } 30 | 31 | .table-preview-body .checkbox:not(:last-child) { 32 | border-bottom: 1px solid var(--border-default); 33 | } 34 | 35 | .table-preview-body .checkbox label { 36 | font-size: 0.9em; 37 | } 38 | -------------------------------------------------------------------------------- /server_app/mail/sender.js: -------------------------------------------------------------------------------- 1 | const formData = require('form-data'); 2 | const Mailgun = require('mailgun.js'); 3 | const mailgun = new Mailgun(formData); 4 | const templateLoader = require('./templateLoader'); 5 | 6 | const getPath = (email, recoveryCode) => { 7 | const baseurl = process.env.PROD_MAIL_ENV || 'http://localhost:3000'; 8 | return `${baseurl}/#!/reset/${email}/${recoveryCode}` 9 | } 10 | 11 | const recovery = async (email, recoveryCode) => { 12 | return new Promise(async (resolve, reject) => { 13 | if(!process.env.PROD_MAIL_ENV) { 14 | return resolve(`reset link: ${getPath(email, recoveryCode)}`) 15 | } 16 | const data = { 17 | from: 'brmodeloweb@gmail.com', 18 | to: [email], 19 | subject: 'Recuperação de senha', 20 | html: templateLoader.loadRecovery(getPath(email, recoveryCode)), 21 | }; 22 | const mg = mailgun.client({ username: 'api', key: process.env.PROD_MAILGUN_API_KEY }); 23 | mg.messages.create('brmodeloweb.com', data) 24 | .then(msg => resolve(msg)) 25 | .catch(err => reject(err)); 26 | }); 27 | }; 28 | 29 | const sender = { 30 | recovery 31 | }; 32 | 33 | module.exports = sender; -------------------------------------------------------------------------------- /app/angular/conceptual/factory.js: -------------------------------------------------------------------------------- 1 | export default class Factory { 2 | 3 | constructor(shapes) { 4 | this.shapes = shapes; 5 | } 6 | 7 | createEntity = (customConfig) => { 8 | return new this.shapes.erd.Entity(customConfig); 9 | }; 10 | 11 | createRelationship = (customConfig) => { 12 | return new this.shapes.erd.Relationship(customConfig); 13 | }; 14 | 15 | createIsa = (customConfig) => { 16 | return new this.shapes.erd.ISA(customConfig); 17 | }; 18 | 19 | createAssociative = (customConfig) => { 20 | return new this.shapes.erd.Associative(customConfig) 21 | }; 22 | 23 | createAttribute = (customConfig) => { 24 | return new this.shapes.erd.Attribute(customConfig); 25 | }; 26 | 27 | createKey = (customConfig) => { 28 | return new this.shapes.erd.Key(customConfig); 29 | }; 30 | 31 | createLink = (customConfig) => { 32 | return new this.shapes.erd.Link(customConfig); 33 | }; 34 | 35 | createBlockAssociative = (customConfig) => { 36 | return new this.shapes.erd.BlockAssociative(customConfig); 37 | }; 38 | 39 | createComposedAttribute = (customConfig) => { 40 | return new this.shapes.erd.ComposedAttribute(customConfig); 41 | }; 42 | 43 | } -------------------------------------------------------------------------------- /app/angular/components/duplicateModelModal.js: -------------------------------------------------------------------------------- 1 | import angular from "angular"; 2 | import template from "./duplicateModelModal.html"; 3 | 4 | const app = angular.module("app.duplicateModelModalController", []); 5 | 6 | const Controller = function(ModelAPI) { 7 | const $ctrl = this; 8 | $ctrl.submitted = false; 9 | 10 | $ctrl.$onInit = () => { 11 | $ctrl.name = $ctrl.suggestedName; 12 | }; 13 | 14 | $ctrl.save = function (newName) { 15 | $ctrl.submitted = true; 16 | if (newName != null && newName != "") { 17 | ModelAPI.duplicate($ctrl.modelId, $ctrl.userId, newName).then((newModelResponse) => { 18 | $ctrl.close({ 19 | result: newModelResponse.data, 20 | }); 21 | }).catch(error => { 22 | $ctrl.dismiss({ 23 | result: "error", 24 | reason: error 25 | }); 26 | }); 27 | } 28 | }; 29 | 30 | $ctrl.cancel = function () { 31 | $ctrl.dismiss({ 32 | reason: "cancel", 33 | }); 34 | }; 35 | }; 36 | 37 | export default app.component("duplicateModelModal", { 38 | template, 39 | bindings: { 40 | close: "&", 41 | dismiss: "&", 42 | suggestedName: "<", 43 | userId: "<", 44 | modelId: "<", 45 | }, 46 | controller: Controller, 47 | }).name; -------------------------------------------------------------------------------- /app/angular/noaccess/noaccess.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 10 |
11 | 12 |

{{ 'Forbidden access' | translate }}

13 |

{{ 'Looks like this model does not exist or you don\'t have access to it.' | translate }}

14 | {{ 'Back to models list' | translate }} 15 |
16 |
-------------------------------------------------------------------------------- /server_app/helpers/crypto.js: -------------------------------------------------------------------------------- 1 | const crypto = require("crypto"); 2 | 3 | const ALGORITHM = "aes-256-ctr"; 4 | const ENCRYPTION_KEY = process.env.SECRET_TOKEN; 5 | const IV_LENGTH = 16; 6 | 7 | const encrypt = (data) => { 8 | const key = crypto 9 | .createHash("sha256") 10 | .update(String(ENCRYPTION_KEY)) 11 | .digest("base64") 12 | .substr(0, 32); 13 | const iv = crypto.randomBytes(IV_LENGTH); 14 | const cipher = crypto.createCipheriv(ALGORITHM, Buffer.from(key), iv); 15 | return Buffer.concat([iv, cipher.update(data), cipher.final()]); 16 | }; 17 | 18 | const decrypt = (hash) => { 19 | const key = crypto 20 | .createHash("sha256") 21 | .update(String(ENCRYPTION_KEY)) 22 | .digest("base64") 23 | .substr(0, 32); 24 | const iv = hash.slice(0, IV_LENGTH); 25 | const decipher = crypto.createDecipheriv(ALGORITHM, Buffer.from(key), iv); 26 | const data = hash.slice(IV_LENGTH); 27 | const decrypted = Buffer.concat([decipher.update(data), decipher.final()]); 28 | return decrypted.toString(); 29 | }; 30 | 31 | const decode = (hash) => { 32 | return Buffer.from(hash, 'base64').toString('ascii'); 33 | }; 34 | 35 | module.exports = { 36 | encrypt, 37 | decrypt, 38 | decode 39 | }; 40 | -------------------------------------------------------------------------------- /app/angular/components/dropdownIcon.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/sass/sql.scss: -------------------------------------------------------------------------------- 1 | // body { 2 | // background: rgb(204,204,204); 3 | // } 4 | page { 5 | background: white; 6 | display: block; 7 | margin: 0 auto; 8 | margin-bottom: 0.5cm; 9 | box-shadow: 0 0 0.5cm rgba(0,0,0,0.5); 10 | } 11 | page[size="A4"] { 12 | width: 21cm; 13 | height: 29.7cm; 14 | } 15 | page[size="A4"][layout="portrait"] { 16 | width: 29.7cm; 17 | height: 21cm; 18 | } 19 | page[size="A3"] { 20 | width: 29.7cm; 21 | height: 42cm; 22 | } 23 | page[size="A3"][layout="portrait"] { 24 | width: 42cm; 25 | height: 29.7cm; 26 | } 27 | page[size="A5"] { 28 | width: 14.8cm; 29 | height: 21cm; 30 | } 31 | page[size="A5"][layout="portrait"] { 32 | width: 21cm; 33 | height: 14.8cm; 34 | } 35 | @media print { 36 | body, page { 37 | margin: 0; 38 | box-shadow: 0; 39 | } 40 | } 41 | 42 | .sqlpage { 43 | margin-top: 75px; 44 | } 45 | 46 | .sqlTitle { 47 | font-size: 24px; 48 | color: var(--brand-default); 49 | display: block; 50 | margin: 0 auto; 51 | text-align: center; 52 | margin: 20px; 53 | } 54 | 55 | .ta-editor { 56 | min-height: 300px; 57 | height: auto; 58 | overflow: auto; 59 | font-family: inherit; 60 | font-size: 100%; 61 | margin:20px 0; 62 | } 63 | 64 | .pageContent { 65 | font-size: 16px; 66 | padding: 35px; 67 | } 68 | -------------------------------------------------------------------------------- /server_app/user/validator.js: -------------------------------------------------------------------------------- 1 | const MESSAGES = { 2 | MISSING_USERNAME: "Missig username param", 3 | MISSING_PASSWORD: "Missig password param", 4 | MISSING_MAIL: "Missig mail param", 5 | VALID_PARAM: "Everything ok with your params", 6 | }; 7 | 8 | class ValidationResponse { 9 | constructor(isValid, message) { 10 | this.valid = isValid; 11 | this.message = message; 12 | } 13 | } 14 | 15 | const validateLoginParams = ({ username, password }) => { 16 | if (username == "") { 17 | return new ValidationResponse(false, MESSAGES.MISSING_USERNAME); 18 | } 19 | 20 | if (password == "") { 21 | return new ValidationResponse(false, MESSAGES.MISSING_PASSWORD); 22 | } 23 | 24 | return new ValidationResponse(true, MESSAGES.VALID_PARAM); 25 | }; 26 | 27 | const validateSignUpParams = ({ username, mail, password }) => { 28 | if (username == "") { 29 | return new ValidationResponse(false, MESSAGES.MISSING_USERNAME); 30 | } 31 | 32 | if (mail == "") { 33 | return new ValidationResponse(false, MESSAGES.MISSING_MAIL); 34 | } 35 | 36 | if (password == "") { 37 | return new ValidationResponse(false, MESSAGES.MISSING_PASSWORD); 38 | } 39 | 40 | return new ValidationResponse(true, MESSAGES.VALID_PARAM); 41 | }; 42 | 43 | module.exports = { 44 | validateLoginParams, 45 | validateSignUpParams, 46 | }; 47 | -------------------------------------------------------------------------------- /app/react/theme/index.ts: -------------------------------------------------------------------------------- 1 | const theme = { 2 | colors: { 3 | primary: { 4 | 500: "hsl(170, 100%, 30%)", 5 | }, 6 | secondary: { 7 | 500: "hsl(170, 100%, 40%)", 8 | }, 9 | gray: { 10 | 100: "hsl(0, 0%, 10%", 11 | }, 12 | white: "hsl(0, 0%, 100%)", 13 | black: "hsl(0, 0, 0)", 14 | }, 15 | shadows: { 16 | default: "0 0 20px rgb(0, 0, 0, 0.1)", 17 | }, 18 | radii: { 19 | none: "0", 20 | sm: "0.125rem", 21 | default: "0.25rem", 22 | md: "0.375rem", 23 | lg: "0.5rem", 24 | xl: "0.75rem", 25 | "2xl": "1rem", 26 | "3xl": "1.5rem", 27 | full: "9999px", 28 | }, 29 | spacings: { 30 | 0.5: "0.125rem", 31 | 1: "0.25rem", 32 | 1.5: "0.375rem", 33 | 2: "0.5rem", 34 | 2.5: "0.625rem", 35 | 3: "0.75rem", 36 | 3.5: "0.875rem", 37 | 4: "1rem", 38 | 5: "1.25rem", 39 | 6: "1.5rem", 40 | 7: "1.75rem", 41 | 8: "2rem", 42 | 9: "2.25rem", 43 | 10: "2.5rem", 44 | 11: "2.75rem", 45 | 12: "3rem", 46 | 14: "3.5rem", 47 | 16: "4rem", 48 | 20: "5rem", 49 | 24: "6rem", 50 | 28: "7rem", 51 | 32: "8rem", 52 | 36: "9rem", 53 | 40: "10rem", 54 | 44: "11rem", 55 | 48: "12rem", 56 | 52: "13rem", 57 | 56: "14rem", 58 | 60: "15rem", 59 | 64: "16rem", 60 | 72: "18rem", 61 | 80: "20rem", 62 | 96: "24rem", 63 | }, 64 | }; 65 | 66 | export default theme; 67 | -------------------------------------------------------------------------------- /app/angular/components/createModelModal.html: -------------------------------------------------------------------------------- 1 | 4 | 5 | 32 | 33 | -------------------------------------------------------------------------------- /app/angular/components/sqlComparasionDropdown/index.js: -------------------------------------------------------------------------------- 1 | import angular from "angular"; 2 | import template from "./template.html"; 3 | 4 | const Controller = function () { 5 | const $ctrl = this; 6 | 7 | $ctrl.select = selected => { 8 | $ctrl.selected = selected; 9 | $ctrl.onSelect({ selected }); 10 | } 11 | 12 | $ctrl.checkConstraintOptions = { 13 | NUMBER: [ 14 | { name: 'Equal to', type: 'EQUAL_TO' }, 15 | { name: 'Not equal to', type: 'NOT_EQUAL_TO' }, 16 | { name: 'Greater than', type: 'GREATER_THAN' }, 17 | { name: 'Less than', type: 'LESS_THAN' }, 18 | { name: 'Between', type: 'BETWEEN' }, 19 | { name: 'Greater than or equal to', type: 'GREATER_THAN_OR_EQUAL_TO' }, 20 | { name: 'Less than or equal to', type: 'LESS_THAN_OR_EQUAL_TO' }, 21 | ], 22 | STRING: [ 23 | { name: 'Is', type: 'IS' }, 24 | { name: 'Is not', type: 'IS_NOT' }, 25 | { name: 'Contains', type: 'CONTAINS' }, 26 | { name: 'Does not contain', type: 'DOES_NOT_CONTAIN' }, 27 | { name: 'Starts with', type: 'STARTS_WITH' }, 28 | { name: 'Ends with', type: 'ENDS_WITH' }, 29 | ] 30 | }; 31 | }; 32 | 33 | export default angular.module("app.sqlComparasionDropdown", []).component("sqlComparasionDropdown", { 34 | template, 35 | controller: Controller, 36 | bindings: { 37 | type: "<", 38 | selected: "<", 39 | onSelect: "&" 40 | }, 41 | }).name; 42 | -------------------------------------------------------------------------------- /app/sass/tables.scss: -------------------------------------------------------------------------------- 1 | /////////////////////////////////////////////////////////////////////////////// 2 | // , adjustments 3 | /////////////////////////////////////////////////////////////////////////////// 4 | .table > thead > tr > th, 5 | .table > tbody > tr > td { 6 | vertical-align: middle; 7 | border-color: var(--border-default); 8 | } 9 | 10 | .table thead th strong { 11 | font-weight: 600; 12 | } 13 | 14 | /////////////////////////////////////////////////////////////////////////////// 15 | // .table-hover 16 | /////////////////////////////////////////////////////////////////////////////// 17 | .table-hover tbody tr { 18 | cursor: pointer; 19 | } 20 | 21 | /////////////////////////////////////////////////////////////////////////////// 22 | // .action-cell 23 | /////////////////////////////////////////////////////////////////////////////// 24 | .actions-cell { 25 | text-align: right; 26 | } 27 | 28 | .actions-cell .fa { 29 | pointer-events: none; 30 | opacity: 0; 31 | font-size: 1.3em; 32 | padding: 4px 6px; 33 | border: 1px solid transparent; 34 | border-radius: 2px; 35 | margin-left: 3px; 36 | } 37 | 38 | tr:hover .actions-cell .fa { 39 | pointer-events: all; 40 | opacity: 1; 41 | border-radius: 4px; 42 | } 43 | 44 | .actions-cell .fa:hover { 45 | border-color: var(--border-default); 46 | background-color: var(--gray-90); 47 | } 48 | 49 | -------------------------------------------------------------------------------- /app/angular/logic/columnForm.js: -------------------------------------------------------------------------------- 1 | import angular from "angular"; 2 | import template from "./columnForm.html"; 3 | 4 | const app = angular.module("app.columnForm", []); 5 | 6 | const Controller = function () { 7 | const $ctrl = this; 8 | 9 | $ctrl.columnTypes = [ 10 | { name: 'DATE', type: 'DATE' }, 11 | { name: 'DATETIME', type: 'DATETIME' }, 12 | { name: 'TIME', type: 'TIME' }, 13 | { name: 'INT', type: 'INT' }, 14 | { name: 'BOOLEAN', type: 'BOOLEAN' }, 15 | { name: 'FLOAT', type: 'FLOAT' }, 16 | { name: 'VARCHAR', type: 'VARCHAR' }, 17 | { name: 'CHAR', type: 'CHAR' }, 18 | { name: 'JSON', type: 'JSON' }, 19 | { name: 'ENUM', type: 'ENUM' }, 20 | { name: 'SET', type: 'SET'} 21 | ]; 22 | 23 | $ctrl.cancel = () => { 24 | $ctrl.dismiss(); 25 | }; 26 | 27 | $ctrl.selectType = function (selected) { 28 | if (!$ctrl.column.PK && !$ctrl.column.FK) { 29 | $ctrl.column.type = selected.type; 30 | } else { 31 | $ctrl.column.type = "INT"; 32 | } 33 | } 34 | 35 | $ctrl.selectTableOrigin = function (selected) { 36 | $ctrl.column.tableOrigin.idName = selected.name; 37 | } 38 | }; 39 | 40 | export default app.component("columnForm", { 41 | template: template, 42 | bindings: { 43 | save: "<", 44 | dismiss: "&", 45 | column: "<", 46 | index: "<", 47 | tableNames: "<", 48 | delete: "<" 49 | }, 50 | controller: Controller, 51 | }).name; 52 | -------------------------------------------------------------------------------- /app/angular/components/createModelModal.js: -------------------------------------------------------------------------------- 1 | import angular from "angular"; 2 | import template from "./createModelModal.html"; 3 | 4 | const app = angular.module("app.createModelModal", []); 5 | 6 | const Controller = function ($rootScope, $filter) { 7 | const $ctrl = this; 8 | $ctrl.typeSelected = {}; 9 | $ctrl.submitted = false; 10 | 11 | $ctrl.options = [ 12 | { name: $filter('translate')('Conceptual'), type: 'conceptual'}, 13 | { name: $filter('translate')('Logical'), type: 'logic' } 14 | ]; 15 | 16 | $ctrl.$onInit = () => { 17 | $ctrl.typeSelected = { name: $filter('translate')("Conceptual"), type: "conceptual" }; 18 | }; 19 | 20 | $ctrl.doSelectType = function (newSelectedType) { 21 | $ctrl.typeSelected = newSelectedType; 22 | }; 23 | 24 | $ctrl.save = function (modelname) { 25 | $ctrl.submitted = true; 26 | if (modelname != null && modelname != "") { 27 | const newmodel = { 28 | name: modelname, 29 | user: $rootScope.loggeduser, 30 | type: $ctrl.typeSelected.type, 31 | model: '{"cells":[]}', 32 | }; 33 | $ctrl.close({ result: newmodel }); 34 | } 35 | }; 36 | 37 | $ctrl.cancel = function () { 38 | $ctrl.dismiss({ 39 | reason: "cancel", 40 | }); 41 | }; 42 | }; 43 | export default app.component("createModelModal", { 44 | template: template, 45 | bindings: { 46 | close: "&", 47 | dismiss: "&", 48 | }, 49 | controller: Controller, 50 | }).name; 51 | -------------------------------------------------------------------------------- /app/react/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "extends": [ 4 | "airbnb-base", 5 | "eslint:recommended", 6 | "plugin:prettier/recommended", 7 | "plugin:react/recommended", 8 | "plugin:@typescript-eslint/eslint-recommended", 9 | "plugin:@typescript-eslint/recommended", 10 | "prettier/@typescript-eslint" 11 | ], 12 | "parser": "@typescript-eslint/parser", 13 | "env": { 14 | "browser": true, 15 | "es6": true, 16 | "jest": true 17 | }, 18 | "parserOptions": { 19 | "ecmaVersion": 2020, 20 | "sourceType": "module", 21 | "ecmaFeatures": { 22 | "jsx": true, 23 | "arrowFunctions": true 24 | } 25 | }, 26 | "plugins": ["react", "@typescript-eslint", "prettier"], 27 | "settings": { 28 | "react": { 29 | "version": "detect" 30 | }, 31 | "import/resolver": { 32 | "typescript": { 33 | "alwaysTryTypes": true, 34 | "project": "app/react/tsconfig.json" 35 | } 36 | } 37 | }, 38 | "rules": { 39 | "no-shadow": "off", 40 | "react/jsx-uses-react": "off", 41 | "react/react-in-jsx-scope": "off", 42 | "@typescript-eslint/no-shadow": ["error"], 43 | "react/jsx-filename-extension": [ 44 | 1, 45 | { 46 | "extensions": [".js", ".jsx", ".ts", ".tsx"] 47 | } 48 | ], 49 | "import/extensions": [ 50 | "error", 51 | "ignorePackages", 52 | { 53 | "js": "never", 54 | "jsx": "never", 55 | "ts": "never", 56 | "tsx": "never" 57 | } 58 | ] 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /server_app/app.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const errorhandler = require("errorhandler"); 3 | const session = require("express-session"); 4 | const bodyParser = require("body-parser"); 5 | const path = require("path"); 6 | const cors = require("cors"); 7 | const enforce = require('express-sslify'); 8 | 9 | require("dotenv").config(); 10 | 11 | let app = express(); 12 | app.use(cors()); 13 | 14 | if (app.get("env") === "production") { 15 | app.use(enforce.HTTPS({ trustProtoHeader: true })); 16 | } 17 | 18 | // Where to find the view files 19 | const viewsPath = path.join(__dirname, "../views"); 20 | app.set("views", viewsPath); 21 | 22 | app.use(bodyParser.json({limit: '50mb'})); // support json encoded bodies 23 | app.use(bodyParser.urlencoded({ extended: true, limit: '50mb'})); // support encoded bodies 24 | 25 | const appPath = path.join(__dirname, "../app"); 26 | app.use(express.static(`${appPath}/dist`)); 27 | 28 | app.use( 29 | session({ 30 | resave: true, 31 | saveUninitialized: true, 32 | secret: "SOMERANDOMSECRETHERE", 33 | cookie: { maxAge: 60000 }, 34 | }) 35 | ); 36 | app.use(errorhandler()); 37 | 38 | const userHandler = require("./user/handler"); 39 | const modelHandler = require("./model/handler"); 40 | 41 | 42 | app.use("/users", userHandler); 43 | app.use("/models", modelHandler); 44 | 45 | app.get("/", (_, res) => { 46 | res.render("index.html"); 47 | }); 48 | 49 | module.exports = app; -------------------------------------------------------------------------------- /app/angular/logic/checkConstraint.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 5 | 6 | 10 | 11 |
12 | 13 |
14 | 19 | 20 | 26 |
27 |
28 | 29 |
30 | 31 | 32 | 39 |
40 | 41 | {{ $ctrl.isManual ? 'Show options to create constraint' : 'Create check constraint manually' | translate }} 42 |
-------------------------------------------------------------------------------- /app/angular/conceptual/validator.test.js: -------------------------------------------------------------------------------- 1 | import Validator from "./validator"; 2 | import Factory from "./factory"; 3 | 4 | import * as joint from "jointjs"; 5 | 6 | import shapes from "../../joint/shapes"; 7 | joint.shapes.erd = shapes; 8 | 9 | describe("Test shape erd validator", () => { 10 | 11 | const validator = new Validator(); 12 | const factory = new Factory(joint.shapes); 13 | 14 | test("It should validate an Entity", async () => { 15 | const entity = factory.createEntity(); 16 | expect(validator.isEntity(entity)).toBeTruthy() 17 | }); 18 | 19 | test("It should validate a Relationship", async () => { 20 | const entity = factory.createRelationship(); 21 | expect(validator.isRelationship(entity)).toBeTruthy() 22 | }); 23 | 24 | test("It should validate an Attribute", async () => { 25 | const entity = factory.createAttribute(); 26 | expect(validator.isAttribute(entity)).toBeTruthy() 27 | }); 28 | 29 | test("It should validate a Extension", async () => { 30 | const entity = factory.createIsa(); 31 | expect(validator.isExtension(entity)).toBeTruthy() 32 | }); 33 | 34 | test("It should validate a Associative Entity", async () => { 35 | const entity = factory.createAssociative(); 36 | expect(validator.isAssociative(entity)).toBeTruthy() 37 | }); 38 | 39 | test("It should validate a Link", async () => { 40 | const entity = factory.createLink(); 41 | expect(validator.isLink(entity)).toBeTruthy() 42 | }); 43 | 44 | }); -------------------------------------------------------------------------------- /cypress/e2e/model.cy.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | describe("Model", () => { 4 | beforeEach(() => { 5 | cy.intercept("GET", "/models?userId=*").as("getUserModels"); 6 | cy.loginViaGui(); 7 | cy.wait("@getUserModels").then((userModels) => { 8 | cy.cleanUpUserModels(userModels); 9 | cy.reload(); 10 | cy.contains("a", "New model").click(); 11 | cy.get("create-model-modal").should("be.visible"); 12 | }); 13 | }); 14 | 15 | it('closes the "New Model" modal', () => { 16 | cy.contains("button", "Cancel").click(); 17 | cy.get("create-model-modal").should("not.exist"); 18 | }); 19 | 20 | it("alerts when clicking SAVE without filling the title", () => { 21 | cy.contains("button", "Save").click(); 22 | cy.get("#name").should("have.class", "error"); 23 | }); 24 | 25 | context("Model creation", () => { 26 | const modelTitle = "User"; 27 | 28 | beforeEach(() => cy.get("#name").type(modelTitle)); 29 | 30 | it("creates a conceptual model", () => { 31 | cy.contains("button", "Save").click(); 32 | 33 | cy.contains("h2", `Conceptual model of: ${modelTitle}`).should( 34 | "be.visible" 35 | ); 36 | }); 37 | 38 | it("creates a logical model", () => { 39 | // Selects the logical model and save 40 | cy.get(".modelselect").click(); 41 | cy.contains("li span", "Logical").click(); 42 | cy.contains("button", "Save").click(); 43 | // Asserts the model was created 44 | cy.contains("h2", `Logical model of: ${modelTitle}`).should("be.visible"); 45 | }); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /app/angular/recovery/recovery.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | Loading 9 |
10 | 13 | 16 |
17 |
18 | 19 | 30 |
31 | 32 |
33 |
39 |
40 |
-------------------------------------------------------------------------------- /app/angular/login/login.js: -------------------------------------------------------------------------------- 1 | import angular from "angular"; 2 | import ReactButton from "../../react/components/Button"; 3 | import authService from "../service/authService"; 4 | import template from "./login.html"; 5 | 6 | const LoginController = function (AuthService, $state, $translate, $filter) { 7 | const ctrl = this; 8 | ctrl.submitted = false; 9 | ctrl.credentials = {}; 10 | ctrl.feedback = { 11 | message: "", 12 | showing: false, 13 | }; 14 | 15 | const showError = (newMessage) => { 16 | ctrl.feedback.message = $filter('translate')(newMessage); 17 | ctrl.feedback.showing = true; 18 | }; 19 | 20 | const handleLoginError = () => { 21 | ctrl.loading = false; 22 | showError("Incorrect login or password"); 23 | }; 24 | 25 | const handleLoginSuccess = () => { 26 | ctrl.loading = false; 27 | $state.go("main"); 28 | }; 29 | 30 | const doLogin = () => { 31 | ctrl.loading = true; 32 | AuthService.login(ctrl.credentials) 33 | .then(handleLoginSuccess) 34 | .catch(handleLoginError); 35 | }; 36 | 37 | ctrl.changeLanguage = (langKey) => { 38 | localStorage.setItem('i18n', langKey); 39 | $translate.use(langKey); 40 | }; 41 | 42 | ctrl.submitForm = (validForm) => { 43 | ctrl.submitted = true; 44 | ctrl.feedback.showing = false; 45 | if (validForm) { 46 | doLogin(); 47 | } else { 48 | showError("Fill the fields in red"); 49 | } 50 | }; 51 | }; 52 | 53 | export default angular 54 | .module("app.login", [authService, ReactButton]) 55 | .component("login", { 56 | template, 57 | controller: LoginController, 58 | }).name; -------------------------------------------------------------------------------- /app/angular/service/preventExitService.js: -------------------------------------------------------------------------------- 1 | import angular from "angular"; 2 | import confirmationModal from "../components/confirmationModal"; 3 | 4 | const preventExitService = ($uibModal, $filter) => { 5 | const __handleBeforeUnload = (scope) => (event) => { 6 | if (scope.modelState.isDirty) return ""; 7 | event.stopPropagation(); 8 | }; 9 | 10 | const __handleTransitionStart = (scope, name) => async (trans) => { 11 | if (trans.from().name === name && scope.modelState.isDirty) { 12 | const modalInstance = $uibModal.open({ 13 | animation: true, 14 | component: "confirmationModal", 15 | resolve: { 16 | modalData: () => ({ 17 | title: $filter('translate')("Unsaved changes"), 18 | content: $filter('translate')("You have unsaved changes. Are you sure you want to exit without saving?"), 19 | cancelLabel: $filter('translate')("Cancel"), 20 | confirmLabel: $filter('translate')("Exit without saving"), 21 | }), 22 | }, 23 | }); 24 | 25 | const result = await modalInstance.result; 26 | return result; 27 | } 28 | return true; 29 | }; 30 | 31 | const __cleanup = (scope) => () => { 32 | window.onbeforeunload = null; 33 | // eslint-disable-next-line no-param-reassign 34 | scope.modelState = {}; 35 | }; 36 | 37 | return { 38 | handleBeforeUnload: __handleBeforeUnload, 39 | handleTransitionStart: __handleTransitionStart, 40 | cleanup: __cleanup, 41 | }; 42 | }; 43 | 44 | export default angular 45 | .module("app.preventExit", [confirmationModal]) 46 | .factory("preventExitService", preventExitService).name; 47 | -------------------------------------------------------------------------------- /app/angular/components/importModelModal.html: -------------------------------------------------------------------------------- 1 | 10 | 11 | 38 | 39 | -------------------------------------------------------------------------------- /app/angular/recovery/recovery.js: -------------------------------------------------------------------------------- 1 | import angular from "angular"; 2 | import template from "./recovery.html"; 3 | import AuthService from "../service/authService" 4 | 5 | const controller = function (AuthService, $filter) { 6 | const ctrl = this; 7 | ctrl.submitted = false; 8 | ctrl.mail = ""; 9 | ctrl.feedback = { 10 | message: "", 11 | showing: false, 12 | type: "error" 13 | }; 14 | 15 | const showFeedback = (newMessage, type = "error") => { 16 | ctrl.feedback.message = $filter('translate')(newMessage); 17 | ctrl.feedback.showing = true; 18 | ctrl.feedback.type = type; 19 | }; 20 | 21 | const handleError = (error) => { 22 | ctrl.loading = false; 23 | if(error.status == 400) { 24 | showFeedback("This email is not registered yet."); 25 | } else { 26 | showFeedback("Ops, we had an internal error, please try again later..."); 27 | } 28 | }; 29 | 30 | const handleSuccess = () => { 31 | ctrl.loading = false; 32 | showFeedback($filter('translate')('An email with instructions has been sent to EMAIL', { email: ctrl.mail }), "success"); 33 | }; 34 | 35 | const doRecover = () => { 36 | ctrl.loading = true; 37 | AuthService.recovery(ctrl.mail) 38 | .then(handleSuccess) 39 | .catch(handleError); 40 | }; 41 | 42 | ctrl.submitForm = (validForm) => { 43 | ctrl.submitted = true; 44 | ctrl.feedback.showing = false; 45 | if (validForm) { 46 | doRecover(); 47 | } else { 48 | showFeedback("Fill the fields in red"); 49 | } 50 | }; 51 | }; 52 | 53 | 54 | export default angular 55 | .module("app.recovery", [AuthService]) 56 | .component("recovery", { 57 | template, 58 | controller, 59 | }).name; -------------------------------------------------------------------------------- /server_app/model/validator.js: -------------------------------------------------------------------------------- 1 | const MESSAGES = { 2 | MISSING_NAME: "Missig model name param", 3 | WRONG_NAME_TYPE: "Name should be a String", 4 | MISSING_TYPE: "Missig type param", 5 | WRONG_TYPE_TYPE: "Type should be a String", 6 | MISSING_MODEL: "Missig model param", 7 | WRONG_MODEL_TYPE: "Model should be an Object", 8 | MISSING_USER: "Missig userId param", 9 | WRONG_USER_TYPE: "UserId should be an Object", 10 | }; 11 | 12 | class ValidationResponse { 13 | constructor(isValid, message) { 14 | this.valid = isValid; 15 | this.message = message; 16 | } 17 | } 18 | 19 | const getMessages = () => { 20 | return MESSAGES; 21 | }; 22 | 23 | const isString = (value) => typeof value === "string" || value instanceof String; 24 | const isNull = (value) => value == null || value == ""; 25 | 26 | const validateSaveParams = ({ name, type, model, userId }) => { 27 | if (isNull(name)) { 28 | return new ValidationResponse(false, MESSAGES.MISSING_NAME); 29 | } 30 | 31 | if (!isString(name)) { 32 | return new ValidationResponse(false, MESSAGES.WRONG_NAME_TYPE); 33 | } 34 | 35 | if (isNull(type)) { 36 | return new ValidationResponse(false, MESSAGES.MISSING_TYPE); 37 | } 38 | 39 | if (!isString(type)) { 40 | return new ValidationResponse(false, MESSAGES.WRONG_TYPE_TYPE); 41 | } 42 | 43 | if (isNull(userId)) { 44 | return new ValidationResponse(false, MESSAGES.MISSING_USER); 45 | } 46 | 47 | if (!isString(userId)) { 48 | return new ValidationResponse(false, MESSAGES.WRONG_USER_TYPE); 49 | } 50 | 51 | return new ValidationResponse(true, MESSAGES.VALID_PARAM); 52 | }; 53 | 54 | module.exports = { 55 | validateSaveParams, 56 | getMessages, 57 | }; 58 | -------------------------------------------------------------------------------- /cypress/support/commands.js: -------------------------------------------------------------------------------- 1 | 2 | import { faker } from '@faker-js/faker/locale/en'; 3 | 4 | Cypress.Commands.add( 5 | "loginViaGui", 6 | (user = Cypress.env("user"), password = Cypress.env("password")) => { 7 | cy.visit("/"); 8 | cy.get("#userEmail").type(user); 9 | cy.get("#userPassword").type(password, { log: false }); 10 | cy.contains("button", "Login").click(); 11 | } 12 | ); 13 | 14 | Cypress.Commands.add("cleanUpUserModels", (userModels) => { 15 | cy.getUserModelsViaApi(userModels.response.url).then( 16 | (userModelsResponseUrl) => { 17 | userModelsResponseUrl.body.forEach((model) => { 18 | cy.deleteModelViaApi(model._id); 19 | }); 20 | } 21 | ); 22 | }); 23 | 24 | Cypress.Commands.add("getUserModelsViaApi", (url) => { 25 | cy.getCookie('userToken').then(({ value }) => { 26 | cy.request({ 27 | method: "GET", 28 | url, 29 | headers: { "brx-access-token": value }, 30 | }); 31 | }); 32 | }); 33 | 34 | Cypress.Commands.add("deleteModelViaApi", (modelId) => { 35 | cy.getCookie('userToken').then(({ value }) => { 36 | cy.request({ 37 | method: "DELETE", 38 | url: `${Cypress.env("apiUrl")}/models/:modelId?modelId=${modelId}`, 39 | headers: { "brx-access-token": value }, 40 | }); 41 | }); 42 | }); 43 | 44 | Cypress.Commands.add( 45 | "createModelViaApi", 46 | (type, userId, model = { cell: [] }) => { 47 | cy.getCookie('userToken').then(({ value }) => { 48 | cy.request({ 49 | method: "POST", 50 | url: `${Cypress.env("apiUrl")}/models`, 51 | headers: { "brx-access-token": value }, 52 | body: { 53 | name: faker.animal.type(), 54 | user: userId, 55 | type, 56 | model, 57 | }, 58 | }); 59 | }); 60 | } 61 | ); 62 | -------------------------------------------------------------------------------- /app/angular/components/importModelModal.js: -------------------------------------------------------------------------------- 1 | import angular from "angular"; 2 | import template from "./importModelModal.html"; 3 | 4 | const app = angular.module("app.importModelModal", []); 5 | 6 | const Controller = function (ModelAPI, AuthService, $timeout) { 7 | const $ctrl = this; 8 | const urlRegex = /^(https?:\/\/(?:[\w.-]+|\[::1\]|localhost)(?::\d{1,5})?\/#!\/publicview\/[\w.-]+)$/;; 9 | 10 | const validateUrl = (url) => { 11 | if(url == null || url == "") { 12 | return false 13 | } 14 | return urlRegex.test(url); 15 | } 16 | 17 | $ctrl.$onInit = () => { 18 | $ctrl.url = ""; 19 | $ctrl.submitted = false; 20 | $ctrl.valid = false; 21 | $ctrl.loading = false; 22 | $ctrl.feedback = { 23 | showing: false 24 | } 25 | }; 26 | 27 | $ctrl.cancel = () => { 28 | $ctrl.dismiss({ 29 | reason: "cancel", 30 | }); 31 | }; 32 | 33 | $ctrl.showErrorFeedback = () => { 34 | $timeout(() => { 35 | $ctrl.feedback.showing = true; 36 | }) 37 | }; 38 | 39 | $ctrl.save = (url) => { 40 | const parts = url.split("/"); 41 | const shareId = parts[parts.length-1]; 42 | $ctrl.submitted = true; 43 | $ctrl.valid = validateUrl(url); 44 | if($ctrl.valid) { 45 | $ctrl.loading = true; 46 | ModelAPI.importModel(shareId, AuthService.loggeduser).then(response => { 47 | $ctrl.loading = false; 48 | $ctrl.close({result: response.data}); 49 | }).catch(error => { 50 | $ctrl.loading = false; 51 | console.log(error); 52 | $ctrl.showErrorFeedback(); 53 | }); 54 | } 55 | }; 56 | }; 57 | 58 | export default app.component("importModelModal", { 59 | template: template, 60 | bindings: { 61 | close: "&", 62 | dismiss: "&" 63 | }, 64 | controller: Controller, 65 | }).name; 66 | -------------------------------------------------------------------------------- /app/sass/note-color-list.scss: -------------------------------------------------------------------------------- 1 | .note-color-list { 2 | display: flex; 3 | gap: 1rem; 4 | list-style: none; 5 | padding: 0; 6 | } 7 | 8 | .note-color-list > li { 9 | position: relative; 10 | width: 32px; 11 | height: 32px; 12 | overflow: hidden; 13 | border: 1px solid transparent; 14 | border-radius: 8px; 15 | } 16 | 17 | li:has(#color-gray) {background-color: hsl(216deg, 25%, 74.9%, 1);} // #afbccf 18 | li:has(#color-salmon) {background-color: hsl(7.13deg, 94.4%, 79%, 1);} // #fca397 19 | li:has(#color-orange) {background-color: hsl(35.2deg, 100%, 72%, 1);} // #ffc470 20 | li:has(#color-yellow) {background-color: hsl(60deg, 100%, 76.7%, 1);} // #ffff88 21 | li:has(#color-green) {background-color: hsl(140deg, 49.7%, 64.9%, 1);} // #79d297 22 | li:has(#color-blue) {background-color: hsl(205deg, 89.9%, 72.9%, 1);} // #7cc4f8 23 | li:has(#color-purple) {background-color: hsl(268deg, 100%, 82.9%, 1);} // #d1a8ff 24 | li:has(#color-pink) {background-color: hsl(318deg, 96%, 80.2%, 1);} // #fd9ce0 25 | 26 | .note-color-list > li label { 27 | position: absolute; 28 | top: 32px 29 | } 30 | 31 | .note-color-list > li [type="radio"] { 32 | display: block; 33 | position: absolute; 34 | // inset: 0; 35 | // Use inset on type="radio" causes issues on Firefox 36 | // Due to that I'm forcing width and height manualy 37 | width: 100%; 38 | height: 100%; 39 | margin: 0; 40 | padding: 0; 41 | opacity: 0; 42 | cursor: pointer; 43 | } 44 | 45 | .note-color-list > li:focus-within{ 46 | outline: dashed var(--brand-primary-10); 47 | } 48 | 49 | .note-color-list > li:has(:checked):after { 50 | display: block; 51 | z-index: 1; 52 | content: "✔︎"; 53 | font-size: 1.5em; 54 | text-align: center; 55 | color: var(--gray-30); 56 | } 57 | 58 | -------------------------------------------------------------------------------- /app/sass/modelWorkspace.scss: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////////////// 2 | // .modelWorkspace .navbar 3 | //////////////////////////////////////////////////////////////////////////////// 4 | .modelWorkspace .navbar { 5 | width: 150px; 6 | border-right: 1px solid var(--border-default); 7 | border-bottom: 1px solid var(--border-default); 8 | background-color: var(--gray-95) 9 | } 10 | 11 | .modelWorkspace .navbar .back-button { 12 | font-size: 1.2em; 13 | color: var(--brand-default); 14 | padding-top: 16px; 15 | padding-bottom: 17px; 16 | border-right: 1px solid var(--border-default); 17 | } 18 | 19 | .modelWorkspace .navbar .back-button:hover { 20 | color: var(--brand-default); 21 | background-color: #e1e1e1; 22 | } 23 | 24 | .modelWorkspace .navbar-header { 25 | float: none; 26 | } 27 | 28 | .modelWorkspace .navbar-header .navbar-brand { 29 | float: none; 30 | display: block; 31 | padding: 15px 0; 32 | text-align: center; 33 | } 34 | 35 | //////////////////////////////////////////////////////////////////////////////// 36 | // .modelWorkspace .mainContent 37 | //////////////////////////////////////////////////////////////////////////////// 38 | .modelWorkspace .mainContent { 39 | padding-top: 0; 40 | } 41 | 42 | .modelWorkspace .model-area { 43 | top: 0; 44 | } 45 | 46 | .modelWorkspace .workspace-header .page-header { 47 | margin-top: 10px; 48 | border-bottom: 1px solid var(--border-default); 49 | } 50 | 51 | //////////////////////////////////////////////////////////////////////////////// 52 | // .share-status 53 | //////////////////////////////////////////////////////////////////////////////// 54 | .share-status { 55 | color: var(--gray-70); 56 | font-size: 1.2rem; 57 | margin-right: 4px; 58 | } -------------------------------------------------------------------------------- /app/angular/components/shareModelModal.js: -------------------------------------------------------------------------------- 1 | import angular from "angular"; 2 | import template from "./shareModelModal.html"; 3 | 4 | const app = angular.module("app.shareModelModal", []); 5 | 6 | const Controller = function (ModelAPI) { 7 | const $ctrl = this; 8 | $ctrl.submitted = false; 9 | 10 | $ctrl.$onInit = () => { 11 | ModelAPI.loadShareOptions($ctrl.modelId).then(response => { 12 | const shareConfig = response.data; 13 | $ctrl.url = shareConfig.active ? shareConfig.url : ""; 14 | $ctrl.shared = shareConfig.active; 15 | $ctrl.importAllowed = shareConfig.importAllowed; 16 | $ctrl.backupConfig = shareConfig; 17 | }); 18 | }; 19 | 20 | const hasChanges = (newActive, newImportAllowed) => { 21 | return $ctrl.backupConfig.active != newActive || $ctrl.backupConfig.importAllowed != newImportAllowed; 22 | } 23 | 24 | $ctrl.toggleShare = (shared) => { 25 | $ctrl.url = shared? $ctrl.backupConfig.url : ""; 26 | if (!shared) { 27 | $ctrl.importAllowed = false; 28 | } 29 | } 30 | 31 | $ctrl.cancel = () => { 32 | $ctrl.dismiss({reason: "cancel"}); 33 | }; 34 | 35 | $ctrl.save = () => { 36 | if(hasChanges($ctrl.shared, $ctrl.importAllowed)) { 37 | ModelAPI.toggleShare($ctrl.modelId, $ctrl.shared, $ctrl.importAllowed).then(() => { 38 | $ctrl.close({result: {'shared': $ctrl.shared}}); 39 | }).catch(error => { 40 | console.log(error); 41 | $ctrl.dismiss({reason: "model share error"}); 42 | }); 43 | } else { 44 | $ctrl.close({reason: "model shared"}); 45 | } 46 | }; 47 | 48 | $ctrl.copy = () => { 49 | navigator.clipboard.writeText($ctrl.url); 50 | }; 51 | }; 52 | 53 | export default app.component("shareModelModal", { 54 | template: template, 55 | bindings: { 56 | close: "&", 57 | dismiss: "&", 58 | modelId: "<", 59 | }, 60 | controller: Controller, 61 | }).name; -------------------------------------------------------------------------------- /app/angular/signup/signup.js: -------------------------------------------------------------------------------- 1 | import angular from "angular"; 2 | import authService from "../service/authService"; 3 | import template from "./signup.html"; 4 | 5 | const SignupController = function ($state, AuthService, $filter) { 6 | const ctrl = this; 7 | ctrl.submitted = false; 8 | ctrl.loading = false; 9 | ctrl.credentials = { 10 | username: "", 11 | email: "", 12 | password: "", 13 | }; 14 | ctrl.feedback = { 15 | message: "", 16 | showing: false, 17 | }; 18 | 19 | const showLoading = (loading) => { 20 | ctrl.loading = loading; 21 | }; 22 | 23 | const handleLoginError = () => { 24 | showLoading(false); 25 | $state.go("login"); 26 | }; 27 | 28 | const handleLoginSuccess = () => { 29 | showLoading(false); 30 | $state.go("main"); 31 | }; 32 | 33 | const handleSignUpSuccess = () => { 34 | AuthService.login({ 35 | username: ctrl.credentials.email, 36 | password: ctrl.credentials.password, 37 | }) 38 | .then(handleLoginSuccess) 39 | .catch(handleLoginError); 40 | }; 41 | 42 | const handleSignUpError = (error) => { 43 | showLoading(false); 44 | showError(error.data); 45 | }; 46 | 47 | const doRegister = () => { 48 | showLoading(true); 49 | AuthService.register(ctrl.credentials) 50 | .then(handleSignUpSuccess) 51 | .catch(handleSignUpError); 52 | }; 53 | 54 | const showError = (newMessage) => { 55 | ctrl.feedback.message = $filter('translate')(newMessage); 56 | ctrl.feedback.showing = true; 57 | }; 58 | 59 | ctrl.submitForm = (validForm) => { 60 | ctrl.submitted = true; 61 | ctrl.feedback.showing = false; 62 | if (validForm) { 63 | doRegister(); 64 | } else { 65 | showError("Fill the fields in red"); 66 | } 67 | }; 68 | }; 69 | 70 | export default angular.module("app.signup", [authService]).component("signup", { 71 | template, 72 | controller: SignupController, 73 | }).name; -------------------------------------------------------------------------------- /server_app/user/validator.test.js: -------------------------------------------------------------------------------- 1 | const userValidator = require("./validator"); 2 | 3 | describe('validateLoginParams test', () => { 4 | test('should fail when username is missing', () => { 5 | const validation = userValidator.validateLoginParams({username: "", password: "123456"}); 6 | expect(validation.valid).toBeFalsy(); 7 | }); 8 | 9 | test('should fail when password is missing', () => { 10 | const validation = userValidator.validateLoginParams({username: "some_username", password: ""}); 11 | expect(validation.valid).toBeFalsy(); 12 | }); 13 | 14 | test('should pass when when we have params all ok', () => { 15 | const validation = userValidator.validateLoginParams({username: "some_username", password: "some_password"}); 16 | expect(validation.valid).toBeTruthy(); 17 | }); 18 | }); 19 | 20 | describe('validateSignUpParams test', () => { 21 | test('should fail when username is missing', () => { 22 | const validation = userValidator.validateSignUpParams({username: "", mail: "", password: "123456"}); 23 | expect(validation.valid).toBeFalsy(); 24 | }); 25 | 26 | test('should fail when mail is missing', () => { 27 | const validation = userValidator.validateSignUpParams({username: "some_username", mail: "", password: ""}); 28 | expect(validation.valid).toBeFalsy(); 29 | }); 30 | 31 | test('should fail when password is missing', () => { 32 | const validation = userValidator.validateSignUpParams({username: "some_username", mail: "some_mail", password: ""}); 33 | expect(validation.valid).toBeFalsy(); 34 | }); 35 | 36 | test('should pass when when we have params all ok', () => { 37 | const validation = userValidator.validateSignUpParams({username: "some_username", mail: "some_mail", password: "some_password"}); 38 | expect(validation.valid).toBeTruthy(); 39 | }); 40 | }); -------------------------------------------------------------------------------- /cypress/fixtures/conceptualModel.json: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "type": "erd.Entity", 5 | "supertype": "Entity", 6 | "isExtended": false, 7 | "autorelationship": false, 8 | "size": { "width": 80, "height": 40 }, 9 | "position": { "x": 170, "y": 50 }, 10 | "angle": 0, 11 | "id": "bdb20ca0-5c43-40b2-94dd-d0aecfa5b856", 12 | "z": 1, 13 | "attrs": {} 14 | }, 15 | { 16 | "type": "erd.Relationship", 17 | "supertype": "Relationship", 18 | "autorelationship": false, 19 | "size": { "width": 85, "height": 45 }, 20 | "position": { "x": 330, "y": 50 }, 21 | "angle": 0, 22 | "id": "36ec2d5d-4084-4c42-add9-aa3afddedae3", 23 | "z": 2, 24 | "attrs": {} 25 | }, 26 | { 27 | "type": "erd.Link", 28 | "source": { "id": "36ec2d5d-4084-4c42-add9-aa3afddedae3" }, 29 | "target": { "id": "bdb20ca0-5c43-40b2-94dd-d0aecfa5b856" }, 30 | "id": "7ad0455f-5a16-46f4-bc69-dc1090a55978", 31 | "z": 3, 32 | "labels": [{ "position": 0.7, "attrs": { "text": { "text": "(0, n)" } } }], 33 | "attrs": {} 34 | }, 35 | { 36 | "type": "erd.Entity", 37 | "supertype": "Entity", 38 | "isExtended": false, 39 | "autorelationship": false, 40 | "size": { "width": 80, "height": 40 }, 41 | "position": { "x": 510, "y": 60 }, 42 | "angle": 0, 43 | "id": "1846af3e-6e30-4471-ba8b-6829cab94050", 44 | "z": 4, 45 | "attrs": {} 46 | }, 47 | { 48 | "type": "erd.Link", 49 | "source": { "id": "36ec2d5d-4084-4c42-add9-aa3afddedae3" }, 50 | "target": { "id": "1846af3e-6e30-4471-ba8b-6829cab94050" }, 51 | "id": "69c1eca8-34f7-4ec0-8855-93f12f1ebc4f", 52 | "z": 5, 53 | "labels": [{ "position": 0.7, "attrs": { "text": { "text": "(0, n)" } } }], 54 | "attrs": {} 55 | } 56 | ] 57 | } 58 | -------------------------------------------------------------------------------- /app/angular/conceptual/entityExtensor.js: -------------------------------------------------------------------------------- 1 | 2 | export default class EntityExtensor { 3 | 4 | constructor(shapeFactory, shapeValidator, linker) { 5 | this.factory = shapeFactory; 6 | this.validator = shapeValidator; 7 | this.linker = linker; 8 | } 9 | 10 | createExtension = (entity, extentionType) => { 11 | const x = entity.model.attributes.position.x; 12 | const y = entity.model.attributes.position.y; 13 | 14 | const isa = this.factory.createIsa(); 15 | const extendedEntity = this.factory.createEntity(); 16 | 17 | isa.attributes.position.x = x + 18; 18 | isa.attributes.position.y = y + 60; 19 | isa.attributes.attrs.text.text = extentionType; 20 | 21 | extendedEntity.attributes.position.x = x; 22 | extendedEntity.attributes.position.y = y + 120; 23 | 24 | const graph = entity.model.graph; 25 | 26 | graph.addCell(isa); 27 | graph.addCell(extendedEntity); 28 | 29 | this.linker.createLink(isa, entity.model, graph); 30 | 31 | entity.model.attributes.isExtended = true; 32 | isa.attributes.parentId = entity.model.attributes.id; 33 | 34 | this.linker.createLink(isa, extendedEntity, graph); 35 | }; 36 | 37 | updateExtension = (entity, extentionType) => { 38 | const graph = entity.model.graph; 39 | const paper = entity.paper; 40 | 41 | const neighbors = graph.getNeighbors(entity.model); 42 | const extension = neighbors.find(element => { 43 | return this.validator.isExtension(element) && element.attributes.parentId == entity.model.attributes.id; 44 | }); 45 | 46 | if(extension != null) { 47 | extension.attributes.attrs.text.text = extentionType; 48 | const extensionView = extension.findView(paper); 49 | extensionView.update(); 50 | } 51 | 52 | return extension; 53 | } 54 | 55 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # System files 3 | ############################################################################### 4 | .idea 5 | .DS_Store 6 | .env 7 | .vscode 8 | 9 | ############################################################################### 10 | # node-waf configuration 11 | ############################################################################### 12 | .lock-wscript 13 | npm-debug.log 14 | 15 | ############################################################################### 16 | # Dependency directories 17 | ############################################################################### 18 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 19 | node_modules/ 20 | bower_components/ 21 | build/ 22 | package-lock.json 23 | coverage/ 24 | 25 | ############################################################################### 26 | # Logs 27 | ############################################################################### 28 | yarn-error.log 29 | 30 | ############################################################################### 31 | # Compiled files 32 | ############################################################################### 33 | app/css 34 | app/assets 35 | app/dist 36 | 37 | ############################################################################### 38 | # ENV files 39 | ############################################################################### 40 | .env 41 | .env.development 42 | .env.production 43 | 44 | ############################################################################### 45 | # docker files 46 | ############################################################################### 47 | data/ 48 | coverage/ 49 | 50 | ############################################################################### 51 | # cypress 52 | ############################################################################### 53 | cypress.env.json 54 | cypress/downloads/ 55 | cypress/screenshots/ 56 | cypress/videos/ 57 | -------------------------------------------------------------------------------- /app/angular/components/shareModelModal.html: -------------------------------------------------------------------------------- 1 | 4 | 5 | 47 | 48 | -------------------------------------------------------------------------------- /app/angular/service/logicFactory.js: -------------------------------------------------------------------------------- 1 | import angular from "angular"; 2 | 3 | import * as joint from "jointjs/dist/joint"; 4 | import "jointjs/dist/joint.min.css"; 5 | 6 | const logicFactory = ($filter) => { 7 | var logic = joint.shapes.uml; 8 | 9 | const _createTable = function () { 10 | var table = new logic.Class({ 11 | position: { x: 12, y: 15 }, 12 | size: { width: 100, height: 100 }, 13 | name: $filter('translate')('Table'), 14 | attributes: [], 15 | attrs: { 16 | '.uml-class-name-rect': { 17 | fill: '#fff', 18 | stroke: '#000', 19 | 'stroke-width': 0.5, 20 | }, 21 | '.uml-class-attrs-rect': { 22 | fill: '#fff', 23 | stroke: '#000', 24 | 'stroke-width': 0.5 25 | }, 26 | '.uml-class-methods-rect': { 27 | fill: '#fff', 28 | stroke: '#000', 29 | 'stroke-width': 0.5 30 | }, 31 | '.uml-class-attrs-text': { 32 | ref: '.uml-class-attrs-rect', 33 | 'ref-y': 0.5, 34 | 'y-alignment': 'middle' 35 | } 36 | } 37 | }); 38 | return table; 39 | } 40 | 41 | const _createView = function () { 42 | var table = new logic.Abstract({ 43 | position: { x: 12, y: 155 }, 44 | size: { width: 100, height: 100 }, 45 | name: $filter('translate')('View'), 46 | attributes: [], 47 | attrs: { 48 | '.uml-class-name-rect': { 49 | fill: 'lightgray', 50 | stroke: '#000', 51 | 'stroke-width': 0.5, 52 | }, 53 | '.uml-class-attrs-rect': { 54 | fill: '#fff', 55 | stroke: '#000', 56 | 'stroke-width': 0.5 57 | }, 58 | '.uml-class-methods-rect': { 59 | fill: '#fff', 60 | stroke: '#000', 61 | 'stroke-width': 0.5 62 | }, 63 | '.uml-class-attrs-text': { 64 | ref: '.uml-class-attrs-rect', 65 | 'ref-y': 0.5, 66 | 'y-alignment': 'middle' 67 | } 68 | } 69 | }); 70 | return table; 71 | } 72 | 73 | return { 74 | createTable: _createTable, 75 | createView: _createView 76 | } 77 | 78 | } 79 | 80 | export default angular 81 | .module("app.LogicFactory", []) 82 | .factory("LogicFactory", logicFactory).name; 83 | -------------------------------------------------------------------------------- /app/joint/notes.js: -------------------------------------------------------------------------------- 1 | import * as joint from "jointjs/dist/joint"; 2 | import _ from "lodash"; 3 | 4 | const Note = joint.dia.Element.extend({ 5 | markup: '', 6 | defaults: _.defaultsDeep( 7 | { 8 | type: "custom.Note", 9 | supertype: "Note", 10 | size: { width: 80, height: 40 }, 11 | attrs: { 12 | ".outer": { 13 | fill: "#afbccf", 14 | stroke: "#afbccf", 15 | "stroke-width": 1, 16 | points: "100,0 100,60 0,60 0,0", 17 | }, 18 | ".inner": { 19 | fill: "#79d297", 20 | stroke: "#27AE60", 21 | "stroke-width": 1, 22 | points: "95,5 95,55 5,55 5,5", 23 | display: "none", 24 | }, 25 | text: { 26 | text: "Nota", 27 | textWrap: { 28 | width: 300, 29 | height: 210, 30 | ellipsis: true, 31 | }, 32 | "font-family": "Arial", 33 | "font-size": 14, 34 | ref: ".outer", 35 | "ref-x": 0.5, 36 | "ref-y": 0.5, 37 | "x-alignment": "middle", 38 | "y-alignment": "middle", 39 | }, 40 | }, 41 | }, 42 | joint.dia.Element.prototype.defaults 43 | ), 44 | getType() { 45 | return "custom.Note"; 46 | } 47 | }); 48 | 49 | const NoteView = joint.dia.ElementView.extend({ 50 | initialize: function () { 51 | joint.dia.ElementView.prototype.initialize.apply(this, arguments); 52 | }, 53 | setText: function (newText) { 54 | this.model.attributes.attrs.text.text = newText; 55 | this.update(); 56 | const textBox = this.$el.find("text")[0].getBBox(); 57 | if (textBox.width > 80) { 58 | this.model.attributes.size.width = textBox.width + 10; 59 | this.resize(); 60 | } 61 | if (textBox.height > 40) { 62 | this.model.attributes.size.height = textBox.height + 10; 63 | this.resize(); 64 | } 65 | }, 66 | setColor: function (newColor) { 67 | this.model.attributes.attrs[".outer"].fill = newColor; 68 | this.model.attributes.attrs[".outer"].stroke = newColor; 69 | this.update(); 70 | } 71 | }); 72 | 73 | export default { 74 | Note, 75 | NoteView 76 | }; -------------------------------------------------------------------------------- /app/angular/preferences/preferences.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 16 | 17 | 18 |
19 |
20 | 23 |
24 | 25 |
26 | Loading 32 | 33 | 36 | 37 | 38 |
39 | 40 |
41 |
42 |

{{ 'Delete account' | translate }}

43 |
44 |

{{ 'Remove all your models before removing your account.' | translate }}
{{ 'Keep in mind this is a destructive action and cannot be undone.' | translate}}

45 | 46 |
47 | 48 |
49 |
50 |
51 |
-------------------------------------------------------------------------------- /app/sass/buttons.scss: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////////////// 2 | // .br-button 3 | //////////////////////////////////////////////////////////////////////////////// 4 | .br-button { 5 | display: inline-block; 6 | cursor: pointer; 7 | font-size: 1em; 8 | padding: 4px 8px; 9 | border: 1px solid var(--brand-primary-30); 10 | border-radius: 2px; 11 | margin: 0 4px 0 0; 12 | color: var(--brand-primary-30); 13 | font-weight: 400; 14 | background-color: transparent; 15 | transition: all 0.25s ease; 16 | } 17 | 18 | .br-button--full { 19 | width: 100%; 20 | } 21 | 22 | .br-button:hover { 23 | color: var(--white); 24 | background: var(--brand-primary-30); 25 | text-decoration: none; 26 | } 27 | 28 | .br-button.warning { 29 | border-color: var(--gray-20); 30 | color: var(--gray-20); 31 | } 32 | 33 | .br-button.warning:hover { 34 | background: var(--gray-30); 35 | color: var(--white); 36 | } 37 | 38 | .br-button.destructive { 39 | border-color: var(--danger-base); 40 | color: var(--danger-base); 41 | } 42 | 43 | .br-button.destructive:hover { 44 | background: var(--danger-base); 45 | color: var(--white); 46 | } 47 | 48 | .br-button.btn-disable { 49 | cursor: not-allowed; 50 | pointer-events: none; 51 | color: #c0c0c0; 52 | background-color: #ddd; 53 | } 54 | 55 | //////////////////////////////////////////////////////////////////////////////// 56 | // .btn-alt 57 | //////////////////////////////////////////////////////////////////////////////// 58 | .btn-alt { 59 | background-color: var(--brand-default); 60 | color: var(--white); 61 | text-transform: none; 62 | } 63 | 64 | .btn-alt:hover { 65 | background-color: transparent; 66 | color: var(--brand-default); 67 | } 68 | 69 | //////////////////////////////////////////////////////////////////////////////// 70 | // .btn-alt 71 | //////////////////////////////////////////////////////////////////////////////// 72 | .btn-alt { 73 | background-color: var(--brand-default); 74 | color: var(--white); 75 | text-transform: none; 76 | } 77 | 78 | .btn-alt:hover { 79 | background-color: transparent; 80 | color: var(--brand-default); 81 | } -------------------------------------------------------------------------------- /server_app/model/handler.test.js: -------------------------------------------------------------------------------- 1 | const request = require("supertest"); 2 | const app = require("../app"); 3 | 4 | jest.mock("./service"); 5 | jest.mock('jsonwebtoken'); 6 | process.env.SECRET_TOKEN = 'mockSecretToken'; 7 | 8 | const mockModelService = require("./service"); 9 | 10 | jest.mock('../helpers/config', () => ({ 11 | SecretToken: 'mockSecretToken', 12 | })); 13 | jest.mock('jsonwebtoken', () => ({ 14 | ...jest.requireActual('jsonwebtoken'), // import and keep all other methods unchanged 15 | sign: jest.fn(() => 'mockedToken'), 16 | verify: jest.fn((token, secret, cb) => { 17 | cb(null, { id: 'global-user-id' }); 18 | }), 19 | })); 20 | 21 | 22 | afterEach(() => { 23 | jest.clearAllMocks(); 24 | }); 25 | 26 | 27 | describe("Test save /models", () => { 28 | test("It should send 422 when validation fails", async () => { 29 | const response = await request(app) 30 | .post("/models") 31 | .set("brx-access-token", 'mockToken') 32 | .send({ 33 | name: "Teste", 34 | type: "conceptual", 35 | model: { id: "1234" }, 36 | }); 37 | expect(response.statusCode).toBe(422); 38 | }); 39 | 40 | test("It should send 200 when user model is saved", async () => { 41 | const response = await request(app) 42 | .post("/models") 43 | .set("brx-access-token", 'mockToken') 44 | .send({ 45 | name: "Teste", 46 | type: "conceptual", 47 | model: { id: "1234" }, 48 | user: "12345", 49 | }); 50 | mockModelService.save.mockResolvedValue({ 51 | name: "Teste", 52 | type: "conceptual", 53 | model: { id: "1234" }, 54 | user: "12345", 55 | }); 56 | expect(response.statusCode).toBe(200); 57 | expect(mockModelService.save).toHaveBeenCalled(); 58 | }); 59 | }); 60 | 61 | describe("Test list all /models", () => { 62 | 63 | beforeEach(() => { 64 | jest.resetModules(); 65 | }); 66 | 67 | test("It should send 200 user exists", async () => { 68 | const response = await request(app) 69 | .get("/models") 70 | .set("brx-access-token", 'mockToken') 71 | .send([]); 72 | mockModelService.listAll.mockResolvedValue([]); 73 | 74 | expect(response.statusCode).toBe(200); 75 | expect(mockModelService.listAll).toHaveBeenCalled(); 76 | }); 77 | }); -------------------------------------------------------------------------------- /app/angular/recovery/reset.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | Loading 9 |
10 | 13 | 16 |
17 |
18 | 19 | 31 |
32 |
33 | 34 | 46 |
47 | 48 |
49 |
55 |
56 |
-------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const MiniCssExtractPlugin = require("mini-css-extract-plugin"); 3 | const HtmlWebpackPlugin = require("html-webpack-plugin"); 4 | const TerserPlugin = require("terser-webpack-plugin"); 5 | const Dotenv = require("dotenv-webpack"); 6 | const TsconfigPathsPlugin = require("tsconfig-paths-webpack-plugin") 7 | 8 | module.exports = { 9 | mode: process.env.NODE_ENV, 10 | context: `${__dirname}/app`, 11 | entry: "./angular/index.js", 12 | output: { 13 | path: `${__dirname}/app/dist`, 14 | filename: "[name].js", 15 | }, 16 | resolve: { 17 | extensions: [".js", ".jsx", ".ts", ".tsx", ".json"], 18 | plugins: [ 19 | new TsconfigPathsPlugin({ 20 | configFile: path.resolve(__dirname, "app/react/tsconfig.json"), 21 | }), 22 | ], 23 | }, 24 | devtool: process.env.NODE_ENV === "production" ? false : "inline-source-map", 25 | devServer: { 26 | static: path.join(__dirname, "app"), 27 | compress: true, 28 | port: 9000, 29 | }, 30 | plugins: [ 31 | new Dotenv({ systemvars: true }), 32 | new HtmlWebpackPlugin({ 33 | template: "index.html", 34 | favicon: `${__dirname}/app/img/brmw-logo.svg`, 35 | }), 36 | new MiniCssExtractPlugin({ 37 | filename: `bundle.css`, 38 | }), 39 | ], 40 | optimization: { 41 | minimizer: [ 42 | new TerserPlugin({ 43 | parallel: true, 44 | }), 45 | ], 46 | splitChunks: { 47 | chunks: "all", 48 | }, 49 | }, 50 | module: { 51 | rules: [ 52 | { 53 | test: /\.tsx?$/, 54 | use: "ts-loader", 55 | exclude: /node_modules/, 56 | }, 57 | { 58 | test: /\.(js|jsx)$/, 59 | exclude: /node_modules/, 60 | use: ["babel-loader"], 61 | }, 62 | { 63 | test: /\.html$/i, 64 | loader: "html-loader", 65 | }, 66 | { 67 | test: /\.(png|jpe?g|gif|svg)$/i, 68 | type: "asset/resource", 69 | }, 70 | { 71 | test: /\.(sa|sc|c)ss$/, 72 | use: [ 73 | { 74 | loader: "style-loader", 75 | }, 76 | { 77 | loader: "css-loader", 78 | }, 79 | { 80 | loader: "postcss-loader", 81 | }, 82 | { 83 | loader: "sass-loader", 84 | options: { 85 | implementation: require("sass"), 86 | }, 87 | }, 88 | ], 89 | }, 90 | { 91 | test: /\.(woff(2)?|ttf|eot)(\?v=\d+\.\d+\.\d+)?$/, 92 | type: "asset/resource", 93 | }, 94 | ], 95 | }, 96 | }; 97 | -------------------------------------------------------------------------------- /app/angular/components/noteEditor.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | 6 |
7 |
8 | 9 |
    10 |
  • 11 | 12 | 14 |
  • 15 |
  • 16 | 17 | 19 |
  • 20 |
  • 21 | 22 | 24 |
  • 25 |
  • 26 | 27 | 29 |
  • 30 |
  • 31 | 32 | 34 |
  • 35 |
  • 36 | 37 | 39 |
  • 40 |
  • 41 | 42 | 44 |
  • 45 |
  • 46 | 47 | 49 |
  • 50 |
51 |
-------------------------------------------------------------------------------- /app/angular/components/keyboardController.js: -------------------------------------------------------------------------------- 1 | import hotkeys from 'hotkeys-js'; 2 | 3 | export const types = { 4 | SAVE: 'save', 5 | UNDO: 'undo', 6 | REDO: 'redo', 7 | ZOOM_IN: 'zoomIn', 8 | ZOOM_OUT: 'zoomOut', 9 | ZOOM_NONE: 'zoomNone', 10 | ESC: 'esc', 11 | DELETE: 'delete', 12 | COPY: 'copy', 13 | PASTE: 'paste', 14 | } 15 | 16 | export default class KeyboardController { 17 | constructor(document) { 18 | this.document = document; 19 | this.spacePressed = false; 20 | this.shortcutHandlers = new Map(); 21 | this.#registerSpaceEvents(); 22 | this.#registerShortcutEvents(); 23 | } 24 | 25 | #registerSpaceEvents() { 26 | this.document.on('keydown', (keyboardEvent) => { 27 | if (keyboardEvent.code === "Space" && keyboardEvent.originalEvent.target == document.body) { 28 | this.spacePressed = true; 29 | keyboardEvent.preventDefault(); 30 | } 31 | }); 32 | 33 | this.document.on('keyup', (keyboardEvent) => { 34 | if (keyboardEvent.code === "Space") { 35 | this.spacePressed = false; 36 | } 37 | }); 38 | } 39 | 40 | #registerShortcutEvents() { 41 | hotkeys.unbind(); 42 | 43 | this.hotkeyConfigs.forEach(({ key, eventName }) => { 44 | hotkeys(key, () => { 45 | this.shortcutHandlers.get(eventName).forEach(action => action()); 46 | return false; 47 | }); 48 | }) 49 | } 50 | 51 | get isSpacePressed() { 52 | return this.spacePressed; 53 | } 54 | 55 | get hotkeyConfigs() { 56 | return [ 57 | { key: 'command+s, ctrl+s', eventName: types.SAVE }, 58 | { key: 'command+z, ctrl+z', eventName: types.UNDO }, 59 | { key: 'command+shift+z, ctrl+shift+z', eventName: types.REDO }, 60 | { key: 'backspace', eventName: types.DELETE }, 61 | { key: 'command+c, ctrl+c', eventName: types.COPY }, 62 | { key: 'command+v, ctrl+v', eventName: types.PASTE }, 63 | { key: 'z+=', eventName: types.ZOOM_IN }, 64 | { key: 'z+-', eventName: types.ZOOM_OUT }, 65 | { key: 'z+0', eventName: types.ZOOM_NONE }, 66 | { key: 'esc', eventName: types.ESC }, 67 | ]; 68 | } 69 | 70 | registerHandler(eventName, action) { 71 | const currentActions = this.shortcutHandlers.has(eventName) ? this.shortcutHandlers.get(eventName) : []; 72 | this.shortcutHandlers.set(eventName, [action, ...currentActions]) 73 | } 74 | 75 | unbindAll() { 76 | this.document.off("keydown"); 77 | this.document.off("keyup"); 78 | hotkeys.unbind(); 79 | } 80 | }; -------------------------------------------------------------------------------- /app/angular/publicview/publicview.html: -------------------------------------------------------------------------------- 1 |
2 | 14 | 15 |
16 | 17 |
18 |
19 | 29 | 35 | 40 |
41 |
42 |
43 |
-------------------------------------------------------------------------------- /app/angular/preferences/preferences.js: -------------------------------------------------------------------------------- 1 | import angular from "angular"; 2 | import template from "./preferences.html"; 3 | import confirmationModal from "../components/confirmationModal"; 4 | 5 | const preferencesController = function(AuthService, $state, $filter, $uibModal, $timeout){ 6 | const ctrl = this; 7 | ctrl.loading = false; 8 | ctrl.feedback = { 9 | message: "", 10 | showing: false 11 | } 12 | 13 | ctrl.showFeedback = (show, newMessage) => { 14 | $timeout(() => { 15 | ctrl.feedback.showing = show; 16 | ctrl.feedback.message = $filter('translate')(newMessage); 17 | }); 18 | } 19 | 20 | const showLoading = (loading) => { 21 | ctrl.loading = loading; 22 | }; 23 | 24 | const handleDeleteAccountError = (error) => { 25 | if(error.status == 400) { 26 | const errorMessage = $filter('translate')("Delete account model error"); 27 | ctrl.showFeedback(true, errorMessage); 28 | } else { 29 | const errorMessage = $filter('translate')("Delete account default error"); 30 | ctrl.showFeedback(true, errorMessage); 31 | } 32 | } 33 | 34 | ctrl.dropdownOptions = [ 35 | { name: $filter('translate')("Preferences"), type: 'preferences' }, 36 | { name: $filter('translate')("Logout"), type: 'logout' } 37 | ]; 38 | 39 | ctrl.deleteAccount = () => { 40 | const modalInstance = $uibModal.open({ 41 | animation: true, 42 | component: "confirmationModal", 43 | resolve: { 44 | modalData: () => ({ 45 | title: $filter('translate')("Delete account"), 46 | content: $filter('translate')("Are you sure you want to delete your account?"), 47 | cancelLabel: $filter('translate')("Cancel"), 48 | confirmLabel: $filter('translate')("Delete"), 49 | }), 50 | }, 51 | }); 52 | 53 | modalInstance.result.then(() => { 54 | showLoading(true); 55 | AuthService.deleteAccount().then(() => { 56 | AuthService.logout(); 57 | $state.go("login"); 58 | }).catch(handleDeleteAccountError) 59 | .finally(() => { 60 | showLoading(false); 61 | }); 62 | }); 63 | } 64 | 65 | ctrl.menuOptionSelected = (option) => { 66 | switch(option.type) { 67 | case "logout": ctrl.doLogout() 68 | break; 69 | case "preferences": $state.go("preferences"); 70 | break; 71 | } 72 | } 73 | 74 | ctrl.doLogout = () => { 75 | AuthService.logout(); 76 | $state.go("login"); 77 | } 78 | } 79 | 80 | export default angular 81 | .module("app.preferences", [confirmationModal]) 82 | .component("preferences", { 83 | template, 84 | controller: preferencesController, 85 | }).name; -------------------------------------------------------------------------------- /cypress/e2e/login.cy.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | describe("Login", () => { 4 | const user = Cypress.env("user"); 5 | const password = Cypress.env("password"); 6 | 7 | it("logs in and out successfully", () => { 8 | cy.intercept("GET", "/models?userId=*").as("getUserModels"); 9 | 10 | cy.loginViaGui(user, password); 11 | 12 | cy.wait("@getUserModels"); 13 | cy.url().should("be.equal", `${Cypress.config("baseUrl")}/#!/main`); 14 | cy.contains("h2", "Models").should("be.visible"); 15 | 16 | cy.get("[data-cy='gear-button']").click(); 17 | cy.contains(".dropdown-menu li", "Logout").click(); 18 | 19 | cy.url().should("be.equal", `${Cypress.config("baseUrl")}/#!/`); 20 | cy.get(".login-form").should("be.visible"); 21 | }); 22 | 23 | it("switches languages", () => { 24 | cy.visit("/"); 25 | 26 | cy.get('button[type="submit"]') 27 | .as("submitButton") 28 | .should("contain", "Login") 29 | .and("be.visible"); 30 | 31 | cy.get("[data-cy='lang-pt-br']").click(); 32 | 33 | cy.get("@submitButton").should("contain", "Entrar").and("be.visible"); 34 | 35 | cy.get("[data-cy='lang-en-us']").click(); 36 | 37 | cy.get("@submitButton").should("contain", "Login").and("be.visible"); 38 | }); 39 | 40 | context("Form validations", () => { 41 | it("alerts on invalid user email", () => { 42 | cy.loginViaGui("invalid#user.com", password); 43 | 44 | cy.contains(".alert-danger", "Fill the fields in red").should( 45 | "be.visible" 46 | ); 47 | cy.get("#userEmail").should("have.class", "error"); 48 | cy.get(".error").should("have.length", 1).and("be.visible"); 49 | }); 50 | 51 | it("alerts when clicking ENTER without filling the form", () => { 52 | cy.visit("/"); 53 | cy.contains("button", "Login").click(); 54 | 55 | cy.contains(".alert-danger", "Fill the fields in red").should( 56 | "be.visible" 57 | ); 58 | cy.get("#userEmail").should("have.class", "error"); 59 | cy.get("#userPassword").should("have.class", "error"); 60 | cy.get(".error").should("have.length", 2).and("be.visible"); 61 | }); 62 | 63 | it("alerts on invalid user", () => { 64 | cy.loginViaGui("invalid@user.com", password); 65 | 66 | cy.contains(".alert-danger", "Incorrect login or password").should( 67 | "be.visible" 68 | ); 69 | }); 70 | 71 | it("alerts on invalid password", () => { 72 | cy.loginViaGui(user, "invalid-pwd"); 73 | 74 | cy.contains(".alert-danger", "Incorrect login or password").should( 75 | "be.visible" 76 | ); 77 | }); 78 | }); 79 | }); 80 | -------------------------------------------------------------------------------- /app/angular/signup/signup.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | Loading 9 |
10 | 13 | 16 |
17 |
18 | 19 | 30 |
31 |
32 | 33 | 44 |
45 |
46 | 47 | 58 |
59 | 60 |
61 |
67 |
68 |
-------------------------------------------------------------------------------- /app/angular/conceptual/factory.test.js: -------------------------------------------------------------------------------- 1 | import Factory from "./factory"; 2 | 3 | import * as joint from "jointjs"; 4 | 5 | import shapes from "../../joint/shapes"; 6 | joint.shapes.erd = shapes; 7 | 8 | describe('factory', () => { 9 | 10 | const factory = new Factory(joint.shapes); 11 | const position = { x: 0, y: 1 }; 12 | 13 | test("It should create an Entity", async () => { 14 | const entity = factory.createEntity( { "position": position }); 15 | 16 | expect(entity.attributes.type).toBe("erd.Entity"); 17 | expect(entity.attributes.supertype).toBe("Entity"); 18 | expect(entity.attributes.isExtended).toBeFalsy(); 19 | expect(entity.attributes.autorelationship).toBeFalsy(); 20 | expect(entity.attributes.position).toMatchObject(position); 21 | }); 22 | 23 | test("It should create a Relationship", async () => { 24 | const entity = factory.createRelationship({ "position": position }); 25 | 26 | expect(entity.attributes.type).toBe("erd.Relationship"); 27 | expect(entity.attributes.supertype).toBe("Relationship"); 28 | expect(entity.attributes.autorelationship).toBeFalsy(); 29 | expect(entity.attributes.position).toMatchObject(position); 30 | }); 31 | 32 | test("It should create an Extension", async () => { 33 | const entity = factory.createIsa({ "position": position }); 34 | 35 | expect(entity.attributes.type).toBe("erd.ISA"); 36 | expect(entity.attributes.supertype).toBe("Inheritance"); 37 | expect(entity.attributes.parentId).toBeNull(); 38 | expect(entity.attributes.position).toMatchObject(position); 39 | }); 40 | 41 | test("It should create an Attribute", async () => { 42 | const entity = factory.createAttribute({ "position": position }); 43 | 44 | expect(entity.attributes.type).toBe("erd.Attribute"); 45 | expect(entity.attributes.supertype).toBe("Attribute"); 46 | expect(entity.attributes.multivalued).toBeFalsy(); 47 | expect(entity.attributes.composed).toBeFalsy(); 48 | }); 49 | 50 | test("It should create a Key", async () => { 51 | const entity = factory.createKey({ "position": position }); 52 | 53 | expect(entity.attributes.type).toBe("erd.Key"); 54 | expect(entity.attributes.supertype).toBe("Key"); 55 | expect(entity.attributes.multivalued).toBeFalsy(); 56 | expect(entity.attributes.composed).toBeFalsy(); 57 | }); 58 | 59 | test("It should create a Link", async () => { 60 | const entity = factory.createLink(); 61 | 62 | expect(entity.attributes.type).toBe("erd.Link"); 63 | expect(entity.attributes.weak).toBeFalsy(); 64 | }); 65 | 66 | }); 67 | -------------------------------------------------------------------------------- /app/angular/login/login.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | Loading 9 | 55 | 56 | 65 |
66 |
-------------------------------------------------------------------------------- /app/angular/service/authService.js: -------------------------------------------------------------------------------- 1 | import angular from "angular"; 2 | import { Buffer } from 'buffer'; 3 | 4 | const authService = function ($http, $cookies) { 5 | const service = {}; 6 | 7 | service.login = function (credentials) { 8 | const body = { 9 | "username": service.encode(credentials.username), 10 | "password": service.encode(credentials.password) 11 | } 12 | return $http.post("/users/login", body).then((res) => { 13 | const user = res.data; 14 | const today = new Date(); 15 | const expired = new Date(today); 16 | expired.setDate(today.getDate() + 1); 17 | $cookies.put("sessionId", user.sessionId, { expires: expired }); 18 | $cookies.put("userId", user.userId, { expires: expired }); 19 | $cookies.put("userName", user.userName, { expires: expired }); 20 | $cookies.put("userToken", user.token, { expires: expired }); 21 | return user; 22 | }); 23 | }; 24 | 25 | service.logout = function () { 26 | $cookies.remove("sessionId"); 27 | $cookies.remove("userId"); 28 | $cookies.remove("userName"); 29 | $cookies.remove("userToken"); 30 | }; 31 | 32 | service.register = function (credentials) { 33 | const body = { 34 | "email": service.encode(credentials.email), 35 | "password": service.encode(credentials.password) 36 | } 37 | return $http.post("/users/create", body).then((res) => {}); 38 | }; 39 | 40 | service.isAuthenticated = function () { 41 | const userId = $cookies.get("userId"); 42 | service.loggeduser = userId; 43 | service.loggeduserName = $cookies.get("userName"); 44 | service.token = $cookies.get("userToken"); 45 | return !!userId; 46 | }; 47 | 48 | service.recovery = (email) => { 49 | return $http.post("/users/recovery", { email }); 50 | }; 51 | 52 | service.validateRecovery = (mail, code) => { 53 | return $http.get("/users/recovery/validate", { 54 | params: { mail, code }, 55 | }); 56 | }; 57 | 58 | service.resetPassword = (mail, code, newPassword) => { 59 | const body = { 60 | "mail": service.encode(mail), 61 | "newPassword": service.encode(newPassword), 62 | "code": code 63 | } 64 | return $http.post("/users/reset", body); 65 | }; 66 | 67 | service.deleteAccount = () => { 68 | return $http({ 69 | method: 'delete', 70 | url: '/users/delete', 71 | data: { 72 | "userId": service.loggeduser 73 | }, 74 | headers: { 75 | 'Content-type': 'application/json;charset=utf-8' 76 | } 77 | }); 78 | }; 79 | 80 | service.encode = (data) => { 81 | return Buffer.from(data).toString('base64'); 82 | }; 83 | 84 | return service; 85 | }; 86 | 87 | export default angular 88 | .module("app.authService", []) 89 | .factory("AuthService", authService).name; 90 | -------------------------------------------------------------------------------- /app/img/brmw-logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/angular/view/view.html: -------------------------------------------------------------------------------- 1 |
2 | 10 | 11 |
12 | 13 |
14 | 15 | 23 |
24 | 25 |
26 | 27 |
28 | 36 |
37 |
38 | 39 |
40 | 41 | 42 |
43 | 44 |
45 |
{{ table.name }}
46 |
47 |
48 |
49 | 57 |
58 |
59 |
60 |
61 | 62 |
63 | 64 | 70 |
71 | 72 | {{ $ctrl.view.queryConditions.values ? 'Edit query for view creation' : 'Include query for view creation' | translate }} 73 | 74 |
75 |
76 | 77 | 78 |
79 | 82 |
83 | 84 |
85 |
-------------------------------------------------------------------------------- /app/angular/view/view.js: -------------------------------------------------------------------------------- 1 | import angular from "angular"; 2 | import { comparasionOperators } from "../service/queryExpressionService"; 3 | import template from "./view.html"; 4 | 5 | const app = angular.module("app.view", []); 6 | 7 | const Controller = function (LogicService, $uibModal) { 8 | const $ctrl = this; 9 | 10 | const updateViewInfo = (view) => { 11 | const allTables = LogicService.loadTables(); 12 | $ctrl.tables = view.tables; 13 | $ctrl.queryConditions = view.queryConditions; 14 | allTables.forEach(table => { 15 | if ($ctrl.tables.some(({ name }) => table.name === name)) return; 16 | $ctrl.tables.push(table); 17 | }); 18 | } 19 | 20 | $ctrl.$onInit = () => { 21 | updateViewInfo($ctrl.view); 22 | }; 23 | 24 | $ctrl.queryExpressionModal = () => { 25 | const modalInstance = $uibModal.open({ 26 | animation: true, 27 | template: '', 28 | controller: function () { 29 | const ctrl = this; 30 | ctrl.tables = $ctrl.tables.filter(({ selected }) => selected); 31 | ctrl.queryConditions = $ctrl.view.queryConditions; 32 | }, 33 | controllerAs: '$ctrl', 34 | }); 35 | modalInstance.result.then(({ conditions, joins }) => { 36 | $ctrl.view.queryConditions = { 37 | joins: joins.filter(join => join.submitted), 38 | values: conditions, 39 | text: `${conditions.map(({ columnName, type, comparativeValue, comparativeValue2, logicalOperator }, index) => 40 | `${columnName} ${comparasionOperators[type](comparativeValue, comparativeValue2)} ${conditions.length - 1 === index ? '' : logicalOperator}`).join(" ")}` 41 | } 42 | }); 43 | }; 44 | 45 | $ctrl.changeName = () => { 46 | if ($ctrl.view.name) { 47 | LogicService.editName($ctrl.view.name); 48 | } 49 | } 50 | 51 | $ctrl.$onChanges = (changes) => { 52 | if (changes.view != null && changes.view.currentValue != null) { 53 | updateViewInfo({ ...changes.view.currentValue }); 54 | } 55 | } 56 | 57 | $ctrl.toggleForm = () => { 58 | $ctrl.formVisible = !$ctrl.formVisible; 59 | }; 60 | 61 | $ctrl.cancel = () => { 62 | $ctrl.dismiss(); 63 | }; 64 | 65 | $ctrl.save = () => { 66 | let columns = []; 67 | const filteredTables = $ctrl.tables.filter(({ selected }) => selected); 68 | filteredTables.forEach(table => { 69 | table.columns.forEach(column => { 70 | if (column.selected) columns.push(column); 71 | }); 72 | }); 73 | const view = { 74 | ...$ctrl.view, 75 | basedIn: filteredTables, 76 | columns, 77 | } 78 | LogicService.save(view); 79 | } 80 | }; 81 | 82 | export default app.component("view", { 83 | template: template, 84 | bindings: { 85 | view: "<", 86 | isEdit: "<", 87 | dismiss: "<" 88 | }, 89 | controller: Controller, 90 | }).name; 91 | -------------------------------------------------------------------------------- /cypress/e2e/models.cy.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import { faker } from '@faker-js/faker/locale/en'; 4 | 5 | describe("Models view", () => { 6 | beforeEach(() => { 7 | cy.intercept("GET", "/models?userId=*").as("getUserModels"); 8 | cy.loginViaGui(); 9 | cy.wait("@getUserModels").then((userModels) => { 10 | const userId = userModels.request.url.match(/userId=([^&]*)/)[1]; 11 | 12 | cy.cleanUpUserModels(userModels); 13 | cy.createModelViaApi("conceptual", userId); 14 | }); 15 | cy.reload(); 16 | cy.wait("@getUserModels"); 17 | }); 18 | 19 | it("edits a model title", () => { 20 | const updatedModelName = faker.lorem.word(); 21 | 22 | cy.get(".fa-pencil").click({ force: true }); 23 | cy.get("#rename-model").type(updatedModelName); 24 | cy.contains("button", "Rename").click(); 25 | cy.contains("tr.listLine", updatedModelName).should("be.visible"); 26 | }); 27 | 28 | // @TODO: figure out why model duplication isn't working. 29 | it.skip("duplicates a model", () => { 30 | cy.intercept("POST", "/models").as("postModel"); 31 | cy.get(".fa-files-o").click({ force: true }); 32 | cy.contains("button", "Save").click(); 33 | cy.wait("@postModel").its("response.statusCode").should("be.equal", 200); 34 | cy.reload(); 35 | cy.get("tr.listLine") 36 | .should("have.length", 2) 37 | .last() 38 | .should("contain", "(copy)"); 39 | }); 40 | 41 | it("deletes a model", () => { 42 | cy.intercept("DELETE", "/models/**").as("deleteModels"); 43 | cy.get(".fa-trash").click({ force: true }); 44 | cy.contains("button", "Delete").click(); 45 | cy.wait("@deleteModels").its("response.statusCode").should("be.equal", 200); 46 | cy.get("table tbody tr.listLine").should("not.exist"); 47 | }); 48 | 49 | it("opens and closes the modals", () => { 50 | // Opens the rename model modal 51 | cy.get(".fa-pencil").click({ force: true }); 52 | // Make sure the modal is visible and closes it 53 | cy.get(".modal-dialog") 54 | .as("modal") 55 | .should("be.visible") 56 | .find("button:contains(Cancel)") 57 | .click(); 58 | // Make sure the modal doesn't exist in the DOM anymore 59 | cy.get("@modal").should("not.exist"); 60 | // Opens the duplicate model modal 61 | cy.get(".fa-files-o").click({ force: true }); 62 | // Make sure the modal is visible and closes it 63 | cy.get("@modal") 64 | .should("be.visible") 65 | .find("button:contains(Cancel)") 66 | .click(); 67 | // Make sure the modal doesn't exist in the DOM anymore 68 | cy.get("@modal").should("not.exist"); 69 | // Opens the delete model modal 70 | cy.get(".fa-trash").click({ force: true }); 71 | // Make sure the modal is visible and closes it 72 | cy.get("@modal") 73 | .should("be.visible") 74 | .find("button:contains(Cancel)") 75 | .click(); 76 | // Make sure the modal doesn't exist in the DOM anymore 77 | cy.get("@modal").should("not.exist"); 78 | }); 79 | }); 80 | -------------------------------------------------------------------------------- /server_app/model/validator.test.js: -------------------------------------------------------------------------------- 1 | const modelValidator = require("./validator"); 2 | 3 | describe("validateSaveModelParams test", () => { 4 | test("should fail when name is missing", () => { 5 | const validation = modelValidator.validateSaveParams({}); 6 | expect(validation.valid).toBeFalsy(); 7 | expect(validation.message).toBe(modelValidator.getMessages().MISSING_NAME); 8 | }); 9 | 10 | test("should fail when name is empty", () => { 11 | const validation = modelValidator.validateSaveParams({ name: "" }); 12 | expect(validation.valid).toBeFalsy(); 13 | expect(validation.message).toBe(modelValidator.getMessages().MISSING_NAME); 14 | }); 15 | 16 | test("should fail when name has a wrong type", () => { 17 | const validation = modelValidator.validateSaveParams({ name: 5 }); 18 | expect(validation.valid).toBeFalsy(); 19 | expect(validation.message).toBe( 20 | modelValidator.getMessages().WRONG_NAME_TYPE 21 | ); 22 | }); 23 | 24 | test("should fail when type is missing", () => { 25 | const validation = modelValidator.validateSaveParams({ name: "Teste" }); 26 | expect(validation.valid).toBeFalsy(); 27 | expect(validation.message).toBe(modelValidator.getMessages().MISSING_TYPE); 28 | }); 29 | 30 | test("should fail when type is empty", () => { 31 | const validation = modelValidator.validateSaveParams({ 32 | name: "Teste", 33 | type: "", 34 | }); 35 | expect(validation.valid).toBeFalsy(); 36 | expect(validation.message).toBe(modelValidator.getMessages().MISSING_TYPE); 37 | }); 38 | 39 | test("should fail when model does not have a type", () => { 40 | const validation = modelValidator.validateSaveParams({ 41 | name: "Teste", 42 | type: 5, 43 | }); 44 | expect(validation.valid).toBeFalsy(); 45 | expect(validation.message).toBe( 46 | modelValidator.getMessages().WRONG_TYPE_TYPE 47 | ); 48 | }); 49 | 50 | test("should fail when user is missing", () => { 51 | const validation = modelValidator.validateSaveParams({ 52 | name: "Teste", 53 | type: "conceltual", 54 | model: { id: "1234" }, 55 | }); 56 | expect(validation.valid).toBeFalsy(); 57 | expect(validation.message).toBe(modelValidator.getMessages().MISSING_USER); 58 | }); 59 | 60 | test("should fail when user is 0", () => { 61 | const validation = modelValidator.validateSaveParams({ 62 | name: "Teste", 63 | type: "conceltual", 64 | model: { id: "1234" }, 65 | userId: "", 66 | }); 67 | expect(validation.valid).toBeFalsy(); 68 | expect(validation.message).toBe(modelValidator.getMessages().MISSING_USER); 69 | }); 70 | 71 | test("should fail when userID has a wrong type", () => { 72 | const validation = modelValidator.validateSaveParams({ 73 | name: "Teste", 74 | type: "conceptual", 75 | model: { id: "1234" }, 76 | userId: 5, 77 | }); 78 | expect(validation.valid).toBeFalsy(); 79 | expect(validation.message).toBe( 80 | modelValidator.getMessages().WRONG_USER_TYPE 81 | ); 82 | }); 83 | }); 84 | -------------------------------------------------------------------------------- /app/sass/colors.scss: -------------------------------------------------------------------------------- 1 | :root { 2 | /////////////////////////////////////////////////////////////////////////////// 3 | // Brand primary colors 4 | /////////////////////////////////////////////////////////////////////////////// 5 | --brand-primary-50: hsl(170, 100%, 50%); 6 | --brand-primary-40: hsl(170, 100%, 40%); 7 | --brand-primary-30: hsl(170, 100%, 30%); 8 | --brand-primary-20: hsl(170, 100%, 20%); 9 | --brand-primary-10: hsl(170, 100%, 10%); 10 | 11 | --brand-default: var(--brand-primary-20); 12 | 13 | /////////////////////////////////////////////////////////////////////////////// 14 | // Gray scale colors 15 | /////////////////////////////////////////////////////////////////////////////// 16 | --gray-95: hsl(0, 0%, 95%); 17 | --gray-90: hsl(0, 0%, 90%); 18 | --gray-80: hsl(0, 0%, 80%); 19 | --gray-70: hsl(0, 0%, 70%); 20 | --gray-50: hsl(0, 0%, 50%); 21 | --gray-30: hsl(0, 0%, 30%); 22 | --gray-20: hsl(0, 0%, 20%); 23 | --gray-10: hsl(0, 0%, 10%); 24 | 25 | /////////////////////////////////////////////////////////////////////////////// 26 | // Black & white pure values 27 | /////////////////////////////////////////////////////////////////////////////// 28 | --white: hsl(0, 0%, 100%); 29 | --black: hsl(0, 0%, 0%); 30 | 31 | /////////////////////////////////////////////////////////////////////////////// 32 | // Accent color 33 | /////////////////////////////////////////////////////////////////////////////// 34 | --accent-light: hsl(60, 70%, 90%); 35 | --accent-base: hsl(60, 70%, 50%); 36 | --accent-dark: hsl(60, 70%, 30%); 37 | 38 | /////////////////////////////////////////////////////////////////////////////// 39 | // Border colors 40 | /////////////////////////////////////////////////////////////////////////////// 41 | --border-light: var(--gray-90); 42 | --border-default: var(--gray-70); 43 | --border-dark: var(--gray-30); 44 | 45 | /////////////////////////////////////////////////////////////////////////////// 46 | // Meaning related colors 47 | /////////////////////////////////////////////////////////////////////////////// 48 | --danger-light: hsl(340, 100%, 60%); 49 | --danger-base: hsl(340, 100%, 40%); 50 | --danger-dark: hsl(340, 100%, 20%); 51 | 52 | --highlight-light: hsl(45, 86%, 80%); 53 | --highlight-base: hsl(45, 86%, 60%); 54 | --highlight-dark: hsl(45, 86%, 30%); 55 | 56 | /////////////////////////////////////////////////////////////////////////////// 57 | // Theme: Gray 58 | /////////////////////////////////////////////////////////////////////////////// 59 | --theme-gray: var(--gray-30); 60 | 61 | /////////////////////////////////////////////////////////////////////////////// 62 | // Theme: Green 63 | /////////////////////////////////////////////////////////////////////////////// 64 | --theme-green: var(--brand-primary-30); 65 | 66 | /////////////////////////////////////////////////////////////////////////////// 67 | // Theme: Purple 68 | /////////////////////////////////////////////////////////////////////////////// 69 | --theme-purple: hsl(24, 80% 60%); 70 | } -------------------------------------------------------------------------------- /app/angular/recovery/reset.js: -------------------------------------------------------------------------------- 1 | import angular from "angular"; 2 | import template from "./reset.html"; 3 | import AuthService from "../service/authService" 4 | 5 | const controller = function (AuthService, $stateParams, $timeout, $state, $filter) { 6 | const ctrl = this; 7 | ctrl.submitted = false; 8 | ctrl.mail = ""; 9 | ctrl.validation = { 10 | validCode: false, 11 | } 12 | ctrl.feedback = { 13 | message: "", 14 | showing: false, 15 | type: "error" 16 | }; 17 | 18 | const setValidation = (valid) => { 19 | $timeout(() => { 20 | ctrl.validation.validCode = valid; 21 | }, 100); 22 | }; 23 | 24 | const setLoading = (loading) => { 25 | $timeout(() => { 26 | ctrl.loading = loading; 27 | }, 100); 28 | }; 29 | 30 | const showFeedback = (newMessage, type = "error") => { 31 | $timeout(() => { 32 | ctrl.feedback.message = $filter('translate')(newMessage); 33 | ctrl.feedback.showing = true; 34 | ctrl.feedback.type = type; 35 | }, 100); 36 | }; 37 | 38 | const handleError = (error) => { 39 | console.log(error); 40 | ctrl.loading = false; 41 | showFeedback("This email is not registered yet."); 42 | }; 43 | 44 | const handleSuccess = () => { 45 | showFeedback("The new password was successfully saved.", "success"); 46 | $timeout(() => { 47 | setLoading(false); 48 | $state.go("login"); 49 | }, 2000); 50 | }; 51 | 52 | const doReset = (newPassword) => { 53 | ctrl.loading = true; 54 | const code = $stateParams.code; 55 | const mail = $stateParams.mail; 56 | AuthService.resetPassword(mail, code, newPassword) 57 | .then(handleSuccess) 58 | .catch(handleError); 59 | }; 60 | 61 | ctrl.submitForm = (validForm, newPassword, repeatedPassword) => { 62 | ctrl.submitted = true; 63 | ctrl.feedback.showing = false; 64 | if (validForm) { 65 | if (newPassword != repeatedPassword) { 66 | showFeedback("The password must be the same in both fields."); 67 | } else if (newPassword.length < 6) { 68 | showFeedback("The password must contain more than 6 digits."); 69 | } else { 70 | doReset(newPassword) 71 | } 72 | } else { 73 | showFeedback("Fill the fields in red"); 74 | } 75 | }; 76 | 77 | ctrl.$onInit = () => { 78 | setLoading(true); 79 | const code = $stateParams.code; 80 | const mail = $stateParams.mail; 81 | setLoading(true); 82 | AuthService.validateRecovery(mail, code) 83 | .then((response) => { 84 | setLoading(false); 85 | if (response.data.valid) { 86 | setValidation(true); 87 | } else { 88 | showFeedback("Password recovery code is invalid, request password recovery again!"); 89 | } 90 | }).catch((error) => { 91 | setLoading(false); 92 | showFeedback("Password recovery code is invalid, request password recovery again!"); 93 | }); 94 | } 95 | 96 | }; 97 | 98 | export default angular 99 | .module("app.reset", [AuthService]) 100 | .component("resetPassword", { 101 | template, 102 | controller, 103 | }).name; -------------------------------------------------------------------------------- /app/angular/service/modelAPI.js: -------------------------------------------------------------------------------- 1 | import angular from "angular"; 2 | 3 | const authService = ($http) => { 4 | const _saveModel = function (model) { 5 | return $http.post("/models", model).then(function (newModel) { 6 | return newModel.data; 7 | }); 8 | }; 9 | 10 | const _getAllModels = function (_userId) { 11 | return $http 12 | .get("/models", { 13 | params: { userId: _userId }, 14 | }) 15 | .then(function (res) { 16 | return res; 17 | }); 18 | }; 19 | 20 | const _updateModel = function (model) { 21 | return $http.put(`/models/${model.id}`, model).then(function(resp) { 22 | return resp; 23 | }); 24 | }; 25 | 26 | const _getModel = function (_modelId, _userId) { 27 | return $http 28 | .get("/models/:modelId", { 29 | params: { userId: _userId, modelId: _modelId }, 30 | }) 31 | .then(function (resp) { 32 | return resp; 33 | }); 34 | }; 35 | 36 | const _deleteModel = function (_modelId) { 37 | return $http 38 | .delete("/models/:modelId", { 39 | params: { modelId: _modelId }, 40 | }) 41 | .then(function (resp) { 42 | return resp; 43 | }); 44 | }; 45 | 46 | const _renameModel = function (_modelId, newName) { 47 | return $http 48 | .put(`/models/${_modelId}/rename`, { name: newName }) 49 | .then(function (resp) { 50 | return resp; 51 | }); 52 | }; 53 | 54 | const _loadShareOptions = function (_modelId) { 55 | return $http 56 | .get(`/models/${_modelId}/share/options`) 57 | .then(function (resp) { 58 | return resp; 59 | }); 60 | }; 61 | 62 | const _toggleShare = function (modelId, active, importAllowed) { 63 | return $http 64 | .post(`/models/share`, {"modelId": modelId, "active": active, "importAllowed": importAllowed}) 65 | .then(function (resp) { 66 | return resp; 67 | }); 68 | }; 69 | 70 | const _getSharedModel = function (shareId) { 71 | return $http 72 | .get(`/models/share/${shareId}`) 73 | .then(function (resp) { 74 | return resp; 75 | }); 76 | }; 77 | 78 | const _importModel = function (shareId, userId) { 79 | return $http 80 | .post(`/models/import`, {shareId, userId}) 81 | .then(function (resp) { 82 | return resp; 83 | }); 84 | }; 85 | 86 | const _duplicate= function (modelId, userId, newName) { 87 | return $http 88 | .post(`/models/${modelId}/duplicate`, {"userId": userId, "newName": newName}) 89 | .then(function (resp) { 90 | return resp; 91 | }); 92 | }; 93 | 94 | return { 95 | saveModel: _saveModel, 96 | getAllModels: _getAllModels, 97 | getModel: _getModel, 98 | updateModel: _updateModel, 99 | deleteModel: _deleteModel, 100 | renameModel: _renameModel, 101 | loadShareOptions: _loadShareOptions, 102 | toggleShare: _toggleShare, 103 | getSharedModel: _getSharedModel, 104 | importModel: _importModel, 105 | duplicate: _duplicate 106 | }; 107 | }; 108 | 109 | export default angular 110 | .module("app.modelAPI", []) 111 | .factory("ModelAPI", authService).name; 112 | -------------------------------------------------------------------------------- /app/sass/elementActions.scss: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////////////// 2 | // .halo 3 | //////////////////////////////////////////////////////////////////////////////// 4 | .element-action { 5 | position: absolute; 6 | pointer-events: none 7 | } 8 | 9 | //////////////////////////////////////////////////////////////////////////////// 10 | // .handles 11 | //////////////////////////////////////////////////////////////////////////////// 12 | .element-action .holder { 13 | display: flex; 14 | position: absolute; 15 | top: -60px; 16 | border-radius: 4px 4px 4px 0; 17 | border: 4px solid hsl(0, 0%, 91%); 18 | background-color: var(--gray-90); 19 | box-shadow: -1px 0 0 rgba(0,0,0, 0.1), 20 | 0 -1px 1px rgba(0,0,0, 0.3); 21 | } 22 | 23 | .element-action .holder:after { 24 | content: ''; 25 | position: absolute; 26 | width: 0; 27 | height: 0; 28 | top: 100%; 29 | left: -4px; 30 | margin-top: 4px; 31 | border-top: 16px solid var(--gray-90); 32 | border-left: 0 solid transparent; 33 | border-right: 13px solid transparent; 34 | box-shadow: -1px 0 0 rgba(0,0,0, 0.2); 35 | } 36 | 37 | //////////////////////////////////////////////////////////////////////////////// 38 | // .halo .handle 39 | //////////////////////////////////////////////////////////////////////////////// 40 | .element-action .item.hidden {display: none;} 41 | 42 | .element-action .item { 43 | display: inline-block; 44 | position: relative; 45 | width: 36px; 46 | height: 36px; 47 | cursor: pointer; 48 | user-select: none; 49 | pointer-events: auto; 50 | background-size: 20px 20px; 51 | background-position: center; 52 | background-repeat: no-repeat; 53 | } 54 | 55 | .element-action .item:hover { 56 | background-color: var(--white); 57 | } 58 | 59 | //////////////////////////////////////////////////////////////////////////////// 60 | // .handle types 61 | //////////////////////////////////////////////////////////////////////////////// 62 | .item.clone, 63 | .item.link, 64 | .item.fork, 65 | .item.rotate {cursor: move;} 66 | 67 | .item.resize {cursor: se-resize;} 68 | 69 | 70 | //////////////////////////////////////////////////////////////////////////////// 71 | // select actions 72 | //////////////////////////////////////////////////////////////////////////////// 73 | .selection { 74 | position: absolute; 75 | border: 2px solid var(--brand-default); 76 | border-radius: 3px; 77 | overflow: visible; 78 | background-color: hsla(153, 43%, 42%, 0.2); 79 | } 80 | 81 | .selected .selection-wrapper { 82 | position: absolute; 83 | transform: scale(1.3); 84 | border: 2px solid var(--brand-default); 85 | border-radius: 3px; 86 | } 87 | 88 | .selection.selected { 89 | position: static; 90 | height: 0 !important; 91 | border: none; 92 | background-color: transparent; 93 | } 94 | 95 | .selection-box { 96 | position: absolute; 97 | border-radius: 2px; 98 | border: 1px dashed var(--brand-default); 99 | transform: scale(1.15); 100 | cursor: move; 101 | } -------------------------------------------------------------------------------- /app/sass/public.scss: -------------------------------------------------------------------------------- 1 | //////////////////////////////////////////////////////////////////////////////// 2 | // body (public) 3 | //////////////////////////////////////////////////////////////////////////////// 4 | .public { 5 | height: 100%; 6 | background-image: url(../img/wave01.svg), 7 | url(../img/wave02.svg), 8 | url(../img/wave03.svg); 9 | background-repeat: repeat-x; 10 | background-position: left bottom; 11 | 12 | } 13 | //////////////////////////////////////////////////////////////////////////////// 14 | // .login-form / .register-page 15 | //////////////////////////////////////////////////////////////////////////////// 16 | .login-page, 17 | .register-page, 18 | .recovery-page, 19 | .reset-page { 20 | position: relative; 21 | width: 375px; 22 | padding: 48px 24px 24px 24px; 23 | border: 1px solid var(--border-default); 24 | margin: 60px auto 0 auto; 25 | background-color: var(--white); 26 | box-shadow: 0 0 20px rgba(0,0,0, 0.1) 27 | } 28 | 29 | //////////////////////////////////////////////////////////////////////////////// 30 | // .login-form / .register-form 31 | //////////////////////////////////////////////////////////////////////////////// 32 | .login-form .br-button, 33 | .register-form .br-button, 34 | .recovery-form .br-button, 35 | .reset-form .br-button { 36 | padding: 10px 16px; 37 | font-size: 18px; 38 | } 39 | 40 | //////////////////////////////////////////////////////////////////////////////// 41 | // .public-logo 42 | //////////////////////////////////////////////////////////////////////////////// 43 | .public-logo { 44 | display: flex; 45 | align-items: center; 46 | justify-content: center; 47 | margin-bottom: 24px; 48 | } 49 | 50 | .public-logo > img { 51 | margin-right: 8px; 52 | } 53 | 54 | .public-logo > h1 { 55 | color: var(--brand-default); 56 | margin: 0; 57 | font-weight: 700; 58 | } 59 | 60 | //////////////////////////////////////////////////////////////////////////////// 61 | // .account-options 62 | //////////////////////////////////////////////////////////////////////////////// 63 | .account-options { 64 | display: flex; 65 | flex-direction: column; 66 | justify-content: center; 67 | margin-top: 12px; 68 | text-align: center; 69 | font-size: 1.2em; 70 | } 71 | 72 | .br-button.register { 73 | padding: 10px 16px; 74 | } 75 | 76 | //////////////////////////////////////////////////////////////////////////////// 77 | // .language-selector 78 | //////////////////////////////////////////////////////////////////////////////// 79 | .language-selector { 80 | width: 375px; 81 | margin: 24px auto 0 auto; 82 | text-align: center; 83 | } 84 | 85 | .language-selector h2 { 86 | font-size: 1.2em; 87 | font-weight: 400; 88 | margin: 0 0 8px 0; 89 | } 90 | 91 | .language-selector h2 .fa { 92 | font-size: 2rem; 93 | } 94 | 95 | .language-list { 96 | display: flex; 97 | justify-content: center; 98 | padding: 0; 99 | list-style: none; 100 | } 101 | 102 | .language-list li::after { 103 | content: "|"; 104 | display: inline-block; 105 | } 106 | 107 | .language-list li:last-child::after { 108 | display: none; 109 | } 110 | 111 | .language-list li a { 112 | margin: 4px; 113 | } -------------------------------------------------------------------------------- /cypress/support/commands.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare namespace Cypress { 4 | interface Chainable { 5 | /** 6 | * **Logs into the BR Modelo Wep App via the Graphical User Interface (GUI).** 7 | * 8 | * @param email string - The email of the user you want to log in with. Defaults to Cypress.env('user') if no value is provided. 9 | * @param password string - The password of the user you want to log in with. Defaults to Cypress.env('password') if no value is provided. 10 | * 11 | * @example cy.loginViaGui() // Logs into the app via GUI using the default email and password (defined as envs). 12 | * @example cy.loginViaGui('user@email.com', 'S3cRe7P@ssW0rd') // Logs into the app via GUI using the provided credentials. 13 | */ 14 | loginViaGui(username?: string, password?: string) 15 | 16 | /** 17 | * **Deletes all models of the logged in user.** 18 | * 19 | * This command is used for cleaning things up before tests start so that they start on a clean state. 20 | * 21 | * @param userModels object - An object with the response of the request to '/models?userId=*' 22 | * 23 | * @example cy.cleanUpUserModels(userModelsResponseObject) // Cleans up all models for the logged in user 24 | */ 25 | cleanUpUserModels(userModelsRespondeObject: object): Cypress.Chainable> 26 | 27 | /** 28 | * **Gets all user models for a specific userId via a GET request.** 29 | * 30 | * @param url string - The URL for getting all the models of a specific userId 31 | * 32 | * @example cy.getUserModelsViaApi('http://localhost:3000/models?userId=618f065ed18dc91b10651g98') // Gets all user models for userId=618f065ed18dc91b10651g98 33 | */ 34 | getUserModelsViaApi(url: string): Cypress.Chainable> 35 | 36 | /** 37 | * **Deletes a model via a DELETE request.** 38 | * 39 | * @param modelId string - The id of a model (of the logged in user) that you want to delete 40 | * 41 | * @example cy.deleteModelViaApi('61a8db9bc67e9824b66fc8g2') 42 | */ 43 | deleteModelViaApi(modelId: string): Cypress.Chainable> 44 | 45 | /** 46 | * **Creates a model via POST request.** 47 | * 48 | * @param type string - The type of the model you want to create. The allowed types are 'conceptual' and 'logical' 49 | * @param userId string - The id of the user for which you want to create the model 50 | * @param model object - An object with a cells property as an array, defining all "cells" of a model. Default is an object with an empty array as the value of the cells property 51 | * 52 | * @example cy.createModelViaApi('conceptual', '618f065ed18dc91b10650f99') // Creates a conceptual model for userId=618f065ed18dc91b10650f99 53 | * @example cy.createModelViaApi('logical', '618f065ed18dc91b10650f99') // Creates a logical model for userId=618f065ed18dc91b10650f99 54 | * @example cy.createModelViaApi('conceptual', '618f065ed18dc91b10650f99', conceptualModel) // Creates a conceptual model for userId=618f065ed18dc91b10650f99 passing a model object (`conceptualModel`), defined earlier, as a variable 55 | */ 56 | createModelViaApi(type: string, userId: string, model?: object): Cypress.Chainable> 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /app/angular/logic/columnForm.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 10 |
11 | 12 |
13 | 14 | 18 | 19 |
20 | 21 |
22 | 23 | 30 |
31 | 32 | 33 | 34 |
35 |
36 | 43 |
44 | 45 |
46 | 53 |
54 | 55 |
56 | 64 |
65 | 66 |
67 | 75 |
76 | 77 |
78 | 86 |
87 |
88 | 89 |
90 | 91 | 96 | 97 |
98 | 99 |
100 |
101 | 102 | 103 |
104 |
105 | 106 |
107 |
108 |
-------------------------------------------------------------------------------- /app/angular/conceptual/conceptual.html: -------------------------------------------------------------------------------- 1 |
4 | 24 | 25 |
26 |
27 | 28 | 29 |
30 | 31 |
32 |
33 | 43 | 54 | 59 |
60 |
61 | 62 |
63 |
-------------------------------------------------------------------------------- /app/angular/logic/logic.html: -------------------------------------------------------------------------------- 1 |
2 | 21 | 22 |
23 |
24 | 25 | 26 |
27 | 28 |
29 |
30 | 40 | 51 | 65 |
66 |
67 | 68 |
69 |
-------------------------------------------------------------------------------- /server_app/user/service.test.js: -------------------------------------------------------------------------------- 1 | const userService = require("./service"); 2 | 3 | jest.mock("./model"); 4 | jest.mock("../model/service"); 5 | const UserRepositoryMock = require("./model"); 6 | const ModelServiceMock = require("../model/service"); 7 | 8 | afterEach(() => { 9 | jest.restoreAllMocks(); 10 | }); 11 | 12 | describe("login", () => { 13 | 14 | test('should return user when login params match', async () => { 15 | const userParam = {username: 'user@mail.com', password: "123456"}; 16 | 17 | const docResponse = { 18 | "id": "507f191e810c19729de860ea", 19 | "name": "name" 20 | }; 21 | 22 | UserRepositoryMock.findOne.mockResolvedValue(docResponse); 23 | 24 | const userSession = await userService.login(userParam); 25 | 26 | const expectedResponse = { 27 | "userId": "507f191e810c19729de860ea", 28 | "userName": "name" 29 | } 30 | 31 | expect(userSession).toMatchObject(expectedResponse); 32 | }); 33 | 34 | test('should return null when login not found', async () => { 35 | const userParam = {username: 'user@mail.com', password: "123456", sessionId:"123456"}; 36 | 37 | UserRepositoryMock.findOne.mockResolvedValue(null); 38 | 39 | const userSession = await userService.login(userParam); 40 | 41 | expect(userSession).toBeNull(); 42 | }); 43 | 44 | test('should catch error when service fires it', async () => { 45 | const userParam = {username: 'user@mail.com', password: "123456", sessionId:"123456"}; 46 | 47 | UserRepositoryMock.findOne.mockResolvedValue(new Error("find error")); 48 | 49 | expect(() => { 50 | userService.login(userParam).toThrow(); 51 | }); 52 | }); 53 | 54 | }); 55 | 56 | describe("create", () => { 57 | 58 | test('should toThrow error when user already exists', async () => { 59 | const userParam = {username: 'user@mail.com', password: "123456", sessionId:"123456"}; 60 | 61 | const docResponse = { 62 | _id: '507f191e810c19729de860ea', 63 | name: 'name', 64 | login: 'user@mail.com', 65 | password: '123456' 66 | }; 67 | 68 | UserRepositoryMock.findOne.mockResolvedValue(docResponse); 69 | 70 | expect(() => { 71 | userService.create(userParam).toThrow(); 72 | }); 73 | 74 | }); 75 | 76 | 77 | test('should create user when it does not exists yet', async () => { 78 | const userParam = {username: 'name', password: "123456", mail:"user@mail.com"}; 79 | 80 | const docResponse = { 81 | _id: '507f191e810c19729de860ea', 82 | name: 'name', 83 | login: 'user@mail.com', 84 | password: '123456' 85 | }; 86 | 87 | UserRepositoryMock.findOne.mockResolvedValue(null); 88 | 89 | UserRepositoryMock.create.mockResolvedValue(docResponse); 90 | 91 | const createResponse = await userService.create(userParam); 92 | 93 | expect(createResponse.login).toBe(docResponse.login); 94 | }); 95 | 96 | }); 97 | 98 | describe("remove", () => { 99 | 100 | test('should toThrow error when user has models', async () => { 101 | 102 | ModelServiceMock.countAll.mockResolvedValue(1); 103 | 104 | expect(() => { 105 | userService.deleteAccount("123456").toThrow(); 106 | }); 107 | 108 | }); 109 | 110 | 111 | test('should delete account', async () => { 112 | ModelServiceMock.countAll.mockResolvedValue(0); 113 | 114 | UserRepositoryMock.deleteOne.mockResolvedValue({"deletedCount": 1}); 115 | 116 | const deleteResponse = await userService.deleteAccount("507f191e810c19729de860ea"); 117 | 118 | expect(deleteResponse).toBe(true); 119 | }); 120 | 121 | }); 122 | 123 | -------------------------------------------------------------------------------- /app/angular/workspace/workspace.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 15 | 16 |
17 |
18 | 19 | 33 | 34 | 41 |
42 | 43 |
44 | Loading 50 |
51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 68 | 72 | 73 | 74 | 80 | 81 | 82 |
{{ 'Type' | translate }}{{ 'Name' | translate }}{{ 'Author' | translate }}{{ 'Created' | translate }}{{ 'Actions' | translate }}
64 | 65 | 66 | {{model.typeName}} 67 | 69 | 70 | {{model.name}} 71 | {{model.authorName}}{{model.created | date:'dd/MM/yyyy'}} 75 | 76 | 77 | 78 | 79 |
83 |
84 |
85 | 86 |
87 | 88 |
89 |
90 |
-------------------------------------------------------------------------------- /app/angular/publicview/publicview.js: -------------------------------------------------------------------------------- 1 | import "backbone"; 2 | import $ from "jquery"; 3 | import "./publicView.scss" 4 | 5 | import * as joint from "jointjs/dist/joint"; 6 | import erd from "../../joint/shapes"; 7 | import uml from "../../joint/table"; 8 | joint.shapes.erd = erd; 9 | joint.shapes.uml = uml; 10 | 11 | import angular from "angular"; 12 | import template from "./publicview.html"; 13 | 14 | import "../editor/editorScroller"; 15 | 16 | import statusBar from "../components/statusBar"; 17 | import KeyboardController, { types } from "../components/keyboardController"; 18 | import iconModelConceptual from "../components/icons/conceptual" 19 | import iconModelLogic from "../components/icons/logic" 20 | 21 | const controller = function (ModelAPI, $stateParams, $timeout, $state) { 22 | const ctrl = this; 23 | ctrl.loading = false; 24 | ctrl.model = { 25 | name: "" 26 | } 27 | const configs = { 28 | graph: {}, 29 | paper: {} 30 | }; 31 | 32 | ctrl.setLoading = (show) => { 33 | $timeout(() => { 34 | ctrl.loading = show; 35 | }); 36 | } 37 | 38 | ctrl.print = () => { 39 | window.print(); 40 | } 41 | 42 | ctrl.zoomIn = () => { 43 | configs.editorScroller.zoom(0.1, { max: 2 }); 44 | } 45 | 46 | ctrl.zoomOut = () => { 47 | configs.editorScroller.zoom(-0.1, { min: 0.2 }); 48 | } 49 | 50 | ctrl.zoomNone = () => { 51 | configs.editorScroller.zoom(); 52 | } 53 | 54 | const registerShortcuts = () => { 55 | configs.keyboardController.registerHandler(types.ZOOM_IN, () => ctrl.zoomIn()); 56 | configs.keyboardController.registerHandler(types.ZOOM_OUT, () => ctrl.zoomOut()); 57 | configs.keyboardController.registerHandler(types.ZOOM_NONE, () => ctrl.zoomNone()); 58 | } 59 | 60 | const buildWorkspace = () => { 61 | configs.graph = new joint.dia.Graph({}, { cellNamespace: joint.shapes }); 62 | 63 | const content = $("#content"); 64 | 65 | configs.paper = new joint.dia.Paper({ 66 | width: content.width(), 67 | height: content.height(), 68 | gridSize: 10, 69 | drawGrid: true, 70 | model: configs.graph, 71 | linkPinning: false, 72 | cellViewNamespace: joint.shapes 73 | }); 74 | 75 | configs.keyboardController = new KeyboardController(configs.paper.$document); 76 | 77 | configs.editorScroller = new joint.ui.EditorScroller({ 78 | paper: configs.paper, 79 | cursor: "grabbing", 80 | autoResizePaper: true, 81 | }); 82 | content.append(configs.editorScroller.render().el); 83 | 84 | registerShortcuts(); 85 | registerPaperEvents(configs.paper); 86 | }; 87 | 88 | const registerPaperEvents = (paper) => { 89 | paper.on('blank:pointerdown', (evt) => { 90 | if(configs.keyboardController.spacePressed){ 91 | configs.editorScroller.startPanning(evt); 92 | } 93 | }); 94 | } 95 | 96 | ctrl.$postLink = () => { 97 | buildWorkspace(); 98 | }; 99 | 100 | ctrl.$onInit = () => { 101 | ctrl.setLoading(true); 102 | 103 | ModelAPI.getSharedModel($stateParams.modelshareid).then((resp) => { 104 | const jsonModel = (typeof resp.data.model == "string") ? JSON.parse(resp.data.model) : resp.data.model; 105 | if(resp.data.type === 'conceptual') { 106 | configs.paper.options.linkConnectionPoint = joint.util.shapePerimeterConnectionPoint; 107 | } 108 | configs.graph.fromJSON(jsonModel); 109 | ctrl.model.name = resp.data.name; 110 | ctrl.model.type = resp.data.type; 111 | ctrl.setLoading(false); 112 | configs.paper.freeze(); 113 | }).catch((error) => { 114 | ctrl.setLoading(false); 115 | if(error.status == 404 || error.status == 401) { 116 | $state.go("noaccess"); 117 | } 118 | }); 119 | } 120 | 121 | ctrl.$onDestroy = () => { 122 | configs.graph = null; 123 | configs.paper = null; 124 | configs.keyboardController.unbindAll(); 125 | configs.keyboardController = null; 126 | } 127 | }; 128 | 129 | export default angular 130 | .module("app.publicview", [statusBar, iconModelConceptual, iconModelLogic]) 131 | .component("publicview", { 132 | template, 133 | controller, 134 | }).name; -------------------------------------------------------------------------------- /app/angular/components/queryExpressionModal/index.js: -------------------------------------------------------------------------------- 1 | import angular from "angular"; 2 | import template from "./template.html"; 3 | import "./styles.scss"; 4 | 5 | const app = angular.module("app.queryExpressionModalController", []); 6 | 7 | const AND_OPERATOR = 'AND'; 8 | const OR_OPERATOR = 'OR'; 9 | 10 | const Controller = function ($filter) { 11 | const $ctrl = this; 12 | $ctrl.submitted = false; 13 | $ctrl.conditions = []; 14 | $ctrl.joins = []; 15 | $ctrl.tableColumns = []; 16 | 17 | const defaultJoin = { 18 | columnNameOrigin: null, 19 | columnNameTarget: null, 20 | } 21 | 22 | const defaultCondition = { 23 | columnName: null, 24 | columnType: null, 25 | type: null, 26 | comparativeValue: null, 27 | submitted: false, 28 | logicalOperator: AND_OPERATOR, 29 | }; 30 | 31 | $ctrl.selectColumnJoin = (selected, attribute, index) => { 32 | const selectedJoin = $ctrl.joins[index]; 33 | $ctrl.joins[index] = { 34 | ...selectedJoin, 35 | [attribute]: selected.name, 36 | }; 37 | } 38 | 39 | $ctrl.saveJoin = (index) => { 40 | const selectedJoin = $ctrl.joins[index]; 41 | $ctrl.joins[index] = { 42 | ...selectedJoin, 43 | submitted: true, 44 | }; 45 | } 46 | 47 | $ctrl.removeJoin = (index) => { 48 | $ctrl.joins.splice(index, 1); 49 | } 50 | 51 | $ctrl.createLabel = condition => { 52 | return `${condition.columnName} ${$filter('translate')(condition.name).toLowerCase()} ${condition.comparativeValue} ${condition.comparativeValue2 ? `${$filter('translate')('and')} ${condition.comparativeValue2}` : '' }`; 53 | } 54 | 55 | $ctrl.changeOperator = (index) => { 56 | const selectedCondition = $ctrl.conditions[index]; 57 | $ctrl.conditions[index] = { 58 | ...selectedCondition, 59 | logicalOperator: selectedCondition.logicalOperator === AND_OPERATOR ? OR_OPERATOR : AND_OPERATOR, 60 | }; 61 | } 62 | 63 | $ctrl.selectComparasion = (selected, index) => { 64 | const selectedCondition = $ctrl.conditions[index]; 65 | $ctrl.conditions[index] = { 66 | ...selectedCondition, 67 | name: selected.name, 68 | type: selected.type, 69 | }; 70 | }; 71 | 72 | $ctrl.removeCondition = (index) => { 73 | $ctrl.conditions.splice(index, 1); 74 | } 75 | 76 | $ctrl.saveCondition = (index) => { 77 | const selectedCondition = $ctrl.conditions[index]; 78 | $ctrl.conditions[index] = { 79 | ...selectedCondition, 80 | submitted: true, 81 | }; 82 | } 83 | 84 | $ctrl.selectColumn = (selected, index) => { 85 | const selectedCondition = $ctrl.conditions[index]; 86 | $ctrl.conditions[index] = { 87 | ...selectedCondition, 88 | columnName: selected.name, 89 | columnType: selected.type 90 | }; 91 | } 92 | 93 | $ctrl.addCondition = () => { 94 | $ctrl.conditions.push(defaultCondition); 95 | }; 96 | 97 | $ctrl.addJoin = () => { 98 | $ctrl.joins.push(defaultJoin); 99 | } 100 | 101 | $ctrl.$onInit = () => { 102 | $ctrl.showJoins = $ctrl.tables.length > 1; 103 | $ctrl.tables.forEach(table => { 104 | table.columns.forEach(column => { 105 | $ctrl.tableColumns.push({ 106 | name: `${table.name}.${column.name}`, 107 | type: column.type, 108 | }); 109 | }); 110 | }); 111 | }; 112 | 113 | $ctrl.$onChanges = (changes) => { 114 | if (changes.queryConditions != null && changes.queryConditions.currentValue != null) { 115 | $ctrl.conditions = changes.queryConditions.currentValue.values || []; 116 | $ctrl.joins = changes.queryConditions.currentValue.joins || []; 117 | } 118 | } 119 | 120 | $ctrl.save = function () { 121 | $ctrl.close({ 122 | result: { 123 | conditions: $ctrl.conditions.filter(condition => condition.submitted), 124 | joins: $ctrl.joins.filter(join => join.submitted), 125 | } 126 | }); 127 | }; 128 | 129 | $ctrl.cancel = function () { 130 | $ctrl.dismiss({ 131 | reason: "cancel", 132 | }); 133 | }; 134 | }; 135 | 136 | export default app.component("queryExpressionModal", { 137 | template, 138 | bindings: { 139 | close: "&", 140 | dismiss: "&", 141 | tables: "<", 142 | queryConditions: '<' 143 | }, 144 | controller: Controller, 145 | }).name; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "brmodeloweb", 3 | "version": "0.0.1", 4 | "private": true, 5 | "scripts": { 6 | "start": "node server.js", 7 | "start:dev": "export NODE_ENV=development && nodemon server.js", 8 | "start:frontend": "export NODE_ENV=development && webpack-dev-server --hot --progress --color", 9 | "build": "export NODE_ENV=production && rimraf ./app/dist && webpack --progress --color", 10 | "test": "jest --coverage", 11 | "test:cy": "yarn cypress run", 12 | "test:watch": "jest --watch", 13 | "cy:open": "yarn cypress open" 14 | }, 15 | "engines": { 16 | "node": "20.x", 17 | "yarn": "1.x" 18 | }, 19 | "dependencies": { 20 | "@babel/runtime": "^7.26.10", 21 | "@uirouter/angularjs": "^1.1.0", 22 | "angular": "^1.8.3", 23 | "angular-cookies": "^1.8.3", 24 | "angular-translate": "^2.18.3", 25 | "angular-ui-bootstrap": "^2.5.6", 26 | "angular-ui-router": "^1.0.30", 27 | "body-parser": "^1.20.2", 28 | "bootstrap": "^3.4.1", 29 | "cors": "^2.8.5", 30 | "dotenv": "^16.3.1", 31 | "errorhandler": "^1.5.1", 32 | "express": "^4.20.0", 33 | "express-session": "^1.17.3", 34 | "express-sslify": "^1.2.0", 35 | "font-awesome": "^4.7.0", 36 | "form-data": "^4.0.4", 37 | "hotkeys-js": "^3.12.0", 38 | "i18next": "^23.5.1", 39 | "i18next-browser-languagedetector": "^7.1.0", 40 | "jointjs": "3.7.5", 41 | "jsonwebtoken": "^9.0.2", 42 | "mailgun.js": "^12.0.1", 43 | "mongodb": "~4.17.0", 44 | "mongoose": "^6.13.6", 45 | "morgan": "^1.10.0", 46 | "oclazyload": "^1.1.0", 47 | "prop-types": "^15.8.1", 48 | "raw-body": "^2.5.2", 49 | "react": "^18.2.0", 50 | "react-dom": "^18.2.0", 51 | "react-i18next": "^13.2.2", 52 | "react2angular": "^4.0.6", 53 | "styled-components": "^6.0.8", 54 | "textangular": "^1.5.16", 55 | "typescript": "^5.2.2" 56 | }, 57 | "devDependencies": { 58 | "@4tw/cypress-drag-drop": "^2.2.5", 59 | "@babel/core": "^7.23.0", 60 | "@babel/node": "^7.22.19", 61 | "@babel/plugin-proposal-class-properties": "^7.18.6", 62 | "@babel/plugin-syntax-dynamic-import": "^7.0.0", 63 | "@babel/plugin-transform-async-to-generator": "^7.22.5", 64 | "@babel/plugin-transform-runtime": "^7.22.15", 65 | "@babel/preset-env": "^7.22.20", 66 | "@babel/preset-react": "^7.22.15", 67 | "@faker-js/faker": "^8.2.0", 68 | "@types/react": "^18.2.24", 69 | "@types/styled-components": "^5.1.28", 70 | "@typescript-eslint/eslint-plugin": "^6.7.3", 71 | "@typescript-eslint/parser": "^6.7.3", 72 | "@webpack-cli/serve": "^2.0.5", 73 | "autoprefixer": "10.4.16", 74 | "babel-loader": "^9.1.3", 75 | "babel-plugin-angularjs-annotate": "^0.10.0", 76 | "css-loader": "^6.8.1", 77 | "cssnano": "6.0.1", 78 | "cypress": "^13.3.0", 79 | "dotenv-webpack": "^8.0.1", 80 | "eslint": "^8.50.0", 81 | "eslint-config-airbnb": "^19.0.4", 82 | "eslint-config-airbnb-base": "^15.0.0", 83 | "eslint-config-prettier": "^9.0.0", 84 | "eslint-import-resolver-typescript": "^3.6.1", 85 | "eslint-plugin-cypress": "^2.15.1", 86 | "eslint-plugin-import": "^2.28.1", 87 | "eslint-plugin-prettier": "^5.0.0", 88 | "eslint-plugin-react": "^7.33.2", 89 | "file-loader": "^6.2.0", 90 | "html-loader": "^4.2.0", 91 | "html-webpack-plugin": "5.5.3", 92 | "jest": "^29.7.0", 93 | "mini-css-extract-plugin": "^2.7.6", 94 | "nodemon": "^3.0.1", 95 | "postcss-loader": "^7.3.3", 96 | "prettier": "^3.0.3", 97 | "raw-loader": "^4.0.2", 98 | "rimraf": "^5.0.5", 99 | "sass": "^1.68.0", 100 | "sass-loader": "^13.3.2", 101 | "style-loader": "^3.3.3", 102 | "supertest": "^6.3.3", 103 | "terser-webpack-plugin": "^5.3.9", 104 | "ts-loader": "^9.4.4", 105 | "tsconfig-paths-webpack-plugin": "^4.1.0", 106 | "url-loader": "^4.1.1", 107 | "webpack": "5.94.0", 108 | "webpack-cli": "^5.1.4", 109 | "webpack-dev-server": "^5.2.1" 110 | }, 111 | "resolutions": { 112 | "styled-components": "^5" 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /app/sass/mainHeader.scss: -------------------------------------------------------------------------------- 1 | /////////////////////////////////////////////////////////////////////////////// 2 | // .navbar-brand 3 | /////////////////////////////////////////////////////////////////////////////// 4 | .navbar-default .navbar-brand, 5 | .navbar-default .navbar-brand:hover { 6 | color: var(--brand-default); 7 | cursor: default; 8 | padding-right: 0; 9 | } 10 | 11 | /////////////////////////////////////////////////////////////////////////////// 12 | // .nav-bar .back-button 13 | /////////////////////////////////////////////////////////////////////////////// 14 | .navbar-default .back-button { 15 | display: block; 16 | float: left; 17 | color: var(--brand-default); 18 | font-size: 1.2em; 19 | padding: 13px 16px; 20 | } 21 | 22 | .navbar-default .back-button:hover { 23 | color: var(--brand-default); 24 | background-color: #e1e1e1; 25 | } 26 | 27 | /////////////////////////////////////////////////////////////////////////////// 28 | // purelly icon nav 29 | /////////////////////////////////////////////////////////////////////////////// 30 | .nav-icons .fa { 31 | font-size: 1.5em; 32 | } 33 | 34 | /////////////////////////////////////////////////////////////////////////////// 35 | // toolbar with icons 36 | /////////////////////////////////////////////////////////////////////////////// 37 | .tool-icons { 38 | float: none; 39 | width: 100%; 40 | border-bottom: 1px solid var(--border-default); 41 | } 42 | 43 | .tool-icons .fa { 44 | font-size: 1.2em; 45 | } 46 | 47 | .navbar-nav.tool-icons > li > a { 48 | padding: 8px 12px; 49 | } 50 | 51 | .tool-icons .divider { 52 | border-right: 1px solid var(--border-default); 53 | } 54 | 55 | //////////////////////////////////////////////////////////////////////////////// 56 | // .modelOptions 57 | //////////////////////////////////////////////////////////////////////////////// 58 | .modelOptions {float: left;} 59 | 60 | .modelName, 61 | .modelViewiewOptions { 62 | float: left; 63 | padding: 8px 0; 64 | margin: 0 10px; 65 | } 66 | 67 | .modelName input { 68 | min-width: 250px; 69 | padding: 3px 25px 3px 5px; 70 | border-color: 1px solid var(--border-default); 71 | background-color: rgba(255, 255, 255, 0.1); 72 | color: grey; 73 | } 74 | 75 | .inputPencil { 76 | float: right; 77 | margin-right: 6px; 78 | margin-top: -20px; 79 | position: relative; 80 | z-index: 2; 81 | color: grey; 82 | } 83 | 84 | //////////////////////////////////////////////////////////////////////////////// 85 | // .modelViewiewOptions 86 | //////////////////////////////////////////////////////////////////////////////// 87 | .modelViewiewOptions { 88 | position: relative; 89 | top: 8px; 90 | } 91 | 92 | .modelViewiewOptions a { 93 | padding: 5px 10px 5px 10px; 94 | color: black; 95 | position: relative; 96 | } 97 | 98 | .modelViewiewOptions a:hover {opacity: 0.6;} 99 | .modelViewiewOptions a:active {opacity: 0.2;} 100 | 101 | //////////////////////////////////////////////////////////////////////////////// 102 | // Loading 103 | //////////////////////////////////////////////////////////////////////////////// 104 | .br-loader { 105 | display: none; 106 | position: fixed; 107 | top: 70px; 108 | left: 50%; 109 | margin-left: -20px; 110 | padding: 3px;; 111 | border: 1px solid var(--border-light); 112 | border-radius: 4px; 113 | background-color: var(--white); 114 | } 115 | 116 | .br-loader.loading {display: block;} 117 | 118 | /////////////////////////////////////////////////////////////////////////////// 119 | // .nav-icons dropdown-icon 120 | /////////////////////////////////////////////////////////////////////////////// 121 | .nav-icons > dropdown-icon > .dropdown > .dropdown-menu { 122 | margin-top: 0; 123 | border-top-left-radius: 0; 124 | border-top-right-radius: 0; 125 | } 126 | 127 | .nav-icons > dropdown-icon > .dropdown > button { 128 | border: 0; 129 | padding: 15px; 130 | color: var(--gray-20); 131 | background-color: transparent; 132 | } 133 | 134 | .nav-icons > dropdown-icon > .dropdown > button:focus { 135 | text-decoration: none; 136 | box-shadow: none; 137 | background-color: var(--gray-90); 138 | } 139 | 140 | .nav-icons .dropdown-menu > li > a { 141 | color: var(--gray-20); 142 | } --------------------------------------------------------------------------------