Allow issue creation via UI
This commit is contained in:
@ -2,13 +2,36 @@ import React from 'react';
|
||||
import { Page } from '../Page';
|
||||
import { connect } from 'react-redux';
|
||||
import Board from '@lourenci/react-kanban';
|
||||
import { fetchIssues } from '../../store/actions/issues';
|
||||
import { fetchBoards } from '../../store/actions/boards';
|
||||
import { createIssue } from '../../store/actions/issues';
|
||||
import { buildKanboard, moveCard } from '../../store/actions/kanboards';
|
||||
import { Loader } from '../Loader';
|
||||
import { IssueCard } from './IssueCard';
|
||||
import { Modal } from '../Modal';
|
||||
|
||||
export class BoardPage extends React.Component {
|
||||
|
||||
state = {
|
||||
newCardModalActive: false,
|
||||
newCardLaneID: 0,
|
||||
newCard: {
|
||||
title: "",
|
||||
body: "",
|
||||
project: ""
|
||||
}
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.renderLaneHeader = this.renderLaneHeader.bind(this);
|
||||
this.onNewCardClick = this.onNewCardClick.bind(this);
|
||||
this.onNewCardCloseClick = this.onNewCardCloseClick.bind(this);
|
||||
this.onNewCardTitleChange = this.onNewCardAttrChange.bind(this, 'title');
|
||||
this.onNewCardBodyChange = this.onNewCardAttrChange.bind(this, 'body');
|
||||
this.onNewCardProjectChange = this.onNewCardAttrChange.bind(this, 'project');
|
||||
this.onNewCardSaveClick = this.onNewCardSaveClick.bind(this);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Page>
|
||||
@ -34,54 +57,99 @@ export class BoardPage extends React.Component {
|
||||
onCardDragEnd={this.onCardDragEnd.bind(this)}>
|
||||
{kanboard}
|
||||
</Board>
|
||||
{ this.renderNewCardModal() }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderCard(card) {
|
||||
renderNewCardModal() {
|
||||
const { newCardModalActive, newCardLaneID } = this.state;
|
||||
const { board } = this.props;
|
||||
if (!board) return null;
|
||||
return (
|
||||
<div className="kanboard-card">
|
||||
<div className="box">
|
||||
<div className="media">
|
||||
{
|
||||
card.issue.assignee ?
|
||||
<div class="media-left">
|
||||
<figure class="image is-64x64">
|
||||
<img src={card.issue.assignee.avatar_url} alt="Image" />
|
||||
<small>{`@${card.issue.assignee.login}`}</small>
|
||||
</figure>
|
||||
<Modal active={newCardModalActive}>
|
||||
<div className="new-card-modal">
|
||||
<article className="message">
|
||||
<div className="message-header">
|
||||
<p><span>"{board.lanes[newCardLaneID].title}" - Nouveau ticket</span></p>
|
||||
<button className="delete" aria-label="delete" onClick={this.onNewCardCloseClick}></button>
|
||||
</div>
|
||||
<div className="message-body">
|
||||
<div className="field">
|
||||
<div className="control">
|
||||
<input className="input is-medium" type="text"
|
||||
placeholder="Titre de votre ticket..."
|
||||
value={this.state.newCard.title}
|
||||
onChange={this.onNewCardTitleChange} />
|
||||
</div>
|
||||
</div>
|
||||
: null
|
||||
}
|
||||
<div className="media-content">
|
||||
<div className="content">
|
||||
<p>
|
||||
<strong>{`#${card.issue.number}`}</strong>
|
||||
{ card.issue.milestone ? <small>{`- ${card.issue.milestone.title}`}</small> : null }
|
||||
<br />
|
||||
<span className="is-size-6">{card.issue.title}</span>
|
||||
</p>
|
||||
<div className="level is-mobile">
|
||||
<div className="level-left"></div>
|
||||
<div className="level-right">
|
||||
<a className="level-item" target="_blank" href={card.issue.url.replace('/api/v1/repos', '')}>
|
||||
<span className="icon is-small has-text-info">
|
||||
<i className="fas fa-search" aria-hidden="true"></i>
|
||||
</span>
|
||||
</a>
|
||||
<div className="field">
|
||||
<div className="control">
|
||||
<textarea className="textarea"
|
||||
placeholder="Description du nouveau ticket..."
|
||||
value={this.state.newCard.body}
|
||||
onChange={this.onNewCardBodyChange}
|
||||
rows="10">
|
||||
</textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div className="field">
|
||||
<div className="control is-expanded">
|
||||
<div className="select is-fullwidth"
|
||||
value={this.state.newCard.project}
|
||||
onChange={this.onNewCardProjectChange}>
|
||||
<select>
|
||||
{
|
||||
board.projects.map((p, i) => {
|
||||
return <option key={`new-card-project-${i}`} value={p}>{p}</option>
|
||||
})
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="field is-grouped is-grouped-right">
|
||||
<p className="control">
|
||||
<a className="button is-light"
|
||||
onClick={this.onNewCardCloseClick}>
|
||||
Annuler
|
||||
</a>
|
||||
</p>
|
||||
<p className="control">
|
||||
<a className="button is-primary"
|
||||
onClick={this.onNewCardSaveClick}>
|
||||
Enregistrer
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
renderCard(card) {
|
||||
return <IssueCard card={card} />;
|
||||
}
|
||||
|
||||
renderLaneHeader(lane) {
|
||||
return (
|
||||
<h3 className="kanboard-lane-title is-size-3">{lane.title}</h3>
|
||||
<div className="kanboard-lane">
|
||||
<div className="level">
|
||||
<div className="level-left">
|
||||
<h3 className="level-item is-size-3">{lane.title}</h3>
|
||||
</div>
|
||||
<div className="level-right">
|
||||
<button className="button is-light level-item is-medium"
|
||||
onClick={this.onNewCardClick.bind(this, lane.id)}>
|
||||
<span className="icon">
|
||||
<i className="fas fa-plus" aria-hidden="true"></i>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -106,6 +174,48 @@ export class BoardPage extends React.Component {
|
||||
this.requestBuildKanboard();
|
||||
}
|
||||
|
||||
onNewCardClick(laneID) {
|
||||
const { board } = this.props;
|
||||
this.setState({
|
||||
newCardModalActive: true,
|
||||
newCardLaneID: laneID,
|
||||
newCard: {
|
||||
title: "",
|
||||
body: "",
|
||||
project: board.projects[0],
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onNewCardCloseClick() {
|
||||
this.setState({ newCardModalActive: false });
|
||||
}
|
||||
|
||||
onNewCardAttrChange(attrName, evt) {
|
||||
const value = evt.target.value;
|
||||
this.setState(state => {
|
||||
return {
|
||||
...state,
|
||||
newCard: {
|
||||
...state.newCard,
|
||||
[attrName]: value,
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
onNewCardSaveClick() {
|
||||
const { newCard, newCardLaneID } = this.state;
|
||||
const { board } = this.props;
|
||||
this.setState({ newCardModalActive: false });
|
||||
this.props.dispatch(createIssue(
|
||||
newCard.project,
|
||||
newCard.title,
|
||||
newCard.body,
|
||||
board.lanes[newCardLaneID].issueLabel
|
||||
));
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (prevProps.board !== this.props.board) this.requestBuildKanboard();
|
||||
}
|
||||
|
47
client/src/components/BoardPage/IssueCard.jsx
Normal file
47
client/src/components/BoardPage/IssueCard.jsx
Normal file
@ -0,0 +1,47 @@
|
||||
import React from 'react';
|
||||
|
||||
export class IssueCard extends React.PureComponent {
|
||||
render() {
|
||||
const { card } = this.props;
|
||||
return (
|
||||
<div className="kanboard-card">
|
||||
<div className="box">
|
||||
<div className="media">
|
||||
{
|
||||
card.issue.assignee ?
|
||||
<div className="media-left">
|
||||
<figure className="image is-64x64">
|
||||
<img src={card.issue.assignee.avatar_url} alt="Image" />
|
||||
</figure>
|
||||
<small>{`@${card.issue.assignee.login}`}</small>
|
||||
</div>
|
||||
: null
|
||||
}
|
||||
<div className="media-content">
|
||||
<div className="content">
|
||||
<p>
|
||||
<strong>{`#${card.issue.number}`}</strong>
|
||||
{ card.issue.milestone ? <small>{`- ${card.issue.milestone.title}`}</small> : null }
|
||||
<br />
|
||||
<span className="is-size-6">{card.issue.title}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="level is-mobile" style={{marginTop:'1rem'}}>
|
||||
<div className="level-left">
|
||||
<small className="level-item"><a href="#">{card.project}</a></small>
|
||||
</div>
|
||||
<div className="level-right">
|
||||
<a className="level-item" target="_blank" href={card.issue.url.replace('/api/v1/repos', '')}>
|
||||
<span className="icon is-small has-text-info">
|
||||
<i className="fas fa-search" aria-hidden="true"></i>
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user