Merge pull request #1 from telerik/2023_R2_wip

2023 r2 wip
This commit is contained in:
Plamen Mitrev 2023-08-03 10:00:16 +03:00 коммит произвёл GitHub
Родитель c45962ab0a 35be9533d9
Коммит 0507272d2a
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
40 изменённых файлов: 23612 добавлений и 12322 удалений

1
.gitignore поставляемый
Просмотреть файл

@ -23,3 +23,4 @@ yarn-debug.log*
yarn-error.log*
kendo-ui-license.txt
package-lock.json

33585
package-lock.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"
},

Просмотреть файл

@ -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">&times;</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">&times;</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">&times;</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">&times;</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;