Коммит
0507272d2a
|
@ -23,3 +23,4 @@ yarn-debug.log*
|
|||
yarn-error.log*
|
||||
|
||||
kendo-ui-license.txt
|
||||
package-lock.json
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
17
package.json
17
package.json
|
@ -4,19 +4,20 @@
|
|||
"private": true,
|
||||
"dependencies": {
|
||||
"bootstrap": "4.6.0",
|
||||
"react": "17.0.1",
|
||||
"react-dom": "17.0.1",
|
||||
"react-router-dom": "5.2.0",
|
||||
"react-scripts": "4.0.1",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-query": "^3.39.2",
|
||||
"react-router-dom": "6.4.2",
|
||||
"react-scripts": "5.0.1",
|
||||
"reactstrap": "8.9.0",
|
||||
"rxjs": "6.6.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "26.0.20",
|
||||
"@types/node": "14.14.22",
|
||||
"@types/react": "17.0.1",
|
||||
"@types/react-dom": "17.0.1",
|
||||
"@types/react-router-dom": "5.1.7",
|
||||
"@types/node": "16.0.0",
|
||||
"@types/react": "18.0.21",
|
||||
"@types/react-dom": "18.0.6",
|
||||
"@types/react-router-dom": "5.3.3",
|
||||
"@types/reactstrap": "8.7.2",
|
||||
"typescript": "4.1.3"
|
||||
},
|
||||
|
|
74
src/App.tsx
74
src/App.tsx
|
@ -1,5 +1,6 @@
|
|||
import React, { Component } from 'react';
|
||||
import { BrowserRouter, Route, Redirect, Switch } from 'react-router-dom';
|
||||
import React, { createContext } from 'react';
|
||||
import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom';
|
||||
import { QueryClient, QueryClientProvider } from 'react-query';
|
||||
|
||||
import './App.css';
|
||||
|
||||
|
@ -9,10 +10,32 @@ import { MainMenu } from './shared/components/main-menu/main-menu';
|
|||
import { SideMenu } from './shared/components/side-menu/side-menu';
|
||||
import { DetailPage } from './modules/backlog/pages/detail/detail-page';
|
||||
|
||||
import { Store } from './core/state/app-store';
|
||||
import { BacklogRepository } from './modules/backlog/repositories/backlog.repository';
|
||||
import { BacklogService } from './modules/backlog/services/backlog.service';
|
||||
import { DashboardRepository } from './modules/dashboard/repositories/dashboard.repository';
|
||||
import { DashboardService } from './modules/dashboard/services/dashboard.service';
|
||||
import { PtUserService } from './core/services/pt-user-service';
|
||||
|
||||
class App extends Component {
|
||||
render() {
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
const store: Store = new Store();
|
||||
const backlogRepo: BacklogRepository = new BacklogRepository();
|
||||
const backlogService: BacklogService = new BacklogService(backlogRepo, store);
|
||||
const dashboardRepo: DashboardRepository = new DashboardRepository();
|
||||
const dashboardService: DashboardService = new DashboardService(dashboardRepo);
|
||||
const userService: PtUserService = new PtUserService(store);
|
||||
|
||||
export const PtStoreContext = createContext(store);
|
||||
export const PtBacklogServiceContext = createContext(backlogService);
|
||||
export const PtDashboardServiceContext = createContext(dashboardService);
|
||||
export const PtUserServiceContext = createContext(userService);
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<PtStoreContext.Provider value={store}>
|
||||
<PtUserServiceContext.Provider value={userService}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<BrowserRouter>
|
||||
<React.Fragment>
|
||||
<MainMenu />
|
||||
|
@ -22,33 +45,38 @@ class App extends Component {
|
|||
<SideMenu></SideMenu>
|
||||
|
||||
<main role="main" className="col-md-9 ml-sm-auto col-lg-10 px-4">
|
||||
<Switch>
|
||||
<Route exact path="/">
|
||||
<Redirect exact to={{ pathname: "/dashboard" }} />
|
||||
</Route>
|
||||
<Route exact path="/dashboard" component={DashboardPage} />
|
||||
<Route exact path="/backlog">
|
||||
<Redirect exact to={{ pathname: "/backlog/open" }} />
|
||||
</Route>
|
||||
<Route exact path="/backlog/:preset" component={BacklogPage} />
|
||||
<Routes>
|
||||
<Route path="dashboard" element={
|
||||
<PtDashboardServiceContext.Provider value={dashboardService}>
|
||||
<DashboardPage/>
|
||||
</PtDashboardServiceContext.Provider>
|
||||
} />
|
||||
<Route path="/" element={<Navigate replace to="/dashboard" />} />
|
||||
|
||||
<Route
|
||||
exact
|
||||
path="/detail/:id"
|
||||
render={({ match }) => (
|
||||
<Redirect to={`/detail/${match.params.id}/details`} />
|
||||
)}
|
||||
/>
|
||||
<Route path="/backlog/:preset" element={
|
||||
<PtBacklogServiceContext.Provider value={backlogService}>
|
||||
<BacklogPage/>
|
||||
</PtBacklogServiceContext.Provider>
|
||||
} />
|
||||
<Route path="backlog" element={<Navigate replace to="/backlog/open" />}/>
|
||||
|
||||
<Route exact path="/detail/:id/:screen" component={DetailPage} />
|
||||
</Switch>
|
||||
<Route path="/detail/:id" element={<DetailPage/>} />
|
||||
|
||||
<Route path="/detail/:id/:screen" element={
|
||||
<PtBacklogServiceContext.Provider value={backlogService}>
|
||||
<DetailPage/>
|
||||
</PtBacklogServiceContext.Provider>
|
||||
} />
|
||||
</Routes>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
</BrowserRouter>
|
||||
</QueryClientProvider>
|
||||
</PtUserServiceContext.Provider>
|
||||
</PtStoreContext.Provider>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default App;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { PtTask, PtItem } from '../models/domain';
|
||||
import { PtTask, PtItem, PtComment } from '../models/domain';
|
||||
|
||||
export function datesForTask(t: PtTask) {
|
||||
t.dateCreated = new Date(t.dateCreated);
|
||||
|
@ -8,6 +8,12 @@ export function datesForTask(t: PtTask) {
|
|||
t.dateStart = t.dateStart ? new Date(t.dateStart) : undefined;
|
||||
}
|
||||
|
||||
export function datesForComment(t: PtComment) {
|
||||
t.dateCreated = new Date(t.dateCreated);
|
||||
t.dateDeleted = t.dateDeleted ? new Date(t.dateDeleted) : undefined;
|
||||
t.dateModified = new Date(t.dateModified);
|
||||
}
|
||||
|
||||
export function formatDateEnUs(date: Date) {
|
||||
return Intl.DateTimeFormat('en-US', {
|
||||
year: 'numeric',
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
export interface PtAuthToken {
|
||||
export type PtAuthToken = {
|
||||
access_token: string;
|
||||
dateExpires: Date;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,26 @@
|
|||
import { PtObjectBase, PtUser } from './';
|
||||
import { PtObjectBase, PtObjectBaseServer, PtUser, PtUserServer, ptUserServerToPtUser } from './';
|
||||
|
||||
export interface PtComment extends PtObjectBase {
|
||||
export type PtComment = PtObjectBase & {
|
||||
user?: PtUser;
|
||||
}
|
||||
|
||||
export type PtCommentServer = PtObjectBaseServer & {
|
||||
user?: PtUserServer;
|
||||
}
|
||||
|
||||
export type PtCommentToBe = Omit<PtComment, 'id'>;
|
||||
|
||||
export function ptCommentServerToPtComment(comment: PtCommentServer): PtComment {
|
||||
return {
|
||||
...comment,
|
||||
dateCreated: new Date(comment.dateCreated),
|
||||
dateModified: new Date(comment.dateModified),
|
||||
dateDeleted: comment.dateDeleted ? new Date(comment.dateDeleted) : undefined,
|
||||
user: comment.user ? ptUserServerToPtUser(comment.user) : undefined
|
||||
};
|
||||
}
|
||||
|
||||
export function ptCommentsServerToPtComments(comments: PtCommentServer[]): PtComment[] {
|
||||
return comments.map(ptCommentServerToPtComment);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,14 +1,160 @@
|
|||
import { PtObjectBase, PtTask, PtComment, PtUser } from './';
|
||||
import { PtObjectBase, PtObjectBaseServer, PtTask, PtComment, PtUser, PtUserServer, PtCommentServer, PtTaskServer, ptUserServerToPtUser, ptCommentsServerToPtComments, ptTasksServerToPtTasks } from './';
|
||||
import { PriorityEnum, StatusEnum } from './enums';
|
||||
import { PtItemType } from '../domain/types';
|
||||
|
||||
export interface PtItem extends PtObjectBase {
|
||||
type PtItemCommon = {
|
||||
description?: string;
|
||||
type: PtItemType;
|
||||
estimate: number;
|
||||
priority: PriorityEnum;
|
||||
status: StatusEnum;
|
||||
type: PtItemType;
|
||||
};
|
||||
|
||||
type PtItemWithAssignee = {
|
||||
assignee: PtUser;
|
||||
tasks: PtTask[];
|
||||
};
|
||||
|
||||
type PtItemWithAssigneeServer = {
|
||||
assignee: PtUserServer;
|
||||
};
|
||||
|
||||
type PtItemWithComments = {
|
||||
comments: PtComment[];
|
||||
};
|
||||
|
||||
type PtItemWithCommentsServer = {
|
||||
comments: PtCommentServer[];
|
||||
};
|
||||
|
||||
type PtItemWithTasks = {
|
||||
tasks: PtTask[];
|
||||
};
|
||||
|
||||
type PtItemWithTasksServer = {
|
||||
tasks: PtTaskServer[];
|
||||
};
|
||||
|
||||
type PtItemWithEstimate = {
|
||||
estimate: number;
|
||||
};
|
||||
|
||||
type PtItemWithEstimateServer = {
|
||||
estimate: string;
|
||||
};
|
||||
|
||||
export type PtItem = PtObjectBase & PtItemCommon & PtItemWithEstimate & PtItemWithAssignee & PtItemWithComments & PtItemWithTasks;
|
||||
export type PtItemServer = PtObjectBaseServer & PtItemCommon & PtItemWithEstimateServer & PtItemWithAssigneeServer & PtItemWithCommentsServer & PtItemWithTasksServer;
|
||||
|
||||
|
||||
export function ptItemServerToPtItem(item: PtItemServer): PtItem {
|
||||
return {
|
||||
...item,
|
||||
estimate: item.estimate ? parseInt(item.estimate, 10) : 0,
|
||||
dateCreated: new Date(item.dateCreated),
|
||||
dateModified: new Date(item.dateModified),
|
||||
dateDeleted: item.dateDeleted ? new Date(item.dateDeleted) : undefined,
|
||||
assignee: ptUserServerToPtUser(item.assignee),
|
||||
comments: item.comments ? ptCommentsServerToPtComments(item.comments) : [],
|
||||
tasks: item.tasks ? ptTasksServerToPtTasks(item.tasks) : []
|
||||
};
|
||||
}
|
||||
|
||||
export function ptItemsServerToPtItems(items: PtItemServer[]): PtItem[] {
|
||||
return items.map(ptItemServerToPtItem);
|
||||
}
|
||||
|
||||
// type tests
|
||||
const b: PtItemServer = {
|
||||
id: 0,
|
||||
title: 'title',
|
||||
description: 'description',
|
||||
priority: PriorityEnum.Medium,
|
||||
status: StatusEnum.Open,
|
||||
estimate: '10',
|
||||
type: 'Bug',
|
||||
assignee: {
|
||||
id: 0,
|
||||
fullName: 'fullName',
|
||||
avatar: 'avatarUrl',
|
||||
dateCreated: 'dateCreated',
|
||||
dateModified: 'dateModified',
|
||||
dateDeleted: 'dateDeleted'
|
||||
},
|
||||
tasks: [
|
||||
{
|
||||
id: 0,
|
||||
title: 'title',
|
||||
completed: false,
|
||||
dateCreated: 'dateCreated',
|
||||
dateModified: 'dateModified',
|
||||
dateDeleted: 'dateDeleted'
|
||||
}
|
||||
],
|
||||
comments: [
|
||||
{
|
||||
id: 0,
|
||||
title: 'title',
|
||||
user: {
|
||||
id: 0,
|
||||
fullName: 'fullName',
|
||||
avatar: 'avatarUrl',
|
||||
dateCreated: 'dateCreated',
|
||||
dateModified: 'dateModified',
|
||||
dateDeleted: 'dateDeleted'
|
||||
},
|
||||
dateCreated: 'dateCreated',
|
||||
dateModified: 'dateModified',
|
||||
dateDeleted: 'dateDeleted'
|
||||
}
|
||||
],
|
||||
dateCreated: 'dateCreated',
|
||||
dateModified: 'dateModified',
|
||||
dateDeleted: 'dateDeleted',
|
||||
};
|
||||
|
||||
const c: PtItem = {
|
||||
id: 0,
|
||||
title: 'title',
|
||||
description: 'description',
|
||||
priority: PriorityEnum.Medium,
|
||||
status: StatusEnum.Open,
|
||||
estimate: 0,
|
||||
type: 'Bug',
|
||||
assignee: {
|
||||
id: 0,
|
||||
fullName: 'fullName',
|
||||
avatar: 'avatarUrl',
|
||||
dateCreated: new Date(),
|
||||
dateModified: new Date(),
|
||||
dateDeleted: new Date(),
|
||||
},
|
||||
tasks: [
|
||||
{
|
||||
id: 0,
|
||||
title: 'title',
|
||||
completed: false,
|
||||
dateCreated: new Date(),
|
||||
dateModified: new Date(),
|
||||
dateDeleted: new Date(),
|
||||
}
|
||||
],
|
||||
comments: [
|
||||
{
|
||||
id: 0,
|
||||
title: 'title',
|
||||
user: {
|
||||
id: 0,
|
||||
fullName: 'fullName',
|
||||
avatar: 'avatarUrl',
|
||||
dateCreated: new Date(),
|
||||
dateModified: new Date(),
|
||||
dateDeleted: new Date(),
|
||||
},
|
||||
dateCreated: new Date(),
|
||||
dateModified: new Date(),
|
||||
dateDeleted: new Date(),
|
||||
}
|
||||
],
|
||||
dateCreated: new Date(),
|
||||
dateModified: new Date(),
|
||||
dateDeleted: new Date(),
|
||||
};
|
|
@ -1,4 +1,4 @@
|
|||
export interface PtLoginModel {
|
||||
export type PtLoginModel = {
|
||||
username: string;
|
||||
password: string;
|
||||
}
|
||||
|
|
|
@ -1,7 +1,13 @@
|
|||
export interface PtObjectBase {
|
||||
export type PtObjectBase = {
|
||||
id: number;
|
||||
title?: string;
|
||||
dateCreated: Date;
|
||||
dateModified: Date;
|
||||
dateDeleted?: Date;
|
||||
}
|
||||
|
||||
export type PtObjectBaseServer = Omit<PtObjectBase, 'dateCreated' | 'dateModified' | 'dateDeleted'> & {
|
||||
dateCreated: string;
|
||||
dateModified: string;
|
||||
dateDeleted?: string;
|
||||
};
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
export interface PtRegisterModel {
|
||||
export type PtRegisterModel = {
|
||||
username: string;
|
||||
password: string;
|
||||
fullName: string;
|
||||
|
|
|
@ -1,7 +1,32 @@
|
|||
import { PtObjectBase } from './';
|
||||
import { PtObjectBase, PtObjectBaseServer } from './';
|
||||
|
||||
export interface PtTask extends PtObjectBase {
|
||||
type PtTaskCommon = {
|
||||
completed: boolean;
|
||||
}
|
||||
|
||||
export type PtTask = PtObjectBase & PtTaskCommon & {
|
||||
dateStart?: Date;
|
||||
dateEnd?: Date;
|
||||
}
|
||||
|
||||
export type PtTaskServer = PtObjectBaseServer & PtTaskCommon & {
|
||||
dateStart?: string;
|
||||
dateEnd?: string;
|
||||
}
|
||||
|
||||
export type PtTaskToBe = Omit<PtTask, 'id'>;
|
||||
|
||||
export function ptTaskServerToPtTask(task: PtTaskServer): PtTask {
|
||||
return {
|
||||
...task,
|
||||
dateCreated: new Date(task.dateCreated),
|
||||
dateModified: new Date(task.dateModified),
|
||||
dateDeleted: task.dateDeleted ? new Date(task.dateDeleted) : undefined,
|
||||
dateStart: task.dateStart ? new Date(task.dateStart) : undefined,
|
||||
dateEnd: task.dateEnd ? new Date(task.dateEnd) : undefined
|
||||
};
|
||||
}
|
||||
|
||||
export function ptTasksServerToPtTasks(tasks: PtTaskServer[]): PtTask[] {
|
||||
return tasks.map(ptTaskServerToPtTask);
|
||||
}
|
||||
|
|
|
@ -1,6 +1,23 @@
|
|||
import { PtObjectBase } from './';
|
||||
import { PtObjectBase, PtObjectBaseServer } from './';
|
||||
|
||||
export interface PtUser extends PtObjectBase {
|
||||
type PtUserCommon = {
|
||||
fullName: string;
|
||||
avatar: string;
|
||||
};
|
||||
|
||||
export type PtUser = PtObjectBase & PtUserCommon;
|
||||
|
||||
export type PtUserServer = PtObjectBaseServer & PtUserCommon;
|
||||
|
||||
export function ptUserServerToPtUser(user: PtUserServer): PtUser {
|
||||
return {
|
||||
...user,
|
||||
dateCreated: new Date(user.dateCreated),
|
||||
dateModified: new Date(user.dateModified),
|
||||
dateDeleted: user.dateDeleted ? new Date(user.dateDeleted) : undefined
|
||||
};
|
||||
}
|
||||
|
||||
export function ptUsersServerToPtUsers(users: PtUserServer[]): PtUser[] {
|
||||
return users.map(ptUserServerToPtUser);
|
||||
}
|
|
@ -1,13 +1,9 @@
|
|||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { createRoot } from "react-dom/client";
|
||||
import './index.css';
|
||||
import 'bootstrap/dist/css/bootstrap.css';
|
||||
import App from './App';
|
||||
import * as serviceWorker from './serviceWorker';
|
||||
|
||||
ReactDOM.render(<App />, document.getElementById('root'));
|
||||
const root = createRoot(document.getElementById("root") as HTMLElement);
|
||||
|
||||
root.render(<App />);
|
||||
|
||||
// If you want your app to work offline and load faster, you can change
|
||||
// unregister() to register() below. Note this comes with some pitfalls.
|
||||
// Learn more about service workers: http://bit.ly/CRA-PWA
|
||||
serviceWorker.unregister();
|
||||
|
|
|
@ -0,0 +1,92 @@
|
|||
import { useState } from "react";
|
||||
import { Button, Modal, ModalBody, ModalFooter } from "reactstrap";
|
||||
import { ItemType } from "../../../../core/constants";
|
||||
import { EMPTY_STRING } from "../../../../core/helpers";
|
||||
import { PtItem } from "../../../../core/models/domain";
|
||||
import { PtNewItem } from "../../../../shared/models/dto/pt-new-item";
|
||||
|
||||
export type AddItemModalProps = {
|
||||
modalShowing: boolean;
|
||||
onNewItemSave: (newItem: PtNewItem) => Promise<PtItem | undefined>;
|
||||
setIsAddModalShowing: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
};
|
||||
|
||||
const initModalNewItem = (): PtNewItem => {
|
||||
return {
|
||||
title: EMPTY_STRING,
|
||||
description: EMPTY_STRING,
|
||||
typeStr: 'PBI'
|
||||
};
|
||||
}
|
||||
|
||||
export function AddItemModal(props: AddItemModalProps) {
|
||||
|
||||
const [newItem, setNewItem] = useState(initModalNewItem());
|
||||
|
||||
const modalShowing = props.modalShowing;
|
||||
const setShowModal = props.setIsAddModalShowing;
|
||||
const itemTypesProvider = ItemType.List.map((t) => t.PtItemType);
|
||||
|
||||
function onFieldChange(e: any, formFieldName: string) {
|
||||
if (!newItem) {
|
||||
return;
|
||||
}
|
||||
setNewItem({ ...newItem, [formFieldName]: e.target.value });
|
||||
}
|
||||
|
||||
async function onAddSave() {
|
||||
const createdItem = await props.onNewItemSave(newItem);
|
||||
setShowModal(false);
|
||||
setNewItem(initModalNewItem());
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal isOpen={modalShowing}>
|
||||
<div className="modal-header">
|
||||
<h4 className="modal-title" id="modal-basic-title">Add New Item</h4>
|
||||
<button type="button" className="close" onClick={()=>setShowModal(false)} aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<ModalBody>
|
||||
<form>
|
||||
<div className="form-group row">
|
||||
<label className="col-sm-2 col-form-label">Title</label>
|
||||
<div className="col-sm-10">
|
||||
<input className="form-control" defaultValue={newItem.title} onChange={(e) => onFieldChange(e, 'title')} name="title" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-group row">
|
||||
<label className="col-sm-2 col-form-label">Description</label>
|
||||
<div className="col-sm-10">
|
||||
<textarea className="form-control" defaultValue={newItem.description} onChange={(e) => onFieldChange(e, 'description')} name="description"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-group row">
|
||||
<label className="col-sm-2 col-form-label">Item Type</label>
|
||||
<div className="col-sm-10">
|
||||
<select className="form-control" defaultValue={newItem.typeStr} onChange={(e) => onFieldChange(e, 'typeStr')} name="itemType">
|
||||
{
|
||||
itemTypesProvider.map(t => {
|
||||
return (
|
||||
<option key={t} value={t}>
|
||||
{t}
|
||||
</option>
|
||||
)
|
||||
})
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form >
|
||||
</ModalBody >
|
||||
<ModalFooter>
|
||||
<Button color="secondary" onClick={()=>setShowModal(false)}>Cancel</Button>
|
||||
<Button color="primary" onClick={onAddSave}>Save</Button>{' '}
|
||||
</ModalFooter>
|
||||
</Modal >
|
||||
);
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
import { Modal, ModalBody, ModalFooter } from "reactstrap";
|
||||
import { PtItem, PtUser } from "../../../../core/models/domain";
|
||||
import { PtNewItem } from "../../../../shared/models/dto/pt-new-item";
|
||||
|
||||
export type AssigneeListModalProps = {
|
||||
modalIsShowing: boolean;
|
||||
setModalIsShowing: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
users: PtUser[];
|
||||
selectAssignee: (user: PtUser) => void;
|
||||
};
|
||||
|
||||
|
||||
export function AssigneeListModal(props: AssigneeListModalProps) {
|
||||
|
||||
const { modalIsShowing, setModalIsShowing, users, selectAssignee } = props;
|
||||
|
||||
return (
|
||||
<Modal isOpen={modalIsShowing}>
|
||||
<div className="modal-header">
|
||||
<h4 className="modal-title" id="modal-basic-title">Select Assignee</h4>
|
||||
<button type="button" className="close" onClick={() => setModalIsShowing(false)} aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<ModalBody>
|
||||
<ul className="list-group list-group-flush">
|
||||
{
|
||||
users.map((u: PtUser) => {
|
||||
return (
|
||||
<li key={u.id} className="list-group-item d-flex justify-content-between align-items-center" onClick={() => selectAssignee(u)}>
|
||||
<span>{u.fullName}</span>
|
||||
<span className="badge ">
|
||||
<img src={u.avatar} className="li-avatar rounded mx-auto d-block" />
|
||||
</span>
|
||||
</li>
|
||||
);
|
||||
})
|
||||
}
|
||||
</ul>
|
||||
</ModalBody>
|
||||
<ModalFooter />
|
||||
</Modal>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
import { PtItem } from "../../../../core/models/domain";
|
||||
import { BacklogRow } from "../backlog-row/backlog-row";
|
||||
|
||||
export type BacklogListProps = {
|
||||
items: PtItem[];
|
||||
};
|
||||
|
||||
export function BacklogList(props: BacklogListProps) {
|
||||
|
||||
const rows = props.items.map(i => {
|
||||
return (
|
||||
<BacklogRow key={i.id} item={i} />
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="table-responsive">
|
||||
<table className="table table-striped table-sm table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>Assignee</th>
|
||||
<th>Title</th>
|
||||
<th>Status</th>
|
||||
<th>Priority</th>
|
||||
<th>Estimate</th>
|
||||
<th>Created</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
import { useNavigate } from "react-router-dom";
|
||||
import { ItemType } from "../../../../core/constants";
|
||||
import { PtItem } from "../../../../core/models/domain";
|
||||
import { getIndicatorClass } from "../../../../shared/helpers/priority-styling";
|
||||
|
||||
export type BacklogRowProps = {
|
||||
item: PtItem;
|
||||
}
|
||||
|
||||
export function BacklogRow(props: BacklogRowProps) {
|
||||
|
||||
const { item: i } = props;
|
||||
const navigate = useNavigate();
|
||||
|
||||
function getIndicatorImage(item: PtItem) {
|
||||
return ItemType.imageResFromType(item.type);
|
||||
}
|
||||
|
||||
function getPriorityClass(item: PtItem): string {
|
||||
const indicatorClass = getIndicatorClass(item.priority);
|
||||
return indicatorClass;
|
||||
}
|
||||
|
||||
function listItemTap(item: PtItem) {
|
||||
// navigate to detail page
|
||||
navigate(`/detail/${item.id}`);
|
||||
}
|
||||
|
||||
return (
|
||||
<tr key={i.id} className="pt-table-row" onClick={(e) => listItemTap(i)}>
|
||||
<td>
|
||||
<img src={getIndicatorImage(i)} className="backlog-icon" />
|
||||
</td>
|
||||
<td>
|
||||
<img src={i.assignee.avatar} className="li-avatar rounded mx-auto d-block" />
|
||||
</td>
|
||||
<td>
|
||||
<span className="li-title">{i.title}</span>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<span>{i.status}</span>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<span className={'badge ' + getPriorityClass(i)}>{i.priority}
|
||||
</span>
|
||||
</td>
|
||||
<td><span className="li-estimate">{i.estimate}</span></td>
|
||||
<td><span className="li-date">{i.dateCreated.toDateString()}</span></td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
import { PtComment } from "../../../../core/models/domain";
|
||||
|
||||
export type PtCommentDisplayComponentProps = {
|
||||
comment: PtComment;
|
||||
};
|
||||
|
||||
export function PtCommentDisplayComponent(props: PtCommentDisplayComponentProps) {
|
||||
|
||||
const { comment } = props;
|
||||
const dateStr = comment.dateCreated.toDateString();
|
||||
|
||||
return (
|
||||
<li key={comment.id} className="media chitchat-item">
|
||||
<img src={comment.user!.avatar} className="mr-3 li-avatar rounded" />
|
||||
<div className="media-body">
|
||||
<h6 className="mt-0 mb-1"><span>{comment.user!.fullName}</span><span className="li-date">{dateStr}</span></h6>
|
||||
<span className="chitchat-text ">{comment.title}</span>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
import { useState } from "react";
|
||||
import { EMPTY_STRING } from "../../../../core/helpers";
|
||||
import { PtUser } from "../../../../core/models/domain";
|
||||
|
||||
export type CommentFormProps = {
|
||||
addComment: (text: string) => void;
|
||||
currentUser: PtUser;
|
||||
};
|
||||
|
||||
export function NewCommentForm(props: CommentFormProps) {
|
||||
|
||||
const [newCommentText, setNewCommentText] = useState<string>(EMPTY_STRING);
|
||||
|
||||
function onNewCommentChanged(e: any) {
|
||||
setNewCommentText(e.target.value);
|
||||
}
|
||||
|
||||
function onAddTapped() {
|
||||
const newTitle = newCommentText.trim();
|
||||
if (newTitle.length === 0) {
|
||||
return;
|
||||
}
|
||||
props.addComment(newTitle);
|
||||
setNewCommentText(EMPTY_STRING);
|
||||
}
|
||||
|
||||
const handleSubmit = (e: any) => {
|
||||
e.preventDefault();
|
||||
onAddTapped();
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="form-row align-items-center">
|
||||
|
||||
<img src={props.currentUser.avatar} className="mr-3 li-avatar rounded" />
|
||||
|
||||
<div className="col-sm-6">
|
||||
<textarea value={newCommentText} onChange={onNewCommentChanged} placeholder="Enter new comment..." className="form-control pt-text-comment-add"
|
||||
name="newComment"></textarea>
|
||||
</div>
|
||||
<button type="button" onClick={onAddTapped} className="btn btn-primary" disabled={!newCommentText}>Add</button>
|
||||
</div>
|
||||
</form >
|
||||
);
|
||||
}
|
|
@ -1,97 +1,52 @@
|
|||
import React from "react";
|
||||
import { useState } from "react";
|
||||
import { PtComment, PtUser } from "../../../../core/models/domain";
|
||||
import { EMPTY_STRING } from "../../../../core/helpers";
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
import { PtNewComment } from "../../../../shared/models/dto/pt-new-comment";
|
||||
|
||||
import './pt-item-chitchat.css';
|
||||
import { NewCommentForm } from "./new-comment-form";
|
||||
import { UseMutationResult } from "react-query";
|
||||
import { PtCommentDisplayComponent } from "./comment-display";
|
||||
|
||||
interface PtItemChitchatComponentProps {
|
||||
comments$: BehaviorSubject<PtComment[]>;
|
||||
currentUser: PtUser;
|
||||
addNewComment: (newComment: PtNewComment) => void;
|
||||
}
|
||||
|
||||
interface PtItemChitchatComponentState {
|
||||
newCommentText: string;
|
||||
export type PtItemChitchatComponentProps = {
|
||||
comments: PtComment[];
|
||||
}
|
||||
|
||||
export class PtItemChitchatComponent extends React.Component<PtItemChitchatComponentProps, PtItemChitchatComponentState> {
|
||||
|
||||
constructor(props: PtItemChitchatComponentProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
newCommentText: EMPTY_STRING,
|
||||
comments: []
|
||||
};
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
this.props.comments$.subscribe((comments: PtComment[]) => {
|
||||
this.setState({
|
||||
comments: comments
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public onNewCommentChanged(e: any) {
|
||||
this.setState({
|
||||
newCommentText: e.target.value
|
||||
});
|
||||
}
|
||||
|
||||
public onAddTapped() {
|
||||
const newTitle = this.state.newCommentText.trim();
|
||||
if (newTitle.length === 0) {
|
||||
return;
|
||||
}
|
||||
const newComment: PtNewComment = {
|
||||
title: newTitle
|
||||
};
|
||||
this.props.addNewComment(newComment);
|
||||
|
||||
this.setState({
|
||||
newCommentText: EMPTY_STRING
|
||||
});
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<form>
|
||||
<div className="form-row align-items-center">
|
||||
|
||||
<img src={this.props.currentUser.avatar} className="mr-3 li-avatar rounded" />
|
||||
|
||||
<div className="col-sm-6">
|
||||
<textarea defaultValue={this.state.newCommentText} onChange={(e) => this.onNewCommentChanged(e)} placeholder="Enter new comment..." className="form-control pt-text-comment-add"
|
||||
name="newComment"></textarea>
|
||||
</div>
|
||||
<button type="button" onClick={() => this.onAddTapped()} className="btn btn-primary" disabled={!this.state.newCommentText}>Add</button>
|
||||
</div>
|
||||
</form >
|
||||
|
||||
<hr />
|
||||
|
||||
<ul className="list-unstyled">
|
||||
{
|
||||
this.state.comments.map(comment => {
|
||||
return (
|
||||
<li key={comment.id} className="media chitchat-item">
|
||||
<img src={comment.user!.avatar} className="mr-3 li-avatar rounded" />
|
||||
<div className="media-body">
|
||||
<h6 className="mt-0 mb-1"><span>{comment.user!.fullName}</span><span className="li-date">{comment.dateCreated}</span></h6>
|
||||
|
||||
<span className="chitchat-text ">{comment.title}</span>
|
||||
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
})
|
||||
}
|
||||
</ul>
|
||||
</React.Fragment >
|
||||
);
|
||||
}
|
||||
currentUser: PtUser;
|
||||
addCommentMutation: UseMutationResult<PtComment, unknown, PtNewComment, unknown>;
|
||||
};
|
||||
|
||||
export function PtItemChitchatComponent(props: PtItemChitchatComponentProps) {
|
||||
|
||||
const [comments, setComments] = useState<PtComment[]>(props.comments);
|
||||
|
||||
const addComment = (text: string) => {
|
||||
const newComment: PtNewComment = { title: text };
|
||||
props.addCommentMutation.mutate(newComment, {
|
||||
onSuccess(createdTask) {
|
||||
const newComments = [createdTask, ...comments];
|
||||
setComments(newComments);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<NewCommentForm
|
||||
addComment={addComment}
|
||||
currentUser={props.currentUser}
|
||||
/>
|
||||
|
||||
<hr />
|
||||
|
||||
<ul className="list-unstyled">
|
||||
{
|
||||
comments.map(comment => {
|
||||
return (
|
||||
<PtCommentDisplayComponent key={comment.id} comment={comment} />
|
||||
);
|
||||
})
|
||||
}
|
||||
</ul>
|
||||
</>
|
||||
);
|
||||
|
||||
}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import React from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { PtItem, PtUser } from "../../../../core/models/domain";
|
||||
import { PtItemDetailsEditFormModel, ptItemToFormModel } from "../../../../shared/models/forms/pt-item-details-edit-form";
|
||||
import { ItemType, PT_ITEM_STATUSES, PT_ITEM_PRIORITIES } from "../../../../core/constants";
|
||||
import { Modal, ModalBody, ModalFooter, Button } from "reactstrap";
|
||||
import { Observable } from "rxjs";
|
||||
import { AssigneeListModal } from "../assignee-list-modal/assignee-list-modal";
|
||||
|
||||
interface PtItemDetailsComponentProps {
|
||||
item: PtItem;
|
||||
|
@ -12,55 +12,46 @@ interface PtItemDetailsComponentProps {
|
|||
users$: Observable<PtUser[]>;
|
||||
}
|
||||
|
||||
interface PtItemDetailsComponentState {
|
||||
showAddModal: boolean;
|
||||
users: PtUser[];
|
||||
}
|
||||
export function PtItemDetailsComponent(props: PtItemDetailsComponentProps) {
|
||||
|
||||
export class PtItemDetailsComponent extends React.Component<PtItemDetailsComponentProps, PtItemDetailsComponentState> {
|
||||
const statusesProvider = PT_ITEM_STATUSES;
|
||||
const prioritiesProvider = PT_ITEM_PRIORITIES;
|
||||
const itemTypesProvider = ItemType.List.map((t) => t.PtItemType);
|
||||
|
||||
private itemForm: PtItemDetailsEditFormModel | undefined;
|
||||
public itemTypesProvider = ItemType.List.map((t) => t.PtItemType);
|
||||
public statusesProvider = PT_ITEM_STATUSES;
|
||||
public prioritiesProvider = PT_ITEM_PRIORITIES;
|
||||
private selectedAssignee: PtUser | undefined;
|
||||
const [itemForm, setItemForm] = useState(ptItemToFormModel(props.item));
|
||||
const [users, setUsers] = useState<PtUser[]>([]);
|
||||
const [modalIsShowing, setModalIsShowing] = useState(false);
|
||||
const [selectedAssignee, setSelectedAssignee] = useState<PtUser>(props.item.assignee);
|
||||
useEffect(()=>{
|
||||
notifyUpdateItem();
|
||||
}, [selectedAssignee]);
|
||||
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
|
||||
this.itemForm = ptItemToFormModel(this.props.item);
|
||||
this.state = {
|
||||
showAddModal: false,
|
||||
users: []
|
||||
};
|
||||
this.selectedAssignee = this.props.item.assignee;
|
||||
}
|
||||
|
||||
public onFieldChange(e: any, formFieldName: string) {
|
||||
if (!this.itemForm) {
|
||||
|
||||
function onFieldChange(e: any, formFieldName: string) {
|
||||
if (!itemForm) {
|
||||
return;
|
||||
}
|
||||
(this.itemForm as any)[formFieldName] = e.target.value;
|
||||
(itemForm as any)[formFieldName] = e.target.value;
|
||||
}
|
||||
|
||||
public onNonTextFieldChange(e: any, formFieldName: string) {
|
||||
this.onFieldChange(e, formFieldName);
|
||||
this.notifyUpdateItem();
|
||||
function onNonTextFieldChange(e: any, formFieldName: string) {
|
||||
onFieldChange(e, formFieldName);
|
||||
notifyUpdateItem();
|
||||
}
|
||||
|
||||
public onBlurTextField() {
|
||||
this.notifyUpdateItem();
|
||||
function onBlurTextField() {
|
||||
notifyUpdateItem();
|
||||
}
|
||||
|
||||
private notifyUpdateItem() {
|
||||
if (!this.itemForm) {
|
||||
function notifyUpdateItem() {
|
||||
if (!itemForm) {
|
||||
return;
|
||||
}
|
||||
const updatedItem = this.getUpdatedItem(this.props.item, this.itemForm, this.selectedAssignee!);
|
||||
this.props.itemSaved(updatedItem);
|
||||
const updatedItem = getUpdatedItem(props.item, itemForm, selectedAssignee!);
|
||||
props.itemSaved(updatedItem);
|
||||
}
|
||||
|
||||
private getUpdatedItem(item: PtItem, itemForm: PtItemDetailsEditFormModel, assignee: PtUser): PtItem {
|
||||
function getUpdatedItem(item: PtItem, itemForm: PtItemDetailsEditFormModel, assignee: PtUser): PtItem {
|
||||
const updatedItem = Object.assign({}, item, {
|
||||
title: itemForm.title,
|
||||
description: itemForm.description,
|
||||
|
@ -73,154 +64,123 @@ export class PtItemDetailsComponent extends React.Component<PtItemDetailsCompone
|
|||
return updatedItem;
|
||||
}
|
||||
|
||||
public assigneePickerOpen() {
|
||||
this.props.users$.subscribe((users: PtUser[]) => {
|
||||
function assigneePickerOpen() {
|
||||
props.users$.subscribe((users: PtUser[]) => {
|
||||
if (users.length > 0) {
|
||||
this.setState({
|
||||
users: users,
|
||||
showAddModal: true
|
||||
});
|
||||
setUsers(users);
|
||||
setModalIsShowing(true);
|
||||
}
|
||||
});
|
||||
|
||||
this.props.usersRequested();
|
||||
props.usersRequested();
|
||||
}
|
||||
|
||||
private toggleModal() {
|
||||
this.setState({
|
||||
showAddModal: !this.state.showAddModal
|
||||
});
|
||||
return false;
|
||||
function selectAssignee(u: PtUser) {
|
||||
setSelectedAssignee(u);
|
||||
setItemForm({ ...itemForm, assigneeName: u.fullName });
|
||||
setModalIsShowing(false);
|
||||
notifyUpdateItem();
|
||||
}
|
||||
|
||||
private selectAssignee(u: PtUser) {
|
||||
this.selectedAssignee = u;
|
||||
this.itemForm!.assigneeName = u.fullName;
|
||||
this.setState({
|
||||
showAddModal: false,
|
||||
});
|
||||
this.notifyUpdateItem();
|
||||
if (!itemForm) {
|
||||
return null;
|
||||
}
|
||||
|
||||
public render() {
|
||||
if (!this.itemForm) {
|
||||
return null;
|
||||
}
|
||||
const itemForm = this.itemForm;
|
||||
return (
|
||||
<React.Fragment>
|
||||
<form>
|
||||
<div className="form-group row">
|
||||
<label className="col-sm-2 col-form-label">Title</label>
|
||||
<div className="col-sm-10">
|
||||
<input className="form-control" defaultValue={itemForm.title} onBlur={() => this.onBlurTextField()} onChange={(e) => this.onFieldChange(e, 'title')} name="title" />
|
||||
</div>
|
||||
return (
|
||||
<React.Fragment>
|
||||
<form>
|
||||
<div className="form-group row">
|
||||
<label className="col-sm-2 col-form-label">Title</label>
|
||||
<div className="col-sm-10">
|
||||
<input className="form-control" defaultValue={itemForm.title} onBlur={() => onBlurTextField()} onChange={(e) => onFieldChange(e, 'title')} name="title" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-group row">
|
||||
<label className="col-sm-2 col-form-label">Description</label>
|
||||
<div className="col-sm-10">
|
||||
<textarea className="form-control" defaultValue={itemForm.description} onBlur={() => this.onBlurTextField()} onChange={(e) => this.onFieldChange(e, 'description')} name="description"></textarea>
|
||||
</div>
|
||||
<div className="form-group row">
|
||||
<label className="col-sm-2 col-form-label">Description</label>
|
||||
<div className="col-sm-10">
|
||||
<textarea className="form-control" defaultValue={itemForm.description} onBlur={() => onBlurTextField()} onChange={(e) => onFieldChange(e, 'description')} name="description"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-group row">
|
||||
<label className="col-sm-2 col-form-label">Item Type</label>
|
||||
<div className="col-sm-10">
|
||||
<select className="form-control" defaultValue={itemForm.typeStr} onChange={(e) => this.onNonTextFieldChange(e, 'typeStr')} name="itemType">
|
||||
{
|
||||
this.itemTypesProvider.map(t => {
|
||||
return (
|
||||
<option key={t} value={t}>
|
||||
{t}
|
||||
</option>
|
||||
)
|
||||
})
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-group row">
|
||||
<label className="col-sm-2 col-form-label">Status</label>
|
||||
<div className="col-sm-10">
|
||||
<select className="form-control" defaultValue={itemForm.statusStr} onChange={(e) => this.onNonTextFieldChange(e, 'statusStr')} name="status">
|
||||
{
|
||||
this.statusesProvider.map(t => {
|
||||
return (
|
||||
<option key={t} value={t}>
|
||||
{t}
|
||||
</option>
|
||||
)
|
||||
})
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-group row">
|
||||
<label className="col-sm-2 col-form-label">Estimate</label>
|
||||
<div className="col-sm-10">
|
||||
<input className="form-control" type="range" step="1" min="0" max="20" value={itemForm.estimate} onChange={(e) => this.onNonTextFieldChange(e, 'estimate')} name="estimate" style={{ width: 300 }} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-group row">
|
||||
<label className="col-sm-2 col-form-label">Priority</label>
|
||||
<div className="col-sm-10">
|
||||
<select className="form-control" defaultValue={itemForm.priorityStr} onChange={(e) => this.onNonTextFieldChange(e, 'priorityStr')} name="priority">
|
||||
{
|
||||
this.prioritiesProvider.map(t => {
|
||||
return (
|
||||
<option key={t} value={t}>
|
||||
{t}
|
||||
</option>
|
||||
)
|
||||
})
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-group row">
|
||||
<label className="col-sm-2 col-form-label">Assignee</label>
|
||||
|
||||
<div className="col-sm-10">
|
||||
<img src={this.selectedAssignee!.avatar} className="li-avatar rounded" />
|
||||
<span>{itemForm.assigneeName}</span>
|
||||
|
||||
<button type="button" className="btn btn-sm btn-outline-secondary" onClick={() => this.assigneePickerOpen()}>Pick assignee</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<Modal isOpen={this.state.showAddModal} toggle={() => this.toggleModal()}>
|
||||
<div className="modal-header">
|
||||
<h4 className="modal-title" id="modal-basic-title">Select Assignee</h4>
|
||||
<button type="button" className="close" onClick={() => this.toggleModal()} aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<ModalBody>
|
||||
<ul className="list-group list-group-flush">
|
||||
<div className="form-group row">
|
||||
<label className="col-sm-2 col-form-label">Item Type</label>
|
||||
<div className="col-sm-10">
|
||||
<select className="form-control" defaultValue={itemForm.typeStr} onChange={(e) => onNonTextFieldChange(e, 'typeStr')} name="itemType">
|
||||
{
|
||||
this.state.users.map((u: PtUser) => {
|
||||
itemTypesProvider.map(t => {
|
||||
return (
|
||||
<li key={u.id} className="list-group-item d-flex justify-content-between align-items-center" onClick={() => this.selectAssignee(u)}>
|
||||
<span>{u.fullName}</span>
|
||||
<span className="badge ">
|
||||
<img src={u.avatar} className="li-avatar rounded mx-auto d-block" />
|
||||
</span>
|
||||
</li>
|
||||
);
|
||||
<option key={t} value={t}>
|
||||
{t}
|
||||
</option>
|
||||
)
|
||||
})
|
||||
}
|
||||
</ul>
|
||||
</ModalBody >
|
||||
<ModalFooter />
|
||||
</Modal >
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
<div className="form-group row">
|
||||
<label className="col-sm-2 col-form-label">Status</label>
|
||||
<div className="col-sm-10">
|
||||
<select className="form-control" defaultValue={itemForm.statusStr} onChange={(e) => onNonTextFieldChange(e, 'statusStr')} name="status">
|
||||
{
|
||||
statusesProvider.map(t => {
|
||||
return (
|
||||
<option key={t} value={t}>
|
||||
{t}
|
||||
</option>
|
||||
)
|
||||
})
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-group row">
|
||||
<label className="col-sm-2 col-form-label">Estimate</label>
|
||||
<div className="col-sm-10">
|
||||
<input className="form-control" type="range" step="1" min="0" max="20" value={itemForm.estimate} onChange={(e) => onNonTextFieldChange(e, 'estimate')} name="estimate" style={{ width: 300 }} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-group row">
|
||||
<label className="col-sm-2 col-form-label">Priority</label>
|
||||
<div className="col-sm-10">
|
||||
<select className="form-control" defaultValue={itemForm.priorityStr} onChange={(e) => onNonTextFieldChange(e, 'priorityStr')} name="priority">
|
||||
{
|
||||
prioritiesProvider.map(t => {
|
||||
return (
|
||||
<option key={t} value={t}>
|
||||
{t}
|
||||
</option>
|
||||
)
|
||||
})
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-group row">
|
||||
<label className="col-sm-2 col-form-label">Assignee</label>
|
||||
|
||||
<div className="col-sm-10">
|
||||
<img src={selectedAssignee!.avatar} className="li-avatar rounded" />
|
||||
<span>{itemForm.assigneeName}</span>
|
||||
|
||||
<button type="button" className="btn btn-sm btn-outline-secondary" onClick={() => assigneePickerOpen()}>Pick assignee</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<AssigneeListModal
|
||||
users={users}
|
||||
modalIsShowing={modalIsShowing}
|
||||
setModalIsShowing={setModalIsShowing}
|
||||
selectAssignee={selectAssignee} />
|
||||
|
||||
|
||||
</React.Fragment>
|
||||
);
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
import { useState } from "react";
|
||||
import { EMPTY_STRING } from "../../../../core/helpers";
|
||||
|
||||
export type TaskFormProps = {
|
||||
addTask: (text: string) => void;
|
||||
};
|
||||
|
||||
export function NewTaskForm(props: TaskFormProps) {
|
||||
|
||||
const [newTaskTitle, setNewTaskTitle] = useState<string>(EMPTY_STRING);
|
||||
|
||||
function onNewTaskTitleChanged(e: any) {
|
||||
setNewTaskTitle(e.target.value);
|
||||
}
|
||||
|
||||
function onAddTapped() {
|
||||
const newTitle = newTaskTitle.trim();
|
||||
if (newTitle.length === 0) {
|
||||
return;
|
||||
}
|
||||
props.addTask(newTitle);
|
||||
setNewTaskTitle(EMPTY_STRING);
|
||||
}
|
||||
|
||||
const handleSubmit = (e: any) => {
|
||||
e.preventDefault();
|
||||
onAddTapped();
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="form-row align-items-center">
|
||||
<div className="col-sm-6">
|
||||
<input value={newTaskTitle} onChange={onNewTaskTitleChanged} placeholder="Enter new task..." className="form-control pt-text-task-add"
|
||||
name="newTask" />
|
||||
</div>
|
||||
<button type="button" onClick={() => onAddTapped()} className="btn btn-primary" disabled={!newTaskTitle}>Add</button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
|
@ -1,142 +1,130 @@
|
|||
import React from "react";
|
||||
import { useState } from "react";
|
||||
import { PtTask } from "../../../../core/models/domain";
|
||||
import { EMPTY_STRING } from "../../../../core/helpers";
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
import { PtTaskUpdate } from "../../../../shared/models/dto/pt-task-update";
|
||||
import { PtTaskTitleUpdate } from "../../../../shared/models/dto/pt-task-update";
|
||||
import { PtNewTask } from "../../../../shared/models/dto/pt-new-task";
|
||||
import { PtTaskDisplayComponent } from "./pt-task-display";
|
||||
import { UseMutationResult } from "react-query";
|
||||
import { NewTaskForm } from "./new-task-form";
|
||||
|
||||
interface PtItemTasksComponentProps {
|
||||
tasks$: BehaviorSubject<PtTask[]>;
|
||||
addNewTask: (newTask: PtNewTask) => void;
|
||||
updateTask: (taskUpdate: PtTaskUpdate) => void;
|
||||
}
|
||||
|
||||
interface PtItemTasksComponentState {
|
||||
newTaskTitle: string;
|
||||
export type PtItemTasksComponentProps = {
|
||||
tasks: PtTask[];
|
||||
}
|
||||
addTaskMutation: UseMutationResult<PtTask, unknown, PtNewTask, unknown>;
|
||||
deleteTaskMutation: UseMutationResult<boolean, unknown, PtTask, unknown>;
|
||||
toggleTaskCompletionMutation: UseMutationResult<PtTask, unknown, PtTask, unknown>;
|
||||
updateTaskMutation: UseMutationResult<PtTask, unknown, PtTaskTitleUpdate, unknown>;
|
||||
};
|
||||
|
||||
export class PtItemTasksComponent extends React.Component<PtItemTasksComponentProps, PtItemTasksComponentState> {
|
||||
export function PtItemTasksComponent(props: PtItemTasksComponentProps) {
|
||||
|
||||
private lastUpdatedTitle = EMPTY_STRING;
|
||||
const [tasks, setTasks] = useState<PtTask[]>(props.tasks);
|
||||
const [lastUpdatedTitle, setLastUpdatedTitle] = useState<string>(EMPTY_STRING);
|
||||
|
||||
constructor(props: PtItemTasksComponentProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
newTaskTitle: EMPTY_STRING,
|
||||
tasks: []
|
||||
};
|
||||
const addTask = (text: string) => {
|
||||
const newTask: PtNewTask = { title: text, completed: false };
|
||||
props.addTaskMutation.mutate(newTask, {
|
||||
onSuccess(createdTask) {
|
||||
const newTasks = [createdTask, ...tasks];
|
||||
setTasks(newTasks);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const toggleTaskCompletion = (index: number) => {
|
||||
const theTask = tasks[index];
|
||||
props.toggleTaskCompletionMutation.mutate(theTask, {
|
||||
onSuccess(toggledTask) {
|
||||
const newTasks = [...tasks];
|
||||
newTasks[index].completed = toggledTask.completed;
|
||||
setTasks(newTasks);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
function toggleTapped(task: PtTask) {
|
||||
const index = tasks.findIndex(t => t.id === task.id);
|
||||
toggleTaskCompletion(index);
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
this.props.tasks$.subscribe((tasks: PtTask[]) => {
|
||||
this.setState({
|
||||
tasks: tasks
|
||||
});
|
||||
|
||||
function taskTitleChange(task: PtTask, newTitle: string) {
|
||||
if (task.title === newTitle) {
|
||||
return;
|
||||
}
|
||||
|
||||
setLastUpdatedTitle(newTitle);
|
||||
}
|
||||
|
||||
function onTaskFocused(task: PtTask) {
|
||||
setLastUpdatedTitle(task.title ? task.title : EMPTY_STRING);
|
||||
}
|
||||
|
||||
function updateTask(task: PtTask) {
|
||||
const index = tasks.findIndex(t => t.id === task.id);
|
||||
const taskUpdate: PtTaskTitleUpdate = {
|
||||
task: task,
|
||||
newTitle: lastUpdatedTitle
|
||||
};
|
||||
|
||||
props.updateTaskMutation.mutate(taskUpdate, {
|
||||
onSuccess(updatedTask) {
|
||||
const newTasks = [...tasks];
|
||||
newTasks[index].title = updatedTask.title;
|
||||
setTasks(newTasks);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public onNewTaskTitleChanged(e: any) {
|
||||
this.setState({
|
||||
newTaskTitle: e.target.value
|
||||
});
|
||||
}
|
||||
|
||||
public onAddTapped() {
|
||||
const newTitle = this.state.newTaskTitle.trim();
|
||||
if (newTitle.length === 0) {
|
||||
function onTaskBlurred(task: PtTask) {
|
||||
if (task.title === lastUpdatedTitle) {
|
||||
return;
|
||||
}
|
||||
const newTask: PtNewTask = {
|
||||
title: newTitle,
|
||||
completed: false
|
||||
};
|
||||
this.props.addNewTask(newTask);
|
||||
updateTask(task);
|
||||
|
||||
this.setState({
|
||||
newTaskTitle: EMPTY_STRING
|
||||
});
|
||||
setLastUpdatedTitle(EMPTY_STRING);
|
||||
}
|
||||
|
||||
public toggleTapped(task: PtTask) {
|
||||
const taskUpdate: PtTaskUpdate = {
|
||||
task: task,
|
||||
toggle: true
|
||||
};
|
||||
this.props.updateTask(taskUpdate);
|
||||
}
|
||||
|
||||
|
||||
public taskTitleChange(task: PtTask, event: any) {
|
||||
if (task.title === event.target.value) {
|
||||
return;
|
||||
}
|
||||
this.lastUpdatedTitle = event.target.value;
|
||||
}
|
||||
|
||||
public onTaskFocused(task: PtTask) {
|
||||
this.lastUpdatedTitle = task.title ? task.title : EMPTY_STRING;
|
||||
}
|
||||
|
||||
public onTaskBlurred(task: PtTask) {
|
||||
if (task.title === this.lastUpdatedTitle) {
|
||||
return;
|
||||
}
|
||||
const taskUpdate: PtTaskUpdate = {
|
||||
task: task,
|
||||
toggle: false,
|
||||
newTitle: this.lastUpdatedTitle
|
||||
};
|
||||
|
||||
this.lastUpdatedTitle = EMPTY_STRING;
|
||||
this.props.updateTask(taskUpdate);
|
||||
}
|
||||
|
||||
public taskDelete(task: PtTask) {
|
||||
const taskUpdate: PtTaskUpdate = {
|
||||
task: task,
|
||||
toggle: false,
|
||||
delete: true
|
||||
};
|
||||
this.props.updateTask(taskUpdate);
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<form>
|
||||
|
||||
<div className="form-row align-items-center">
|
||||
<div className="col-sm-6">
|
||||
<input defaultValue={this.state.newTaskTitle} onChange={(e) => this.onNewTaskTitleChanged(e)} placeholder="Enter new task..." className="form-control pt-text-task-add"
|
||||
name="newTask" />
|
||||
</div>
|
||||
<button type="button" onClick={() => this.onAddTapped()} className="btn btn-primary" disabled={!this.state.newTaskTitle}>Add</button>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
{
|
||||
this.state.tasks.map(task => {
|
||||
return (
|
||||
<div key={task.id} className="input-group mb-3 col-sm-6">
|
||||
<div className="input-group-prepend">
|
||||
<div className="input-group-text">
|
||||
<input type="checkbox" checked={task.completed} onChange={() => this.toggleTapped(task)} aria-label="Checkbox for following text input"
|
||||
name={'checked' + task.id} />
|
||||
</div>
|
||||
</div>
|
||||
<input defaultValue={task.title} onChange={(e) => this.taskTitleChange(task, e)} onFocus={() => this.onTaskFocused(task)} onBlur={() => this.onTaskBlurred(task)}
|
||||
type="text" className="form-control" aria-label="Text input with checkbox" name={'tasktitle' + task.id} />
|
||||
|
||||
<div className="input-group-append">
|
||||
<button className="btn btn-danger" type="button" onClick={() => this.taskDelete(task)}>Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
const removeTask = (index: number) => {
|
||||
const theTask = tasks[index];
|
||||
props.deleteTaskMutation.mutate(theTask!, {
|
||||
onSuccess(deleted) {
|
||||
if (deleted) {
|
||||
const newChatEntries = [...tasks];
|
||||
newChatEntries.splice(index, 1);
|
||||
setTasks(newChatEntries);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
</form>
|
||||
);
|
||||
function deleteTapped(task: PtTask) {
|
||||
const index = tasks.findIndex(t => t.id === task.id);
|
||||
removeTask(index);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<NewTaskForm addTask={addTask} />
|
||||
|
||||
<hr />
|
||||
|
||||
{
|
||||
tasks.map(task => {
|
||||
return (
|
||||
<PtTaskDisplayComponent
|
||||
key={task.id}
|
||||
task={task}
|
||||
onToggleTaskCompletion={toggleTapped}
|
||||
onDeleteTask={deleteTapped}
|
||||
onTaskFocused={onTaskFocused}
|
||||
onTaskBlurred={onTaskBlurred}
|
||||
taskTitleChange={taskTitleChange}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
);
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,55 @@
|
|||
import { PtTask } from "../../../../core/models/domain";
|
||||
|
||||
export type PtTaskDisplayComponentProps = {
|
||||
task: PtTask;
|
||||
onToggleTaskCompletion: (task: PtTask) => void;
|
||||
onDeleteTask: (task: PtTask) => void;
|
||||
onTaskFocused: (task: PtTask) => void;
|
||||
onTaskBlurred: (task: PtTask) => void;
|
||||
taskTitleChange: (task: PtTask, newTitle: string) => void;
|
||||
};
|
||||
|
||||
export function PtTaskDisplayComponent(props: PtTaskDisplayComponentProps) {
|
||||
|
||||
const { task, onToggleTaskCompletion, onDeleteTask } = props;
|
||||
|
||||
function taskTitleChange(event: any) {
|
||||
if (task.title === event.target.value) {
|
||||
return;
|
||||
}
|
||||
props.taskTitleChange(task, event.target.value);
|
||||
}
|
||||
|
||||
function toggleTapped() {
|
||||
onToggleTaskCompletion(task);
|
||||
}
|
||||
|
||||
function deleteTapped() {
|
||||
onDeleteTask(task);
|
||||
}
|
||||
|
||||
function onFocused() {
|
||||
props.onTaskFocused(task);
|
||||
}
|
||||
|
||||
function onBlurred() {
|
||||
props.onTaskBlurred(task);
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={task.id} className="input-group mb-3 col-sm-12">
|
||||
<div className="input-group-prepend">
|
||||
<div className="input-group-text">
|
||||
<input type="checkbox" checked={task.completed} onChange={toggleTapped} aria-label="Checkbox for following text input"
|
||||
name={'checked' + task.id} />
|
||||
</div>
|
||||
</div>
|
||||
<input defaultValue={task.title} onChange={taskTitleChange} onFocus={onFocused} onBlur={onBlurred}
|
||||
type="text" className="form-control" aria-label="Text input with checkbox" name={'tasktitle' + task.id} />
|
||||
|
||||
<div className="input-group-append">
|
||||
<button className="btn btn-danger" type="button" onClick={deleteTapped}>Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -1,238 +1,107 @@
|
|||
import React from "react";
|
||||
import { BacklogService } from "../../services/backlog.service";
|
||||
import { BacklogRepository } from "../../repositories/backlog.repository";
|
||||
import { Store } from "../../../../core/state/app-store";
|
||||
import { PresetType } from "../../../../core/models/domain/types";
|
||||
import { PtItem } from "../../../../core/models/domain";
|
||||
import { ItemType } from "../../../../core/constants";
|
||||
import React, { useContext, useEffect, useState } from "react";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
|
||||
import './backlog-page.css';
|
||||
|
||||
import { PresetType } from "../../../../core/models/domain/types";
|
||||
import { PtItem } from "../../../../core/models/domain";
|
||||
import { AppPresetFilter } from "../../../../shared/components/preset-filter/preset-filter";
|
||||
import { Modal, ModalHeader, ModalBody, ModalFooter, Button } from "reactstrap";
|
||||
import { PtNewItem } from "../../../../shared/models/dto/pt-new-item";
|
||||
import { EMPTY_STRING } from "../../../../core/helpers";
|
||||
import { getIndicatorClass } from "../../../../shared/helpers/priority-styling";
|
||||
import { useMutation, useQuery, useQueryClient } from "react-query";
|
||||
import { AddItemModal } from "../../components/add-item-modal/add-item-modal";
|
||||
import { BacklogList } from "../../components/backlog-list/backlog-list";
|
||||
import { PtBacklogServiceContext, PtStoreContext } from "../../../../App";
|
||||
|
||||
|
||||
interface BacklogPageState {
|
||||
currentPreset: PresetType;
|
||||
items: PtItem[];
|
||||
showAddModal: boolean;
|
||||
newItem: PtNewItem;
|
||||
}
|
||||
export function BacklogPage() {
|
||||
const store = useContext(PtStoreContext);
|
||||
const backlogService = useContext(PtBacklogServiceContext);
|
||||
|
||||
export class BacklogPage extends React.Component<any, BacklogPageState> {
|
||||
|
||||
|
||||
private store: Store = new Store();
|
||||
private backlogRepo: BacklogRepository = new BacklogRepository();
|
||||
private backlogService: BacklogService = new BacklogService(this.backlogRepo, this.store);
|
||||
const queryClient = useQueryClient();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { preset } = useParams() as {preset: PresetType};
|
||||
const [currentPreset, setCurrentPreset] = useState<PresetType>(preset ? preset : 'open');
|
||||
|
||||
public items: PtItem[] = [];
|
||||
public itemTypesProvider = ItemType.List.map((t) => t.PtItemType);
|
||||
const useItems = (...params: Parameters<typeof backlogService.getItems>) => {
|
||||
return useQuery<PtItem[], Error>(getQueryKey(), () => backlogService.getItems(...params));
|
||||
}
|
||||
const queryResult = useItems(currentPreset);
|
||||
const items = queryResult.data;
|
||||
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
const { preset } = this.props.match.params;
|
||||
this.state = {
|
||||
currentPreset: preset ? preset : 'open',
|
||||
items: [],
|
||||
showAddModal: false,
|
||||
newItem: this.initModalNewItem()
|
||||
};
|
||||
function getQueryKey() {
|
||||
return ['items', currentPreset];
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
public componentDidUpdate(prevsProps: any, prevState: BacklogPageState) {
|
||||
if (this.state.currentPreset !== prevState.currentPreset) {
|
||||
this.refresh();
|
||||
const addItemMutation = useMutation(async (newItem: PtNewItem) => {
|
||||
if (store.value.currentUser) {
|
||||
const createdItem = await backlogService.addNewPtItem(newItem, store.value.currentUser);
|
||||
return createdItem;
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(()=>{
|
||||
navigate(`/backlog/${[currentPreset]}`);
|
||||
},[currentPreset]);
|
||||
|
||||
const [isAddModalShowing, setIsAddModalShowing] = useState(false);
|
||||
|
||||
function onSelectPresetTap(preset: PresetType) {
|
||||
setCurrentPreset(preset);
|
||||
}
|
||||
|
||||
public getIndicatorImage(item: PtItem) {
|
||||
return ItemType.imageResFromType(item.type);
|
||||
function toggleModal() {
|
||||
setIsAddModalShowing(!isAddModalShowing);
|
||||
}
|
||||
|
||||
public getPriorityClass(item: PtItem): string {
|
||||
const indicatorClass = getIndicatorClass(item.priority);
|
||||
return indicatorClass;
|
||||
}
|
||||
|
||||
private onSelectPresetTap(preset: PresetType) {
|
||||
this.setState({
|
||||
currentPreset: preset
|
||||
});
|
||||
this.props.history.push(`/backlog/${[preset]}`);
|
||||
}
|
||||
|
||||
private refresh() {
|
||||
this.backlogService.getItems(this.state.currentPreset)
|
||||
.then(ptItems => {
|
||||
this.setState({
|
||||
items: ptItems
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public listItemTap(item: PtItem) {
|
||||
// navigate to detail page
|
||||
this.props.history.push(`/detail/${item.id}`);
|
||||
}
|
||||
|
||||
|
||||
private toggleModal() {
|
||||
this.setState({
|
||||
showAddModal: !this.state.showAddModal
|
||||
function onNewItemSave(newItem: PtNewItem) {
|
||||
return addItemMutation.mutateAsync(newItem, {
|
||||
onSuccess(createdItem, variables, context) {
|
||||
queryClient.invalidateQueries(getQueryKey());
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public onFieldChange(e: any, formFieldName: string) {
|
||||
if (!this.state.newItem) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
newItem: { ...this.state.newItem, [formFieldName]: e.target.value }
|
||||
});
|
||||
}
|
||||
|
||||
public onAddSave() {
|
||||
if (this.store.value.currentUser) {
|
||||
this.backlogService.addNewPtItem(this.state.newItem, this.store.value.currentUser)
|
||||
.then((nextItem: PtItem) => {
|
||||
this.setState({
|
||||
showAddModal: false,
|
||||
newItem: this.initModalNewItem(),
|
||||
items: [nextItem, ...this.state.items]
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private initModalNewItem(): PtNewItem {
|
||||
return {
|
||||
title: EMPTY_STRING,
|
||||
description: EMPTY_STRING,
|
||||
typeStr: 'PBI'
|
||||
};
|
||||
}
|
||||
|
||||
public render() {
|
||||
|
||||
const rows = this.state.items.map(i => {
|
||||
return (
|
||||
<tr key={i.id} className="pt-table-row" onClick={(e) => this.listItemTap(i)}>
|
||||
<td>
|
||||
<img src={this.getIndicatorImage(i)} className="backlog-icon" />
|
||||
</td>
|
||||
<td>
|
||||
<img src={i.assignee.avatar} className="li-avatar rounded mx-auto d-block" />
|
||||
</td>
|
||||
<td>
|
||||
<span className="li-title">{i.title}</span>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<span>{i.status}</span>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<span className={'badge ' + this.getPriorityClass(i)}>{i.priority}
|
||||
</span>
|
||||
</td>
|
||||
<td><span className="li-estimate">{i.estimate}</span></td>
|
||||
<td><span className="li-date">{i.dateCreated.toDateString()}</span></td>
|
||||
</tr>
|
||||
);
|
||||
});
|
||||
|
||||
if (queryResult.isLoading) {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div className="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3">
|
||||
<h1 className="h2">Backlog</h1>
|
||||
<div className="btn-toolbar mb-2 mb-md-0">
|
||||
<AppPresetFilter selectedPreset={this.state.currentPreset} onSelectPresetTap={(p) => this.onSelectPresetTap(p)} />
|
||||
|
||||
<div className="btn-group mr-2">
|
||||
<button type="button" className="btn btn-sm btn-outline-secondary" onClick={() => this.toggleModal()}>Add</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="table-responsive">
|
||||
<table className="table table-striped table-sm table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>Assignee</th>
|
||||
<th>Title</th>
|
||||
<th>Status</th>
|
||||
<th>Priority</th>
|
||||
<th>Estimate</th>
|
||||
<th>Created</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<Modal isOpen={this.state.showAddModal} toggle={() => this.toggleModal()} className={this.props.className}>
|
||||
<div className="modal-header">
|
||||
<h4 className="modal-title" id="modal-basic-title">Add New Item</h4>
|
||||
<button type="button" className="close" onClick={() => this.toggleModal()} aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<ModalBody>
|
||||
<form>
|
||||
<div className="form-group row">
|
||||
<label className="col-sm-2 col-form-label">Title</label>
|
||||
<div className="col-sm-10">
|
||||
<input className="form-control" defaultValue={this.state.newItem.title} onChange={(e) => this.onFieldChange(e, 'title')} name="title" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-group row">
|
||||
<label className="col-sm-2 col-form-label">Description</label>
|
||||
<div className="col-sm-10">
|
||||
<textarea className="form-control" defaultValue={this.state.newItem.description} onChange={(e) => this.onFieldChange(e, 'description')} name="description"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-group row">
|
||||
<label className="col-sm-2 col-form-label">Item Type</label>
|
||||
<div className="col-sm-10">
|
||||
<select className="form-control" defaultValue={this.state.newItem.typeStr} onChange={(e) => this.onFieldChange(e, 'typeStr')} name="itemType">
|
||||
{
|
||||
this.itemTypesProvider.map(t => {
|
||||
return (
|
||||
<option key={t} value={t}>
|
||||
{t}
|
||||
</option>
|
||||
)
|
||||
})
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form >
|
||||
</ModalBody >
|
||||
<ModalFooter>
|
||||
<Button color="secondary" onClick={() => this.toggleModal()}>Cancel</Button>
|
||||
<Button color="primary" onClick={() => this.onAddSave()}>Save</Button>{' '}
|
||||
|
||||
</ModalFooter>
|
||||
</Modal >
|
||||
|
||||
|
||||
</React.Fragment >
|
||||
|
||||
<div>
|
||||
Loading...
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!items) {
|
||||
return (
|
||||
<div>No items</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div className="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3">
|
||||
<h1 className="h2">Backlog</h1>
|
||||
<div className="btn-toolbar mb-2 mb-md-0">
|
||||
<AppPresetFilter selectedPreset={currentPreset} onSelectPresetTap={onSelectPresetTap} />
|
||||
|
||||
<div className="btn-group mr-2">
|
||||
<button type="button" className="btn btn-sm btn-outline-secondary" onClick={toggleModal}>Add</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<BacklogList items={items} />
|
||||
|
||||
<AddItemModal
|
||||
onNewItemSave={onNewItemSave}
|
||||
modalShowing={isAddModalShowing}
|
||||
setIsAddModalShowing={setIsAddModalShowing}
|
||||
/>
|
||||
|
||||
</React.Fragment >
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,185 +1,163 @@
|
|||
import React from "react";
|
||||
import { useContext, useState } from "react";
|
||||
import { useMutation, useQuery, useQueryClient } from "react-query";
|
||||
import { Navigate, useNavigate, useParams } from "react-router-dom";
|
||||
|
||||
import { PtItem, PtUser, PtTask, PtComment } from "../../../../core/models/domain";
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
import { PtItem, PtUser, PtTask } from "../../../../core/models/domain";
|
||||
import { DetailScreenType } from "../../../../shared/models/ui/types/detail-screens";
|
||||
import { Store } from "../../../../core/state/app-store";
|
||||
import { BacklogRepository } from "../../repositories/backlog.repository";
|
||||
import { BacklogService } from "../../services/backlog.service";
|
||||
import { PtItemDetailsComponent } from "../../components/item-details/pt-item-details";
|
||||
import { PtItemTasksComponent } from "../../components/item-tasks/pt-item-tasks";
|
||||
// import { debug } from "util";
|
||||
import { PtUserService } from "../../../../core/services/pt-user-service";
|
||||
import { Observable, BehaviorSubject } from "rxjs";
|
||||
import { PtNewTask } from "../../../../shared/models/dto/pt-new-task";
|
||||
import { PtTaskUpdate } from "../../../../shared/models/dto/pt-task-update";
|
||||
import { PtTaskTitleUpdate } from "../../../../shared/models/dto/pt-task-update";
|
||||
import { PtItemChitchatComponent } from "../../components/item-chitchat/pt-item-chitchat";
|
||||
import { PtNewComment } from "../../../../shared/models/dto/pt-new-comment";
|
||||
|
||||
interface DetailPageState {
|
||||
item: PtItem | undefined;
|
||||
selectedDetailsScreen: DetailScreenType;
|
||||
}
|
||||
import { PtBacklogServiceContext, PtStoreContext, PtUserServiceContext } from "../../../../App";
|
||||
|
||||
export class DetailPage extends React.Component<any, DetailPageState> {
|
||||
|
||||
private store: Store = new Store();
|
||||
private backlogRepo: BacklogRepository = new BacklogRepository();
|
||||
private backlogService: BacklogService = new BacklogService(this.backlogRepo, this.store);
|
||||
private ptUserService: PtUserService = new PtUserService(this.store);
|
||||
const queryTag = 'item';
|
||||
|
||||
private itemId = 0;
|
||||
private users$: Observable<PtUser[]> = this.store.select<PtUser[]>('users');
|
||||
public tasks$: BehaviorSubject<PtTask[]> = new BehaviorSubject<PtTask[]>([]);
|
||||
public comments$: BehaviorSubject<PtComment[]> = new BehaviorSubject<PtComment[]>([]);
|
||||
public currentUser: PtUser | undefined;
|
||||
const screenPositionMap: { [key in DetailScreenType | number]: number | DetailScreenType } = {
|
||||
0: 'details',
|
||||
1: 'tasks',
|
||||
2: 'chitchat',
|
||||
'details': 0,
|
||||
'tasks': 1,
|
||||
'chitchat': 2
|
||||
};
|
||||
|
||||
private screenPositionMap: { [key in DetailScreenType | number]: number | DetailScreenType } = {
|
||||
0: 'details',
|
||||
1: 'tasks',
|
||||
2: 'chitchat',
|
||||
'details': 0,
|
||||
'tasks': 1,
|
||||
'chitchat': 2
|
||||
};
|
||||
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
export function DetailPage() {
|
||||
|
||||
const { id, screen } = this.props.match.params;
|
||||
this.itemId = id;
|
||||
this.currentUser = this.store.value.currentUser;
|
||||
const store = useContext(PtStoreContext);
|
||||
const backlogService = useContext(PtBacklogServiceContext);
|
||||
const userService = useContext(PtUserServiceContext);
|
||||
|
||||
this.state = {
|
||||
item: undefined,
|
||||
selectedDetailsScreen: screen ? screen : 'details'
|
||||
};
|
||||
const currentUser = store.value.currentUser;
|
||||
const users$: Observable<PtUser[]> = store.select<PtUser[]>('users');
|
||||
|
||||
const { id: itemId, screen } = useParams() as { id: string, screen: DetailScreenType };
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const useItem = (...params: Parameters<typeof backlogService.getPtItem>) => {
|
||||
return useQuery<PtItem, Error>(queryTag, () => backlogService.getPtItem(...params));
|
||||
}
|
||||
const queryResult = useItem(parseInt(itemId));
|
||||
const item = queryResult.data;
|
||||
|
||||
const [selectedDetailsScreen, setSelectedDetailsScreen] = useState<DetailScreenType>(screen ? screen : 'details');
|
||||
|
||||
const updateItemMutation = useMutation(async (itemToUpdate: PtItem) => {
|
||||
const updatedItem = await backlogService.updatePtItem(itemToUpdate);
|
||||
return updatedItem;
|
||||
});
|
||||
|
||||
const addTaskMutation = useMutation(async (newTaskItem: PtNewTask) => {
|
||||
const createdTask = await backlogService.addNewPtTask(newTaskItem, item!);
|
||||
return createdTask;
|
||||
});
|
||||
|
||||
const toggleTaskCompletionMutation = useMutation(async (task: PtTask) => {
|
||||
const updatedTask = await backlogService.updatePtTask(item!, task, true);
|
||||
return updatedTask;
|
||||
});
|
||||
|
||||
const updateTaskMutation = useMutation(async (taskUpdate: PtTaskTitleUpdate) => {
|
||||
const updatedTask = await backlogService.updatePtTask(item!, taskUpdate.task, taskUpdate.task.completed, taskUpdate.newTitle);
|
||||
return updatedTask;
|
||||
});
|
||||
|
||||
const deleteTaskMutation = useMutation(async (task: PtTask ) => {
|
||||
const ok = await backlogService.deletePtTask(item!, task);
|
||||
return ok;
|
||||
});
|
||||
|
||||
const addCommentMutation = useMutation(async (newCommentItem: PtNewComment) => {
|
||||
const createdComment = await backlogService.addNewPtComment(newCommentItem, item!);
|
||||
return createdComment;
|
||||
});
|
||||
|
||||
function onScreenSelected(screen: DetailScreenType) {
|
||||
setSelectedDetailsScreen(screen);
|
||||
navigate(`/detail/${itemId}/${screen}`);
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
public componentDidUpdate(prevsProps: any, prevState: DetailPageState) {
|
||||
|
||||
}
|
||||
|
||||
private refresh() {
|
||||
this.backlogService.getPtItem(this.itemId)
|
||||
.then(item => {
|
||||
this.setState({
|
||||
item: item
|
||||
});
|
||||
this.tasks$.next(item.tasks);
|
||||
this.comments$.next(item.comments);
|
||||
});
|
||||
}
|
||||
|
||||
public onScreenSelected(screen: DetailScreenType) {
|
||||
this.setState({
|
||||
selectedDetailsScreen: screen
|
||||
});
|
||||
this.props.history.push(`/detail/${this.itemId}/${screen}`);
|
||||
}
|
||||
|
||||
public onItemSaved(item: PtItem) {
|
||||
this.backlogService.updatePtItem(item)
|
||||
.then((updateItem: PtItem) => {
|
||||
this.setState({
|
||||
item: updateItem
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public onAddNewTask(newTask: PtNewTask) {
|
||||
if (this.state.item) {
|
||||
this.backlogService.addNewPtTask(newTask, this.state.item).then(nextTask => {
|
||||
this.tasks$.next([nextTask].concat(this.tasks$.value));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public onUpdateTask(taskUpdate: PtTaskUpdate) {
|
||||
if (this.state.item) {
|
||||
if (taskUpdate.delete) {
|
||||
this.backlogService.deletePtTask(this.state.item, taskUpdate.task).then(ok => {
|
||||
if (ok) {
|
||||
const newTasks = this.tasks$.value.filter(task => {
|
||||
if (task.id !== taskUpdate.task.id) {
|
||||
return task;
|
||||
}
|
||||
});
|
||||
this.tasks$.next(newTasks);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.backlogService.updatePtTask(this.state.item, taskUpdate.task, taskUpdate.toggle, taskUpdate.newTitle).then(updatedTask => {
|
||||
const newTasks = this.tasks$.value.map(task => {
|
||||
if (task.id === updatedTask.id) {
|
||||
return updatedTask;
|
||||
} else {
|
||||
return task;
|
||||
}
|
||||
});
|
||||
this.tasks$.next(newTasks);
|
||||
});
|
||||
function onItemSaved(item: PtItem) {
|
||||
updateItemMutation.mutate(item, {
|
||||
onSuccess: (updatedItem) => {
|
||||
queryClient.setQueryData(queryTag, updatedItem);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
public onAddNewComment(newComment: PtNewComment) {
|
||||
if (this.state.item) {
|
||||
this.backlogService.addNewPtComment(newComment, this.state.item).then(nextComment => {
|
||||
this.comments$.next([nextComment].concat(this.comments$.value));
|
||||
});
|
||||
}
|
||||
function onUsersRequested() {
|
||||
userService.fetchUsers();
|
||||
}
|
||||
|
||||
public onUsersRequested() {
|
||||
this.ptUserService.fetchUsers();
|
||||
}
|
||||
|
||||
private screenRender(screen: DetailScreenType, item: PtItem) {
|
||||
function screenRender(screen: DetailScreenType, item: PtItem) {
|
||||
switch (screen) {
|
||||
case 'details':
|
||||
return <PtItemDetailsComponent item={item} users$={this.users$} usersRequested={() => this.onUsersRequested()} itemSaved={(item) => this.onItemSaved(item)} />;
|
||||
return <PtItemDetailsComponent
|
||||
item={item}
|
||||
users$={users$}
|
||||
usersRequested={onUsersRequested}
|
||||
itemSaved={onItemSaved} />;
|
||||
case 'tasks':
|
||||
return <PtItemTasksComponent tasks$={this.tasks$} addNewTask={(newTask) => this.onAddNewTask(newTask)} updateTask={(taskUpdate) => this.onUpdateTask(taskUpdate)} />;
|
||||
return <PtItemTasksComponent
|
||||
tasks={item.tasks}
|
||||
addTaskMutation={addTaskMutation}
|
||||
deleteTaskMutation={deleteTaskMutation}
|
||||
toggleTaskCompletionMutation={toggleTaskCompletionMutation}
|
||||
updateTaskMutation={updateTaskMutation}
|
||||
/>;
|
||||
case 'chitchat':
|
||||
return <PtItemChitchatComponent comments$={this.comments$} currentUser={this.currentUser!} addNewComment={(newComment) => this.onAddNewComment(newComment)} />;
|
||||
return <PtItemChitchatComponent
|
||||
comments={item.comments}
|
||||
currentUser={currentUser!}
|
||||
addCommentMutation={addCommentMutation}
|
||||
/>;
|
||||
|
||||
default:
|
||||
return <PtItemDetailsComponent item={item} users$={this.users$} usersRequested={() => this.onUsersRequested()} itemSaved={(item) => this.onItemSaved(item)} />;
|
||||
return <PtItemDetailsComponent item={item} users$={users$} usersRequested={() => onUsersRequested()} itemSaved={(item) => onItemSaved(item)} />;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
public render() {
|
||||
const item = this.state.item;
|
||||
|
||||
if (!item) {
|
||||
return null;
|
||||
}
|
||||
if (!screen) {
|
||||
return (
|
||||
|
||||
<div>
|
||||
<div className="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3">
|
||||
<h1 className="h2">{item.title}</h1>
|
||||
<div className="btn-toolbar mb-2 mb-md-0">
|
||||
<div className="btn-group mr-2">
|
||||
<button type="button" onClick={(e) => this.onScreenSelected('details')} className={'btn btn-sm btn-outline-secondary ' + this.state.selectedDetailsScreen === 'details' ? 'active' : ''}>Details</button>
|
||||
|
||||
<button type="button" onClick={(e) => this.onScreenSelected('tasks')} className={"btn btn-sm btn-outline-secondary " + this.state.selectedDetailsScreen === 'tasks' ? 'active' : ''}>Tasks</button>
|
||||
|
||||
<button type="button" onClick={(e) => this.onScreenSelected('chitchat')} className={"btn btn-sm btn-outline-secondary " + this.state.selectedDetailsScreen === 'chitchat' ? 'active' : ''}>Chitchat</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{this.screenRender(this.state.selectedDetailsScreen, item)}
|
||||
|
||||
</div>
|
||||
<Navigate replace to={`/detail/${itemId}/details`}/>
|
||||
);
|
||||
}
|
||||
|
||||
if (queryResult.isLoading) {
|
||||
return <div>Loading...</div>
|
||||
}
|
||||
|
||||
if (!item) {
|
||||
return <div>No item</div>
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
<div>
|
||||
<div className="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3">
|
||||
<h1 className="h2">{item.title}</h1>
|
||||
<div className="btn-toolbar mb-2 mb-md-0">
|
||||
<div className="btn-group mr-2">
|
||||
<button type="button" onClick={(e) => onScreenSelected('details')} className={'btn btn-sm btn-outline-secondary ' + selectedDetailsScreen === 'details' ? 'active' : ''}>Details</button>
|
||||
|
||||
<button type="button" onClick={(e) => onScreenSelected('tasks')} className={"btn btn-sm btn-outline-secondary " + selectedDetailsScreen === 'tasks' ? 'active' : ''}>Tasks</button>
|
||||
|
||||
<button type="button" onClick={(e) => onScreenSelected('chitchat')} className={"btn btn-sm btn-outline-secondary " + selectedDetailsScreen === 'chitchat' ? 'active' : ''}>Chitchat</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{screenRender(selectedDetailsScreen, item)}
|
||||
|
||||
</div>
|
||||
);
|
||||
|
||||
}
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
import { PtTask, PtItem, PtComment } from '../../../core/models/domain';
|
||||
import { PtTask, PtItem, PtComment, PtCommentToBe, PtTaskToBe, PtItemServer, PtTaskServer, PtCommentServer } from '../../../core/models/domain';
|
||||
import { CONFIG } from '../../../config';
|
||||
import { PresetType } from '../../../core/models/domain/types';
|
||||
|
||||
|
||||
export class BacklogRepository {
|
||||
|
||||
private getFilteredBacklogUrl(currentPreset: PresetType, currentUserId?: number) {
|
||||
|
@ -61,7 +60,7 @@ export class BacklogRepository {
|
|||
public getPtItems(
|
||||
currentPreset: PresetType,
|
||||
currentUserId: number | undefined
|
||||
): Promise<PtItem[]> {
|
||||
): Promise<PtItemServer[]> {
|
||||
return fetch(this.getFilteredBacklogUrl(currentPreset, currentUserId))
|
||||
.then((response: Response) => response.json());
|
||||
}
|
||||
|
@ -69,14 +68,14 @@ export class BacklogRepository {
|
|||
|
||||
public getPtItem(
|
||||
ptItemId: number,
|
||||
): Promise<PtItem> {
|
||||
): Promise<PtItemServer> {
|
||||
return fetch(this.getPtItemUrl(ptItemId))
|
||||
.then((response: Response) => response.json());
|
||||
}
|
||||
|
||||
public insertPtItem(
|
||||
item: PtItem
|
||||
): Promise<PtItem> {
|
||||
): Promise<PtItemServer> {
|
||||
return fetch(this.postPtItemUrl(),
|
||||
{
|
||||
method: 'POST',
|
||||
|
@ -88,7 +87,7 @@ export class BacklogRepository {
|
|||
|
||||
public updatePtItem(
|
||||
item: PtItem,
|
||||
): Promise<PtItem> {
|
||||
): Promise<PtItemServer> {
|
||||
return fetch(this.putPtItemUrl(item.id),
|
||||
{
|
||||
method: 'PUT',
|
||||
|
@ -98,25 +97,13 @@ export class BacklogRepository {
|
|||
.then((response: Response) => response.json());
|
||||
}
|
||||
|
||||
/*
|
||||
public deletePtItem(
|
||||
itemId: number,
|
||||
successHandler: () => void
|
||||
) {
|
||||
this.http.delete(
|
||||
this.deletePtItemUrl(itemId)
|
||||
)
|
||||
.subscribe(successHandler);
|
||||
}
|
||||
*/
|
||||
|
||||
public insertPtTask(
|
||||
task: PtTask,
|
||||
taskToBe: PtTaskToBe,
|
||||
ptItemId: number
|
||||
): Promise<PtTask> {
|
||||
): Promise<PtTaskServer> {
|
||||
return fetch(this.postPtTaskUrl(), {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ task: task, itemId: ptItemId }),
|
||||
body: JSON.stringify({ task: taskToBe, itemId: ptItemId }),
|
||||
headers: this.getJSONHeader()
|
||||
})
|
||||
.then(response => response.json());
|
||||
|
@ -125,7 +112,7 @@ export class BacklogRepository {
|
|||
public updatePtTask(
|
||||
task: PtTask,
|
||||
ptItemId: number
|
||||
): Promise<PtTask> {
|
||||
): Promise<PtTaskServer> {
|
||||
return fetch(this.putPtTaskUrl(task.id), {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ task: task, itemId: ptItemId }),
|
||||
|
@ -146,28 +133,17 @@ export class BacklogRepository {
|
|||
|
||||
|
||||
public insertPtComment(
|
||||
comment: PtComment,
|
||||
commentToBe: PtCommentToBe,
|
||||
ptItemId: number
|
||||
): Promise<PtComment> {
|
||||
): Promise<PtCommentServer> {
|
||||
return fetch(this.postPtCommentUrl(), {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ comment: comment, itemId: ptItemId }),
|
||||
body: JSON.stringify({ comment: commentToBe, itemId: ptItemId }),
|
||||
headers: this.getJSONHeader()
|
||||
})
|
||||
.then(response => response.json());
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
public deletePtComment(
|
||||
ptCommentId: number,
|
||||
successHandler: () => void
|
||||
) {
|
||||
this.http.delete(this.deletePtCommentUrl(ptCommentId))
|
||||
.subscribe(successHandler);
|
||||
}
|
||||
*/
|
||||
|
||||
private getJSONHeader() {
|
||||
return new Headers({
|
||||
'Content-Type': 'application/json'
|
||||
|
|
|
@ -1,16 +1,14 @@
|
|||
import { Store } from '../../../core/state/app-store';
|
||||
import { BacklogRepository } from '../repositories/backlog.repository';
|
||||
|
||||
import { PtItem, PtUser, PtTask, PtComment } from '../../../core/models/domain';
|
||||
import { PtItem, PtUser, PtTask, PtComment, PtCommentToBe, PtTaskToBe, PtItemServer, ptItemsServerToPtItems, ptItemServerToPtItem, PtTaskServer, ptTaskServerToPtTask, PtCommentServer, ptCommentServerToPtComment } from '../../../core/models/domain';
|
||||
|
||||
import { PriorityEnum, StatusEnum } from '../../../core/models/domain/enums';
|
||||
import { getUserAvatarUrl } from '../../../core/helpers/user-avatar-helper';
|
||||
import { Observable } from 'rxjs';
|
||||
import { map, tap } from 'rxjs/operators';
|
||||
|
||||
|
||||
import { CONFIG } from '../../../config';
|
||||
import { PresetType } from '../../../core/models/domain/types';
|
||||
import { datesForTask, datesForPtItem } from '../../../core/helpers/date-utils';
|
||||
import { PtNewItem } from '../../../shared/models/dto/pt-new-item';
|
||||
import { PtNewTask } from '../../../shared/models/dto/pt-new-task';
|
||||
import { PtNewComment } from '../../../shared/models/dto/pt-new-comment';
|
||||
|
@ -50,10 +48,9 @@ export class BacklogService {
|
|||
|
||||
public getItems(preset: PresetType): Promise<PtItem[]> {
|
||||
return this.repo.getPtItems(preset, this.currentUserId)
|
||||
.then((ptItems: PtItem[]) => {
|
||||
|
||||
.then((ptItemsServer: PtItemServer[]) => {
|
||||
const ptItems = ptItemsServerToPtItems(ptItemsServer);
|
||||
ptItems.forEach(i => {
|
||||
datesForPtItem(i);
|
||||
this.setUserAvatarUrl(i.assignee);
|
||||
i.comments.forEach(c => this.setUserAvatarUrl(c.user));
|
||||
});
|
||||
|
@ -61,33 +58,16 @@ export class BacklogService {
|
|||
});
|
||||
}
|
||||
|
||||
/*
|
||||
public getItemFromCacheOrServer(id: number) {
|
||||
// const selectedItem = _.find(this.store.value.backlogItems, i => i.id === id);
|
||||
const selectedItem = this.store.value.backlogItems.find(i => i.id === id);
|
||||
if (selectedItem) {
|
||||
|
||||
this.store.set('currentSelectedItem', selectedItem);
|
||||
|
||||
} else {
|
||||
this.getPtItem(id);
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
|
||||
public getPtItem(id: number): Promise<PtItem> {
|
||||
return this.repo.getPtItem(id)
|
||||
.then((ptItem: PtItem) => {
|
||||
datesForPtItem(ptItem);
|
||||
.then((ptItemServer: PtItemServer) => {
|
||||
const ptItem = ptItemServerToPtItem(ptItemServer);
|
||||
this.setUserAvatarUrl(ptItem.assignee);
|
||||
ptItem.comments.forEach(c => this.setUserAvatarUrl(c.user));
|
||||
ptItem.tasks.forEach(t => datesForTask(t));
|
||||
return ptItem;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
public addNewPtItem(newItem: PtNewItem, assignee: PtUser): Promise<PtItem> {
|
||||
const item: PtItem = {
|
||||
id: 0,
|
||||
|
@ -105,11 +85,9 @@ export class BacklogService {
|
|||
};
|
||||
return new Promise<PtItem>((resolve, reject) => {
|
||||
this.repo.insertPtItem(item)
|
||||
.then((nextItem: PtItem) => {
|
||||
datesForPtItem(nextItem);
|
||||
.then((nextItemServer: PtItemServer) => {
|
||||
const nextItem = ptItemServerToPtItem(nextItemServer);
|
||||
this.setUserAvatar(nextItem.assignee);
|
||||
|
||||
nextItem.tasks.forEach(t => datesForTask(t));
|
||||
resolve(nextItem);
|
||||
});
|
||||
});
|
||||
|
@ -117,7 +95,14 @@ export class BacklogService {
|
|||
|
||||
|
||||
public updatePtItem(item: PtItem): Promise<PtItem> {
|
||||
return this.repo.updatePtItem(item);
|
||||
return new Promise<PtItem>((resolve, reject) => {
|
||||
this.repo.updatePtItem(item)
|
||||
.then((updatedItemServer: PtItemServer) => {
|
||||
const updatedItem = ptItemServerToPtItem(updatedItemServer);
|
||||
this.setUserAvatar(updatedItem.assignee);
|
||||
resolve(updatedItem);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
|
@ -137,8 +122,7 @@ export class BacklogService {
|
|||
*/
|
||||
|
||||
public addNewPtTask(newTask: PtNewTask, currentItem: PtItem): Promise<PtTask> {
|
||||
const task: PtTask = {
|
||||
id: 0,
|
||||
const taskToBe: PtTaskToBe = {
|
||||
title: newTask.title,
|
||||
completed: false,
|
||||
dateCreated: new Date(),
|
||||
|
@ -148,13 +132,13 @@ export class BacklogService {
|
|||
};
|
||||
return new Promise<PtTask>((resolve, reject) => {
|
||||
this.repo.insertPtTask(
|
||||
task,
|
||||
taskToBe,
|
||||
currentItem.id)
|
||||
.then((nextTask: PtTask) => {
|
||||
datesForTask(nextTask);
|
||||
.then((nextTaskServer: PtTaskServer) => {
|
||||
const nextTask = ptTaskServerToPtTask(nextTaskServer);
|
||||
resolve(nextTask);
|
||||
}
|
||||
);
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -173,11 +157,11 @@ export class BacklogService {
|
|||
this.repo.updatePtTask(
|
||||
taskToUpdate,
|
||||
currentItem.id)
|
||||
.then((updatedTask: PtTask) => {
|
||||
datesForTask(updatedTask);
|
||||
.then((updatedTaskServer: PtTaskServer) => {
|
||||
const updatedTask = ptTaskServerToPtTask(updatedTaskServer);
|
||||
resolve(updatedTask);
|
||||
}
|
||||
);
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -198,22 +182,22 @@ export class BacklogService {
|
|||
}
|
||||
|
||||
public addNewPtComment(newComment: PtNewComment, currentItem: PtItem): Promise<PtComment> {
|
||||
const comment: PtComment = {
|
||||
id: 0,
|
||||
const commentToBe: PtCommentToBe = {
|
||||
title: newComment.title,
|
||||
user: this.store.value.currentUser,
|
||||
dateCreated: new Date(),
|
||||
dateModified: new Date()
|
||||
};
|
||||
|
||||
return new Promise<PtComment>((resolve, reject) => {
|
||||
this.repo.insertPtComment(
|
||||
comment,
|
||||
commentToBe,
|
||||
currentItem.id
|
||||
)
|
||||
.then((nextComment: PtComment) => {
|
||||
.then((nextCommentServer: PtCommentServer) => {
|
||||
const nextComment = ptCommentServerToPtComment(nextCommentServer);
|
||||
resolve(nextComment);
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -6,8 +6,7 @@ interface WelcomeProps {
|
|||
statusCounts: StatusCounts
|
||||
}
|
||||
|
||||
export const ActiveIssuesComponent: React.SFC<WelcomeProps> = (props) => {
|
||||
|
||||
export function ActiveIssuesComponent(props: WelcomeProps) {
|
||||
if (!props.statusCounts) {
|
||||
return (
|
||||
<div className="card">
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
export interface StatusCounts {
|
||||
export type StatusCounts = {
|
||||
activeItemsCount: number;
|
||||
closeRate: number;
|
||||
closedItemsCount: number;
|
||||
|
|
|
@ -1,62 +1,44 @@
|
|||
import React from "react";
|
||||
import { DashboardFilter, DashboardRepository } from "../../repositories/dashboard.repository";
|
||||
import { useContext, useState } from "react";
|
||||
import { useQuery } from "react-query";
|
||||
|
||||
import { DashboardFilter } from "../../repositories/dashboard.repository";
|
||||
import { formatDateEnUs } from "../../../../core/helpers/date-utils";
|
||||
import { ActiveIssuesComponent } from "../../components/active-issues/active-issues";
|
||||
import { DashboardService } from "../../services/dashboard.service";
|
||||
import { StatusCounts } from "../../models";
|
||||
import { PtDashboardServiceContext } from "../../../../App";
|
||||
|
||||
|
||||
interface DateRange {
|
||||
type DateRange = {
|
||||
dateStart: Date;
|
||||
dateEnd: Date;
|
||||
}
|
||||
};
|
||||
|
||||
interface DashboardPageState {
|
||||
statusCounts: StatusCounts;
|
||||
filter: DashboardFilter;
|
||||
}
|
||||
|
||||
export class DashboardPage extends React.Component<any, DashboardPageState> {
|
||||
export function DashboardPage() {
|
||||
const dashboardService = useContext(PtDashboardServiceContext);
|
||||
|
||||
private dashboardRepo: DashboardRepository = new DashboardRepository();
|
||||
private dashboardService: DashboardService = new DashboardService(this.dashboardRepo);
|
||||
public filter: DashboardFilter = {};
|
||||
const [filter, setFilter] = useState<DashboardFilter>({});
|
||||
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
this.state = {
|
||||
statusCounts: {
|
||||
activeItemsCount: 0,
|
||||
closeRate: 0,
|
||||
closedItemsCount: 0,
|
||||
openItemsCount: 0
|
||||
},
|
||||
filter: {}
|
||||
};
|
||||
function getQueryKey() {
|
||||
return ['items', filter];
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
this.refresh();
|
||||
const useStatusCounts = (...params: Parameters<typeof dashboardService.getStatusCounts>) => {
|
||||
return useQuery<StatusCounts, Error>(getQueryKey(), () => dashboardService.getStatusCounts(...params));
|
||||
}
|
||||
const queryResult = useStatusCounts(filter);
|
||||
const statusCounts = queryResult.data;
|
||||
|
||||
private onMonthRangeTap(months: number) {
|
||||
const range = this.getDateRange(months);
|
||||
this.filter = {
|
||||
userId: this.filter.userId,
|
||||
function onMonthRangeTap(months: number) {
|
||||
const range = getDateRange(months);
|
||||
setFilter({
|
||||
userId: filter.userId,
|
||||
dateEnd: range.dateEnd,
|
||||
dateStart: range.dateStart
|
||||
};
|
||||
this.setState({
|
||||
filter: {
|
||||
userId: this.state.filter.userId,
|
||||
dateEnd: range.dateEnd,
|
||||
dateStart: range.dateStart
|
||||
}
|
||||
});
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
private getDateRange(months: number): DateRange {
|
||||
function getDateRange(months: number): DateRange {
|
||||
const now = new Date();
|
||||
const start = new Date();
|
||||
start.setMonth(start.getMonth() - months);
|
||||
|
@ -66,58 +48,62 @@ export class DashboardPage extends React.Component<any, DashboardPageState> {
|
|||
};
|
||||
}
|
||||
|
||||
private refresh() {
|
||||
this.dashboardService.getStatusCounts(this.filter)
|
||||
.then(result => {
|
||||
this.setState({
|
||||
statusCounts: result
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public render() {
|
||||
if (queryResult.isLoading) {
|
||||
return (
|
||||
<div className="dashboard">
|
||||
|
||||
<div className="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3">
|
||||
|
||||
<div className="col-md order-md-first text-center text-md-left">
|
||||
<h2>
|
||||
<span className="small text-uppercase text-muted d-block">Statistics</span>
|
||||
{
|
||||
(this.state.filter.dateStart && this.state.filter.dateEnd) && (
|
||||
<span> {formatDateEnUs(this.state.filter.dateStart)} - {formatDateEnUs(this.state.filter.dateEnd)}</span>
|
||||
)
|
||||
}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="btn-toolbar mb-2 mb-md-0">
|
||||
<div className="btn-group mr-2">
|
||||
<button type="button" className="btn btn-sm btn-outline-secondary" onClick={(e) => this.onMonthRangeTap(3)}>3 Months</button>
|
||||
<button type="button" className="btn btn-sm btn-outline-secondary" onClick={(e) => this.onMonthRangeTap(6)}>6 Months</button>
|
||||
<button type="button" className="btn btn-sm btn-outline-secondary" onClick={(e) => this.onMonthRangeTap(12)}>1 Year</button>
|
||||
</div >
|
||||
|
||||
</div >
|
||||
</div >
|
||||
|
||||
<div className="card">
|
||||
<h3 className="card-header">Active Issues</h3>
|
||||
<div className="card-block">
|
||||
|
||||
<ActiveIssuesComponent statusCounts={this.state.statusCounts} />
|
||||
|
||||
<div className="row">
|
||||
<div className="col-sm-12">
|
||||
<h3>All issues</h3>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div >
|
||||
|
||||
</div >
|
||||
<div>
|
||||
Loading...
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!statusCounts) {
|
||||
return (
|
||||
<div>No data</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="dashboard">
|
||||
|
||||
<div className="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3">
|
||||
|
||||
<div className="col-md order-md-first text-center text-md-left">
|
||||
<h2>
|
||||
<span className="small text-uppercase text-muted d-block">Statistics</span>
|
||||
{
|
||||
(filter.dateStart && filter.dateEnd) && (
|
||||
<span> {formatDateEnUs(filter.dateStart)} - {formatDateEnUs(filter.dateEnd)}</span>
|
||||
)
|
||||
}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="btn-toolbar mb-2 mb-md-0" style={{gap: 20}}>
|
||||
<div className="btn-group mr-2">
|
||||
<button type="button" className="btn btn-sm btn-outline-secondary" onClick={(e) => onMonthRangeTap(3)}>3 Months</button>
|
||||
<button type="button" className="btn btn-sm btn-outline-secondary" onClick={(e) => onMonthRangeTap(6)}>6 Months</button>
|
||||
<button type="button" className="btn btn-sm btn-outline-secondary" onClick={(e) => onMonthRangeTap(12)}>1 Year</button>
|
||||
</div >
|
||||
|
||||
</div >
|
||||
</div >
|
||||
|
||||
<div className="card">
|
||||
<h3 className="card-header">Active Issues</h3>
|
||||
<div className="card-block">
|
||||
|
||||
<ActiveIssuesComponent statusCounts={statusCounts} />
|
||||
|
||||
<div className="row">
|
||||
<div className="col-sm-12">
|
||||
<h3>All issues</h3>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div >
|
||||
|
||||
</div >
|
||||
);
|
||||
|
||||
}
|
||||
|
|
|
@ -1,25 +1,15 @@
|
|||
import React from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
|
||||
|
||||
export class MainMenu extends React.PureComponent<any, any> {
|
||||
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<div className="navbar navbar-dark fixed-top bg-dark flex-md-nowrap p-0 shadow">
|
||||
<a className="navbar-brand col-sm-3 col-md-2 mr-0">
|
||||
<img src="/assets/img/rpslogo.png" className="logo" />
|
||||
</a>
|
||||
<nav className="my-2 my-md-0 mr-md-3">
|
||||
<Link className="p-2 text-light" to="/dashboard">Dashboard</Link>
|
||||
<Link className="p-2 text-light" to="/backlog">Backlog</Link>
|
||||
</nav>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export function MainMenu(){
|
||||
return (
|
||||
<div className="navbar navbar-dark fixed-top bg-dark flex-md-nowrap p-0 shadow">
|
||||
<a className="navbar-brand col-sm-3 col-md-2 mr-0">
|
||||
<img src="/assets/img/rpslogo.png" className="logo" />
|
||||
</a>
|
||||
<nav className="my-2 my-md-0 mr-md-3">
|
||||
<Link className="p-2 text-light" to="/dashboard">Dashboard</Link>
|
||||
<Link className="p-2 text-light" to="/backlog">Backlog</Link>
|
||||
</nav>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,25 +1,15 @@
|
|||
import React, { ReactNode } from "react";
|
||||
import { PresetType } from "../../../core/models/domain/types";
|
||||
|
||||
|
||||
interface AppPresetFilterProps {
|
||||
type AppPresetFilterProps = {
|
||||
selectedPreset: PresetType;
|
||||
onSelectPresetTap: (preset: PresetType) => void;
|
||||
}
|
||||
|
||||
export class AppPresetFilter extends React.PureComponent<AppPresetFilterProps, any> {
|
||||
|
||||
constructor(props: AppPresetFilterProps) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<div className="btn-group mr-2">
|
||||
<button type="button" className="btn btn-sm btn-outline-secondary" onClick={(e) => this.props.onSelectPresetTap('my')}>My Items</button>
|
||||
<button type="button" className="btn btn-sm btn-outline-secondary" onClick={(e) => this.props.onSelectPresetTap('open')} > Open Items</button >
|
||||
<button type="button" className="btn btn-sm btn-outline-secondary" onClick={(e) => this.props.onSelectPresetTap('closed')} > Done Items</button >
|
||||
</div >
|
||||
);
|
||||
}
|
||||
export function AppPresetFilter(props: AppPresetFilterProps) {
|
||||
return (
|
||||
<div className="btn-group mr-2">
|
||||
<button type="button" className="btn btn-sm btn-outline-secondary" onClick={(e) => props.onSelectPresetTap('my')}>My Items</button>
|
||||
<button type="button" className="btn btn-sm btn-outline-secondary" onClick={(e) => props.onSelectPresetTap('open')} > Open Items</button >
|
||||
<button type="button" className="btn btn-sm btn-outline-secondary" onClick={(e) => props.onSelectPresetTap('closed')} > Done Items</button >
|
||||
</div >
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import React from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
export const SideMenu: React.SFC<any> = () => {
|
||||
export function SideMenu() {
|
||||
return (
|
||||
<nav className="col-md-2 d-none d-md-block bg-light sidebar">
|
||||
<div className="sidebar-sticky">
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
export interface PtNewComment {
|
||||
export type PtNewComment = {
|
||||
title: string;
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { PtItemType } from '../../../core/models/domain/types';
|
||||
|
||||
export interface PtNewItem {
|
||||
export type PtNewItem = {
|
||||
title: string;
|
||||
description?: string;
|
||||
typeStr: PtItemType;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
export interface PtNewTask {
|
||||
export type PtNewTask = {
|
||||
title: string;
|
||||
completed: boolean;
|
||||
dateStart?: Date;
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
import { PtTask } from '../../../core/models/domain';
|
||||
|
||||
export interface PtTaskUpdate {
|
||||
export type PtTaskTitleUpdate = {
|
||||
task: PtTask;
|
||||
toggle: boolean;
|
||||
newTitle?: string;
|
||||
delete?: boolean;
|
||||
newTitle: string;
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { PtItem } from '../../../core/models/domain';
|
||||
|
||||
export interface PtItemDetailsEditFormModel {
|
||||
export type PtItemDetailsEditFormModel = {
|
||||
title: string;
|
||||
description: string;
|
||||
typeStr: string;
|
||||
|
|
Загрузка…
Ссылка в новой задаче