| @ -0,0 +1 @@ | |||||
| If you find software that doesn’t have a license, that means you have no permission from the creators of the software to use, modify, or share the software. Although a code host such as GitHub may allow you to view and fork the code, this does not imply that you are permitted to use, modify, or share the software for any purpose. | |||||
| @ -0,0 +1,45 @@ | |||||
| # Kanban | |||||
| ## Description | |||||
| Proof of concept of a kanban style application. | |||||
| ### Technology stack | |||||
| + [MongoDB](https://www.mongodb.com/) for the database with [Mongoose](https://mongoosejs.com/) as the Object Data Manager. | |||||
| + [ExpressJS](https://expressjs.com/) as the backend web framework. | |||||
| + [React](https://reactjs.org/) for the frontend with [Redux](https://redux.js.org/) to handle the application states. | |||||
| + [NodeJS](https://nodejs.org) as the JavaScript runtime. | |||||
| ## Installation | |||||
| ### Using docker | |||||
| Use the provided `docker-compose.yml` script to launch the application directly. | |||||
| While being at the root of the application, use: | |||||
| ```bash | |||||
| docker-compose up | |||||
| ``` | |||||
| ### Using npm | |||||
| If you prefer launching the application directly, you will need to provide a MongoDB instance for the application to connect. | |||||
| Edit `back/config/keys` with the necessary information. | |||||
| You can then launch both the back and frontend with ```npm start``` at the root of the application. | |||||
| ## Usage | |||||
| Navigate to [127.0.0.1:3000](http://127.0.0.1:3000) to see the frontend. | |||||
| Backend accessible via [127.0.0.1:3001](http://127.0.0.1:3001) (obviously not much to see here) | |||||
| ## Screenshots | |||||
| **Main screen populated** | |||||
|  | |||||
| **Task update screen** | |||||
|  | |||||
| @ -0,0 +1,115 @@ | |||||
| # Logs | |||||
| logs | |||||
| *.log | |||||
| npm-debug.log* | |||||
| yarn-debug.log* | |||||
| yarn-error.log* | |||||
| lerna-debug.log* | |||||
| # Diagnostic reports (https://nodejs.org/api/report.html) | |||||
| report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json | |||||
| # Runtime data | |||||
| pids | |||||
| *.pid | |||||
| *.seed | |||||
| *.pid.lock | |||||
| # Directory for instrumented libs generated by jscoverage/JSCover | |||||
| lib-cov | |||||
| # Coverage directory used by tools like istanbul | |||||
| coverage | |||||
| *.lcov | |||||
| # nyc test coverage | |||||
| .nyc_output | |||||
| # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) | |||||
| .grunt | |||||
| # Bower dependency directory (https://bower.io/) | |||||
| bower_components | |||||
| # node-waf configuration | |||||
| .lock-wscript | |||||
| # Compiled binary addons (https://nodejs.org/api/addons.html) | |||||
| build/Release | |||||
| # Dependency directories | |||||
| node_modules/ | |||||
| jspm_packages/ | |||||
| # Snowpack dependency directory (https://snowpack.dev/) | |||||
| web_modules/ | |||||
| # TypeScript cache | |||||
| *.tsbuildinfo | |||||
| # Optional npm cache directory | |||||
| .npm | |||||
| # Optional eslint cache | |||||
| .eslintcache | |||||
| # Microbundle cache | |||||
| .rpt2_cache/ | |||||
| .rts2_cache_cjs/ | |||||
| .rts2_cache_es/ | |||||
| .rts2_cache_umd/ | |||||
| # Optional REPL history | |||||
| .node_repl_history | |||||
| # Output of 'npm pack' | |||||
| *.tgz | |||||
| # Yarn Integrity file | |||||
| .yarn-integrity | |||||
| # dotenv environment variables file | |||||
| .env | |||||
| .env.test | |||||
| # parcel-bundler cache (https://parceljs.org/) | |||||
| .cache | |||||
| .parcel-cache | |||||
| # Next.js build output | |||||
| .next | |||||
| # Nuxt.js build / generate output | |||||
| .nuxt | |||||
| dist | |||||
| # Gatsby files | |||||
| .cache/ | |||||
| # Comment in the public line in if your project uses Gatsby and not Next.js | |||||
| # https://nextjs.org/blog/next-9-1#public-directory-support | |||||
| # public | |||||
| # vuepress build output | |||||
| .vuepress/dist | |||||
| # Serverless directories | |||||
| .serverless/ | |||||
| # FuseBox cache | |||||
| .fusebox/ | |||||
| # DynamoDB Local files | |||||
| .dynamodb/ | |||||
| # TernJS port file | |||||
| .tern-port | |||||
| # Stores VSCode versions used for testing VSCode extensions | |||||
| .vscode-test | |||||
| # yarn v2 | |||||
| .yarn/cache | |||||
| .yarn/unplugged | |||||
| .yarn/build-state.yml | |||||
| .pnp.* | |||||
| @ -0,0 +1,6 @@ | |||||
| { | |||||
| "express":{ | |||||
| "port":3001, | |||||
| "baseUrl": "/api" | |||||
| } | |||||
| } | |||||
| @ -0,0 +1,9 @@ | |||||
| const mongodb_user="test"; | |||||
| const mongodb_password="test"; | |||||
| const mongodb_url="mongo"; | |||||
| const mongodb_database="kanban-mongo" | |||||
| module.exports = { | |||||
| //mongoURI: 'mongodb://'+mongodb_user+":"+mongodb_password+"@"+mongodb_url+"/"+mongodb_database | |||||
| mongoURI: 'mongodb://'+mongodb_url+"/"+mongodb_database | |||||
| } | |||||
| @ -0,0 +1,6 @@ | |||||
| { | |||||
| "express":{ | |||||
| "port":2999, | |||||
| "baseUrl": "/api" | |||||
| } | |||||
| } | |||||
| @ -0,0 +1,15 @@ | |||||
| const keys=require("../config/keys.js"); | |||||
| const mongoose=require("mongoose"); | |||||
| exports.connectToDatabase=()=>{ | |||||
| return mongoose.connect( | |||||
| keys.mongoURI, | |||||
| { | |||||
| useNewUrlParser: true, | |||||
| useUnifiedTopology: true, | |||||
| useFindAndModify: false | |||||
| }); | |||||
| } | |||||
| exports.disconnectFromDatabase=()=>{ | |||||
| return mongoose.disconnect(); | |||||
| } | |||||
| @ -0,0 +1,26 @@ | |||||
| const { findByIdAndUpdate } = require('../model/Task'); | |||||
| const Model=require('../model/Task'); | |||||
| exports.findAllTask=()=>{ | |||||
| return Model | |||||
| .find() | |||||
| .sort({name:"asc"}) | |||||
| } | |||||
| exports.findTaskById=(id)=>{ | |||||
| return Model.findById(id); | |||||
| } | |||||
| exports.addTask=(taskData)=>{ | |||||
| let task=new Model(taskData); | |||||
| let errorValidate=task.validateSync(); | |||||
| if(errorValidate){ | |||||
| throw errorValidate; | |||||
| } | |||||
| return task.save(); | |||||
| } | |||||
| exports.updateTask=(id,update)=>{ | |||||
| return Model.findByIdAndUpdate(id,update,{new:true, runValidators:true}); | |||||
| } | |||||
| exports.deleteTask=(id)=>{ | |||||
| return Model.findByIdAndDelete(id); | |||||
| } | |||||
| @ -0,0 +1,24 @@ | |||||
| const express=require("express"); | |||||
| const bodyParser=require("body-parser"); | |||||
| const config=require("config"); | |||||
| const { connectToDatabase } = require("./controller/MongoController.js"); | |||||
| const taskRoute=require("./route/TaskRoute"); | |||||
| const webapp=express(); | |||||
| connectToDatabase() | |||||
| .then(()=>console.log("Database online")) | |||||
| .catch(err=>console.error(err)); | |||||
| webapp.use(bodyParser.json()); | |||||
| webapp.use(config.express.baseUrl+"/task",taskRoute); | |||||
| function startWebServer(){ | |||||
| if(config.util.getEnv('NODE_ENV') !== 'test') { | |||||
| webapp.listen( | |||||
| config.express.port, | |||||
| ()=>console.log("Web server started on port "+config.express.port) | |||||
| ); | |||||
| } | |||||
| } | |||||
| startWebServer(); | |||||
| module.exports=webapp; | |||||
| @ -0,0 +1,22 @@ | |||||
| const mongoose=require("mongoose"); | |||||
| const validatorId=require('mongoose-id-validator'); | |||||
| var taskSchema=new mongoose.Schema({ | |||||
| name:{ type: String, required: true }, | |||||
| description:{ type: String, default:"" }, | |||||
| parent:{ type: mongoose.Schema.Types.ObjectId, ref: 'Task' } | |||||
| }); | |||||
| taskSchema.plugin(validatorId); | |||||
| taskSchema.pre("findOneAndUpdate",function(next){ | |||||
| if( | |||||
| this._update | |||||
| && this._update.hasOwnProperty("parent") | |||||
| && this._update.parent.toString()===this._conditions._id.toString() | |||||
| ){ | |||||
| next(new Error("parent must be different of _id.")); | |||||
| }else{ | |||||
| next(); | |||||
| } | |||||
| }); | |||||
| module.exports=mongoose.model("Task",taskSchema); | |||||
| @ -0,0 +1,30 @@ | |||||
| { | |||||
| "name": "back", | |||||
| "version": "1.0.0", | |||||
| "description": "", | |||||
| "main": "index.js", | |||||
| "scripts": { | |||||
| "start": "node index.js", | |||||
| "dev": "nodemon index.js", | |||||
| "test": "nyc mocha --recursive test/controller && nyc mocha --recursive test/route" | |||||
| }, | |||||
| "keywords": [], | |||||
| "author": "", | |||||
| "license": "ISC", | |||||
| "dependencies": { | |||||
| "body-parser": "^1.19.0", | |||||
| "config": "^3.3.6", | |||||
| "express": "^4.17.1", | |||||
| "express-rate-limit": "^5.2.6", | |||||
| "express-slow-down": "^1.4.0", | |||||
| "mongoose": "5.11.15", | |||||
| "mongoose-id-validator": "^0.6.0" | |||||
| }, | |||||
| "devDependencies": { | |||||
| "chai": "^4.3.3", | |||||
| "chai-http": "^4.3.0", | |||||
| "mocha": "^8.3.1", | |||||
| "nodemon": "^2.0.7", | |||||
| "nyc": "^15.1.0" | |||||
| } | |||||
| } | |||||
| @ -0,0 +1,75 @@ | |||||
| const express=require("express"); | |||||
| const { findAllTask, findTaskById, addTask, updateTask, deleteTask } = require("../controller/TaskController"); | |||||
| const router=express.Router(); | |||||
| const Model=require('../model/Task'); | |||||
| router.get("/", (req, res) =>{ | |||||
| findAllTask().then((value)=>{ | |||||
| res.status(200).send(value); | |||||
| }).catch((reason)=>{ | |||||
| res.status(500).send(reason); | |||||
| }); | |||||
| }); | |||||
| router.get("/:id", (req, res) =>{ | |||||
| findTaskById(req.params.id).then((value)=>{ | |||||
| res.status(200).send(value); | |||||
| }).catch((reason)=>{ | |||||
| res.status(500).send(reason); | |||||
| }); | |||||
| }); | |||||
| router.post("/", (req, res) =>{ | |||||
| //Delete parent if set to "" | |||||
| if(req.body.parent===""){ | |||||
| delete req.body.parent; | |||||
| } | |||||
| try{ | |||||
| addTask(req.body).then((value)=>{ | |||||
| res.status(200).send(value); | |||||
| }).catch((reason)=>{ | |||||
| res.status(500).send(reason); | |||||
| }); | |||||
| }catch(error){ | |||||
| res.status(500).send(error); | |||||
| } | |||||
| }); | |||||
| router.put("/:id",(req,res)=>{ | |||||
| //Delete parent if set to "" | |||||
| if(req.body.parent===""){ | |||||
| delete req.body.parent; | |||||
| req.body=Object.assign(req.body, {$unset:{parent:1}}); | |||||
| } | |||||
| updateTask(req.params.id,req.body) | |||||
| .then((value)=> | |||||
| { | |||||
| if(value===null){ | |||||
| res.status(500).send("No changes, invalid id ?"); | |||||
| }else{ | |||||
| res.status(200).send(value); | |||||
| } | |||||
| },(reason)=>{ | |||||
| res.status(500).send(reason.toString()); | |||||
| }) | |||||
| .catch((reason)=>{ | |||||
| res.status(500).send(reason); | |||||
| }); | |||||
| }); | |||||
| router.delete("/:id", (req, res) =>{ | |||||
| deleteTask(req.params.id).then((value)=>{ | |||||
| if(value===null){ | |||||
| res.status(500).send("No changes, invalid id ?"); | |||||
| }else{ | |||||
| res.status(200).send(value); | |||||
| } | |||||
| }).catch((reason)=>{ | |||||
| res.status(500).send(reason); | |||||
| }); | |||||
| }); | |||||
| module.exports=router; | |||||
| @ -0,0 +1,171 @@ | |||||
| process.env.NODE_ENV = 'test'; | |||||
| const { describe }=require("mocha"); | |||||
| const { assert, expect } = require("chai"); | |||||
| const { connectToDatabase, disconnectFromDatabase } = require("../../controller/MongoController"); | |||||
| const { findAllTask,findTaskById,addTask, updateTask, deleteTask } = require("../../controller/TaskController"); | |||||
| const Task = require("../../model/Task"); | |||||
| describe("TaskController",()=>{ | |||||
| before(()=>{ | |||||
| connectToDatabase() | |||||
| .then(()=>{}) | |||||
| .catch(err=>console.error(err)); | |||||
| }); | |||||
| beforeEach((done) => { | |||||
| Task.deleteMany({}, (err) => { | |||||
| if(err){ | |||||
| console.error(err); | |||||
| }else{ | |||||
| done(); | |||||
| } | |||||
| }); | |||||
| }); | |||||
| afterEach((done) => { | |||||
| Task.deleteMany({}, (err) => { | |||||
| if(err){ | |||||
| console.error(err); | |||||
| }else{ | |||||
| done(); | |||||
| } | |||||
| }); | |||||
| }); | |||||
| after(()=>{ | |||||
| disconnectFromDatabase(); | |||||
| }); | |||||
| describe("findAllTask()",(done)=>{ | |||||
| it("it should return empty object",(done)=>{ | |||||
| findAllTask().then((value)=>{ | |||||
| assert.isTrue(Object.keys(value).length===0); | |||||
| done(); | |||||
| }).catch((reason)=>{assert.fail(reason)}); | |||||
| }); | |||||
| it("it should return objects",(done)=>{ | |||||
| let taskOne=new Task({name:"One", _id:"5e99ac68604ea9031e8545c8"}); | |||||
| let taskTwo=new Task({name:"Two", _id:"6e99ac68604ea9031e8545c8"}); | |||||
| taskOne.save((errOne,docOne)=>{ | |||||
| taskTwo.save((errTwo,docTwo)=>{ | |||||
| findAllTask().then((value)=>{ | |||||
| assert.isTrue(Object.keys(value).length===2); | |||||
| assert.containsAllKeys(value[0],taskOne); | |||||
| assert.containsAllKeys(value[1],taskTwo); | |||||
| done(); | |||||
| }).catch((reason)=>{assert.fail(reason)}); | |||||
| }); | |||||
| }); | |||||
| }); | |||||
| }); | |||||
| describe("findTaskById()",(done)=>{ | |||||
| it("it should not return non-existing id",(done)=>{ | |||||
| findTaskById(0) | |||||
| .then((item)=>{assert.fail("got "+item)}) | |||||
| .catch(done()); | |||||
| }); | |||||
| it("it should return valid id",(done)=>{ | |||||
| let task=new Task({name:"One", _id:"5e99ac68604ea9031e8545c8"}); | |||||
| task.save((err,doc)=>{ | |||||
| findTaskById(task._id) | |||||
| .then((value)=>{ | |||||
| assert.equal(value._id.toString(),task._id.toString()); | |||||
| assert.equal(value.name,task.name); | |||||
| assert.equal(value.description,task.description); | |||||
| done(); | |||||
| }) | |||||
| .catch((reason)=>{assert.fail(reason);}); | |||||
| }); | |||||
| }); | |||||
| }); | |||||
| describe("addTask()",(done)=>{ | |||||
| it("it should not accept empty object",(done)=>{ | |||||
| let data={}; | |||||
| expect(()=>{addTask(data)}).to.throw(); | |||||
| done(); | |||||
| }); | |||||
| it("it should accept object with name",(done)=>{ | |||||
| let data={"name":"random"}; | |||||
| addTask(data) | |||||
| .then((value)=>{ | |||||
| assert.isObject(value); | |||||
| assert.equal(value.name,data.name); | |||||
| existingTask=value; | |||||
| done(); | |||||
| }) | |||||
| .catch((reason)=>{assert.fail(reason);}); | |||||
| }); | |||||
| it("it should have empty description if none provided",(done)=>{ | |||||
| let data={"name":"random"}; | |||||
| addTask(data) | |||||
| .then((value)=>{ | |||||
| assert.isObject(value); | |||||
| assert.equal(value.description,""); | |||||
| done(); | |||||
| }) | |||||
| .catch((reason)=>{assert.fail(reason);}); | |||||
| }); | |||||
| }); | |||||
| describe("updateTask()",(done)=>{ | |||||
| it("it should not update non-existing id",(done)=>{ | |||||
| expect(()=>{updateTask(0,{})}).not.to.throw(); | |||||
| updateTask(0,{}) | |||||
| .then((value)=>{assert.fail(value);}) | |||||
| .catch((reason)=>{done();}) | |||||
| }); | |||||
| it("it should not accept parent as itself",(done)=>{ | |||||
| let task=new Task({name:"One", _id:"5e99ac68604ea9031e8545c8"}); | |||||
| task.save((err,doc)=>{ | |||||
| let updateParent={"parent":task._id}; | |||||
| updateTask(task._id,updateParent) | |||||
| .then((value)=>{ | |||||
| assert.fail(value); | |||||
| },(reason)=>{ | |||||
| assert.isNotEmpty(reason.toString()); | |||||
| done(); | |||||
| }) | |||||
| .catch((reason)=>{assert.fail(reason)}); | |||||
| }); | |||||
| }); | |||||
| it("it should update properties without touching others",(done)=>{ | |||||
| let task=new Task({name:"One", _id:"5e99ac68604ea9031e8545c8"}); | |||||
| task.save((err,doc)=>{ | |||||
| let newName={"name":"fooIsMyNewName"}; | |||||
| let newDescription={"description":"barIsMyNewDescription"}; | |||||
| updateTask(task._id,newName).then((value)=>{ | |||||
| assert.equal(value.name,newName.name); | |||||
| assert.equal(value.description,task.description); | |||||
| updateTask(task._id,newDescription).then((valueD)=>{ | |||||
| assert.equal(valueD.name,newName.name); | |||||
| assert.equal(valueD.description,newDescription.description); | |||||
| existingTask=valueD; | |||||
| done(); | |||||
| }).catch((reasonD)=>{assert.fail(reasonD)}); | |||||
| }).catch((reason)=>{assert.fail(reason)}); | |||||
| }); | |||||
| }); | |||||
| }); | |||||
| describe("deleteTask()",(done)=>{ | |||||
| it("it should not delete non-existing id",(done)=>{ | |||||
| deleteTask(0) | |||||
| .then((value)=>{assert.fail(value)}) | |||||
| .catch((reason)=>{done()}) | |||||
| }); | |||||
| it("it should delete existing id",(done)=>{ | |||||
| let task=new Task({name:"One", _id:"5e99ac68604ea9031e8545c8"}); | |||||
| task.save((err,doc)=>{ | |||||
| deleteTask(task._id) | |||||
| .then((value)=>{ | |||||
| assert.isObject(value); | |||||
| assert.equal(value._id.toString(),task._id.toString()); | |||||
| done(); | |||||
| }) | |||||
| .catch((reason)=>{console.error(reason)}) | |||||
| }); | |||||
| }); | |||||
| }); | |||||
| }); | |||||
| @ -0,0 +1,385 @@ | |||||
| process.env.NODE_ENV = 'test'; | |||||
| const { assert } = require('chai'); | |||||
| const chai=require('chai'); | |||||
| const chaiHttp=require('chai-http'); | |||||
| const { disconnectFromDatabase } = require('../../controller/MongoController'); | |||||
| const server=require("../../index"); | |||||
| const should = chai.should(); | |||||
| chai.use(chaiHttp); | |||||
| const Task=require("../../model/Task"); | |||||
| const config=require("config"); | |||||
| let baseURL=config.express.baseUrl+"/task"; | |||||
| describe('Task', () => { | |||||
| beforeEach((done) => { | |||||
| Task.deleteMany({}, (err) => { | |||||
| if(err){ | |||||
| console.error(err); | |||||
| }else{ | |||||
| done(); | |||||
| } | |||||
| }); | |||||
| }); | |||||
| afterEach((done) => { | |||||
| Task.deleteMany({}, (err) => { | |||||
| if(err){ | |||||
| console.error(err); | |||||
| }else{ | |||||
| done(); | |||||
| } | |||||
| }); | |||||
| }); | |||||
| after(()=>{ | |||||
| disconnectFromDatabase(); | |||||
| }) | |||||
| describe("/ GET", () => { | |||||
| it("Should get empty", (done)=>{ | |||||
| chai.request(server) | |||||
| .get(baseURL) | |||||
| .end((err,res)=>{ | |||||
| res.should.have.status(200); | |||||
| res.body.should.not.have.property('errors'); | |||||
| res.body.should.be.a('array'); | |||||
| res.body.length.should.be.eql(0); | |||||
| done(); | |||||
| }); | |||||
| }); | |||||
| }); | |||||
| describe("/:id GET", () => { | |||||
| it("Should return empty object if id not found", (done)=>{ | |||||
| chai.request(server) | |||||
| .get(baseURL+"/5e99ac68604ea9031e8545c8") | |||||
| .end((err,res)=>{ | |||||
| res.should.have.status(200); | |||||
| assert.isObject(res.body); | |||||
| assert.isEmpty(res.body); | |||||
| done(); | |||||
| }); | |||||
| }); | |||||
| it("Should return existing object", (done)=>{ | |||||
| let task=new Task({name:"Parent", _id:"5e99ac68604ea9031e8545c8"}); | |||||
| task.save((err,parent)=> { | |||||
| chai.request(server) | |||||
| .get(baseURL+"/5e99ac68604ea9031e8545c8") | |||||
| .end((err,res)=>{ | |||||
| res.should.have.status(200); | |||||
| res.body.should.be.a('object'); | |||||
| res.body.should.not.have.property('errors'); | |||||
| res.body.should.have.property('name'); | |||||
| res.body.name.should.equal(task.name); | |||||
| res.body.should.have.property('description'); | |||||
| res.body.description.should.equal(task.description); | |||||
| res.body.should.have.property('_id'); | |||||
| res.body._id.should.be.not.empty; | |||||
| res.body._id.should.equal(task.id.toString()); | |||||
| done(); | |||||
| }); | |||||
| }); | |||||
| }); | |||||
| }); | |||||
| describe("/ POST", () => { | |||||
| it("Should not accept empty name", (done)=>{ | |||||
| let task={ | |||||
| } | |||||
| chai.request(server) | |||||
| .post(baseURL) | |||||
| .send(task) | |||||
| .end((err,res)=>{ | |||||
| res.should.have.status(500); | |||||
| res.body.should.be.a('object'); | |||||
| res.body.should.have.property('errors'); | |||||
| res.body.errors.should.have.property('name'); | |||||
| res.body.errors.name.should.have.property('name').eql('ValidatorError'); | |||||
| res.body.errors.name.should.have.property('path').eql('name'); | |||||
| res.body.errors.name.should.have.property('kind').eql('required'); | |||||
| done(); | |||||
| }); | |||||
| }); | |||||
| it("Should add non empty task", (done)=>{ | |||||
| let task={ | |||||
| name: "Non empty name", | |||||
| description: "description" | |||||
| } | |||||
| chai.request(server) | |||||
| .post(baseURL+"/") | |||||
| .send(task) | |||||
| .end((err,res)=>{ | |||||
| res.should.have.status(200); | |||||
| res.body.should.be.a('object'); | |||||
| res.body.should.not.have.property('errors'); | |||||
| res.body.should.have.property('name'); | |||||
| res.body.name.should.equal(task.name); | |||||
| res.body.should.have.property('description'); | |||||
| res.body.description.should.equal(task.description); | |||||
| res.body.should.have.property('_id'); | |||||
| res.body._id.should.be.not.empty; | |||||
| done(); | |||||
| }); | |||||
| }); | |||||
| it("Should not create task with invalid parent id", (done)=>{ | |||||
| let task={ | |||||
| name: "TaskName", | |||||
| description: "TaskDescription", | |||||
| parent: "5e99ac36a60d08030772df30" | |||||
| } | |||||
| chai.request(server) | |||||
| .post(baseURL+"/") | |||||
| .send(task) | |||||
| .end((err,res)=>{ | |||||
| res.should.have.status(500); | |||||
| res.body.should.be.a('object'); | |||||
| res.body.should.have.property('errors'); | |||||
| res.body.errors.should.have.property('parent'); | |||||
| res.body.errors.parent.should.have.property('name').eql('ValidatorError'); | |||||
| res.body.errors.parent.should.have.property('path').eql('parent'); | |||||
| res.body.errors.parent.should.have.property('kind').eql('user defined'); | |||||
| done(); | |||||
| }); | |||||
| }); | |||||
| it("Should create task with valid parent id", (done)=>{ | |||||
| let task={ | |||||
| name: "TaskName", | |||||
| description: "description", | |||||
| parent: "5e99acf261eea30341340560" | |||||
| } | |||||
| let parentTask=new Task({name:"Parent", _id:"5e99acf261eea30341340560"}); | |||||
| parentTask.save((err,parent)=> { | |||||
| chai.request(server) | |||||
| .post(baseURL+"/") | |||||
| .send(task) | |||||
| .end((err,res)=>{ | |||||
| res.should.have.status(200); | |||||
| res.body.should.be.a('object'); | |||||
| res.body.should.not.have.property('errors'); | |||||
| res.body.should.have.property('name'); | |||||
| res.body.name.should.equal(task.name); | |||||
| res.body.should.have.property('description'); | |||||
| res.body.description.should.equal(task.description); | |||||
| res.body.should.have.property('_id'); | |||||
| res.body._id.should.be.not.empty; | |||||
| res.body.should.have.property("parent"); | |||||
| res.body.parent.should.equal(task.parent); | |||||
| done(); | |||||
| }); | |||||
| }); | |||||
| }); | |||||
| it("Should create task with no parent if parent=\"\"", (done)=>{ | |||||
| let task={ | |||||
| name: "TaskName", | |||||
| description: "description", | |||||
| parent: "" | |||||
| } | |||||
| chai.request(server) | |||||
| .post(baseURL+"/") | |||||
| .send(task) | |||||
| .end((err,res)=>{ | |||||
| res.should.have.status(200); | |||||
| res.body.should.be.a('object'); | |||||
| res.body.should.not.have.property('errors'); | |||||
| res.body.should.have.property('name'); | |||||
| res.body.name.should.equal(task.name); | |||||
| res.body.should.have.property('description'); | |||||
| res.body.description.should.equal(task.description); | |||||
| res.body.should.have.property('_id'); | |||||
| res.body._id.should.be.not.empty; | |||||
| res.body.should.not.have.property("parent"); | |||||
| done(); | |||||
| }); | |||||
| }); | |||||
| }); | |||||
| describe("/:id PUT", () => { | |||||
| it("Should not change invalid id", (done)=>{ | |||||
| chai.request(server) | |||||
| .put(baseURL+"/5e99ac36a60d08030772df30") | |||||
| .send({description:"New description"}) | |||||
| .end((err,res)=>{ | |||||
| res.should.have.status(500); | |||||
| assert.isNotEmpty(res.error.text); | |||||
| done(); | |||||
| }); | |||||
| }); | |||||
| it("Should change Task", (done)=>{ | |||||
| let task=new Task({ | |||||
| name: "TaskName", | |||||
| description: "description", | |||||
| _id: "5e99acf261eea30341340560" | |||||
| }); | |||||
| let updateValue={"description":"New description"} | |||||
| task.save((err,doc)=> { | |||||
| chai.request(server) | |||||
| .put(baseURL+"/"+task._id) | |||||
| .send(updateValue) | |||||
| .end((err,res)=>{ | |||||
| res.should.have.status(200); | |||||
| res.body.should.be.a('object'); | |||||
| res.body.should.not.have.property('errors'); | |||||
| res.body.should.have.property('name'); | |||||
| res.body.name.should.equal(task.name); | |||||
| res.body.should.have.property('description'); | |||||
| res.body.description.should.equal(updateValue.description); | |||||
| res.body.should.have.property('_id'); | |||||
| res.body._id.should.equal(task.id.toString()); | |||||
| done(); | |||||
| }); | |||||
| }); | |||||
| }); | |||||
| it("Should not change name empty", (done)=>{ | |||||
| let task=new Task({ | |||||
| name: "TaskName", | |||||
| description: "description", | |||||
| _id: "5e99acf261eea30341340560" | |||||
| }); | |||||
| task.save((err,doc)=> { | |||||
| chai.request(server) | |||||
| .put(baseURL+"/"+task._id) | |||||
| .send({title:""}) | |||||
| .end((err,res)=>{ | |||||
| res.should.have.status(200); | |||||
| res.body.should.be.a('object'); | |||||
| res.body.should.not.have.property('errors'); | |||||
| res.body.should.have.property('name'); | |||||
| res.body.name.should.equal(task.name); | |||||
| res.body.should.have.property('description'); | |||||
| res.body.description.should.equal(task.description); | |||||
| res.body.should.have.property('_id'); | |||||
| res.body._id.should.equal(task.id.toString()); | |||||
| done(); | |||||
| }); | |||||
| }); | |||||
| }); | |||||
| it("Should not change parent to invalid", (done)=>{ | |||||
| let task=new Task({ | |||||
| name: "TaskName", | |||||
| description: "description", | |||||
| _id: "5e99acf261eea30341340560" | |||||
| }); | |||||
| task.save((err,doc)=> { | |||||
| chai.request(server) | |||||
| .put(baseURL+"/"+task._id) | |||||
| .send({parent:"56"}) | |||||
| .end((err,res)=>{ | |||||
| res.should.have.status(500); | |||||
| res.body.should.be.a('object'); | |||||
| Task.findById(task._id).then((value)=>{ | |||||
| value.parent.should.be(task.parent); | |||||
| }).catch((reason)=>{assert.fail(reason)}); | |||||
| done(); | |||||
| }); | |||||
| }); | |||||
| }); | |||||
| it("Should not change parent to itself", (done)=>{ | |||||
| let task=new Task({ | |||||
| name: "TaskName", | |||||
| description: "description", | |||||
| _id: "5e99acf261eea30341340560" | |||||
| }); | |||||
| task.save((err,doc)=> { | |||||
| chai.request(server) | |||||
| .put(baseURL+"/"+task._id) | |||||
| .send({"parent":task._id}) | |||||
| .end((err,res)=>{ | |||||
| res.should.have.status(500); | |||||
| res.body.should.be.a('object'); | |||||
| Task.findById(task._id).then((value)=>{ | |||||
| value.parent.should.be(task.parent); | |||||
| }).catch((reason)=>{assert.fail(reason)}); | |||||
| done(); | |||||
| }); | |||||
| }); | |||||
| }); | |||||
| it("Should not change non specified property", (done)=>{ | |||||
| let task=new Task({ | |||||
| name: "TaskName", | |||||
| description: "description", | |||||
| parent: "5e99ac36a60d08030772df30", | |||||
| _id: "5e99acf261eea30341340560" | |||||
| }); | |||||
| let parentTask=new Task({name:"Parent", _id:"5e99ac36a60d08030772df30"}); | |||||
| parentTask.save((perr,pdoc)=>{ | |||||
| task.save((err,doc)=> { | |||||
| chai.request(server) | |||||
| .put(baseURL+"/"+task._id) | |||||
| .send({}) | |||||
| .end((err,res)=>{ | |||||
| res.should.have.status(200); | |||||
| res.body.should.be.a('object'); | |||||
| res.body.should.not.have.property('errors'); | |||||
| res.body.should.have.property('name'); | |||||
| res.body.name.should.equal(task.name); | |||||
| res.body.should.have.property('description'); | |||||
| res.body.description.should.equal(task.description); | |||||
| res.body.should.have.property('parent'); | |||||
| res.body.parent.should.equal(task.parent.toString()); | |||||
| res.body.should.have.property('_id'); | |||||
| res.body._id.should.equal(task.id.toString()); | |||||
| done(); | |||||
| }); | |||||
| }); | |||||
| }); | |||||
| }); | |||||
| it("Should remove parent on empty string", (done)=>{ | |||||
| let childTask=new Task({ | |||||
| name: "TaskName", | |||||
| description: "description", | |||||
| parent: "5e99ac36a60d08030772df30", | |||||
| _id: "5e99acf261eea30341340560" | |||||
| }); | |||||
| let parentTask=new Task({name:"Parent", _id:"5e99ac36a60d08030772df30"}); | |||||
| parentTask.save((perr,pdoc)=>{ | |||||
| childTask.save((err,doc)=> { | |||||
| chai.request(server) | |||||
| .put(baseURL+"/"+childTask._id) | |||||
| .send({parent:""}) | |||||
| .end((err,res)=>{ | |||||
| res.should.have.status(200); | |||||
| res.body.should.be.a('object'); | |||||
| res.body.should.not.have.property('errors'); | |||||
| res.body.should.have.property('name'); | |||||
| res.body.name.should.equal(childTask.name); | |||||
| res.body.should.have.property('description'); | |||||
| res.body.description.should.equal(childTask.description); | |||||
| res.body.should.not.have.property('parent'); | |||||
| res.body.should.have.property('_id'); | |||||
| res.body._id.should.equal(childTask.id.toString()); | |||||
| done(); | |||||
| }); | |||||
| }); | |||||
| }); | |||||
| }); | |||||
| }); | |||||
| describe("/:id DELETE", () => { | |||||
| it("Should not delete invalid id", (done)=>{ | |||||
| chai.request(server) | |||||
| .delete(baseURL+"/5e99ac36a60d08030772df99") | |||||
| .end((err,res)=>{ | |||||
| res.should.have.status(500); | |||||
| assert.isNotEmpty(res.error.text); | |||||
| done(); | |||||
| }); | |||||
| }); | |||||
| it("Should delete", (done)=>{ | |||||
| let task=new Task({ | |||||
| name: "TaskName", | |||||
| description: "description", | |||||
| _id: "5e99ac36a60d08030772df99" | |||||
| }); | |||||
| task.save((err,doc)=> { | |||||
| chai.request(server) | |||||
| .delete(baseURL+"/"+task._id) | |||||
| .end((err,res)=>{ | |||||
| res.should.have.status(200); | |||||
| res.body.should.be.an('object'); | |||||
| res.body._id.should.equal(task._id.toString()); | |||||
| done(); | |||||
| }); | |||||
| }); | |||||
| }); | |||||
| }); | |||||
| }); | |||||
| @ -0,0 +1,30 @@ | |||||
| version: "3" | |||||
| networks: | |||||
| kanban: | |||||
| external: false | |||||
| services: | |||||
| node: | |||||
| container_name: kanban_front | |||||
| image: node:13 | |||||
| networks: | |||||
| - kanban | |||||
| depends_on: | |||||
| - mongo | |||||
| volumes: | |||||
| - .:/usr/src/app | |||||
| # - /etc/localtime:/etc/localtime:ro | |||||
| ports: | |||||
| - '3000:3000' | |||||
| - '3001:3001' | |||||
| working_dir: "/usr/src/app/" | |||||
| command: bash -c "npm install --prefix back/ && npm install --prefix front/ && npm install && npm start" | |||||
| mongo: | |||||
| container_name: kanban_back | |||||
| image: mongo:4.2 | |||||
| networks: | |||||
| - kanban | |||||
| #volumes: | |||||
| # - ./mongo:/data/db | |||||
| # - /etc/localtime:/etc/localtime:ro | |||||
| #ports: | |||||
| # - '27017:27017' | |||||
| @ -0,0 +1,23 @@ | |||||
| # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. | |||||
| # dependencies | |||||
| /node_modules | |||||
| /.pnp | |||||
| .pnp.js | |||||
| # testing | |||||
| /coverage | |||||
| # production | |||||
| /build | |||||
| # misc | |||||
| .DS_Store | |||||
| .env.local | |||||
| .env.development.local | |||||
| .env.test.local | |||||
| .env.production.local | |||||
| npm-debug.log* | |||||
| yarn-debug.log* | |||||
| yarn-error.log* | |||||
| @ -0,0 +1,45 @@ | |||||
| { | |||||
| "name": "front", | |||||
| "version": "0.1.0", | |||||
| "private": true, | |||||
| "dependencies": { | |||||
| "@testing-library/jest-dom": "^5.11.9", | |||||
| "@testing-library/react": "^11.2.5", | |||||
| "@testing-library/user-event": "^12.8.3", | |||||
| "axios": "^0.21.1", | |||||
| "node-sass": "4.14.1", | |||||
| "react": "^17.0.1", | |||||
| "react-dom": "^17.0.1", | |||||
| "react-redux": "^7.2.2", | |||||
| "react-router-dom": "^5.2.0", | |||||
| "react-scripts": "4.0.3", | |||||
| "redux": "^4.0.5", | |||||
| "redux-thunk": "^2.3.0", | |||||
| "web-vitals": "^1.1.0" | |||||
| }, | |||||
| "scripts": { | |||||
| "start": "react-scripts start", | |||||
| "build": "react-scripts build", | |||||
| "test": "react-scripts test", | |||||
| "eject": "react-scripts eject" | |||||
| }, | |||||
| "eslintConfig": { | |||||
| "extends": [ | |||||
| "react-app", | |||||
| "react-app/jest" | |||||
| ] | |||||
| }, | |||||
| "browserslist": { | |||||
| "production": [ | |||||
| ">0.2%", | |||||
| "not dead", | |||||
| "not op_mini all" | |||||
| ], | |||||
| "development": [ | |||||
| "last 1 chrome version", | |||||
| "last 1 firefox version", | |||||
| "last 1 safari version" | |||||
| ] | |||||
| }, | |||||
| "proxy": "http://localhost:3001" | |||||
| } | |||||
| @ -0,0 +1,20 @@ | |||||
| <!DOCTYPE html> | |||||
| <html lang="en"> | |||||
| <head> | |||||
| <meta charset="utf-8" /> | |||||
| <link rel="icon" href="%PUBLIC_URL%/favicon.ico" /> | |||||
| <meta name="viewport" content="width=device-width, initial-scale=1" /> | |||||
| <meta name="theme-color" content="#000000" /> | |||||
| <meta | |||||
| name="description" | |||||
| content="Kanban web app" | |||||
| /> | |||||
| <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" /> | |||||
| <link rel="manifest" href="%PUBLIC_URL%/manifest.json" /> | |||||
| <title>Kanban</title> | |||||
| </head> | |||||
| <body> | |||||
| <noscript>You need to enable JavaScript to run this app.</noscript> | |||||
| <div id="root"></div> | |||||
| </body> | |||||
| </html> | |||||
| @ -0,0 +1,25 @@ | |||||
| { | |||||
| "short_name": "React App", | |||||
| "name": "Create React App Sample", | |||||
| "icons": [ | |||||
| { | |||||
| "src": "favicon.ico", | |||||
| "sizes": "64x64 32x32 24x24 16x16", | |||||
| "type": "image/x-icon" | |||||
| }, | |||||
| { | |||||
| "src": "logo192.png", | |||||
| "type": "image/png", | |||||
| "sizes": "192x192" | |||||
| }, | |||||
| { | |||||
| "src": "logo512.png", | |||||
| "type": "image/png", | |||||
| "sizes": "512x512" | |||||
| } | |||||
| ], | |||||
| "start_url": ".", | |||||
| "display": "standalone", | |||||
| "theme_color": "#000000", | |||||
| "background_color": "#ffffff" | |||||
| } | |||||
| @ -0,0 +1,3 @@ | |||||
| # https://www.robotstxt.org/robotstxt.html | |||||
| User-agent: * | |||||
| Disallow: | |||||
| @ -0,0 +1,19 @@ | |||||
| import React from 'react'; | |||||
| import { BrowserRouter as Router, Route } from 'react-router-dom'; | |||||
| import { Provider } from 'react-redux'; | |||||
| import store from './store'; | |||||
| import TaskIndex from './component/task/TaskIndex'; | |||||
| function App() { | |||||
| return ( | |||||
| <Provider store={store}> | |||||
| <Router> | |||||
| <Route exact path="/" render={ props =>( | |||||
| <TaskIndex/> | |||||
| ) } /> | |||||
| </Router> | |||||
| </Provider> | |||||
| ); | |||||
| } | |||||
| export default App; | |||||
| @ -0,0 +1,81 @@ | |||||
| import axios from 'axios'; | |||||
| import store from '../store'; | |||||
| import { ADD, GET, GET_ALL, UPDATE, DELETE, INSPECT } from './TaskType'; | |||||
| const base_url='/api/task'; | |||||
| export const getTasks= (error=(reason)=>{console.error(reason)}) => dispatch => { | |||||
| axios | |||||
| .get(base_url) | |||||
| .then( | |||||
| (value)=> | |||||
| dispatch({ | |||||
| type: GET_ALL, | |||||
| payload: value.data | |||||
| }) | |||||
| ,(reason)=>{ | |||||
| if(error&&typeof(error)==="function") error(reason) | |||||
| } | |||||
| ); | |||||
| } | |||||
| export const getTask= (id,error=(reason)=>{console.error(reason)}) => dispatch => { | |||||
| axios | |||||
| .get(base_url+`/${id}`) | |||||
| .then( | |||||
| (value)=> | |||||
| dispatch({ | |||||
| type: GET, | |||||
| payload: value.data | |||||
| }) | |||||
| ,(reason)=>{ | |||||
| if(error&&typeof(error)==="function") error(reason) | |||||
| } | |||||
| ); | |||||
| } | |||||
| export const addTask= (task,error=(reason)=>{console.error(reason)}) => dispatch => { | |||||
| axios | |||||
| .post(base_url, task) | |||||
| .then( | |||||
| (value)=> | |||||
| dispatch({ | |||||
| type: ADD, | |||||
| payload: value.data | |||||
| }) | |||||
| ,(reason)=>{ | |||||
| if(error&&typeof(error)==="function") error(reason) | |||||
| } | |||||
| ); | |||||
| } | |||||
| export const deleteTask= (id,error=(reason)=>{console.error(reason)}) => dispatch => { | |||||
| axios | |||||
| .delete(base_url+`/${id}`) | |||||
| .then( | |||||
| (value)=> | |||||
| dispatch({ | |||||
| type: DELETE, | |||||
| payload: id | |||||
| }) | |||||
| ,(reason)=>{ | |||||
| if(error&&typeof(error)==="function") error(reason) | |||||
| } | |||||
| ); | |||||
| } | |||||
| export const updateTask= (task,error=(reason)=>{console.error(reason)}) => dispatch => { | |||||
| axios | |||||
| .put(base_url+`/${task._id}`, task) | |||||
| .then( | |||||
| (value)=> | |||||
| dispatch({ | |||||
| type: UPDATE, | |||||
| payload: task | |||||
| }) | |||||
| ,(reason)=>{ | |||||
| if(error&&typeof(error)==="function") error(reason) | |||||
| } | |||||
| ); | |||||
| } | |||||
| export const inspectTask= (id) => { | |||||
| return store.dispatch ({ | |||||
| type: INSPECT, | |||||
| payload: id | |||||
| }); | |||||
| } | |||||
| @ -0,0 +1,7 @@ | |||||
| export const ADD='CREATE'; | |||||
| export const GET='READ'; | |||||
| export const GET_ALL='READ_ALL'; | |||||
| export const UPDATE='UPDATE'; | |||||
| export const DELETE='DELETE'; | |||||
| export const INSPECT='INSPECT'; | |||||
| @ -0,0 +1,21 @@ | |||||
| import React from "react"; | |||||
| import PropTypes from "prop-types"; | |||||
| import "../../style/generic/DisplayError.scss"; | |||||
| function DisplayError(props) { | |||||
| let message=""; | |||||
| if(props.hasOwnProperty("axios")){ | |||||
| message=<div className="errorTitle">Status {props.axios.response.status} ({props.axios.response.statusText})</div>; | |||||
| if(typeof(props.axios.response.data)==="object" && props.axios.response.data.hasOwnProperty("message")){ | |||||
| message=<React.Fragment>{message}<div className="errorMessage">{props.axios.response.data.message}</div></React.Fragment>; | |||||
| } | |||||
| } | |||||
| return ( | |||||
| <div className="displayError">{message}</div> | |||||
| ); | |||||
| } | |||||
| DisplayError.propTypes = { | |||||
| axios: PropTypes.object | |||||
| }; | |||||
| export default DisplayError; | |||||
| @ -0,0 +1,49 @@ | |||||
| import React, { Component } from 'react'; | |||||
| import PropTypes from 'prop-types'; | |||||
| import '../../style/generic/Modal.scss'; | |||||
| export default class Modal extends Component { | |||||
| static propTypes = { | |||||
| show: PropTypes.bool.isRequired, | |||||
| onClose: PropTypes.func.isRequired | |||||
| } | |||||
| componentDidMount(){ | |||||
| if(this.props.show){ | |||||
| this.hideBodyOverflow(); | |||||
| } | |||||
| } | |||||
| componentDidUpdate(prevProps, prevState, snapshot){ | |||||
| if(this.props.show){ | |||||
| this.hideBodyOverflow(); | |||||
| }else{ | |||||
| this.resetBodyOverflow(); | |||||
| } | |||||
| } | |||||
| componentWillUnmount(){ | |||||
| this.resetBodyOverflow(); | |||||
| } | |||||
| hideBodyOverflow(){ | |||||
| document.body.style.overflow="hidden"; | |||||
| } | |||||
| resetBodyOverflow(){ | |||||
| document.body.style.overflow="auto"; | |||||
| } | |||||
| render() { | |||||
| return ( | |||||
| <div className="modal" style={{display: this.props.show ? "grid":"none"}}> | |||||
| <div className="modalForeground"> | |||||
| <div className="modalTitle">{this.props.title}</div> | |||||
| <div className="modalClose icon-cancel-circled" onClick={this.props.onClose}></div> | |||||
| <div className="modalContent">{ this.props.children }</div> | |||||
| </div> | |||||
| </div> | |||||
| ) | |||||
| } | |||||
| } | |||||
| @ -0,0 +1,22 @@ | |||||
| import React from "react"; | |||||
| import PropTypes from "prop-types"; | |||||
| import { connect } from "react-redux"; | |||||
| import { inspectTask } from "../../action/TaskAction"; | |||||
| import "../../style/task/Task.scss"; | |||||
| function Task(props) { | |||||
| return ( | |||||
| <div id={props.task._id} className="task"> | |||||
| <button className="edit icon-pencil-circled" type="button" onClick={()=>{inspectTask(props.task._id);}} title="Edit"></button> | |||||
| <div className="id">{props.task._id}</div> | |||||
| <div className="name">{props.task.name}</div> | |||||
| <div className="description">{props.task.description}</div> | |||||
| </div> | |||||
| ); | |||||
| } | |||||
| Task.propTypes = { | |||||
| inspectTask: PropTypes.func.isRequired, | |||||
| task: PropTypes.object.isRequired | |||||
| }; | |||||
| export default connect(null,{inspectTask})(Task); | |||||
| @ -0,0 +1,22 @@ | |||||
| import React, { Component } from 'react' | |||||
| import TaskForm from './TaskForm'; | |||||
| import '../../style/task/TaskCreate.scss'; | |||||
| export default class TaskCreate extends Component { | |||||
| state={ | |||||
| "open":false | |||||
| } | |||||
| render() { | |||||
| return ( | |||||
| <div className="taskCreate"> | |||||
| <button | |||||
| className={this.state.open?"icon-minus-circled":"icon-plus-circled"} | |||||
| type="button" | |||||
| onClick={()=>{this.setState({"open":!this.state.open});}} | |||||
| title={this.state.open?"Hide add task":"Add task"} | |||||
| /> | |||||
| {this.state.open?<TaskForm name="create"/>:""} | |||||
| </div> | |||||
| ) | |||||
| } | |||||
| } | |||||
| @ -0,0 +1,137 @@ | |||||
| import React, { Component } from 'react' | |||||
| import PropTypes from 'prop-types' | |||||
| import { connect } from 'react-redux'; | |||||
| import { addTask, updateTask, deleteTask } from '../../action/TaskAction'; | |||||
| import '../../style/task/TaskForm.scss'; | |||||
| import DisplayError from '../generic/DisplayError'; | |||||
| class TaskForm extends Component { | |||||
| static propTypes = { | |||||
| task: PropTypes.object, | |||||
| addTask: PropTypes.func.isRequired, | |||||
| updateTask: PropTypes.func.isRequired, | |||||
| deleteTask: PropTypes.func.isRequired, | |||||
| } | |||||
| state={ | |||||
| submitValue:"Create", | |||||
| isUpdate:false, | |||||
| task:{ | |||||
| name: "", | |||||
| description: "", | |||||
| parent: "" | |||||
| }, | |||||
| backendError:false | |||||
| } | |||||
| componentDidMount(){ | |||||
| if(this.props.task){ | |||||
| this.setState({ | |||||
| "isUpdate":true, | |||||
| "submitValue":"Update", | |||||
| "task": {...this.state.task,...this.props.task} | |||||
| }); | |||||
| } | |||||
| this.onBackendError=this.onBackendError.bind(this); | |||||
| } | |||||
| componentDidUpdate(prevProps, prevState, snapshot){ | |||||
| if(this.props.task && this.props.task!==prevProps.task){ | |||||
| this.setState({ | |||||
| "isUpdate":true, | |||||
| "submitValue":"Update", | |||||
| "task": {...this.state.task,...this.props.task} | |||||
| }); | |||||
| } | |||||
| } | |||||
| onChange=(e)=>{ | |||||
| e.preventDefault(); | |||||
| let n=e.target.name; | |||||
| let v=e.target.value; | |||||
| this.setState(prevState=>({ | |||||
| task: { | |||||
| ...prevState.task, | |||||
| [n]:v | |||||
| } | |||||
| })); | |||||
| } | |||||
| onSubmit=(e)=>{ | |||||
| e.preventDefault(); | |||||
| if(this.state.isUpdate){ | |||||
| this.props.updateTask(this.state.task,this.onBackendError); | |||||
| }else{ | |||||
| this.props.addTask(this.state.task,this.onBackendError); | |||||
| } | |||||
| } | |||||
| onBackendError(reason){ | |||||
| this.setState({backendError:reason}); | |||||
| } | |||||
| onDelete=(e)=>{ | |||||
| e.preventDefault(); | |||||
| this.props.deleteTask(this.state.task._id); | |||||
| } | |||||
| renderDeleteButton(){ | |||||
| if(!this.state.isUpdate) return; | |||||
| return( | |||||
| <button className="taskFormDelete" onClick={this.onDelete}>Delete</button> | |||||
| ); | |||||
| } | |||||
| renderFormElement(id,element,labelText=""){ | |||||
| if(labelText==="") labelText=id; | |||||
| return( | |||||
| <React.Fragment> | |||||
| <label htmlFor={id}>{labelText}</label> | |||||
| {element} | |||||
| </React.Fragment> | |||||
| ) | |||||
| } | |||||
| render() { | |||||
| let formName=""; | |||||
| if(this.props.hasOwnProperty("name")) formName=this.props.name; | |||||
| return ( | |||||
| <div className="taskForm"> | |||||
| <form onSubmit={this.onSubmit}> | |||||
| {this.renderFormElement( | |||||
| "name"+formName, | |||||
| <input id={"name"+formName} type="text" name="name" placeholder="name" value={this.state.task.name} onChange={this.onChange}/>, | |||||
| "Name" | |||||
| )} | |||||
| {this.renderFormElement( | |||||
| "description"+formName, | |||||
| <textarea id={"description"+formName} type="text" name="description" placeholder="description" value={this.state.task.description} onChange={this.onChange}/>, | |||||
| "Description" | |||||
| )} | |||||
| {this.renderFormElement( | |||||
| "parent"+formName, | |||||
| <select id={"parent"+formName} name="parent" value={this.state.task.parent} onChange={this.onChange}> | |||||
| <option value="">No parent task</option> | |||||
| {this.props.tasks.map(task=>{ | |||||
| return this.state.isUpdate && task._id === this.state.task._id ? "":<option key={task._id} value={task._id}>{task.name}</option> | |||||
| })} | |||||
| </select>, | |||||
| "Parent task" | |||||
| )} | |||||
| <input type="hidden" name="isUpdate" value={this.state.isUpdate}/> | |||||
| <input type="submit" value={this.state.submitValue}/> | |||||
| </form> | |||||
| <div>{this.state.backendError?<DisplayError axios={this.state.backendError}/>:""}</div> | |||||
| {this.renderDeleteButton()} | |||||
| </div> | |||||
| ) | |||||
| } | |||||
| } | |||||
| const mapStateToProps = (state) => ({ | |||||
| tasks: state.task.tasks | |||||
| }); | |||||
| export default connect(mapStateToProps, {addTask, updateTask, deleteTask})(TaskForm); | |||||
| @ -0,0 +1,35 @@ | |||||
| import React, { Component } from 'react'; | |||||
| import PropTypes from 'prop-types'; | |||||
| import { connect } from 'react-redux'; | |||||
| import { inspectTask } from '../../action/TaskAction'; | |||||
| import TaskList from './TaskList'; | |||||
| import TaskForm from './TaskForm'; | |||||
| import Modal from '../generic/Modal'; | |||||
| import TaskCreate from './TaskCreate'; | |||||
| class TaskIndex extends Component { | |||||
| static propTypes = { | |||||
| inspectTask: PropTypes.func.isRequired | |||||
| } | |||||
| closeInspected=()=>{ | |||||
| this.props.inspectTask(""); | |||||
| } | |||||
| render() { | |||||
| let taskInspectedShow=this.props.taskInspected ? true:false; | |||||
| return ( | |||||
| <div> | |||||
| <TaskCreate/> | |||||
| <TaskList/> | |||||
| <Modal show={taskInspectedShow} title="Update" onClose={this.closeInspected}> | |||||
| <TaskForm task={this.props.taskInspected} name="inspected"/> | |||||
| </Modal> | |||||
| </div> | |||||
| ) | |||||
| } | |||||
| } | |||||
| const mapStateToProps = (state) => ({ | |||||
| taskInspected: state.task.tasks.filter(t=>t._id===state.task.taskInspected)[0] | |||||
| }); | |||||
| export default connect(mapStateToProps, {inspectTask})(TaskIndex); | |||||
| @ -0,0 +1,52 @@ | |||||
| import React, { Component } from 'react'; | |||||
| import PropTypes from 'prop-types'; | |||||
| import { connect } from 'react-redux'; | |||||
| import { getTasks } from '../../action/TaskAction'; | |||||
| import Task from './Task'; | |||||
| import DisplayError from '../generic/DisplayError'; | |||||
| import '../../style/task/TaskList.scss'; | |||||
| class TaskList extends Component { | |||||
| static propTypes = { | |||||
| getTasks: PropTypes.func.isRequired, | |||||
| task: PropTypes.object.isRequired | |||||
| } | |||||
| state={ | |||||
| error:false | |||||
| } | |||||
| componentDidMount(){ | |||||
| this.props.getTasks((reason)=>{this.setState({"error":reason})}); | |||||
| } | |||||
| renderTaskGroup(parent,generation=0){ | |||||
| let taskCollection; | |||||
| if(generation===0){ | |||||
| taskCollection=this.props.sortedTask.filter(task=>!task.parent) | |||||
| }else{ | |||||
| taskCollection=this.props.sortedTask.filter(task=>task.parent===parent._id); | |||||
| } | |||||
| return( | |||||
| taskCollection.map(t=>{ | |||||
| return( | |||||
| <div key={t._id} className={"taskGroup taskGroup_"+(generation)}> | |||||
| <Task task={t} /> | |||||
| {this.renderTaskGroup(t,generation+1)} | |||||
| </div> | |||||
| ) | |||||
| }) | |||||
| ) | |||||
| } | |||||
| render(){ | |||||
| return ( | |||||
| <React.Fragment> | |||||
| {(this.state && this.state.error)?<DisplayError axios={this.state.error}/>:""} | |||||
| <div className="taskList">{ this.renderTaskGroup() }</div> | |||||
| </React.Fragment> | |||||
| ); | |||||
| } | |||||
| } | |||||
| const mapStateToProps = (state) => ({ | |||||
| task: state.task, | |||||
| sortedTask: state.task.tasks.sort((a,b)=>(a.name).localeCompare(b.name)) | |||||
| }); | |||||
| export default connect(mapStateToProps, {getTasks})(TaskList); | |||||
| @ -0,0 +1,34 @@ | |||||
| { | |||||
| "name": "", | |||||
| "css_prefix_text": "icon-", | |||||
| "css_use_suffix": false, | |||||
| "hinting": true, | |||||
| "units_per_em": 1000, | |||||
| "ascent": 850, | |||||
| "glyphs": [ | |||||
| { | |||||
| "uid": "4ba33d2607902cf690dd45df09774cb0", | |||||
| "css": "plus-circled", | |||||
| "code": 59392, | |||||
| "src": "fontawesome" | |||||
| }, | |||||
| { | |||||
| "uid": "0f4cae16f34ae243a6144c18a003f2d8", | |||||
| "css": "cancel-circled", | |||||
| "code": 59393, | |||||
| "src": "fontawesome" | |||||
| }, | |||||
| { | |||||
| "uid": "19dae18c34431934a781773e241faec2", | |||||
| "css": "pencil-circled", | |||||
| "code": 59395, | |||||
| "src": "elusive" | |||||
| }, | |||||
| { | |||||
| "uid": "eeadb020bb75d089b25d8424aabe19e0", | |||||
| "css": "minus-circled", | |||||
| "code": 59394, | |||||
| "src": "fontawesome" | |||||
| } | |||||
| ] | |||||
| } | |||||
| @ -0,0 +1,85 @@ | |||||
| /* | |||||
| Animation example, for spinners | |||||
| */ | |||||
| .animate-spin { | |||||
| -moz-animation: spin 2s infinite linear; | |||||
| -o-animation: spin 2s infinite linear; | |||||
| -webkit-animation: spin 2s infinite linear; | |||||
| animation: spin 2s infinite linear; | |||||
| display: inline-block; | |||||
| } | |||||
| @-moz-keyframes spin { | |||||
| 0% { | |||||
| -moz-transform: rotate(0deg); | |||||
| -o-transform: rotate(0deg); | |||||
| -webkit-transform: rotate(0deg); | |||||
| transform: rotate(0deg); | |||||
| } | |||||
| 100% { | |||||
| -moz-transform: rotate(359deg); | |||||
| -o-transform: rotate(359deg); | |||||
| -webkit-transform: rotate(359deg); | |||||
| transform: rotate(359deg); | |||||
| } | |||||
| } | |||||
| @-webkit-keyframes spin { | |||||
| 0% { | |||||
| -moz-transform: rotate(0deg); | |||||
| -o-transform: rotate(0deg); | |||||
| -webkit-transform: rotate(0deg); | |||||
| transform: rotate(0deg); | |||||
| } | |||||
| 100% { | |||||
| -moz-transform: rotate(359deg); | |||||
| -o-transform: rotate(359deg); | |||||
| -webkit-transform: rotate(359deg); | |||||
| transform: rotate(359deg); | |||||
| } | |||||
| } | |||||
| @-o-keyframes spin { | |||||
| 0% { | |||||
| -moz-transform: rotate(0deg); | |||||
| -o-transform: rotate(0deg); | |||||
| -webkit-transform: rotate(0deg); | |||||
| transform: rotate(0deg); | |||||
| } | |||||
| 100% { | |||||
| -moz-transform: rotate(359deg); | |||||
| -o-transform: rotate(359deg); | |||||
| -webkit-transform: rotate(359deg); | |||||
| transform: rotate(359deg); | |||||
| } | |||||
| } | |||||
| @-ms-keyframes spin { | |||||
| 0% { | |||||
| -moz-transform: rotate(0deg); | |||||
| -o-transform: rotate(0deg); | |||||
| -webkit-transform: rotate(0deg); | |||||
| transform: rotate(0deg); | |||||
| } | |||||
| 100% { | |||||
| -moz-transform: rotate(359deg); | |||||
| -o-transform: rotate(359deg); | |||||
| -webkit-transform: rotate(359deg); | |||||
| transform: rotate(359deg); | |||||
| } | |||||
| } | |||||
| @keyframes spin { | |||||
| 0% { | |||||
| -moz-transform: rotate(0deg); | |||||
| -o-transform: rotate(0deg); | |||||
| -webkit-transform: rotate(0deg); | |||||
| transform: rotate(0deg); | |||||
| } | |||||
| 100% { | |||||
| -moz-transform: rotate(359deg); | |||||
| -o-transform: rotate(359deg); | |||||
| -webkit-transform: rotate(359deg); | |||||
| transform: rotate(359deg); | |||||
| } | |||||
| } | |||||
| @ -0,0 +1,5 @@ | |||||
| .icon-plus-circled:before { content: '\e800'; } /* '' */ | |||||
| .icon-cancel-circled:before { content: '\e801'; } /* '' */ | |||||
| .icon-minus-circled:before { content: '\e802'; } /* '' */ | |||||
| .icon-pencil-circled:before { content: '\e803'; } /* '' */ | |||||
| @ -0,0 +1,5 @@ | |||||
| .icon-plus-circled { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } | |||||
| .icon-cancel-circled { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } | |||||
| .icon-minus-circled { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } | |||||
| .icon-pencil-circled { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } | |||||
| @ -0,0 +1,16 @@ | |||||
| [class^="icon-"], [class*=" icon-"] { | |||||
| font-family: 'fontello'; | |||||
| font-style: normal; | |||||
| font-weight: normal; | |||||
| /* fix buttons height */ | |||||
| line-height: 1em; | |||||
| /* you can be more comfortable with increased icons size */ | |||||
| /* font-size: 120%; */ | |||||
| } | |||||
| .icon-plus-circled { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } | |||||
| .icon-cancel-circled { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } | |||||
| .icon-minus-circled { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } | |||||
| .icon-pencil-circled { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } | |||||
| @ -0,0 +1,61 @@ | |||||
| @font-face { | |||||
| font-family: 'fontello'; | |||||
| src: url('../font/fontello.eot?10524901'); | |||||
| src: url('../font/fontello.eot?10524901#iefix') format('embedded-opentype'), | |||||
| url('../font/fontello.woff2?10524901') format('woff2'), | |||||
| url('../font/fontello.woff?10524901') format('woff'), | |||||
| url('../font/fontello.ttf?10524901') format('truetype'), | |||||
| url('../font/fontello.svg?10524901#fontello') format('svg'); | |||||
| font-weight: normal; | |||||
| font-style: normal; | |||||
| } | |||||
| /* Chrome hack: SVG is rendered more smooth in Windozze. 100% magic, uncomment if you need it. */ | |||||
| /* Note, that will break hinting! In other OS-es font will be not as sharp as it could be */ | |||||
| /* | |||||
| @media screen and (-webkit-min-device-pixel-ratio:0) { | |||||
| @font-face { | |||||
| font-family: 'fontello'; | |||||
| src: url('../font/fontello.svg?10524901#fontello') format('svg'); | |||||
| } | |||||
| } | |||||
| */ | |||||
| [class^="icon-"]:before, [class*=" icon-"]:before { | |||||
| font-family: "fontello"; | |||||
| font-style: normal; | |||||
| font-weight: normal; | |||||
| speak: never; | |||||
| display: inline-block; | |||||
| text-decoration: inherit; | |||||
| width: 1em; | |||||
| margin-right: .2em; | |||||
| text-align: center; | |||||
| /* opacity: .8; */ | |||||
| /* For safety - reset parent styles, that can break glyph codes*/ | |||||
| font-variant: normal; | |||||
| text-transform: none; | |||||
| /* fix buttons height, for twitter bootstrap */ | |||||
| line-height: 1em; | |||||
| /* Animation center compensation - margins should be symmetric */ | |||||
| /* remove if not needed */ | |||||
| margin-left: .2em; | |||||
| /* you can be more comfortable with increased icons size */ | |||||
| /* font-size: 120%; */ | |||||
| /* Font smoothing. That was taken from TWBS */ | |||||
| -webkit-font-smoothing: antialiased; | |||||
| -moz-osx-font-smoothing: grayscale; | |||||
| /* Uncomment for 3D effect */ | |||||
| /* text-shadow: 1px 1px 1px rgba(127, 127, 127, 0.3); */ | |||||
| } | |||||
| .icon-plus-circled:before { content: '\e800'; } /* '' */ | |||||
| .icon-cancel-circled:before { content: '\e801'; } /* '' */ | |||||
| .icon-minus-circled:before { content: '\e802'; } /* '' */ | |||||
| .icon-pencil-circled:before { content: '\e803'; } /* '' */ | |||||
| @ -0,0 +1,18 @@ | |||||
| <?xml version="1.0" standalone="no"?> | |||||
| <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> | |||||
| <svg xmlns="http://www.w3.org/2000/svg"> | |||||
| <metadata>Copyright (C) 2021 by original authors @ fontello.com</metadata> | |||||
| <defs> | |||||
| <font id="fontello" horiz-adv-x="1000" > | |||||
| <font-face font-family="fontello" font-weight="400" font-stretch="normal" units-per-em="1000" ascent="850" descent="-150" /> | |||||
| <missing-glyph horiz-adv-x="1000" /> | |||||
| <glyph glyph-name="plus-circled" unicode="" d="M679 314v72q0 14-11 25t-25 10h-143v143q0 15-11 25t-25 11h-71q-15 0-25-11t-11-25v-143h-143q-14 0-25-10t-10-25v-72q0-14 10-25t25-10h143v-143q0-15 11-25t25-11h71q15 0 25 11t11 25v143h143q14 0 25 10t11 25z m178 36q0-117-57-215t-156-156-215-58-216 58-155 156-58 215 58 215 155 156 216 58 215-58 156-156 57-215z" horiz-adv-x="857.1" /> | |||||
| <glyph glyph-name="cancel-circled" unicode="" d="M641 224q0 14-10 25l-101 101 101 101q10 11 10 25 0 15-10 26l-51 50q-10 11-25 11-15 0-25-11l-101-101-101 101q-11 11-25 11-16 0-26-11l-50-50q-11-11-11-26 0-14 11-25l101-101-101-101q-11-11-11-25 0-15 11-26l50-50q10-11 26-11 14 0 25 11l101 101 101-101q10-11 25-11 15 0 25 11l51 50q10 11 10 26z m216 126q0-117-57-215t-156-156-215-58-216 58-155 156-58 215 58 215 155 156 216 58 215-58 156-156 57-215z" horiz-adv-x="857.1" /> | |||||
| <glyph glyph-name="minus-circled" unicode="" d="M679 314v72q0 14-11 25t-25 10h-429q-14 0-25-10t-10-25v-72q0-14 10-25t25-10h429q14 0 25 10t11 25z m178 36q0-117-57-215t-156-156-215-58-216 58-155 156-58 215 58 215 155 156 216 58 215-58 156-156 57-215z" horiz-adv-x="857.1" /> | |||||
| <glyph glyph-name="pencil-circled" unicode="" d="M0 350q0 207 147 354t353 146 354-146 146-354-146-354-354-146-353 146-147 354z m174-287l219 43-176 176z m86 250l168-166 269 271-166 166z m342 316q-2-25 15-41l84-84q18-17 43-16t44 19 20 44-17 43l-84 84q-16 16-39 16-27 0-47-20-17-19-19-45z" horiz-adv-x="1000" /> | |||||
| </font> | |||||
| </defs> | |||||
| </svg> | |||||
| @ -0,0 +1,13 @@ | |||||
| body { | |||||
| margin: 0; | |||||
| font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', | |||||
| 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', | |||||
| sans-serif; | |||||
| -webkit-font-smoothing: antialiased; | |||||
| -moz-osx-font-smoothing: grayscale; | |||||
| } | |||||
| code { | |||||
| font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', | |||||
| monospace; | |||||
| } | |||||
| @ -0,0 +1,17 @@ | |||||
| import React from 'react'; | |||||
| import ReactDOM from 'react-dom'; | |||||
| import './index.css'; | |||||
| import App from './App'; | |||||
| import reportWebVitals from './reportWebVitals'; | |||||
| ReactDOM.render( | |||||
| <React.StrictMode> | |||||
| <App /> | |||||
| </React.StrictMode>, | |||||
| document.getElementById('root') | |||||
| ); | |||||
| // If you want to start measuring performance in your app, pass a function | |||||
| // to log results (for example: reportWebVitals(console.log)) | |||||
| // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals | |||||
| reportWebVitals(); | |||||
| @ -0,0 +1,52 @@ | |||||
| import { ADD, GET, GET_ALL, UPDATE, DELETE, INSPECT } from '../action/TaskType'; | |||||
| const initialState={ | |||||
| tasks:[], | |||||
| taskInspected: "" | |||||
| } | |||||
| export default function taskReducer(state=initialState, action){ | |||||
| let response; | |||||
| switch (action.type) { | |||||
| case GET_ALL: | |||||
| response={ | |||||
| ...state, | |||||
| tasks: action.payload | |||||
| }; | |||||
| break; | |||||
| case GET: | |||||
| response={ | |||||
| ...state, | |||||
| task: action.payload | |||||
| }; | |||||
| break; | |||||
| case ADD: | |||||
| response={ | |||||
| ...state, | |||||
| tasks: [action.payload, ...state.tasks] | |||||
| }; | |||||
| break; | |||||
| case UPDATE: | |||||
| response={ | |||||
| ...state, | |||||
| tasks: [action.payload, ...state.tasks.filter(task=>task._id !== action.payload._id)] | |||||
| }; | |||||
| break; | |||||
| case DELETE: | |||||
| response={ | |||||
| ...state, | |||||
| tasks: state.tasks.filter(task=>task._id !== action.payload) | |||||
| }; | |||||
| break; | |||||
| case INSPECT: | |||||
| response={ | |||||
| ...state, | |||||
| taskInspected: action.payload | |||||
| } | |||||
| break; | |||||
| default: | |||||
| response=state; | |||||
| break; | |||||
| } | |||||
| return response; | |||||
| } | |||||
| @ -0,0 +1,6 @@ | |||||
| import {combineReducers} from 'redux'; | |||||
| import taskReducer from './TaskReducer'; | |||||
| export default combineReducers({ | |||||
| task: taskReducer | |||||
| }); | |||||
| @ -0,0 +1,13 @@ | |||||
| const reportWebVitals = onPerfEntry => { | |||||
| if (onPerfEntry && onPerfEntry instanceof Function) { | |||||
| import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { | |||||
| getCLS(onPerfEntry); | |||||
| getFID(onPerfEntry); | |||||
| getFCP(onPerfEntry); | |||||
| getLCP(onPerfEntry); | |||||
| getTTFB(onPerfEntry); | |||||
| }); | |||||
| } | |||||
| }; | |||||
| export default reportWebVitals; | |||||
| @ -0,0 +1,5 @@ | |||||
| // jest-dom adds custom jest matchers for asserting on DOM nodes. | |||||
| // allows you to do things like: | |||||
| // expect(element).toHaveTextContent(/react/i) | |||||
| // learn more: https://github.com/testing-library/jest-dom | |||||
| import '@testing-library/jest-dom'; | |||||
| @ -0,0 +1,16 @@ | |||||
| import {createStore,applyMiddleware,compose} from 'redux'; | |||||
| import thunk from 'redux-thunk'; | |||||
| import rootReducer from './reducer'; | |||||
| const initialState={}; | |||||
| const middleware=[thunk]; | |||||
| const store=createStore( | |||||
| rootReducer, | |||||
| initialState, | |||||
| compose( | |||||
| applyMiddleware(...middleware), | |||||
| window.__REDUX_DEVTOOLS_EXTENSION__ ? window.__REDUX_DEVTOOLS_EXTENSION__() :f=>f | |||||
| ) | |||||
| ); | |||||
| export default store; | |||||
| @ -0,0 +1,4 @@ | |||||
| @import '_reset.scss'; | |||||
| @import '_screen.scss'; | |||||
| @import '_icon.scss'; | |||||
| @import '_variable.scss'; | |||||
| @ -0,0 +1,26 @@ | |||||
| @font-face { | |||||
| font-family: 'fontello'; | |||||
| src: url('../font/fontello/font/fontello.eot?4356932'); | |||||
| src: url('../font/fontello/font/fontello.eot?4356932#iefix') format('embedded-opentype'), | |||||
| url('../font/fontello/font/fontello.woff2?4356932') format('woff2'), | |||||
| url('../font/fontello/font/fontello.woff?4356932') format('woff'), | |||||
| url('../font/fontello/font/fontello.ttf?4356932') format('truetype'), | |||||
| url('../font/fontello/font/fontello.svg?4356932#fontello') format('svg'); | |||||
| font-weight: normal; | |||||
| font-style: normal; | |||||
| } | |||||
| @import "../font/fontello/css/fontello"; | |||||
| .icon{ | |||||
| font-family: 'fontello'; | |||||
| font-size: 28px; | |||||
| } | |||||
| .interactiveIcon{ | |||||
| @extend .icon; | |||||
| cursor: pointer; | |||||
| } | |||||
| [class^="icon-"]:before, [class*=" icon-"]:before{ | |||||
| @extend .interactiveIcon; | |||||
| } | |||||
| @ -0,0 +1,5 @@ | |||||
| @import-normalize; | |||||
| * { | |||||
| box-sizing : border-box; | |||||
| } | |||||
| @ -0,0 +1,34 @@ | |||||
| @mixin media-min-width($width){ | |||||
| @media (min-width: $width){ | |||||
| @content; | |||||
| } | |||||
| } | |||||
| @mixin small-screen() { | |||||
| @include media-min-width(576px){ | |||||
| @content; | |||||
| } | |||||
| } | |||||
| @mixin medium-screen() { | |||||
| @include media-min-width(768px){ | |||||
| @content; | |||||
| } | |||||
| } | |||||
| @mixin large-screen() { | |||||
| @include media-min-width(992px){ | |||||
| @content; | |||||
| } | |||||
| } | |||||
| @mixin xlarge-screen() { | |||||
| @include media-min-width(1200px){ | |||||
| @content; | |||||
| } | |||||
| } | |||||
| $smallScreenButton:( | |||||
| small:42px, | |||||
| medium:60px, | |||||
| large:72px, | |||||
| smallSpacing:36px, | |||||
| mediumSpacing:24px, | |||||
| largeSpacing:12px | |||||
| ); | |||||
| @ -0,0 +1,17 @@ | |||||
| $color-task: #BFBFBF; | |||||
| $colors: ( | |||||
| error: #FF0000, | |||||
| warning: #FF0000, | |||||
| modalBackground: rgba(64,64,64,0.7), | |||||
| modalForeground: $color-task | |||||
| ); | |||||
| .normalizeButton{ | |||||
| background: unset; | |||||
| color: inherit; | |||||
| border: unset; | |||||
| padding: 0; | |||||
| font: inherit; | |||||
| cursor: pointer; | |||||
| outline: inherit; | |||||
| } | |||||
| @ -0,0 +1,8 @@ | |||||
| @import "../base"; | |||||
| .displayError{ | |||||
| color: map-get($colors,"error"); | |||||
| .errorTitle{ | |||||
| font-weight: bold; | |||||
| } | |||||
| } | |||||
| @ -0,0 +1,47 @@ | |||||
| @import '../_base.scss'; | |||||
| .modal{ | |||||
| position: fixed; | |||||
| width: 100vw; | |||||
| height: 100vh; | |||||
| top: 0px; | |||||
| left: 0px; | |||||
| background-color: map-get($colors,modalBackground); | |||||
| .modalForeground{ | |||||
| align-self: center; | |||||
| justify-self: center; | |||||
| display: grid; | |||||
| padding: map-get($smallScreenButton,"largeSpacing"); | |||||
| max-width: calc( 100% - #{map-get($smallScreenButton,"smallSpacing")} ); | |||||
| max-height: calc( 100% - #{map-get($smallScreenButton,"smallSpacing")} ); | |||||
| margin: auto; | |||||
| overflow: hidden; | |||||
| background-color: map-get($colors,modalForeground); | |||||
| } | |||||
| .modalTitle{ | |||||
| grid-row: 1; | |||||
| grid-column: 1; | |||||
| align-self: center; | |||||
| } | |||||
| .modalClose{ | |||||
| @extend .interactiveIcon; | |||||
| grid-row: 1; | |||||
| grid-column: 2; | |||||
| justify-self: right; | |||||
| } | |||||
| .modalContent{ | |||||
| grid-row: 2; | |||||
| grid-column: 1/3; | |||||
| overflow: auto; | |||||
| max-height: calc( 100vh - 150px ); | |||||
| } | |||||
| } | |||||
| @ -0,0 +1,29 @@ | |||||
| @import '../base'; | |||||
| .task{ | |||||
| padding: map-get($smallScreenButton,"largeSpacing"); | |||||
| display: grid; | |||||
| grid-template-areas: | |||||
| "name edit" | |||||
| "id id" | |||||
| "description description"; | |||||
| .id{ | |||||
| grid-area: id; | |||||
| font-style: italic; | |||||
| color: grey; | |||||
| } | |||||
| .name{ | |||||
| grid-area: name; | |||||
| font-weight: bold; | |||||
| } | |||||
| .description{ | |||||
| grid-area: description; | |||||
| } | |||||
| .edit{ | |||||
| @extend .normalizeButton; | |||||
| grid-area: edit; | |||||
| justify-self: right; | |||||
| } | |||||
| } | |||||
| @ -0,0 +1,7 @@ | |||||
| @import "../base"; | |||||
| .taskCreate{ | |||||
| button{ | |||||
| @extend .normalizeButton; | |||||
| } | |||||
| } | |||||
| @ -0,0 +1,23 @@ | |||||
| @import '../base'; | |||||
| .taskForm{ | |||||
| form{ | |||||
| display: grid; | |||||
| grid-template-columns: repeat(1,1fr); | |||||
| label,input[type=submit]{ | |||||
| margin-top: map-get($smallScreenButton,"largeSpacing"); | |||||
| } | |||||
| @include medium-screen{ | |||||
| min-width: 500px; | |||||
| } | |||||
| } | |||||
| .taskFormDelete,input[type=submit]{ | |||||
| cursor: pointer; | |||||
| } | |||||
| .taskFormDelete { | |||||
| margin-top: map-get($smallScreenButton, "smallSpacing"); | |||||
| width: 100%; | |||||
| color: map-get($colors,"warning"); | |||||
| font-weight: bold; | |||||
| } | |||||
| } | |||||
| @ -0,0 +1,19 @@ | |||||
| @import '../base'; | |||||
| .taskList{ | |||||
| display: grid; | |||||
| } | |||||
| .taskGroup{ | |||||
| &:not(.taskGroup_0) &:not(.taskGroup_1){ | |||||
| margin-left: 30px; | |||||
| } | |||||
| box-shadow: | |||||
| 2px 0 0 0 #888, | |||||
| 0 2px 0 0 #888, | |||||
| 2px 2px 0 0 #888, /* Just to fix the corner */ | |||||
| 2px 0 0 0 #888 inset, | |||||
| 0 2px 0 0 #888 inset; | |||||
| } | |||||
| .taskGroup_0{ | |||||
| grid-row: 1; | |||||
| } | |||||
| @ -0,0 +1,429 @@ | |||||
| { | |||||
| "name": "kb", | |||||
| "version": "1.0.0", | |||||
| "lockfileVersion": 1, | |||||
| "requires": true, | |||||
| "dependencies": { | |||||
| "@babel/code-frame": { | |||||
| "version": "7.12.13", | |||||
| "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.13.tgz", | |||||
| "integrity": "sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g==", | |||||
| "requires": { | |||||
| "@babel/highlight": "^7.12.13" | |||||
| } | |||||
| }, | |||||
| "@babel/helper-validator-identifier": { | |||||
| "version": "7.12.11", | |||||
| "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz", | |||||
| "integrity": "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==" | |||||
| }, | |||||
| "@babel/highlight": { | |||||
| "version": "7.13.10", | |||||
| "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.13.10.tgz", | |||||
| "integrity": "sha512-5aPpe5XQPzflQrFwL1/QoeHkP2MsA4JCntcXHRhEsdsfPVkvPi2w7Qix4iV7t5S/oC9OodGrggd8aco1g3SZFg==", | |||||
| "requires": { | |||||
| "@babel/helper-validator-identifier": "^7.12.11", | |||||
| "chalk": "^2.0.0", | |||||
| "js-tokens": "^4.0.0" | |||||
| }, | |||||
| "dependencies": { | |||||
| "ansi-styles": { | |||||
| "version": "3.2.1", | |||||
| "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", | |||||
| "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", | |||||
| "requires": { | |||||
| "color-convert": "^1.9.0" | |||||
| } | |||||
| }, | |||||
| "chalk": { | |||||
| "version": "2.4.2", | |||||
| "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", | |||||
| "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", | |||||
| "requires": { | |||||
| "ansi-styles": "^3.2.1", | |||||
| "escape-string-regexp": "^1.0.5", | |||||
| "supports-color": "^5.3.0" | |||||
| } | |||||
| }, | |||||
| "color-convert": { | |||||
| "version": "1.9.3", | |||||
| "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", | |||||
| "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", | |||||
| "requires": { | |||||
| "color-name": "1.1.3" | |||||
| } | |||||
| }, | |||||
| "color-name": { | |||||
| "version": "1.1.3", | |||||
| "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", | |||||
| "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" | |||||
| }, | |||||
| "has-flag": { | |||||
| "version": "3.0.0", | |||||
| "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", | |||||
| "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" | |||||
| }, | |||||
| "supports-color": { | |||||
| "version": "5.5.0", | |||||
| "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", | |||||
| "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", | |||||
| "requires": { | |||||
| "has-flag": "^3.0.0" | |||||
| } | |||||
| } | |||||
| } | |||||
| }, | |||||
| "@types/normalize-package-data": { | |||||
| "version": "2.4.0", | |||||
| "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz", | |||||
| "integrity": "sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA==" | |||||
| }, | |||||
| "ansi-regex": { | |||||
| "version": "5.0.0", | |||||
| "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", | |||||
| "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==" | |||||
| }, | |||||
| "ansi-styles": { | |||||
| "version": "4.3.0", | |||||
| "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", | |||||
| "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", | |||||
| "requires": { | |||||
| "color-convert": "^2.0.1" | |||||
| } | |||||
| }, | |||||
| "chalk": { | |||||
| "version": "4.1.0", | |||||
| "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", | |||||
| "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", | |||||
| "requires": { | |||||
| "ansi-styles": "^4.1.0", | |||||
| "supports-color": "^7.1.0" | |||||
| }, | |||||
| "dependencies": { | |||||
| "supports-color": { | |||||
| "version": "7.2.0", | |||||
| "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", | |||||
| "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", | |||||
| "requires": { | |||||
| "has-flag": "^4.0.0" | |||||
| } | |||||
| } | |||||
| } | |||||
| }, | |||||
| "cliui": { | |||||
| "version": "7.0.4", | |||||
| "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", | |||||
| "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", | |||||
| "requires": { | |||||
| "string-width": "^4.2.0", | |||||
| "strip-ansi": "^6.0.0", | |||||
| "wrap-ansi": "^7.0.0" | |||||
| } | |||||
| }, | |||||
| "color-convert": { | |||||
| "version": "2.0.1", | |||||
| "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", | |||||
| "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", | |||||
| "requires": { | |||||
| "color-name": "~1.1.4" | |||||
| } | |||||
| }, | |||||
| "color-name": { | |||||
| "version": "1.1.4", | |||||
| "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", | |||||
| "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" | |||||
| }, | |||||
| "concurrently": { | |||||
| "version": "6.0.0", | |||||
| "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-6.0.0.tgz", | |||||
| "integrity": "sha512-Ik9Igqnef2ONLjN2o/OVx1Ow5tymVvvEwQeYCQdD/oV+CN9oWhxLk7ibcBdOtv0UzBqHCEKRwbKceYoTK8t3fQ==", | |||||
| "requires": { | |||||
| "chalk": "^4.1.0", | |||||
| "date-fns": "^2.16.1", | |||||
| "lodash": "^4.17.20", | |||||
| "read-pkg": "^5.2.0", | |||||
| "rxjs": "^6.6.3", | |||||
| "spawn-command": "^0.0.2-1", | |||||
| "supports-color": "^8.1.0", | |||||
| "tree-kill": "^1.2.2", | |||||
| "yargs": "^16.2.0" | |||||
| } | |||||
| }, | |||||
| "date-fns": { | |||||
| "version": "2.19.0", | |||||
| "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.19.0.tgz", | |||||
| "integrity": "sha512-X3bf2iTPgCAQp9wvjOQytnf5vO5rESYRXlPIVcgSbtT5OTScPcsf9eZU+B/YIkKAtYr5WeCii58BgATrNitlWg==" | |||||
| }, | |||||
| "emoji-regex": { | |||||
| "version": "8.0.0", | |||||
| "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", | |||||
| "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" | |||||
| }, | |||||
| "error-ex": { | |||||
| "version": "1.3.2", | |||||
| "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", | |||||
| "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", | |||||
| "requires": { | |||||
| "is-arrayish": "^0.2.1" | |||||
| } | |||||
| }, | |||||
| "escalade": { | |||||
| "version": "3.1.1", | |||||
| "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", | |||||
| "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==" | |||||
| }, | |||||
| "escape-string-regexp": { | |||||
| "version": "1.0.5", | |||||
| "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", | |||||
| "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" | |||||
| }, | |||||
| "function-bind": { | |||||
| "version": "1.1.1", | |||||
| "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", | |||||
| "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" | |||||
| }, | |||||
| "get-caller-file": { | |||||
| "version": "2.0.5", | |||||
| "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", | |||||
| "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" | |||||
| }, | |||||
| "has": { | |||||
| "version": "1.0.3", | |||||
| "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", | |||||
| "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", | |||||
| "requires": { | |||||
| "function-bind": "^1.1.1" | |||||
| } | |||||
| }, | |||||
| "has-flag": { | |||||
| "version": "4.0.0", | |||||
| "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", | |||||
| "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" | |||||
| }, | |||||
| "hosted-git-info": { | |||||
| "version": "2.8.8", | |||||
| "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz", | |||||
| "integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==" | |||||
| }, | |||||
| "is-arrayish": { | |||||
| "version": "0.2.1", | |||||
| "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", | |||||
| "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=" | |||||
| }, | |||||
| "is-core-module": { | |||||
| "version": "2.2.0", | |||||
| "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.2.0.tgz", | |||||
| "integrity": "sha512-XRAfAdyyY5F5cOXn7hYQDqh2Xmii+DEfIcQGxK/uNwMHhIkPWO0g8msXcbzLe+MpGoR951MlqM/2iIlU4vKDdQ==", | |||||
| "requires": { | |||||
| "has": "^1.0.3" | |||||
| } | |||||
| }, | |||||
| "is-fullwidth-code-point": { | |||||
| "version": "3.0.0", | |||||
| "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", | |||||
| "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" | |||||
| }, | |||||
| "js-tokens": { | |||||
| "version": "4.0.0", | |||||
| "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", | |||||
| "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" | |||||
| }, | |||||
| "json-parse-even-better-errors": { | |||||
| "version": "2.3.1", | |||||
| "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", | |||||
| "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" | |||||
| }, | |||||
| "lines-and-columns": { | |||||
| "version": "1.1.6", | |||||
| "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz", | |||||
| "integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=" | |||||
| }, | |||||
| "lodash": { | |||||
| "version": "4.17.21", | |||||
| "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", | |||||
| "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" | |||||
| }, | |||||
| "normalize-package-data": { | |||||
| "version": "2.5.0", | |||||
| "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", | |||||
| "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", | |||||
| "requires": { | |||||
| "hosted-git-info": "^2.1.4", | |||||
| "resolve": "^1.10.0", | |||||
| "semver": "2 || 3 || 4 || 5", | |||||
| "validate-npm-package-license": "^3.0.1" | |||||
| } | |||||
| }, | |||||
| "parse-json": { | |||||
| "version": "5.2.0", | |||||
| "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", | |||||
| "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", | |||||
| "requires": { | |||||
| "@babel/code-frame": "^7.0.0", | |||||
| "error-ex": "^1.3.1", | |||||
| "json-parse-even-better-errors": "^2.3.0", | |||||
| "lines-and-columns": "^1.1.6" | |||||
| } | |||||
| }, | |||||
| "path-parse": { | |||||
| "version": "1.0.6", | |||||
| "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", | |||||
| "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==" | |||||
| }, | |||||
| "read-pkg": { | |||||
| "version": "5.2.0", | |||||
| "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", | |||||
| "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", | |||||
| "requires": { | |||||
| "@types/normalize-package-data": "^2.4.0", | |||||
| "normalize-package-data": "^2.5.0", | |||||
| "parse-json": "^5.0.0", | |||||
| "type-fest": "^0.6.0" | |||||
| } | |||||
| }, | |||||
| "require-directory": { | |||||
| "version": "2.1.1", | |||||
| "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", | |||||
| "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=" | |||||
| }, | |||||
| "resolve": { | |||||
| "version": "1.20.0", | |||||
| "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz", | |||||
| "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==", | |||||
| "requires": { | |||||
| "is-core-module": "^2.2.0", | |||||
| "path-parse": "^1.0.6" | |||||
| } | |||||
| }, | |||||
| "rxjs": { | |||||
| "version": "6.6.6", | |||||
| "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.6.tgz", | |||||
| "integrity": "sha512-/oTwee4N4iWzAMAL9xdGKjkEHmIwupR3oXbQjCKywF1BeFohswF3vZdogbmEF6pZkOsXTzWkrZszrWpQTByYVg==", | |||||
| "requires": { | |||||
| "tslib": "^1.9.0" | |||||
| } | |||||
| }, | |||||
| "semver": { | |||||
| "version": "5.7.1", | |||||
| "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", | |||||
| "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" | |||||
| }, | |||||
| "spawn-command": { | |||||
| "version": "0.0.2-1", | |||||
| "resolved": "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2-1.tgz", | |||||
| "integrity": "sha1-YvXpRmmBwbeW3Fkpk34RycaSG9A=" | |||||
| }, | |||||
| "spdx-correct": { | |||||
| "version": "3.1.1", | |||||
| "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz", | |||||
| "integrity": "sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==", | |||||
| "requires": { | |||||
| "spdx-expression-parse": "^3.0.0", | |||||
| "spdx-license-ids": "^3.0.0" | |||||
| } | |||||
| }, | |||||
| "spdx-exceptions": { | |||||
| "version": "2.3.0", | |||||
| "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", | |||||
| "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==" | |||||
| }, | |||||
| "spdx-expression-parse": { | |||||
| "version": "3.0.1", | |||||
| "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", | |||||
| "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", | |||||
| "requires": { | |||||
| "spdx-exceptions": "^2.1.0", | |||||
| "spdx-license-ids": "^3.0.0" | |||||
| } | |||||
| }, | |||||
| "spdx-license-ids": { | |||||
| "version": "3.0.7", | |||||
| "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.7.tgz", | |||||
| "integrity": "sha512-U+MTEOO0AiDzxwFvoa4JVnMV6mZlJKk2sBLt90s7G0Gd0Mlknc7kxEn3nuDPNZRta7O2uy8oLcZLVT+4sqNZHQ==" | |||||
| }, | |||||
| "string-width": { | |||||
| "version": "4.2.2", | |||||
| "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz", | |||||
| "integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==", | |||||
| "requires": { | |||||
| "emoji-regex": "^8.0.0", | |||||
| "is-fullwidth-code-point": "^3.0.0", | |||||
| "strip-ansi": "^6.0.0" | |||||
| } | |||||
| }, | |||||
| "strip-ansi": { | |||||
| "version": "6.0.0", | |||||
| "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", | |||||
| "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", | |||||
| "requires": { | |||||
| "ansi-regex": "^5.0.0" | |||||
| } | |||||
| }, | |||||
| "supports-color": { | |||||
| "version": "8.1.1", | |||||
| "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", | |||||
| "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", | |||||
| "requires": { | |||||
| "has-flag": "^4.0.0" | |||||
| } | |||||
| }, | |||||
| "tree-kill": { | |||||
| "version": "1.2.2", | |||||
| "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", | |||||
| "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==" | |||||
| }, | |||||
| "tslib": { | |||||
| "version": "1.14.1", | |||||
| "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", | |||||
| "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" | |||||
| }, | |||||
| "type-fest": { | |||||
| "version": "0.6.0", | |||||
| "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", | |||||
| "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==" | |||||
| }, | |||||
| "validate-npm-package-license": { | |||||
| "version": "3.0.4", | |||||
| "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", | |||||
| "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", | |||||
| "requires": { | |||||
| "spdx-correct": "^3.0.0", | |||||
| "spdx-expression-parse": "^3.0.0" | |||||
| } | |||||
| }, | |||||
| "wrap-ansi": { | |||||
| "version": "7.0.0", | |||||
| "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", | |||||
| "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", | |||||
| "requires": { | |||||
| "ansi-styles": "^4.0.0", | |||||
| "string-width": "^4.1.0", | |||||
| "strip-ansi": "^6.0.0" | |||||
| } | |||||
| }, | |||||
| "y18n": { | |||||
| "version": "5.0.5", | |||||
| "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.5.tgz", | |||||
| "integrity": "sha512-hsRUr4FFrvhhRH12wOdfs38Gy7k2FFzB9qgN9v3aLykRq0dRcdcpz5C9FxdS2NuhOrI/628b/KSTJ3rwHysYSg==" | |||||
| }, | |||||
| "yargs": { | |||||
| "version": "16.2.0", | |||||
| "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", | |||||
| "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", | |||||
| "requires": { | |||||
| "cliui": "^7.0.2", | |||||
| "escalade": "^3.1.1", | |||||
| "get-caller-file": "^2.0.5", | |||||
| "require-directory": "^2.1.1", | |||||
| "string-width": "^4.2.0", | |||||
| "y18n": "^5.0.5", | |||||
| "yargs-parser": "^20.2.2" | |||||
| } | |||||
| }, | |||||
| "yargs-parser": { | |||||
| "version": "20.2.7", | |||||
| "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.7.tgz", | |||||
| "integrity": "sha512-FiNkvbeHzB/syOjIUxFDCnhSfzAL8R5vs40MgLFBorXACCOAEaWu0gRZl14vG8MR9AOJIZbmkjhusqBYZ3HTHw==" | |||||
| } | |||||
| } | |||||
| } | |||||
| @ -0,0 +1,17 @@ | |||||
| { | |||||
| "name": "kb", | |||||
| "version": "1.0.0", | |||||
| "description": "## Description", | |||||
| "main": "index.js", | |||||
| "scripts": { | |||||
| "start": "concurrently 'npm start --prefix back' 'npm start --prefix front'", | |||||
| "dev": "concurrently 'npm run dev --prefix back' 'npm start --prefix front'", | |||||
| "test": "npm run test --prefix back" | |||||
| }, | |||||
| "keywords": [], | |||||
| "author": "", | |||||
| "license": "ISC", | |||||
| "dependencies": { | |||||
| "concurrently": "^6.0.0" | |||||
| } | |||||
| } | |||||