@ -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** | |||
![main](assets/main.png) | |||
**Task update screen** | |||
![update](assets/update.png) |
@ -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" | |||
} | |||
} |