first commit
This commit is contained in:
commit
823b31e3fe
38
App.css
Normal file
38
App.css
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
.App {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.App-logo {
|
||||||
|
height: 40vmin;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: no-preference) {
|
||||||
|
.App-logo {
|
||||||
|
animation: App-logo-spin infinite 20s linear;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.App-header {
|
||||||
|
background-color: #282c34;
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: calc(10px + 2vmin);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.App-link {
|
||||||
|
color: #61dafb;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes App-logo-spin {
|
||||||
|
from {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
18
App.jsx
Normal file
18
App.jsx
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import Layout from "./components/layout";
|
||||||
|
import { toggleCompleted } from "./redux/actions";
|
||||||
|
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Layout />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(null, { toggleCompleted })(App);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
8
App.test.jsx
Normal file
8
App.test.jsx
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { render, screen } from '@testing-library/react';
|
||||||
|
import App from './App';
|
||||||
|
|
||||||
|
test('renders learn react link', () => {
|
||||||
|
render(<App />);
|
||||||
|
const linkElement = screen.getByText(/learn react/i);
|
||||||
|
expect(linkElement).toBeInTheDocument();
|
||||||
|
});
|
||||||
36
Jenkinsfile
vendored
Normal file
36
Jenkinsfile
vendored
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
pipeline {
|
||||||
|
|
||||||
|
agent {
|
||||||
|
node {
|
||||||
|
label 'master'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
options {
|
||||||
|
buildDiscarder logRotator(
|
||||||
|
daysToKeepStr: '16',
|
||||||
|
numToKeepStr: '10'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
stages {
|
||||||
|
|
||||||
|
|
||||||
|
stage(' Unit Testing') {
|
||||||
|
steps {
|
||||||
|
sh """
|
||||||
|
echo "Running Unit Tests"
|
||||||
|
"""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('Code Analysis') {
|
||||||
|
steps {
|
||||||
|
sh """
|
||||||
|
echo "Running Code Analysis"
|
||||||
|
"""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
9
LICENSE
Normal file
9
LICENSE
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) <year> <copyright holders>
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
72
PseudoCode.txt
Normal file
72
PseudoCode.txt
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
|
||||||
|
Alpha 0.01 Create couchdb/pouchdb databases as well as redux flow and access to django authentication/user management API (Status - WIP)
|
||||||
|
start
|
||||||
|
load index.html automatically into a login page
|
||||||
|
After login is succesful use security token to pull list of access and permissions information
|
||||||
|
From this point on to load to pouchdb any new database use project access id + user access token
|
||||||
|
Each project has an id which is required to find it in the couchdb and unexpired access id requried to read it
|
||||||
|
# I am not sure how I am going to synchronize security per project information with django server which has all the security
|
||||||
|
# information - most probably every 15 minutes django will contact couchdb and update the access tokens for each project
|
||||||
|
# since they are just certificates it should be pretty quick
|
||||||
|
# I will also investigate use of a common authenticator like oauth
|
||||||
|
Now user has their security paradigm loaded we need to load what they can see based on their clearance
|
||||||
|
play intro animation
|
||||||
|
While playing intro animation initiate pouchdb from couchdb and throw connection error if cannot pull from couchdb
|
||||||
|
From pouchdb initialize state and load redux
|
||||||
|
Check length of companies array in projects registry
|
||||||
|
This is a sanity check as each user should be part of at least one company and this list should not be empty
|
||||||
|
Else exit program and let user know that their security information could not be pulled (contact admin)
|
||||||
|
From redux check length of projects array in projects registry with filter for currentSelectedCompanyId
|
||||||
|
This is a sanity check as each user should be part of at least one project and this list should not be empty
|
||||||
|
Else exit program and let user know that their security information could not be pulled (contact admin)
|
||||||
|
Open project document using currentSelectedProjectId in dashboard
|
||||||
|
Load security tokens from API for project/company/tenant management django cloud app
|
||||||
|
Dashboard should contain a pull down for current company
|
||||||
|
Dashboard should contain a pull down for current project filtered based on company
|
||||||
|
Dashboard should have a new company button if they are cleared to created companies (otherwise don't show button)
|
||||||
|
Dashboard should contain a new project button based on company selected if they are cleared to create projects (otherwise don't show button)
|
||||||
|
Dashboard should contain a button for managing/viewing selected project
|
||||||
|
Dashboard should contain a button to manage company security if cleared (othewise no button)
|
||||||
|
Dashboard should contain a botton to manage PMO security if cleared (otherwise no button)
|
||||||
|
This concludes initial load - At this point user have following options
|
||||||
|
-Can select company or add a new company and activate it as selected company (if companyAdmin - otherwise new company is greyed out )
|
||||||
|
-Can select project or add a new project into company project registry for the company (if companyAdmin - otherwise new project is greyed out)
|
||||||
|
-If has permissions can initiate roles and responsibilties module for tenant adminitration to exit dasboard to go to tenant security module
|
||||||
|
-If has permissions can initiate roles and responsibilities module for company administration to exit dashboard to go to company security module
|
||||||
|
-Can chose to manage selected project and exits to that projects management module
|
||||||
|
Project Management Modules are dynamically created as each project has a different management
|
||||||
|
paradigm associated with project management model template. Each project has two different databases
|
||||||
|
meta-data database with contains relations based project management model as well as security. And another
|
||||||
|
unstructured document for each "leaf" in WBS structure. This allows flexibility to be managed on
|
||||||
|
schema created in metadata. That is to say each atomic work package at end of of WBS tree is a free form
|
||||||
|
document that can contain many data types including attachments, pictures,...
|
||||||
|
-Once in project management module we have different layout and options loaded based on management module template chosen during creation
|
||||||
|
-All KPIs and reporting of relations is built dynamically as each "object" of WBS tree only contains parents. Reason for multiple parents is that
|
||||||
|
an object can be related multiple objects. Objects are created based on project management model chosen.
|
||||||
|
|
||||||
|
|
||||||
|
Alpha 0.02 Use user profile model to load company/project dashboard as per user profile
|
||||||
|
Instead of writing to console, write to logs
|
||||||
|
Load with new project window a selector for template
|
||||||
|
Template will contain project schema for NoSQL for data that will be captured - Template will be versioned
|
||||||
|
Template will contain task definitions, relation definition, methodology and reporting framework
|
||||||
|
Template will also be mother of project specific workers, actioners, collectors and other artifacts
|
||||||
|
Create default template which is "DITSWBS instance configuration" project
|
||||||
|
Proces expected as follows:
|
||||||
|
-Since we don't have cloud instantiation defined yet user logs into dev DITSWBS environment
|
||||||
|
-User logs in
|
||||||
|
-If no company defined default admin will create the first company
|
||||||
|
-If no project is defined it will default to initial project which is configuration of DITSWBS
|
||||||
|
-If both company and project is defined user will default to last used company/project combination or to the default
|
||||||
|
-User presented with project dashboard defined as per template used
|
||||||
|
-User continues to work through drilling down this dashboard or selects different company/project pair (if user pulls down company change selector, upon selection project selector pops up. If user selects another project in same company this is only one step of selecting project. Cannot commit selections without both company and project being defined as selection)
|
||||||
|
Alpha 0.03 Create default project management model (PMP6) and couchdb database for PModels
|
||||||
|
Alpha 0.04 Synch django with couchdb for list of projects and security paradigm
|
||||||
|
Alpha 0.05 Create company form with upward sync
|
||||||
|
Alpha 0.06 Project creation form using PMP6 model as default with upward sync
|
||||||
|
Alpha 0.07 Create initial project management pages for a project created using PMP6 model
|
||||||
|
At this point admin can create users in django and projects created with PMP6 model will appear django as well as new database in couchdb (user has full rights to do everything)
|
||||||
|
...
|
||||||
|
Alpha 0.49 Create production build for react WBS app container
|
||||||
|
Alpha 0.50 Create container deployment scripts for kubernetes
|
||||||
|
Alpha 0.51 Create cton
|
||||||
6
README.md
Normal file
6
README.md
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
# p00021
|
||||||
|
# MIT License initially
|
||||||
|
# subject to change
|
||||||
|
#
|
||||||
|
#
|
||||||
|
|
||||||
66
components/Dashboard.jsx
Normal file
66
components/Dashboard.jsx
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import ProjectList from "./projectList";
|
||||||
|
import TaskList from "./taskList";
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import Paper from '@mui/material/Paper';
|
||||||
|
|
||||||
|
// Dashboard shows projects loaded from cloud based NoSqL which is in sync with a relational
|
||||||
|
// database which holds security and other relationship based information about the projects
|
||||||
|
// When a project is selected various artifacts in the project are shown based on template
|
||||||
|
// it was based on.
|
||||||
|
function Dashboard(props) {
|
||||||
|
if (props.selectedProjectId !== "0") {
|
||||||
|
const selectedProject = (projects, selectedProjectId) => {
|
||||||
|
return projects.filter((project) => project.id === selectedProjectId);
|
||||||
|
};
|
||||||
|
|
||||||
|
const project = props.projects.find(
|
||||||
|
(project) => project.projectId === props.selectedProjectId
|
||||||
|
);
|
||||||
|
|
||||||
|
// return TaskList as a table with two columns WBS numbers and Tasks when a project is selected
|
||||||
|
// WBS number is recalculated when change occurs
|
||||||
|
// regardless of the template used task decomposition is utilized until task can be assigned to
|
||||||
|
// one and only one resource. Resource can be a person, team, departement or company.
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
'& > :not(style)': {
|
||||||
|
m: 1,
|
||||||
|
width: 480,
|
||||||
|
height: 1/2,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Paper elevation={2}>
|
||||||
|
<h4> {project ? project.projectName : "No Project Selected - Please select one"} </h4>
|
||||||
|
<ProjectList />
|
||||||
|
<TaskList />
|
||||||
|
</Paper>
|
||||||
|
</Box>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h2> Please select a project </h2>
|
||||||
|
<ProjectList />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps)(Dashboard);
|
||||||
|
|
||||||
|
function mapStateToProps(state) {
|
||||||
|
return {
|
||||||
|
projects: state.projectsReducer.projects,
|
||||||
|
selectedProjectId: state.projectsReducer.selectedProjectId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
14
components/about.jsx
Normal file
14
components/about.jsx
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
function About() {
|
||||||
|
return ( <>
|
||||||
|
<div>
|
||||||
|
<h3>DITSWBS is mobile first client application with a cloud backend:</h3>
|
||||||
|
<p>Goal of the application is to separate concerns. Administration has its own portal that manages users, permissions, and project initiation.</p>
|
||||||
|
<p>Templates are static, open, currated, and JSON based. </p>
|
||||||
|
<p>Main program that client uses helps project manager, coordinator, user or guest to interract with project to break down work packages which come from Templates.</p>
|
||||||
|
</div>
|
||||||
|
</> );
|
||||||
|
}
|
||||||
|
|
||||||
|
export default About;
|
||||||
41
components/addTask.jsx
Normal file
41
components/addTask.jsx
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
|
||||||
|
function TaskList(props) {
|
||||||
|
const handleAddTask = () => {
|
||||||
|
const text = prompt("Enter a new task:");
|
||||||
|
if (text) {
|
||||||
|
props.dispatch({ type: "ADD_TASK", text, id });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleToggleTask = (id) => {
|
||||||
|
props.dispatch({ type: "TOGGLE_TASK", id });
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<ul>
|
||||||
|
{props.tasks.map((task) => (
|
||||||
|
<li key={task.id}>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={task.completed}
|
||||||
|
onChange={() => handleToggleTask(task.id)}
|
||||||
|
/>
|
||||||
|
{task.text}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
<button onClick={handleAddTask}>Add Task</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapStateToProps(state) {
|
||||||
|
return {
|
||||||
|
tasks: state.tasksReducer.tasks,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps)(TaskList);
|
||||||
70
components/commitToPDB.jsx
Normal file
70
components/commitToPDB.jsx
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import PouchDB from "pouchdb";
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { store } from "../redux/store";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
|
function CommitToPouchDB(props) {
|
||||||
|
|
||||||
|
const project = props.projects.find(
|
||||||
|
(project) => project.projectId === props.selectedProjectId
|
||||||
|
);
|
||||||
|
|
||||||
|
const currentProject = project.projectName
|
||||||
|
// use selected project to commit
|
||||||
|
// please note project creation and management is done through project system administration web app
|
||||||
|
const projectLink = "http://192.168.0.234:5982/" + currentProject
|
||||||
|
// get latest version of the project from nosql database
|
||||||
|
console.log({projectLink})
|
||||||
|
|
||||||
|
const localTasksDB = new PouchDB({currentProject})
|
||||||
|
const remoteTasksDB = new PouchDB( {projectLink} , {
|
||||||
|
auth: {
|
||||||
|
username: "ditswbsclient",
|
||||||
|
password: "bluebeard",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const syncHandler = localTasksDB
|
||||||
|
.sync(remoteTasksDB, {
|
||||||
|
live: true,
|
||||||
|
retry: true,
|
||||||
|
})
|
||||||
|
.on("paused", (info) => {
|
||||||
|
console.log("Task replication paused");
|
||||||
|
})
|
||||||
|
.on("active", (info) => {
|
||||||
|
console.log("Task replication resumed");
|
||||||
|
})
|
||||||
|
.on("change", (change) => {
|
||||||
|
console.log("Task change detected");
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
const commitTasks = () => {
|
||||||
|
try {
|
||||||
|
|
||||||
|
const result = localTasksDB
|
||||||
|
.bulkDocs(props.tasks)
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error uploading new tasks to cloud: ", error);
|
||||||
|
}
|
||||||
|
return () => console.log('unmounting...');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
commitTasks();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps)(CommitToPouchDB);
|
||||||
|
|
||||||
|
function mapStateToProps(state) {
|
||||||
|
return {
|
||||||
|
tasks: state.tasksReducer.tasks,
|
||||||
|
selectedProjectId: state.projectsReducer.selectedProjectId,
|
||||||
|
projects: state.projectsReducer.projects,
|
||||||
|
};
|
||||||
|
}
|
||||||
0
components/createCompany.jsx
Normal file
0
components/createCompany.jsx
Normal file
11
components/createDatabaseCDB.jsx
Normal file
11
components/createDatabaseCDB.jsx
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PouchDB from 'pouchdb';
|
||||||
|
|
||||||
|
function createDatabaseCDB (props.databaseName) {
|
||||||
|
const db = new PouchDB(props.databaseName);
|
||||||
|
try {
|
||||||
|
await db.replicate.to('http://192.168.0.234:5894/'+{props.databaseName});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error synchronizing local database with remote: ', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
16
components/home.jsx
Normal file
16
components/home.jsx
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
function Home() {
|
||||||
|
return ( <>
|
||||||
|
<div>
|
||||||
|
<h2>Welcome to DITSWBS application</h2>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3>Please use administrative portal to manage users and projects</h3>
|
||||||
|
<h3>Dashboard is where you will coordinate your project tasks and work breakdown</h3>
|
||||||
|
<p>Templates that will play an important part in growth of DITSWBS are under development. This section will contain description of each curated template.</p>
|
||||||
|
</div>
|
||||||
|
</>);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Home;
|
||||||
194
components/layout.jsx
Normal file
194
components/layout.jsx
Normal file
@ -0,0 +1,194 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import AppBar from '@mui/material/AppBar';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import Toolbar from '@mui/material/Toolbar';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
import ClickAwayListener from '@mui/material/ClickAwayListener';
|
||||||
|
import Grow from '@mui/material/Grow';
|
||||||
|
import Paper from '@mui/material/Paper';
|
||||||
|
import Popper from '@mui/material/Popper';
|
||||||
|
import MenuItem from '@mui/material/MenuItem';
|
||||||
|
import MenuList from '@mui/material/MenuList';
|
||||||
|
import Stack from '@mui/material/Stack';
|
||||||
|
import IconButton from '@mui/material/IconButton';
|
||||||
|
import MenuIcon from '@mui/icons-material/Menu';
|
||||||
|
//import List from '@mui/material/List';
|
||||||
|
import ListItem from '@mui/material/ListItem';
|
||||||
|
import ListItemIcon from '@mui/material/ListItemIcon';
|
||||||
|
import ListItemText from '@mui/material/ListItemText';
|
||||||
|
//import Divider from '@mui/material/Divider';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
//import Tabs from '@mui/material/Tabs';
|
||||||
|
//import Tab from '@mui/material/Tab';
|
||||||
|
import {
|
||||||
|
Link as RouterLink,
|
||||||
|
Route,
|
||||||
|
Routes,
|
||||||
|
MemoryRouter,
|
||||||
|
} from 'react-router-dom';
|
||||||
|
|
||||||
|
import { StaticRouter } from 'react-router-dom/server';
|
||||||
|
import Home from "./home";
|
||||||
|
import Dashboard from "./Dashboard";
|
||||||
|
import About from "./about";
|
||||||
|
import PrivateRoute from "./privateRoute";
|
||||||
|
import Login from "./login";
|
||||||
|
|
||||||
|
|
||||||
|
//Start in root folder
|
||||||
|
function Router(props) {
|
||||||
|
const { children } = props;
|
||||||
|
if (typeof window === 'undefined') {
|
||||||
|
return <StaticRouter location="/">{children}</StaticRouter>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MemoryRouter initialEntries={['/']} initialIndex={0}>
|
||||||
|
{children}
|
||||||
|
</MemoryRouter>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Router.propTypes = {
|
||||||
|
children: PropTypes.node,
|
||||||
|
};
|
||||||
|
|
||||||
|
const Link = React.forwardRef(function Link(itemProps, ref) {
|
||||||
|
return <RouterLink ref={ref} {...itemProps} role={undefined} />;
|
||||||
|
});
|
||||||
|
|
||||||
|
function ListItemLink(props) {
|
||||||
|
const { icon, primary, to } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li>
|
||||||
|
<ListItem button component={Link} to={to}>
|
||||||
|
{icon ? <ListItemIcon>{icon}</ListItemIcon> : null}
|
||||||
|
<ListItemText primary={primary} />
|
||||||
|
</ListItem>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ListItemLink.propTypes = {
|
||||||
|
icon: PropTypes.element,
|
||||||
|
primary: PropTypes.string.isRequired,
|
||||||
|
to: PropTypes.string.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export default function Layout() {
|
||||||
|
|
||||||
|
const [open, setOpen] = React.useState(false);
|
||||||
|
const anchorRef = React.useRef(null);
|
||||||
|
|
||||||
|
const handleToggle = () => {
|
||||||
|
setOpen((prevOpen) => !prevOpen);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClose = (event) => {
|
||||||
|
if (anchorRef.current && anchorRef.current.contains(event.target)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
function handleListKeyDown(event) {
|
||||||
|
if (event.key === 'Tab') {
|
||||||
|
event.preventDefault();
|
||||||
|
setOpen(false);
|
||||||
|
} else if (event.key === 'Escape') {
|
||||||
|
setOpen(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// return focus to the button when we transitioned from !open -> open
|
||||||
|
const prevOpen = React.useRef(open);
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (prevOpen.current === true && open === false) {
|
||||||
|
anchorRef.current.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
prevOpen.current = open;
|
||||||
|
}, [open]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Box sx={{ flexGrow: 1 }}>
|
||||||
|
<AppBar position="static">
|
||||||
|
<Toolbar>
|
||||||
|
<Stack direction="row" spacing={2}>
|
||||||
|
<IconButton
|
||||||
|
size="large"
|
||||||
|
edge="start"
|
||||||
|
color="inherit"
|
||||||
|
aria-label="menu"
|
||||||
|
sx={{ mr: 2 }}
|
||||||
|
ref={anchorRef}
|
||||||
|
id="composition-button"
|
||||||
|
aria-controls={open ? 'composition-menu' : undefined}
|
||||||
|
aria-expanded={open ? 'true' : undefined}
|
||||||
|
aria-haspopup="true"
|
||||||
|
onClick={handleToggle}
|
||||||
|
>
|
||||||
|
<MenuIcon />
|
||||||
|
<Popper
|
||||||
|
open={open}
|
||||||
|
anchorEl={anchorRef.current}
|
||||||
|
role={undefined}
|
||||||
|
placement="bottom-start"
|
||||||
|
transition
|
||||||
|
disablePortal
|
||||||
|
>
|
||||||
|
{({ TransitionProps, placement }) => (
|
||||||
|
<Grow
|
||||||
|
{...TransitionProps}
|
||||||
|
style={{
|
||||||
|
transformOrigin:
|
||||||
|
placement === 'bottom-start' ? 'left top' : 'left bottom',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Paper>
|
||||||
|
<ClickAwayListener onClickAway={handleClose}>
|
||||||
|
<MenuList
|
||||||
|
autoFocusItem={open}
|
||||||
|
id="composition-menu"
|
||||||
|
aria-labelledby="composition-button"
|
||||||
|
onKeyDown={handleListKeyDown}
|
||||||
|
>
|
||||||
|
<MenuItem onClick={handleClose}>
|
||||||
|
<Link component={RouterLink} to="/"> Home </Link>
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem onClick={handleClose}>
|
||||||
|
<PrivateRoute component={RouterLink} />
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem onClick={handleClose}>
|
||||||
|
<Link component={RouterLink} to="/about"> About </Link>
|
||||||
|
</MenuItem>
|
||||||
|
</MenuList>
|
||||||
|
</ClickAwayListener>
|
||||||
|
</Paper>
|
||||||
|
</Grow>
|
||||||
|
)}
|
||||||
|
</Popper>
|
||||||
|
</IconButton>
|
||||||
|
</Stack>
|
||||||
|
<Typography variant="h6" component="div" sx={{ flexGrow: 1 }}>
|
||||||
|
DITSWBS
|
||||||
|
</Typography>
|
||||||
|
</Toolbar>
|
||||||
|
</AppBar>
|
||||||
|
</Box>
|
||||||
|
<div>
|
||||||
|
<Routes>
|
||||||
|
<Route path="/" element=<Home /> >Home</Route>
|
||||||
|
<Route path="/about" element=<About /> >About</Route>
|
||||||
|
<Route path="/login" element=<Login /> >Login</Route>
|
||||||
|
<Route path="/dashboard" element=<Dashboard /> >Dashboard</Route>
|
||||||
|
</Routes>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
129
components/ldbcfg.jsx
Normal file
129
components/ldbcfg.jsx
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
import PouchDB from "pouchdb";
|
||||||
|
import { ADD_PROJECT,ADD_CDB_TASK } from "../redux/actionTypes";
|
||||||
|
import { store } from "../redux/store";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
function LoadDB() {
|
||||||
|
|
||||||
|
const projectsDB = new PouchDB("projectsDB");
|
||||||
|
const remoteDB = new PouchDB("http://192.168.0.234:5984/ditswbs", {
|
||||||
|
auth: {
|
||||||
|
username: "ditswbsclient",
|
||||||
|
password: "bluebeard",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
const syncHandler = projectsDB
|
||||||
|
.sync(remoteDB, {
|
||||||
|
live: true,
|
||||||
|
retry: true,
|
||||||
|
})
|
||||||
|
.on("paused", (info) => {
|
||||||
|
console.log("Project replication paused");
|
||||||
|
})
|
||||||
|
.on("active", (info) => {
|
||||||
|
console.log("Project replication resumed");
|
||||||
|
})
|
||||||
|
.on("change", (change) => {
|
||||||
|
console.log("Project change detected");
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const initializeRedux = () => {
|
||||||
|
try {
|
||||||
|
|
||||||
|
const result = projectsDB
|
||||||
|
.allDocs({ include_docs: true })
|
||||||
|
.then((result) => {
|
||||||
|
const projects = result.rows.map((row) => row.doc);
|
||||||
|
projects.forEach((project) =>
|
||||||
|
{
|
||||||
|
store.dispatch({ type: ADD_PROJECT, project });
|
||||||
|
let cdbsite = "http://192.168.0.234:5984"+project.projectId
|
||||||
|
const remoteTasksDB = new PouchDB(cdbsite, {
|
||||||
|
auth: {
|
||||||
|
username: "ditswbsclient",
|
||||||
|
password: "bluebeard",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const taskResult = remoteTasksDB
|
||||||
|
.allDocs({ include_docs: true })
|
||||||
|
.then((taskResult) => {
|
||||||
|
const tasks = taskResult.rows.map((row) => row.doc);
|
||||||
|
tasks.forEach((task) => {
|
||||||
|
if(task.taskName !== null && task.taskName !== undefined) {
|
||||||
|
store.dispatch({ type: ADD_CDB_TASK, task })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error initialiazing redux: ", error);
|
||||||
|
}
|
||||||
|
return () => console.log('unmounting...');
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
initializeRedux();
|
||||||
|
}, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export default LoadDB;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
//if(!ignore) {
|
||||||
|
//return () => {
|
||||||
|
// ignore = true;
|
||||||
|
// };
|
||||||
|
// Fetch initial projects
|
||||||
|
|
||||||
|
// const [documents, setDocuments] = useState([]);
|
||||||
|
// const db = new PouchDB('projectDocs');
|
||||||
|
// console.log ("Database created Successfully.");
|
||||||
|
//
|
||||||
|
// useEffect(() => {
|
||||||
|
// const initializeDatabase = async () => {
|
||||||
|
// try {
|
||||||
|
// const initialProject = await db.put ({
|
||||||
|
// _id: "99",
|
||||||
|
// projects: [
|
||||||
|
// {
|
||||||
|
// id: 0,
|
||||||
|
// projectName: "Default",
|
||||||
|
// description: "Default project for testing",
|
||||||
|
// status: "initiated" },
|
||||||
|
// ]}
|
||||||
|
// )
|
||||||
|
// } catch (error) {
|
||||||
|
// console.error('Error initializing database: ',error)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// const fetchDocuments = async () => {
|
||||||
|
// try {
|
||||||
|
// const result = await db.allDocs({ include_docs: true });
|
||||||
|
// const documents = result.rows.map(row => row.doc);
|
||||||
|
// setDocuments(documents);
|
||||||
|
// } catch (error) {
|
||||||
|
// console.error('Error fetching documents: ', error);
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
//
|
||||||
|
// initializeDatabase();
|
||||||
|
// fetchDocuments();
|
||||||
|
// }, []);
|
||||||
|
|
||||||
|
|
||||||
2
components/logger.jsx
Normal file
2
components/logger.jsx
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
# logger service - will log what is going on
|
||||||
|
# write to log instead of console out
|
||||||
59
components/login.jsx
Normal file
59
components/login.jsx
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import AUTH_API from "../constants";
|
||||||
|
//import AdminSite from "../configs/admincfgs";
|
||||||
|
|
||||||
|
const Login = () => {
|
||||||
|
const [username, setUsername] = useState("");
|
||||||
|
const [password, setPassword] = useState("");
|
||||||
|
|
||||||
|
const handleLogin = async () => {
|
||||||
|
try {
|
||||||
|
// Make a POST request to the external REST API for authentication
|
||||||
|
const ditswbsapisite = AUTH_API;
|
||||||
|
const response = await fetch(
|
||||||
|
ditswbsapisite,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ username, password }),
|
||||||
|
},
|
||||||
|
{ mode: "cors" }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
// Store the authentication token in local storage or cookies
|
||||||
|
localStorage.setItem("authToken", data.token);
|
||||||
|
// Redirect to the authenticated page
|
||||||
|
window.location.href = "/dashboard";
|
||||||
|
} else {
|
||||||
|
// Handle login error (e.g., show error message to the user)
|
||||||
|
console.log("Login failed.");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error during login:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
label="Username"
|
||||||
|
type="text"
|
||||||
|
value={username}
|
||||||
|
onChange={(e) => setUsername(e.target.value)}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
label="Password"
|
||||||
|
type="password"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
/>
|
||||||
|
<button onClick={handleLogin}>Login</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Login;
|
||||||
79
components/managePDB.js
Normal file
79
components/managePDB.js
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
import PouchDB from 'pouchdb';
|
||||||
|
import { connect } from 'react-redux'
|
||||||
|
|
||||||
|
function PDB(){
|
||||||
|
const projectsDB = new PouchDB("projectDocs");
|
||||||
|
const wbsDB = new PouchDB("wbsDocs");
|
||||||
|
const tasksDB = new PouchDB("taskDocs");
|
||||||
|
|
||||||
|
const remoteDB = new PouchDB('http://192.168.0.234:5984/ditswbs');
|
||||||
|
projectsDB.sync(remoteDB, { live: true, retry: true });
|
||||||
|
tasksDB.sync(remoteDB, { live: true, retry: true });
|
||||||
|
wbsDB.sync(remoteDB, { live: true, retry: true });
|
||||||
|
|
||||||
|
function GetDoc(docID) {
|
||||||
|
return new Promise((resolve, reject)=> {
|
||||||
|
projectsDB.get(docID)
|
||||||
|
.then((result)=> {return resolve(result)})
|
||||||
|
.catch((err)=> {
|
||||||
|
if (err.status === 404) {
|
||||||
|
console.log('error 404')
|
||||||
|
return resolve(null)
|
||||||
|
} else {
|
||||||
|
console.log('other:', err)
|
||||||
|
return reject(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
//function commitProjects() {
|
||||||
|
// db.put(
|
||||||
|
// projects: props.projects,
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
|
||||||
|
// function commitWBS() {
|
||||||
|
// evaluate parent child relation based on IDs
|
||||||
|
// and determine WBS level of each task in selected project
|
||||||
|
// this is dictionary where WBS level against project ID
|
||||||
|
// reorder the dictionary
|
||||||
|
// put the dictionary in local pouchdb
|
||||||
|
// db.put(
|
||||||
|
// projects: props.projects,
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
|
||||||
|
// function commitTasks(Tasks) {
|
||||||
|
// first check if a project is selected. This is a requirement before tasks can be
|
||||||
|
// commmitted
|
||||||
|
// if (!props.selectedProjectId) {
|
||||||
|
// commit tasks for selected projectID equals to one of the entries in task parentID
|
||||||
|
// Step1 verify if current input list of tasks have selected selected projectID as
|
||||||
|
// parentCount = 0
|
||||||
|
// const parents = Tasks.parentId
|
||||||
|
// parents.map ( parent => {if ( parent seletecProjectId eq ) {
|
||||||
|
// parentCount = parentCount+1 }}
|
||||||
|
//
|
||||||
|
// parent. If one or more do not have selected tasks fail the check return their
|
||||||
|
// with the a descriptive error message.
|
||||||
|
//...
|
||||||
|
// Step2
|
||||||
|
// This step is nested in step 1. If initial check is successful then commit inputed
|
||||||
|
// if (parents.length = parentCount ) {
|
||||||
|
// ... commit code}
|
||||||
|
// exit with error
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps)(PDB);
|
||||||
|
|
||||||
|
function mapStateToProps(state) {
|
||||||
|
return {
|
||||||
|
tasks: state.tasksReducer.tasks,
|
||||||
|
projects: state.projectsReducer.projects,
|
||||||
|
selectedProjectId: state.projectsReducer.selectedProjectId,
|
||||||
|
};
|
||||||
|
}
|
||||||
1
components/manageProjects.jsx
Normal file
1
components/manageProjects.jsx
Normal file
@ -0,0 +1 @@
|
|||||||
|
|
||||||
1
components/navigation.jsx
Normal file
1
components/navigation.jsx
Normal file
@ -0,0 +1 @@
|
|||||||
|
navigation
|
||||||
25
components/privateRoute.jsx
Normal file
25
components/privateRoute.jsx
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import React from "react";
|
||||||
|
//import { StaticRouter } from "react-router-dom/server";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
import Login from "./login";
|
||||||
|
|
||||||
|
// Function to check if the user is authenticated (You can implement this based on your authentication method)
|
||||||
|
const PrivateRoute = ({ component: Component }) => {
|
||||||
|
const isAuthenticated = () => {
|
||||||
|
// Check if the user is authenticated (e.g., check for a valid authentication token in local storage)
|
||||||
|
return localStorage.getItem("authToken") !== null;
|
||||||
|
};
|
||||||
|
return isAuthenticated() ? (
|
||||||
|
<Link component={Component} to="/dashboard">
|
||||||
|
{" "}
|
||||||
|
Dashboard{" "}
|
||||||
|
</Link>
|
||||||
|
) : (
|
||||||
|
<Link component={Login} to="/login">
|
||||||
|
{" "}
|
||||||
|
Login{" "}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PrivateRoute;
|
||||||
17
components/project.jsx
Normal file
17
components/project.jsx
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import cx from "classnames";
|
||||||
|
import { selectProject } from "../redux/actions";
|
||||||
|
|
||||||
|
|
||||||
|
const Task = ({ id, status, assigned2, projectName, description }) => (
|
||||||
|
<li className="project-item">
|
||||||
|
|
||||||
|
<span>
|
||||||
|
{status} {assigned2} {project} {description}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default connect(null, { projectSelected })(Task);
|
||||||
|
|
||||||
54
components/projectList.jsx
Normal file
54
components/projectList.jsx
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import InputLabel from '@mui/material/InputLabel';
|
||||||
|
import MenuItem from '@mui/material/MenuItem';
|
||||||
|
import FormControl from '@mui/material/FormControl';
|
||||||
|
import Select from '@mui/material/Select';
|
||||||
|
|
||||||
|
function ProjectList(props) {
|
||||||
|
function handleSelectProject(id) {
|
||||||
|
props.dispatch({ type: "SELECT_PROJECT", id });
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadedProjects = props.projects
|
||||||
|
const loadedProjectID = props.selectedProjectId
|
||||||
|
console.log(loadedProjects)
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div>
|
||||||
|
<Box sx={{ minWidth: 100 }}>
|
||||||
|
<FormControl fullWidth>
|
||||||
|
<InputLabel id="demo-simple-select-label">Projects</InputLabel>
|
||||||
|
<Select
|
||||||
|
labelId="demo-simple-select-label"
|
||||||
|
id="demo-simple-select"
|
||||||
|
// value={JSON.stringify(loadedProjectID)}
|
||||||
|
label="Projects"
|
||||||
|
>
|
||||||
|
{loadedProjects.map(({ projectId, projectName }) => (
|
||||||
|
<MenuItem
|
||||||
|
key={projectId}
|
||||||
|
onClick={() => handleSelectProject(projectId)}
|
||||||
|
value={projectName}
|
||||||
|
// className={loadedProjectID === projectId ? "selected" : ""}
|
||||||
|
>
|
||||||
|
{projectName}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
</Box>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps)(ProjectList);
|
||||||
|
|
||||||
|
function mapStateToProps(state) {
|
||||||
|
return {
|
||||||
|
projects: state.projectsReducer.projects,
|
||||||
|
selectedProjectId: state.projectsReducer.selectedProjectId,
|
||||||
|
};
|
||||||
|
}
|
||||||
0
components/routes.jsx
Normal file
0
components/routes.jsx
Normal file
24
components/task.jsx
Normal file
24
components/task.jsx
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import cx from "classnames";
|
||||||
|
import { toggleCompleted } from "../redux/actions";
|
||||||
|
|
||||||
|
|
||||||
|
const Task = ({ taskId, completed, assigned2, taskName, description, parentIds, status }) => (
|
||||||
|
<li className="task-item" onClick={() => toggleCompleted(id)}>
|
||||||
|
{"["} {completed ? true : false } {"]"}
|
||||||
|
|
||||||
|
<span
|
||||||
|
className={cx(
|
||||||
|
"task-item__text",
|
||||||
|
completed && "task-item__text--completed"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{taskId} {assigned2} {task} {description} {parentIds} {status}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default connect(null, { toggleCompleted })(Task);
|
||||||
|
|
||||||
|
// {(item.assigned2, item.task, item.description)}
|
||||||
226
components/taskList.jsx
Normal file
226
components/taskList.jsx
Normal file
@ -0,0 +1,226 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import Grid from "@mui/material/Grid";
|
||||||
|
import Box from "@mui/material/Box";
|
||||||
|
import Paper from "@mui/material/Paper";
|
||||||
|
import { styled } from "@mui/material/styles";
|
||||||
|
import PouchDB from "pouchdb";
|
||||||
|
import { v4 as uuidv4 } from "uuid";
|
||||||
|
|
||||||
|
function TaskList(props) {
|
||||||
|
// ToDo: Setup alerting for various actions through MaterialToas
|
||||||
|
|
||||||
|
const project = props.projects.find(
|
||||||
|
(project) => project.projectId === props.selectedProjectId
|
||||||
|
);
|
||||||
|
const currentProject = project.projectId;
|
||||||
|
const currentProjectStr = JSON.stringify(project.projectId);
|
||||||
|
const projectLink = "http://192.168.0.234:5984/" + currentProject;
|
||||||
|
// Setup the data persistence
|
||||||
|
const localTasksDB = new PouchDB(currentProjectStr);
|
||||||
|
const remoteTasksDB = new PouchDB(projectLink, {
|
||||||
|
auth: {
|
||||||
|
username: "ditswbsclient",
|
||||||
|
password: "bluebeard",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const syncHandler = localTasksDB
|
||||||
|
.sync(remoteTasksDB, {
|
||||||
|
live: true,
|
||||||
|
retry: true,
|
||||||
|
})
|
||||||
|
.on("paused", (info) => {
|
||||||
|
console.log("Task replication paused");
|
||||||
|
})
|
||||||
|
.on("active", (info) => {
|
||||||
|
console.log("Task replication resumed");
|
||||||
|
})
|
||||||
|
.on("change", (change) => {
|
||||||
|
console.log("Task change detected");
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleAddTask = () => {
|
||||||
|
const taskName = prompt("Enter a new task name:");
|
||||||
|
const description = prompt("Enter description for the task");
|
||||||
|
const uniqueTaskId = uuidv4();
|
||||||
|
console.log(uniqueTaskId);
|
||||||
|
const commitToPDB = () => {
|
||||||
|
console.log("entering commit to pouchdb");
|
||||||
|
try {
|
||||||
|
const response = localTasksDB.put({
|
||||||
|
_id: uniqueTaskId,
|
||||||
|
taskId: uniqueTaskId,
|
||||||
|
taskName: taskName,
|
||||||
|
description: description,
|
||||||
|
status: "not started",
|
||||||
|
parentIds: [project.projectId],
|
||||||
|
completed: false,
|
||||||
|
});
|
||||||
|
console.log("added task", taskName);
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const commitToRedux = () => {
|
||||||
|
console.log("entering commit to redux");
|
||||||
|
try {
|
||||||
|
if (taskName) {
|
||||||
|
props.dispatch({
|
||||||
|
type: "ADD_TASK",
|
||||||
|
taskId: uniqueTaskId,
|
||||||
|
taskName: taskName,
|
||||||
|
description: description,
|
||||||
|
status: "not started",
|
||||||
|
parentIds: [project.projectId],
|
||||||
|
completed: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Could not add task to redux", error);
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
console.log("Task added to local store!");
|
||||||
|
};
|
||||||
|
};
|
||||||
|
commitToRedux();
|
||||||
|
commitToPDB();
|
||||||
|
};
|
||||||
|
|
||||||
|
function compareArraysAndFilterChanges(oldArray, newArray) {
|
||||||
|
const changedTasks = [];
|
||||||
|
|
||||||
|
oldArray.forEach((oldItem, index) => {
|
||||||
|
const newItem = newArray[index];
|
||||||
|
|
||||||
|
// Compare items based on your criteria
|
||||||
|
if (!isEqual(oldItem, newItem)) {
|
||||||
|
changedTasks.push(newItem);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return changedTasks;
|
||||||
|
}
|
||||||
|
function isEqual(a, b) {
|
||||||
|
// Implement your own comparison logic here
|
||||||
|
// This can be a deep equality check using a library like lodash or a custom function
|
||||||
|
return (
|
||||||
|
JSON.stringify(a.completed) === JSON.stringify(b.completed) ||
|
||||||
|
JSON.stringify(a.status) === JSON.stringify(b.status) ||
|
||||||
|
JSON.stringify(a.description) === JSON.stringify(b.description) ||
|
||||||
|
JSON.stringify(a.taskName) === JSON.stringify(b.taskName)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
//Handle modifications to tasks
|
||||||
|
const handleCommitChangesToPDB = () => {
|
||||||
|
//identify if any field changed in
|
||||||
|
//list of tasks in current redux state
|
||||||
|
try {
|
||||||
|
const filteredTasks = props.tasks.filter(
|
||||||
|
(task) => task.parentIds[0] === project.projectId
|
||||||
|
);
|
||||||
|
localTasksDB
|
||||||
|
.allDocs({ include_docs: true })
|
||||||
|
.then((result) => {
|
||||||
|
const existingTasks = result.rows.map((row) => row.doc);
|
||||||
|
const uniqueChangedTasks = compareArraysAndFilterChanges(existingTasks,filteredTasks);
|
||||||
|
console.log("changes", uniqueChangedTasks);
|
||||||
|
if (uniqueChangedTasks !== undefined) {
|
||||||
|
console.log("task change detected");
|
||||||
|
uniqueChangedTasks.map((u) => {
|
||||||
|
localTasksDB.get(u.taskId).then(function(doc) {
|
||||||
|
return localTasksDB.put({
|
||||||
|
_id: u.taskId,
|
||||||
|
_rev: doc._rev,
|
||||||
|
taskId: u.taskId,
|
||||||
|
taskName: u.taskName,
|
||||||
|
description: u.description,
|
||||||
|
status: u.status,
|
||||||
|
parentIds: u.parentIds,
|
||||||
|
completed: u.completed,
|
||||||
|
});
|
||||||
|
}).then(function(response) {
|
||||||
|
console.log("Some tasks have been changed ")
|
||||||
|
}).catch(function (err) {
|
||||||
|
console.log(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.log("no task change detected");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then((result) => {
|
||||||
|
console.log("Data change is successful.", result);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error("Error during change staging:", error);
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error during data committement from staging: ", error);
|
||||||
|
}
|
||||||
|
return () => console.log("unmounting...");
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleToggleTask = (id) => {
|
||||||
|
props.dispatch({
|
||||||
|
type: "TOGGLE_COMPLETED",
|
||||||
|
id,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const Item = styled(Paper)(({ theme }) => ({
|
||||||
|
backgroundColor: theme.palette.mode === "dark" ? "#1A2027" : "#fff",
|
||||||
|
...theme.typography.body2,
|
||||||
|
padding: theme.spacing(1),
|
||||||
|
textAlign: "center",
|
||||||
|
color: theme.palette.text.secondary,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const filterByParentId = (taskList) => {
|
||||||
|
return taskList
|
||||||
|
.map((task) => {
|
||||||
|
console.log(props.selectedProjectId);
|
||||||
|
if (task.parentIds[0] === props.selectedProjectId) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Box sx={{ flexGrow: 1 }}>
|
||||||
|
<Grid item xs="auto" key={task.taskId}>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={task.completed}
|
||||||
|
onChange={() => handleToggleTask(task.taskId)}
|
||||||
|
/>
|
||||||
|
<Item key={task.taskId} value={task.taskName}>
|
||||||
|
{task.taskName}
|
||||||
|
</Item>
|
||||||
|
</Grid>
|
||||||
|
</Box>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
})
|
||||||
|
.filter(Boolean);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<ul>{filterByParentId(props.tasks)}</ul>
|
||||||
|
<button onClick={handleAddTask}>Add Task</button>
|
||||||
|
<button onClick={handleCommitChangesToPDB}>
|
||||||
|
Commit Changes To Tasks
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps)(TaskList);
|
||||||
|
|
||||||
|
function mapStateToProps(state) {
|
||||||
|
return {
|
||||||
|
tasks: state.tasksReducer.tasks,
|
||||||
|
selectedProjectId: state.projectsReducer.selectedProjectId,
|
||||||
|
projects: state.projectsReducer.projects,
|
||||||
|
};
|
||||||
|
}
|
||||||
46
components/test
Normal file
46
components/test
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
www
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
task list
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import Task from "./task";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import { getTasksByVisibilityFilter } from "../redux/selector";
|
||||||
|
import AddTask from "./addTask";
|
||||||
|
import VisibilityFilters from "./visibilityFilters";
|
||||||
|
//import { toHaveDescription } from "@testing-library/jest-dom/dist/matchers";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const TaskList = ({ Tasks }) => (
|
||||||
|
<>
|
||||||
|
<div>
|
||||||
|
<AddTask />
|
||||||
|
</div>
|
||||||
|
<ul className="task-list">
|
||||||
|
{ Tasks && Tasks.length ? Tasks.map((item, index) => {
|
||||||
|
return <Task key={item.id} id={item.id} completed={item.completed} assigned2={item.assigned2} task={item.task} description={item.description} />;
|
||||||
|
})
|
||||||
|
: "List empty"}
|
||||||
|
</ul>
|
||||||
|
<VisibilityFilters />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
//
|
||||||
|
|
||||||
|
const mapStateToProps = (state) => {
|
||||||
|
const { visibilityFilter } = state;
|
||||||
|
const Tasks = getTasksByVisibilityFilter(state, visibilityFilter);
|
||||||
|
return { Tasks };
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export default connect(mapStateToProps)(TaskList);
|
||||||
|
|
||||||
|
// return ;Tasks ?
|
||||||
|
|
||||||
39
components/viewDocs.jsx
Normal file
39
components/viewDocs.jsx
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import db from "./ldbcfg";
|
||||||
|
import ModifyPDB from "./modifypdb";
|
||||||
|
|
||||||
|
|
||||||
|
const ShowDocs = () => {
|
||||||
|
let [flatListItems, setFlatListItems] = useState([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
db.allDocs({ include_docs: true, descending: true })
|
||||||
|
.then((results) => {
|
||||||
|
let temp = results.rows.map((row) => row.doc);
|
||||||
|
setFlatListItems(temp);
|
||||||
|
})
|
||||||
|
.catch((err) => alert("Unable to get data"));
|
||||||
|
}, [flatListItems]);
|
||||||
|
|
||||||
|
const handleClick = (doc) => {
|
||||||
|
<ModifyPDB />
|
||||||
|
console.log({doc})
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ModifyPDB />
|
||||||
|
<ul>
|
||||||
|
{flatListItems.map(doc => (
|
||||||
|
<li key={doc._id} onClick={() => handleClick({doc})} > {doc.task} {doc.description} {doc.assigned2} </li>
|
||||||
|
)) }
|
||||||
|
</ul>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export default ShowDocs;
|
||||||
|
|
||||||
|
|
||||||
38
components/visibilityFilters.jsx
Normal file
38
components/visibilityFilters.jsx
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import React from "react";
|
||||||
|
import cx from "classnames";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import { setFilter } from "../redux/actions";
|
||||||
|
import { VISIBILITY_FILTERS } from "../constants";
|
||||||
|
|
||||||
|
const VisibilityFilters = ({ activeFilter, setFilter }) => {
|
||||||
|
return (
|
||||||
|
<div className="visibility-filters">
|
||||||
|
{Object.keys(VISIBILITY_FILTERS).map(filterKey => {
|
||||||
|
const currentFilter = VISIBILITY_FILTERS[filterKey];
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
key={`visibility-filter-${currentFilter}`}
|
||||||
|
className={cx(
|
||||||
|
"filter",
|
||||||
|
currentFilter === activeFilter && "filter--active"
|
||||||
|
)}
|
||||||
|
onClick={() => {
|
||||||
|
setFilter(currentFilter);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{currentFilter}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapStateToProps = state => {
|
||||||
|
return { activeFilter: state.visibilityFilter };
|
||||||
|
};
|
||||||
|
// export default VisibilityFilters;
|
||||||
|
export default connect(
|
||||||
|
mapStateToProps,
|
||||||
|
{ setFilter }
|
||||||
|
)(VisibilityFilters);
|
||||||
5
configs/admincfgs.jsx
Normal file
5
configs/admincfgs.jsx
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
const admconfig = {
|
||||||
|
ditswbsApiUrl: process.env.REACT_APP_DITSWBS_API_URL,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default admconfig;
|
||||||
8
constants.jsx
Normal file
8
constants.jsx
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
export const VISIBILITY_FILTERS = {
|
||||||
|
ALL: " all ",
|
||||||
|
COMPLETED: true,
|
||||||
|
INCOMPLETE: false
|
||||||
|
};
|
||||||
|
export const AUTH_API = {
|
||||||
|
GET_TOKEN = "http://192.168.0.234:8000/api/token/"
|
||||||
|
};
|
||||||
1
ditswbs.env
Normal file
1
ditswbs.env
Normal file
@ -0,0 +1 @@
|
|||||||
|
REACT_APP_DITSWBS_API_URL=http://192.168.0.234
|
||||||
13
index.css
Normal file
13
index.css
Normal file
@ -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;
|
||||||
|
}
|
||||||
25
index.jsx
Normal file
25
index.jsx
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import React from "react";
|
||||||
|
import ReactDOM from "react-dom/client";
|
||||||
|
import reportWebVitals from "./reportWebVitals";
|
||||||
|
import "./index.css";
|
||||||
|
import "bootstrap/dist/css/bootstrap.min.css";
|
||||||
|
import App from "./App";
|
||||||
|
import { Provider } from "react-redux";
|
||||||
|
import { store } from "./redux/store";
|
||||||
|
import LoadDB from "./components/ldbcfg";
|
||||||
|
import { BrowserRouter } from "react-router-dom";
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById("root")).render(
|
||||||
|
// <React.StrictMode>
|
||||||
|
<Provider store={store}>
|
||||||
|
<LoadDB />
|
||||||
|
<BrowserRouter>
|
||||||
|
<App />
|
||||||
|
</BrowserRouter>
|
||||||
|
</Provider>
|
||||||
|
// </React.StrictMode>
|
||||||
|
);
|
||||||
|
// 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();
|
||||||
1
logo.svg
Normal file
1
logo.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>
|
||||||
|
After Width: | Height: | Size: 2.6 KiB |
11
redux/actionTypes.jsx
Normal file
11
redux/actionTypes.jsx
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
export const ADD_TASK = "ADD_TASK";
|
||||||
|
export const ADD_CDB_TASK = "ADD_CDB_TASK";
|
||||||
|
export const ADD_TASK_PARENT = "ADD_TASK_PARENT";
|
||||||
|
export const DELETE_TASK = "DELETE_TASK";
|
||||||
|
export const TOGGLE_COMPLETED = "TOGGLE_COMPLETED";
|
||||||
|
export const SET_FILTER = "SET_FILTER";
|
||||||
|
export const ADD_PROJECT = "ADD_PROJECT";
|
||||||
|
export const DELETE_PROJECT = "DELETE_PROJECT";
|
||||||
|
export const ARCHIVE_PROJECT = "ARCHIVE_PROJECT";
|
||||||
|
export const SELECT_PROJECT = "SELECT_PROJECT";
|
||||||
|
|
||||||
66
redux/actions.jsx
Normal file
66
redux/actions.jsx
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
import {
|
||||||
|
ADD_TASK,
|
||||||
|
ADD_CDB_TASK,
|
||||||
|
ADD_TASK_PARENT,
|
||||||
|
TOGGLE_COMPLETED,
|
||||||
|
SET_FILTER,
|
||||||
|
SELECT_PROJECT,
|
||||||
|
ADD_PROJECT,
|
||||||
|
DELETE_PROJECT,
|
||||||
|
ARCHIVE_PROJECT,
|
||||||
|
DELETE_TASK,
|
||||||
|
} from "./actionTypes";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export const addTask = (taskName) => ({
|
||||||
|
type: ADD_TASK,
|
||||||
|
payload: { taskName },
|
||||||
|
});
|
||||||
|
|
||||||
|
export const addCdbTask = (taskName) => ({
|
||||||
|
type: ADD_CDB_TASK,
|
||||||
|
payload: { taskName },
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
export const addTaskParent = (id) => ({
|
||||||
|
type: ADD_TASK_PARENT,
|
||||||
|
payload: { id },
|
||||||
|
});
|
||||||
|
|
||||||
|
export const deleteTask = (id) => ({
|
||||||
|
type: DELETE_TASK,
|
||||||
|
payload: { id },
|
||||||
|
});
|
||||||
|
|
||||||
|
export const toggleCompleted = (id) => ({
|
||||||
|
type: TOGGLE_COMPLETED,
|
||||||
|
payload: { id },
|
||||||
|
});
|
||||||
|
|
||||||
|
export const setFilter = (filter) => ({
|
||||||
|
type: SET_FILTER,
|
||||||
|
payload: { filter },
|
||||||
|
});
|
||||||
|
|
||||||
|
export const addProject = (projectName) => ({
|
||||||
|
type: ADD_PROJECT,
|
||||||
|
payload: { projectName },
|
||||||
|
});
|
||||||
|
|
||||||
|
export const deleteProject = (id) => ({
|
||||||
|
type: DELETE_PROJECT,
|
||||||
|
payload: { id },
|
||||||
|
});
|
||||||
|
|
||||||
|
export const selectProject = (id) => ({
|
||||||
|
type: SELECT_PROJECT,
|
||||||
|
payload: { id },
|
||||||
|
});
|
||||||
|
|
||||||
|
export const archiveProject = (id) => ({
|
||||||
|
type: ARCHIVE_PROJECT,
|
||||||
|
payload: { id },
|
||||||
|
});
|
||||||
|
|
||||||
15
redux/connect.jsx
Normal file
15
redux/connect.jsx
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
const mapStateToProps = (state, ownProps) => ({
|
||||||
|
// ... computed data from state and optionally ownProps
|
||||||
|
})
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
// ... normally is an object full of action creators
|
||||||
|
}
|
||||||
|
|
||||||
|
// `connect` returns a new function that accepts the component to wrap:
|
||||||
|
const connectToStore = connect(mapStateToProps, mapDispatchToProps)
|
||||||
|
// and that function returns the connected, wrapper component:
|
||||||
|
const ConnectedComponent = connectToStore(Component)
|
||||||
|
|
||||||
|
// We normally do both in one step, like this:
|
||||||
|
connect(mapStateToProps, mapDispatchToProps)(Component)
|
||||||
6
redux/reducers/index.jsx
Normal file
6
redux/reducers/index.jsx
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { combineReducers } from "redux";
|
||||||
|
import visibilityFilter from "./visibilityFilter";
|
||||||
|
import tasksReducer from "./tasksReducer";
|
||||||
|
import projectsReducer from "./projectsReducer";
|
||||||
|
|
||||||
|
export default combineReducers({ tasksReducer, projectsReducer, visibilityFilter });
|
||||||
55
redux/reducers/projectsReducer.jsx
Normal file
55
redux/reducers/projectsReducer.jsx
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import {
|
||||||
|
ADD_PROJECT,
|
||||||
|
SELECT_PROJECT,
|
||||||
|
ARCHIVE_PROJECT,
|
||||||
|
} from "../actionTypes";
|
||||||
|
|
||||||
|
const initialState = {
|
||||||
|
projects: [],
|
||||||
|
selectedProjectId: "0",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function projectReducer(state = initialState, action) {
|
||||||
|
switch (action.type) {
|
||||||
|
// Projects are added only during initialization of the program
|
||||||
|
// New projects and resources it can use are defined outside of the client program
|
||||||
|
// Project initiation and closure are considered a separate endeavors
|
||||||
|
// to project planning, execution, and control in this application.
|
||||||
|
case ADD_PROJECT: {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
projects: [
|
||||||
|
...state.projects,
|
||||||
|
{
|
||||||
|
projectId: action.project.projectId,
|
||||||
|
projectName: action.project.projectName,
|
||||||
|
description: action.project.projectDescription,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
selectedProjectId: action.project.projectId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// This is an important switch as everything user does in the session is tied to
|
||||||
|
// selected project. Selected project must always be prominently displayed in every
|
||||||
|
// view.
|
||||||
|
case SELECT_PROJECT: {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
projects: [...state.projects],
|
||||||
|
selectedProjectId: action.id,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// Default action when project is closed. Not yet implemented. Archived projects are just
|
||||||
|
// moved to another view. Project deletion can only be done through admin program and will
|
||||||
|
// only become noticeable to user after client program restart.
|
||||||
|
case ARCHIVE_PROJECT: {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
projects: state.projects.map((project) => project.id === action.id),
|
||||||
|
selectedProjectId: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
61
redux/reducers/tasksReducer.jsx
Normal file
61
redux/reducers/tasksReducer.jsx
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
import { ADD_TASK, ADD_CDB_TASK, TOGGLE_COMPLETED } from "../actionTypes";
|
||||||
|
|
||||||
|
import { v4 as uuidv4 } from "uuid";
|
||||||
|
|
||||||
|
const initialState = { tasks: [] };
|
||||||
|
|
||||||
|
export default function taskReducer(state = initialState, action) {
|
||||||
|
switch (action.type) {
|
||||||
|
// Used when task is added during session but not committed to cloud
|
||||||
|
case ADD_TASK: {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
tasks: [
|
||||||
|
...state.tasks,
|
||||||
|
{
|
||||||
|
taskId: action.taskId,
|
||||||
|
taskName: action.taskName,
|
||||||
|
description: action.description,
|
||||||
|
status: action.status,
|
||||||
|
parentIds: action.parentIds,
|
||||||
|
completed: action.completed,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// Used during initial synchronization of the local storage using cloud data
|
||||||
|
// a local pouchdb database is also created into which task commits can be done
|
||||||
|
// during initialization of the database
|
||||||
|
case ADD_CDB_TASK: {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
tasks: [
|
||||||
|
...state.tasks,
|
||||||
|
{
|
||||||
|
_id: action.task.taskId,
|
||||||
|
taskId: action.task.taskId,
|
||||||
|
taskName: action.task.taskName,
|
||||||
|
description: action.task.description,
|
||||||
|
status: action.task.status,
|
||||||
|
parentIds: action.task.parentIds,
|
||||||
|
completed: action.task.completed,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// Toggle completed
|
||||||
|
case TOGGLE_COMPLETED: {
|
||||||
|
console.log(action.id);
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
tasks: state.tasks.map((task) =>
|
||||||
|
task.taskId === action.id
|
||||||
|
? { ...task, completed: !task.completed }
|
||||||
|
: task
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
17
redux/reducers/visibilityFilter.jsx
Normal file
17
redux/reducers/visibilityFilter.jsx
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { SET_FILTER } from "../actionTypes";
|
||||||
|
import { VISIBILITY_FILTERS } from "../../constants";
|
||||||
|
|
||||||
|
const initialState = VISIBILITY_FILTERS.ALL;
|
||||||
|
|
||||||
|
const visibilityFilter = (state = initialState, action) => {
|
||||||
|
switch (action.type) {
|
||||||
|
case SET_FILTER: {
|
||||||
|
return action.payload.filter;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default visibilityFilter;
|
||||||
27
redux/selector.jsx
Normal file
27
redux/selector.jsx
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { VISIBILITY_FILTERS } from "../constants";
|
||||||
|
|
||||||
|
export const getTasksState = (store) => store.tasks;
|
||||||
|
|
||||||
|
export const getTaskList = (store) =>
|
||||||
|
getTasksState(store) ? getTasksState(store).allIds : [];
|
||||||
|
|
||||||
|
export const getTasksById = (store, id) =>
|
||||||
|
getTasksState(store) ? { ...getTasksState(store).byIds[id], id } : {};
|
||||||
|
|
||||||
|
export const getTasks = (store) =>
|
||||||
|
getTaskList(store).map((id) => getTasksById(store, id));
|
||||||
|
|
||||||
|
export const getTasksByVisibilityFilter = (store, visibilityFilter) => {
|
||||||
|
const allTasks = getTasks(store);
|
||||||
|
switch (visibilityFilter) {
|
||||||
|
case VISIBILITY_FILTERS.COMPLETED:
|
||||||
|
console.log("contains completed tasks");
|
||||||
|
return allTasks.filter((task) => task.completed);
|
||||||
|
case VISIBILITY_FILTERS.INCOMPLETE:
|
||||||
|
console.log("contains uncompleted tasks");
|
||||||
|
return allTasks.filter((task) => !task.completed);
|
||||||
|
case VISIBILITY_FILTERS.ALL:
|
||||||
|
default:
|
||||||
|
return allTasks;
|
||||||
|
}
|
||||||
|
};
|
||||||
10
redux/store.jsx
Normal file
10
redux/store.jsx
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { createStore } from "redux";
|
||||||
|
import rootReducer from "./reducers";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export const store = createStore(rootReducer, /* preLoadedState, */
|
||||||
|
window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
|
||||||
|
);
|
||||||
|
|
||||||
12
redux/visibilityFilters.jsx
Normal file
12
redux/visibilityFilters.jsx
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { connect } from "react-redux";
|
||||||
|
import { setFilter } from "../redux/actions";
|
||||||
|
|
||||||
|
const VisibilityFilters = // ... component implementation
|
||||||
|
|
||||||
|
const mapStateToProps = state => {
|
||||||
|
return { activeFilter: state.visibilityFilter };
|
||||||
|
};
|
||||||
|
export default connect(
|
||||||
|
mapStateToProps,
|
||||||
|
{ setFilter }
|
||||||
|
)(VisibilityFilters);
|
||||||
13
reportWebVitals.js
Normal file
13
reportWebVitals.js
Normal file
@ -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;
|
||||||
5
setupTests.js
Normal file
5
setupTests.js
Normal file
@ -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';
|
||||||
Loading…
x
Reference in New Issue
Block a user