Compare commits
51 Commits
master
...
feature/fi
Author | SHA1 | Date | |
---|---|---|---|
3324f71544 | |||
7ca6e63680 | |||
6ffb2915bf | |||
6bb8afd914 | |||
3eb96fc75e | |||
d6597270dd | |||
fbad143bed | |||
e3459d136e | |||
fe0e2667a0 | |||
0e93e7c52f | |||
832cca1c66 | |||
94bfb77d87 | |||
5a677d2491 | |||
fbb2b3f8da | |||
44182fd1cd | |||
4e9298f5b6 | |||
9dce43fd58 | |||
3fa2b5905a | |||
b456fe9f65 | |||
7309a2157b | |||
79a12e89f7 | |||
b4ce7c3777 | |||
647c5c0806 | |||
676df834f7 | |||
7df6e9ee01 | |||
38815389bc | |||
0074f318fd | |||
a7f0eabb97 | |||
477f221b24 | |||
b087e50292 | |||
33a0c7850a | |||
860ee438fc | |||
1dedda7d50 | |||
e20bf045cf | |||
6580d01370 | |||
da6a408634 | |||
10151db229 | |||
bdd5ce7ebe | |||
29f44f8c75 | |||
a7297e3d12 | |||
d510116c4b | |||
b9ebf29711 | |||
b29e48dc68 | |||
0524cfe4ca | |||
e5eb2e0a7e | |||
1f3f4bdeed | |||
d374999d88 | |||
39e2de3ff1 | |||
14d13c238a | |||
048ef49933 | |||
c6851f3f42 |
@ -1,4 +1,4 @@
|
||||
[*.js]
|
||||
[*.{js,jsx}]
|
||||
indent_size = 2
|
||||
indent_style = space
|
||||
|
||||
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -2,4 +2,5 @@
|
||||
/bin
|
||||
/data
|
||||
node_modules
|
||||
/client/dist
|
||||
/client/dist
|
||||
/release
|
12
Makefile
12
Makefile
@ -1,6 +1,9 @@
|
||||
build: vendor
|
||||
go build -mod=vendor -v -o bin/server ./cmd/server
|
||||
|
||||
client-dist:
|
||||
cd client && npm run build
|
||||
|
||||
test:
|
||||
go test -v -race ./...
|
||||
|
||||
@ -13,8 +16,11 @@ vendor:
|
||||
tidy:
|
||||
go mod tidy
|
||||
|
||||
watch:
|
||||
modd
|
||||
deps:
|
||||
go get github.com/cortesi/modd/cmd/modd
|
||||
|
||||
watch: deps
|
||||
go run github.com/cortesi/modd/cmd/modd
|
||||
|
||||
lint:
|
||||
golangci-lint run --enable-all
|
||||
@ -23,5 +29,7 @@ clean:
|
||||
rm -rf release
|
||||
rm -rf vendor
|
||||
rm -rf bin
|
||||
rm -rf client/node_modules
|
||||
rm -rf client/dist/*
|
||||
|
||||
.PHONY: lint watch build vendor tidy release
|
34
README.md
34
README.md
@ -1,4 +1,4 @@
|
||||
# Gitea Kan
|
||||
# GenGitKan
|
||||
|
||||
## Démarrer avec les sources
|
||||
|
||||
@ -9,9 +9,41 @@
|
||||
### Procédure
|
||||
|
||||
```bash
|
||||
cd client && npm install # Installation des dépendances client
|
||||
make watch # Surveiller les modifications sur le sources et compiler/démarrer le serveur
|
||||
```
|
||||
|
||||
Au premier lancement, le fichier de configuration doit être complété afin de d'utiliser l'instance Gitea ciblée comme fournisseur OAuth2.
|
||||
|
||||
Modifier le fichier `data/server.conf` de la manière suivante (la forge Cadoles est utilisée dans cet exemple):
|
||||
|
||||
```ini
|
||||
Debug = false
|
||||
|
||||
[HTTP]
|
||||
Address = :3000
|
||||
PublicDir = ${GENGITKAN_HTTP_PUBDIR}
|
||||
|
||||
[Gitea]
|
||||
BaseURL = https://forge.cadoles.com
|
||||
ClientID = <ClientID>
|
||||
ClientSecret = <ClientSecret>
|
||||
RedirectURL = http://localhost:3000/callback
|
||||
AuthURL = https://forge.cadoles.com/login/oauth/authorize
|
||||
LogoutURL = https://forge.cadoles.com/user/logout
|
||||
TokenURL = https://forge.cadoles.com/login/oauth/access_token
|
||||
APIBaseURL = https://forge.cadoles.com/api
|
||||
Scopes = api
|
||||
|
||||
[Data]
|
||||
|
||||
DBPath = ${GENGITKAN_DATA_DBPATH}
|
||||
```
|
||||
|
||||
Les valeurs pour `<ClientID>` et `<ClientSecret>` sont à récupérer sur la page https://forge.cadoles.com/user/settings/applications, dans la section `Gérer les applications OAuth2`.
|
||||
|
||||
Vous devrez créer une application et configurer l'URL de redirection pour qu'elle corresponde à la valeur `RedirectURL` de votre fichier de configuration afin de pouvoir utiliser votre instance locale de GenGitKan.
|
||||
|
||||
## Licence
|
||||
|
||||
[AGPL-3.0](./LICENSE)
|
11568
client/package-lock.json
generated
11568
client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "gitea-apps",
|
||||
"name": "gitea-kan",
|
||||
"version": "0.0.0",
|
||||
"description": "Gitea Apps",
|
||||
"description": "Gitea Kan",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
@ -19,27 +19,47 @@
|
||||
},
|
||||
"homepage": "https://forge.cadoles.com/wpetit/gitea-apps#readme",
|
||||
"devDependencies": {
|
||||
"@riotjs/compiler": "^4.5.2",
|
||||
"@riotjs/webpack-loader": "^4.0.1",
|
||||
"@babel/core": "^7.7.2",
|
||||
"@babel/plugin-proposal-class-properties": "^7.7.4",
|
||||
"@babel/plugin-transform-runtime": "^7.7.4",
|
||||
"@babel/preset-env": "^7.7.1",
|
||||
"@babel/preset-react": "^7.7.4",
|
||||
"@fortawesome/fontawesome-free": "^5.11.2",
|
||||
"@types/node": "^13.13.4",
|
||||
"@types/react-dom": "^16.9.7",
|
||||
"@types/react-redux": "^7.1.7",
|
||||
"@types/react-router-dom": "^5.1.5",
|
||||
"@types/uuid": "^7.0.3",
|
||||
"babel-loader": "^8.0.6",
|
||||
"css-loader": "^1.0.1",
|
||||
"extract-loader": "^3.1.0",
|
||||
"file-loader": "^2.0.0",
|
||||
"html-loader": "^0.5.5",
|
||||
"json-loader": "^0.5.7",
|
||||
"html-webpack-plugin": "^3.2.0",
|
||||
"mini-css-extract-plugin": "^0.4.4",
|
||||
"node-sass": "^4.10.0",
|
||||
"node-sass": "^4.14.0",
|
||||
"redux-logger": "^3.0.6",
|
||||
"resolve-url-loader": "^3.0.0",
|
||||
"sass-loader": "^7.1.0",
|
||||
"style-loader": "^0.23.1",
|
||||
"ts-loader": "^7.0.2",
|
||||
"webpack": "^4.25.0",
|
||||
"webpack-cleanup-plugin": "^0.5.1",
|
||||
"webpack-cli": "^3.1.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.7.2",
|
||||
"@babel/preset-env": "^7.7.1",
|
||||
"@riotjs/route": "^4.0.0",
|
||||
"babel-loader": "^8.0.6",
|
||||
"@lourenci/react-kanban": "^2.0.0",
|
||||
"bulma": "^0.7.2",
|
||||
"riot": "^4.6.6"
|
||||
"bulma-switch": "^2.0.0",
|
||||
"react": "^16.12.0",
|
||||
"react-dom": "^16.12.0",
|
||||
"react-redux": "^7.1.3",
|
||||
"react-router": "^5.1.2",
|
||||
"react-router-dom": "^5.1.2",
|
||||
"redux": "^4.0.4",
|
||||
"redux-saga": "^1.1.3",
|
||||
"styled-components": "^4.4.1",
|
||||
"typescript": "^3.8.3",
|
||||
"uuid": "^3.3.3"
|
||||
}
|
||||
}
|
||||
|
34
client/src/components/App.tsx
Normal file
34
client/src/components/App.tsx
Normal file
@ -0,0 +1,34 @@
|
||||
import React from 'react';
|
||||
import { HashRouter as Router, Route, Redirect, Switch } from "react-router-dom";
|
||||
import { ConnectedHomePage as HomePage } from './HomePage/HomePage';
|
||||
import { ConnectedBoardPage as BoardPage } from './BoardPage/BoardPage';
|
||||
import { ConnectedEditBoardPage as EditBoardPage } from './BoardPage/EditBoardPage';
|
||||
import { store } from '../store/store';
|
||||
import { Provider } from 'react-redux';
|
||||
import { logout } from '../store/actions/logout';
|
||||
|
||||
export class App extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<Provider store={store}>
|
||||
<Router>
|
||||
<Switch>
|
||||
<Route path="/" exact component={HomePage} />
|
||||
<Route path="/boards/new" exact component={EditBoardPage} />
|
||||
<Route path="/boards/:id" exact component={BoardPage} />
|
||||
<Route path="/boards/:id/edit" exact component={EditBoardPage} />
|
||||
<Route path="/logout" exact component={() => {
|
||||
this.logout();
|
||||
return <Redirect to="/" />;
|
||||
}} />
|
||||
<Route component={() => <Redirect to="/" />} />
|
||||
</Switch>
|
||||
</Router>
|
||||
</Provider>
|
||||
);
|
||||
}
|
||||
|
||||
logout() {
|
||||
store.dispatch(logout());
|
||||
}
|
||||
}
|
356
client/src/components/BoardPage/BoardPage.tsx
Normal file
356
client/src/components/BoardPage/BoardPage.tsx
Normal file
@ -0,0 +1,356 @@
|
||||
import React, { Fragment } from 'react';
|
||||
import { Page } from '../Page';
|
||||
import { connect, DispatchProp } from 'react-redux';
|
||||
import Board, { addColumn } from '@lourenci/react-kanban';
|
||||
import { fetchBoards } from '../../store/actions/boards';
|
||||
import { createIssue, fetchIssues } from '../../store/actions/issues';
|
||||
import { buildKanboard, moveCard } from '../../store/actions/kanboards';
|
||||
import { Loader } from '../Loader';
|
||||
import { IssueCard } from './IssueCard';
|
||||
import { Modal } from '../Modal';
|
||||
import { fetchProjectsMilestones } from '../../store/actions/projects';
|
||||
|
||||
export interface BoardPageProps extends DispatchProp {
|
||||
board: any
|
||||
kanboard: any
|
||||
milestones: any
|
||||
}
|
||||
|
||||
export class BoardPage extends React.Component<BoardPageProps> {
|
||||
|
||||
state = {
|
||||
newCardModalActive: false,
|
||||
newCardLaneID: 0,
|
||||
newCard: {
|
||||
title: "",
|
||||
body: "",
|
||||
project: ""
|
||||
},
|
||||
compactMode: true,
|
||||
hasError: false,
|
||||
selectedMilestone: "",
|
||||
}
|
||||
|
||||
onNewCardTitleChange: (evt: any) => void;
|
||||
onNewCardBodyChange: (evt: any) => void;
|
||||
onNewCardProjectChange: (evt: any) => void;
|
||||
|
||||
constructor(props: BoardPageProps) {
|
||||
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);
|
||||
}
|
||||
|
||||
componentDidCatch(error, errorInfo) {
|
||||
// You can also log the error to an error reporting service
|
||||
console.error(error, errorInfo);
|
||||
}
|
||||
|
||||
handleMilestonesChange(e: any) {
|
||||
let m = (e.target as HTMLInputElement).value;
|
||||
this.setState(state => ({ ...state, selectedMilestone: m }), this.requestBuildKanboard);
|
||||
//this.requestBuildKanboard();
|
||||
}
|
||||
|
||||
render() {
|
||||
const { board } = this.props;
|
||||
return (
|
||||
<Page title={`${board ? (board.title + ' - ') : ''}GenGitKan`}>
|
||||
{this.renderBoard()}
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
||||
renderBoard() {
|
||||
const { kanboard, board, milestones } = this.props;
|
||||
const { selectedMilestone } = this.state;
|
||||
|
||||
if (!kanboard) {
|
||||
return <Loader></Loader>
|
||||
}
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<nav className="navbar is-light">
|
||||
<div className="container is-fluid">
|
||||
<div className="navbar-start">
|
||||
<div className="navbar-item">
|
||||
{board.title}
|
||||
</div>
|
||||
</div>
|
||||
<div className="navbar-end">
|
||||
<div className="navbar-item">
|
||||
<div className="select">
|
||||
<select onChange={this.handleMilestonesChange.bind(this)}>
|
||||
<option value="">Sélectionner un jalon</option>
|
||||
{
|
||||
milestones.length > 0 ? milestones.map((k: any) => {
|
||||
return (
|
||||
<optgroup label={k.project}>
|
||||
{
|
||||
k.milestones.map((m: any) => {
|
||||
return (
|
||||
<option value={m.title} key={m.id}>{m.title}</option>
|
||||
)
|
||||
})
|
||||
}
|
||||
</optgroup>
|
||||
)
|
||||
}) : ""
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="navbar-item">
|
||||
|
||||
<div className="field">
|
||||
|
||||
<input id="compactMode"
|
||||
checked={this.state.compactMode}
|
||||
onChange={this.onCompactModeChange.bind(this)}
|
||||
type="checkbox"
|
||||
className="switch is-outlined is-success"
|
||||
name="compactMode"
|
||||
/>
|
||||
<label htmlFor="compactMode">Mode compact</label>
|
||||
</div>
|
||||
</div>
|
||||
<a href={`#/boards/${board.id}/edit`} className="navbar-item">
|
||||
<span className="icon">
|
||||
<i className="fa fa-edit fa-fw"></i>
|
||||
</span>
|
||||
<span>Modifier le tableau</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<div className="container is-fluid">
|
||||
<div className="kanboard-container is-fullheight">
|
||||
<Board
|
||||
renderCard={this.renderCard.bind(this)}
|
||||
renderColumnHeader={this.renderLaneHeader.bind(this)}
|
||||
onCardDragEnd={this.onCardDragEnd.bind(this)}
|
||||
disableColumnDrag={true}
|
||||
>
|
||||
{kanboard}
|
||||
</Board>
|
||||
{this.renderNewCardModal()}
|
||||
</div>
|
||||
</div>
|
||||
</Fragment>
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
renderNewCardModal() {
|
||||
const { newCardModalActive, newCardLaneID } = this.state;
|
||||
const { board } = this.props;
|
||||
if (!board || newCardLaneID === undefined) return null;
|
||||
|
||||
return (
|
||||
<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>
|
||||
<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">
|
||||
<select
|
||||
value={this.state.newCard.project}
|
||||
onChange={this.onNewCardProjectChange}>
|
||||
{
|
||||
board.projects.map((p: any, i: number) => {
|
||||
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>
|
||||
</article>
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
renderCard(card: any) {
|
||||
return <IssueCard compact={this.state.compactMode} card={card} />;
|
||||
}
|
||||
|
||||
renderLaneHeader(lane: any) {
|
||||
return (
|
||||
<div className="kanboard-lane">
|
||||
<div className="level">
|
||||
<div className="level-left">
|
||||
<div className="level-item">
|
||||
<span className="tag is-primary is-light is-normal">{lane.cards.length}</span>
|
||||
</div>
|
||||
<button className="button is-light level-item is-small expand"
|
||||
onClick={this.onMinimizeColumn}>
|
||||
<span className="icon">
|
||||
<i className="fas fa-chevron-right" aria-hidden="true"></i>
|
||||
</span>
|
||||
</button>
|
||||
<h3 className="level-item is-size-5">
|
||||
{lane.title}
|
||||
</h3>
|
||||
</div>
|
||||
<div className="level-right is-show-expand">
|
||||
<button className="button is-light level-item is-small"
|
||||
onClick={this.onNewCardClick.bind(this, lane.id)}>
|
||||
<span className="icon">
|
||||
<i className="fas fa-plus" aria-hidden="true"></i>
|
||||
</span>
|
||||
</button>
|
||||
<button className="button is-light level-item is-small"
|
||||
onClick={this.onMinimizeColumn}>
|
||||
<span className="icon">
|
||||
<i className="fas fa-chevron-left" aria-hidden="true"></i>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
onMinimizeColumn(e: any) {
|
||||
e.currentTarget.closest('.react-kanban-column').classList.toggle('minimized');
|
||||
}
|
||||
|
||||
onCardDragEnd(card: any, source: any, dest: any) {
|
||||
const { board } = this.props;
|
||||
this.props.dispatch(moveCard(
|
||||
board.id,
|
||||
source.fromColumnId,
|
||||
source.fromPosition,
|
||||
dest.toColumnId,
|
||||
dest.toPosition
|
||||
));
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { board } = this.props;
|
||||
if (!board) {
|
||||
this.requestBoardsUpdate();
|
||||
return
|
||||
}
|
||||
|
||||
this.requestBuildKanboard();
|
||||
}
|
||||
|
||||
onNewCardClick(laneID: string) {
|
||||
const { board } = this.props;
|
||||
this.setState({
|
||||
newCardModalActive: true,
|
||||
newCardLaneID: laneID,
|
||||
newCard: {
|
||||
title: "",
|
||||
body: "",
|
||||
project: board.projects[0],
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onNewCardCloseClick() {
|
||||
this.setState({ newCardModalActive: false });
|
||||
}
|
||||
|
||||
onNewCardAttrChange(attrName: string, evt: React.ChangeEvent) {
|
||||
const value = (evt.target as HTMLInputElement).value;
|
||||
this.setState((state: any) => {
|
||||
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: any) {
|
||||
if (prevProps.board !== this.props.board) this.requestBuildKanboard();
|
||||
}
|
||||
|
||||
requestBoardsUpdate() {
|
||||
this.props.dispatch(fetchBoards());
|
||||
}
|
||||
|
||||
requestBuildKanboard() {
|
||||
const { board } = this.props;
|
||||
const { selectedMilestone } = this.state;
|
||||
if (!board) return;
|
||||
this.props.dispatch(fetchProjectsMilestones(board.projects));
|
||||
this.props.dispatch(buildKanboard(board, selectedMilestone));
|
||||
}
|
||||
|
||||
onCompactModeChange(evt: React.ChangeEvent) {
|
||||
const checked = (evt.currentTarget as HTMLInputElement).checked;
|
||||
this.setState(state => ({ ...state, compactMode: checked }));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export const ConnectedBoardPage = connect(function (state: any, props: any) {
|
||||
const boardID = props.match.params.id;
|
||||
return {
|
||||
board: state.boards.byID[boardID],
|
||||
kanboard: state.kanboards.byID[boardID],
|
||||
milestones: state.projects.milestones
|
||||
};
|
||||
})(BoardPage);
|
448
client/src/components/BoardPage/EditBoardPage.tsx
Normal file
448
client/src/components/BoardPage/EditBoardPage.tsx
Normal file
@ -0,0 +1,448 @@
|
||||
import React from 'react';
|
||||
import { Page } from '../Page';
|
||||
import { connect, DispatchProp } from 'react-redux';
|
||||
import { selectFlagsIsLoading } from '../../store/selectors/flags';
|
||||
import { fetchBoards, saveBoard, deleteBoard } from '../../store/actions/boards';
|
||||
import { fetchProjects } from '../../store/actions/projects';
|
||||
import uuidv4 from 'uuid/v4';
|
||||
import { Loader } from '../Loader';
|
||||
import { RouteComponentProps } from 'react-router';
|
||||
|
||||
export interface EditorBoardPageProps extends DispatchProp, RouteComponentProps {
|
||||
isLoading: boolean
|
||||
projects: any
|
||||
}
|
||||
|
||||
export class EditBoardPage extends React.Component<EditorBoardPageProps> {
|
||||
|
||||
state = {
|
||||
edited: false,
|
||||
board: {
|
||||
id: "",
|
||||
title: "",
|
||||
description: "",
|
||||
projects: [],
|
||||
lanes: []
|
||||
},
|
||||
}
|
||||
|
||||
onBoardTitleChange: (evt: any) => void;
|
||||
onBoardDescriptionChange: (evt: any) => void;
|
||||
onBoardLaneTitleChange: (laneIndex: any, evt: any) => void;
|
||||
onBoardLaneIssueLabelChange: (laneIndex: any, evt: any) => void;
|
||||
onBoardLaneIssueCollectRemainingIssuesChange: (laneIndex: any, evt: any) => void;
|
||||
|
||||
static getDerivedStateFromProps(props: any, state: any) {
|
||||
const { board, isLoading } = props;
|
||||
|
||||
if (isLoading || !board || state.edited) return state;
|
||||
|
||||
return {
|
||||
edited: false,
|
||||
board: {
|
||||
id: board.id,
|
||||
title: board.title,
|
||||
description: board.description,
|
||||
projects: [ ...board.projects ],
|
||||
lanes: [ ...board.lanes.map((l: any) => ({ ...l })) ]
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
this.onBoardTitleChange = this.onBoardAttrChange.bind(this, 'title');
|
||||
this.onBoardDescriptionChange = this.onBoardAttrChange.bind(this, 'description');
|
||||
this.onBoardLaneTitleChange = this.onBoardLaneAttrChange.bind(this, 'title');
|
||||
this.onBoardLaneIssueLabelChange = this.onBoardLaneAttrChange.bind(this, 'issueLabel');
|
||||
this.onBoardLaneIssueCollectRemainingIssuesChange = this.onBoardLaneAttrChange.bind(this, 'collectRemainingIssues');
|
||||
this.onDeleteBoardClick = this.onDeleteBoardClick.bind(this);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { isLoading } = this.props;
|
||||
const { board } = this.state;
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Page>
|
||||
<Loader></Loader>
|
||||
</Page>
|
||||
)
|
||||
};
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<div className="container is-fluid has-margin-top-normal">
|
||||
<div className="columns">
|
||||
<div className="column is-6 is-offset-3">
|
||||
<div className="level is-mobile">
|
||||
<div className="level-left">
|
||||
{
|
||||
board.id ?
|
||||
<h3 className="is-size-3 level-item">Éditer le tableau</h3> :
|
||||
<h3 className="is-size-3 level-item">Nouveau tableau</h3>
|
||||
}
|
||||
</div>
|
||||
<div className="level-right">
|
||||
{
|
||||
board.id ?
|
||||
<button onClick={this.onDeleteBoardClick} className="level-item button is-danger">
|
||||
<span className="icon">
|
||||
<i className="fas fa-trash"></i>
|
||||
</span>
|
||||
<span>Supprimer</span>
|
||||
</button> :
|
||||
null
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div className="field">
|
||||
<label className="label">Titre</label>
|
||||
<div className="control">
|
||||
<input className="input" type="text"
|
||||
value={board.title}
|
||||
onChange={this.onBoardTitleChange} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="field">
|
||||
<label className="label">Description</label>
|
||||
<div className="control">
|
||||
<textarea className="textarea"
|
||||
value={board.description}
|
||||
onChange={this.onBoardDescriptionChange}>
|
||||
</textarea>
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
{ this.renderProjectSelect() }
|
||||
<hr />
|
||||
{ this.renderLanesSection() }
|
||||
<div className="field is-grouped is-grouped-right">
|
||||
<p className="control">
|
||||
<a className="button is-light is-normal" href="#/">
|
||||
Annuler
|
||||
</a>
|
||||
</p>
|
||||
<p className="control">
|
||||
<a className="button is-success is-normal"
|
||||
onClick={this.onSaveBoardClick.bind(this)}>
|
||||
Enregistrer
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
||||
renderProjectSelect() {
|
||||
const { projects } = this.props;
|
||||
const { board } = this.state;
|
||||
|
||||
const projectSelectField = (projectIndex: number, value: any, withDeleteAddon: boolean) => {
|
||||
return (
|
||||
<div key={`project-${projectIndex}`} className="field has-addons">
|
||||
<div className="control is-expanded">
|
||||
<div className="select is-fullwidth">
|
||||
<select value={value} onChange={this.onBoardProjectChange.bind(this, projectIndex)}>
|
||||
<option value=""></option>
|
||||
{
|
||||
projects.map((p: any) => {
|
||||
return <option key={`project-${p}`} value={p}>{p}</option>;
|
||||
})
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
withDeleteAddon ?
|
||||
<div className="control">
|
||||
<button type="submit" className="button is-danger"
|
||||
onClick={this.onBoardProjectDelete.bind(this, projectIndex)}>
|
||||
<span className="icon">
|
||||
<i className="fas fa-trash"></i>
|
||||
</span>
|
||||
</button>
|
||||
</div> :
|
||||
null
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<label className="label">Projets</label>
|
||||
{
|
||||
board.projects.map((p, i) => {
|
||||
return projectSelectField(i, p, true);
|
||||
})
|
||||
}
|
||||
{ projectSelectField(board.projects.length, '', false) }
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
renderLanesSection() {
|
||||
|
||||
const { board } = this.state;
|
||||
|
||||
const laneSection = (laneIndex: number, lane: any) => {
|
||||
return (
|
||||
<React.Fragment key={`board-lane-${laneIndex}`}>
|
||||
<div className="columns">
|
||||
<div className="column is-10">
|
||||
<div className="field is-horizontal">
|
||||
<div className="field-label is-normal">
|
||||
<label className="label">Titre</label>
|
||||
</div>
|
||||
<div className="field-body">
|
||||
<div className="field">
|
||||
<div className="control">
|
||||
<input className="input" type="text"
|
||||
value={lane.title}
|
||||
onChange={this.onBoardLaneTitleChange.bind(this, laneIndex)} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="field is-horizontal">
|
||||
<div className="field-label is-normal">
|
||||
<label className="label">Label associé</label>
|
||||
</div>
|
||||
<div className="field-body">
|
||||
<div className="field">
|
||||
<div className="control">
|
||||
<input className="input" type="text"
|
||||
value={lane.issueLabel}
|
||||
onChange={this.onBoardLaneIssueLabelChange.bind(this, laneIndex)} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="field is-horizontal">
|
||||
<div className="field-label is-normal"></div>
|
||||
<div className="field-body">
|
||||
<div className="field">
|
||||
<div className="control">
|
||||
<label className="checkbox">
|
||||
<input type="checkbox"
|
||||
checked={lane.hasOwnProperty('collectRemainingIssues') ? !!lane.collectRemainingIssues : false}
|
||||
onChange={this.onBoardLaneIssueCollectRemainingIssuesChange.bind(this, laneIndex)} />
|
||||
Inclure tous les tickets "restants" (i.e. non sélectionnés par les autres voies)
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="column is-2 is-flex">
|
||||
<div className="buttons">
|
||||
<button className="button is-danger is-small"
|
||||
onClick={this.onBoardLaneDelete.bind(this, laneIndex)}>
|
||||
<span className="icon">
|
||||
<i className="fas fa-trash"></i>
|
||||
</span>
|
||||
</button>
|
||||
<button className="button is-primary is-small"
|
||||
onClick={this.onBoardLaneMove.bind(this, laneIndex, -1)}>
|
||||
<span className="icon">
|
||||
<i className="fas fa-chevron-up"></i>
|
||||
</span>
|
||||
</button>
|
||||
<button className="button is-primary is-small"
|
||||
onClick={this.onBoardLaneMove.bind(this, laneIndex, 1)}>
|
||||
<span className="icon">
|
||||
<i className="fas fa-chevron-down"></i>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
const lanes = board.lanes.map((l, i) => laneSection(i, l))
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div className="level">
|
||||
<div className="level-left">
|
||||
<label className="label level-item">Voies</label>
|
||||
</div>
|
||||
<div className="level-right">
|
||||
<div className="field is-grouped is-grouped-right level-item">
|
||||
<p className="control">
|
||||
<a className="button is-primary is-small"
|
||||
onClick={this.onBoardLaneAdd.bind(this)}>
|
||||
Ajouter une voie
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{ lanes }
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
onBoardLaneAdd() {
|
||||
this.setState((state: any) => {
|
||||
const lanes = [
|
||||
...state.board.lanes,
|
||||
{ id: uuidv4(), title: "", issueLabel: "" }
|
||||
];
|
||||
return {
|
||||
...state,
|
||||
edited: true,
|
||||
board: {
|
||||
...state.board,
|
||||
lanes
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onBoardProjectDelete(projectIndex: number) {
|
||||
this.setState((state: any) => {
|
||||
const projects = [ ...state.board.projects ]
|
||||
projects.splice(projectIndex, 1);
|
||||
return {
|
||||
...state,
|
||||
edited: true,
|
||||
board: {
|
||||
...state.board,
|
||||
projects
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onBoardLaneMove(laneIndex: number, direction: number) {
|
||||
this.setState((state: any) => {
|
||||
const lanes = [ ...state.board.lanes ];
|
||||
|
||||
const nextLaneIndex = laneIndex+direction;
|
||||
if (nextLaneIndex < 0 || nextLaneIndex >= lanes.length) {
|
||||
return state;
|
||||
}
|
||||
|
||||
const l = lanes[laneIndex];
|
||||
lanes.splice(laneIndex, 1);
|
||||
lanes.splice(nextLaneIndex, 0, l);
|
||||
|
||||
return {
|
||||
...state,
|
||||
edited: true,
|
||||
board: {
|
||||
...state.board,
|
||||
lanes
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onBoardLaneDelete(laneIndex: number) {
|
||||
this.setState((state: any) => {
|
||||
const lanes = [ ...state.board.lanes ]
|
||||
lanes.splice(laneIndex, 1);
|
||||
return {
|
||||
...state,
|
||||
edited: true,
|
||||
board: {
|
||||
...state.board,
|
||||
lanes
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onBoardProjectChange(projectIndex: number, evt: React.ChangeEvent) {
|
||||
const value = (evt.target as HTMLInputElement).value ;
|
||||
this.setState((state: any) => {
|
||||
const projects = [ ...state.board.projects ];
|
||||
projects[projectIndex] = value;
|
||||
return {
|
||||
...state,
|
||||
edited: true,
|
||||
board: {
|
||||
...state.board,
|
||||
projects
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onBoardAttrChange(attrName: string, evt: React.ChangeEvent) {
|
||||
const value = (evt.target as HTMLInputElement).value;
|
||||
this.setState((state: any) => {
|
||||
return {
|
||||
...state,
|
||||
edited: true,
|
||||
board: {
|
||||
...state.board,
|
||||
[attrName]: value,
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onBoardLaneAttrChange(attrName: string, laneIndex: number, evt: React.ChangeEvent) {
|
||||
const input = evt.target as HTMLInputElement;
|
||||
const value = input.type === "checkbox" ? input.checked : input.value;
|
||||
this.setState((state: any) => {
|
||||
const lanes = [ ...state.board.lanes ];
|
||||
lanes[laneIndex] = {
|
||||
...state.board.lanes[laneIndex],
|
||||
[attrName]: value
|
||||
};
|
||||
console.log(lanes);
|
||||
return {
|
||||
...state,
|
||||
edited: true,
|
||||
board: {
|
||||
...state.board,
|
||||
lanes
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onSaveBoardClick() {
|
||||
let { board } = this.state;
|
||||
board = { ...board };
|
||||
if (!board.id) board.id = uuidv4();
|
||||
this.props.dispatch(saveBoard({...board}));
|
||||
this.props.history.push('/');
|
||||
}
|
||||
|
||||
onDeleteBoardClick() {
|
||||
const { board } = this.state;
|
||||
this.props.dispatch(deleteBoard(board.id));
|
||||
this.props.history.push('/');
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.props.dispatch(fetchBoards());
|
||||
this.props.dispatch(fetchProjects());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export const ConnectedEditBoardPage = connect(function(state: any, props: any) {
|
||||
const boardID = props.match.params.id;
|
||||
const board = boardID ? state.boards.byID[boardID] : null;
|
||||
|
||||
const projects = Object.keys(state.projects.byName).sort();
|
||||
|
||||
const isLoading = selectFlagsIsLoading(state, 'FETCH_BOARDS', 'FETCH_PROJECTS');
|
||||
|
||||
return { board, isLoading, projects };
|
||||
})(EditBoardPage);
|
80
client/src/components/BoardPage/IssueCard.tsx
Normal file
80
client/src/components/BoardPage/IssueCard.tsx
Normal file
@ -0,0 +1,80 @@
|
||||
import React from 'react';
|
||||
import { KanboardCard } from '../../types/kanboard';
|
||||
|
||||
export interface IssueCardProps {
|
||||
card: KanboardCard
|
||||
compact: boolean
|
||||
}
|
||||
|
||||
export class IssueCard extends React.PureComponent<IssueCardProps> {
|
||||
render() {
|
||||
|
||||
const { card, compact } = this.props;
|
||||
const issueURLInfo = extractInfoFromIssueURL(card.issue.url);
|
||||
const projectURL = `${issueURLInfo.baseURL}/${issueURLInfo.owner}/${issueURLInfo.projectName}`;
|
||||
const issueURL = `${projectURL}/issues/${card.issue.number}`;
|
||||
|
||||
return (
|
||||
<div className="kanboard-card">
|
||||
<div className="box has-padding-small is-radiusless">
|
||||
<div className="media">
|
||||
<div className="media-content">
|
||||
<div className="content">
|
||||
{ !compact &&
|
||||
<nav className="level">
|
||||
<div className="level-left">
|
||||
<div className="level-item">
|
||||
<a target="_blank" href={issueURL}><strong>{`#${card.issue.number}`}</strong></a>
|
||||
</div>
|
||||
{ !compact &&
|
||||
<div className="level-item">
|
||||
<a target="_blank" href={projectURL}>{card.project}</a>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div className="level-right">
|
||||
{
|
||||
card.issue.assignee && !compact ?
|
||||
<div className="level-item">
|
||||
<small>{`@${card.issue.assignee.login}`}</small>
|
||||
</div>
|
||||
: null
|
||||
}
|
||||
</div>
|
||||
</nav>
|
||||
}
|
||||
{ compact &&
|
||||
<a target="_blank" className="mr-1" href={issueURL}><strong>{`#${card.issue.number}`}</strong></a>
|
||||
}
|
||||
<span>{card.issue.title ? card.issue.title : ''}</span>
|
||||
</div>
|
||||
{ !compact &&
|
||||
<nav className="level">
|
||||
<div className="level-left"></div>
|
||||
<div className="level-right">
|
||||
<div className="level-item is-size-7">
|
||||
{card.issue.milestone ? card.issue.milestone.title : ''}
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function extractInfoFromIssueURL(issueURL: string): any|void {
|
||||
const pattern = /^(https?:\/\/[^\/]+)\/api\/v1\/repos\/([^\/]+)\/([^\/]+)\/.*$/;
|
||||
const matches = pattern.exec(issueURL);
|
||||
|
||||
if (!matches) return;
|
||||
|
||||
return {
|
||||
baseURL: matches[1],
|
||||
owner: matches[2],
|
||||
projectName: matches[3],
|
||||
};
|
||||
}
|
39
client/src/components/HomePage/BoardCard.tsx
Normal file
39
client/src/components/HomePage/BoardCard.tsx
Normal file
@ -0,0 +1,39 @@
|
||||
import React from 'react';
|
||||
|
||||
export interface BoardProps {
|
||||
board: any
|
||||
}
|
||||
|
||||
export class BoardCard extends React.PureComponent<BoardProps> {
|
||||
render() {
|
||||
const { board } = this.props;
|
||||
return (
|
||||
<div className="box">
|
||||
<div className="media">
|
||||
<div className="media-content">
|
||||
<div className="content">
|
||||
<p>
|
||||
<a href={`#/boards/${board.id}`}>
|
||||
<strong className="is-size-4">{board.title}</strong>
|
||||
</a>
|
||||
<br />
|
||||
{board.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="level is-mobile">
|
||||
<div className="level-left"></div>
|
||||
<div className="level-right">
|
||||
<a className="level-item" aria-label="edit" href={`#/boards/${board.id}/edit`}>
|
||||
<span className="icon is-small">
|
||||
<i className="fas fa-edit" aria-hidden="true"></i>
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
84
client/src/components/HomePage/HomePage.tsx
Normal file
84
client/src/components/HomePage/HomePage.tsx
Normal file
@ -0,0 +1,84 @@
|
||||
import React from 'react';
|
||||
import { Page } from '../Page';
|
||||
import { BoardCard } from './BoardCard';
|
||||
import { connect, DispatchProp } from 'react-redux';
|
||||
import { fetchBoards } from '../../store/actions/boards';
|
||||
import { fetchProjects } from '../../store/actions/projects';
|
||||
import { selectBoardByUserProjects } from '../../store/selectors/boards';
|
||||
|
||||
export interface HomePageProps extends DispatchProp {
|
||||
boards: any[]
|
||||
}
|
||||
|
||||
|
||||
export class HomePage extends React.Component<HomePageProps> {
|
||||
render() {
|
||||
return (
|
||||
<Page title="GenGitKan - Accueil">
|
||||
<div className="container is-fluid">
|
||||
<div className="level has-margin-top-normal">
|
||||
<div className="level-left"></div>
|
||||
<div className="level-right">
|
||||
<div className="buttons">
|
||||
<a className="button is-primary" href="#/boards/new">
|
||||
<span className="icon">
|
||||
<i className="fas fa-plus"></i>
|
||||
</span>
|
||||
<span>Nouveau tableau</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{ this.renderBoards() }
|
||||
</div>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
||||
renderBoards() {
|
||||
const { boards } = this.props;
|
||||
const rows = Object.values(boards)
|
||||
.reduce((boardRows, board, index) => {
|
||||
if (index % 3 === 0) {
|
||||
boardRows.push([]);
|
||||
}
|
||||
boardRows[boardRows.length-1].push(board);
|
||||
return boardRows;
|
||||
}, [])
|
||||
.map((row: any, rowIndex: number) => {
|
||||
const tiles = row.map((board: any) => {
|
||||
return (
|
||||
<div key={`board-${board.id}`} className={`tile is-parent is-4`}>
|
||||
<div className="tile is-child">
|
||||
<BoardCard board={board} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
return (
|
||||
<div key={`boards-row-${rowIndex}`} className="tile">
|
||||
{tiles}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
;
|
||||
|
||||
return (
|
||||
<div className="tile is-ancestor is-vertical">
|
||||
{ rows }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.props.dispatch(fetchBoards());
|
||||
this.props.dispatch(fetchProjects());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export const ConnectedHomePage = connect(function(state: any) {
|
||||
return {
|
||||
boards: selectBoardByUserProjects(state.boards.byID, state.projects.byName)
|
||||
};
|
||||
})(HomePage);
|
14
client/src/components/Loader.tsx
Normal file
14
client/src/components/Loader.tsx
Normal file
@ -0,0 +1,14 @@
|
||||
import React from 'react';
|
||||
|
||||
export class Loader extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<div className="loader-container">
|
||||
<div className="lds-ripple">
|
||||
<div></div>
|
||||
<div></div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
26
client/src/components/Modal.tsx
Normal file
26
client/src/components/Modal.tsx
Normal file
@ -0,0 +1,26 @@
|
||||
import React, { PropsWithChildren } from 'react';
|
||||
|
||||
export interface ModalProps {
|
||||
active: boolean
|
||||
showCloseButton?: boolean
|
||||
onClose?: (evt: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void
|
||||
}
|
||||
|
||||
export class Modal extends React.PureComponent<PropsWithChildren<ModalProps>> {
|
||||
render() {
|
||||
const { children, active, showCloseButton, onClose } = this.props;
|
||||
return (
|
||||
<div className={`modal ${active ? 'is-active': ''}`}>
|
||||
<div className="modal-background"></div>
|
||||
<div className="modal-content">
|
||||
{children}
|
||||
</div>
|
||||
{
|
||||
showCloseButton ?
|
||||
<button onClick={onClose} className="modal-close is-large" aria-label="close"></button> :
|
||||
null
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
36
client/src/components/Navbar.tsx
Normal file
36
client/src/components/Navbar.tsx
Normal file
@ -0,0 +1,36 @@
|
||||
import React from 'react';
|
||||
import logo from '../resources/logo.svg';
|
||||
|
||||
export class Navbar extends React.PureComponent {
|
||||
render() {
|
||||
return (
|
||||
<nav className="navbar" role="navigation" aria-label="main navigation">
|
||||
<div className="container is-fluid">
|
||||
<div className="navbar-brand">
|
||||
<a className="navbar-item" href="#/">
|
||||
<img src={logo} style={{marginRight:'5px',width:'28px',height:'28px'}} />
|
||||
<h1 className="is-size-4">GenGitKan</h1>
|
||||
</a>
|
||||
<a role="button" className="navbar-burger" aria-label="menu" aria-expanded="false">
|
||||
<span aria-hidden="true"></span>
|
||||
<span aria-hidden="true"></span>
|
||||
<span aria-hidden="true"></span>
|
||||
</a>
|
||||
</div>
|
||||
<div className="navbar-menu">
|
||||
<div className="navbar-end">
|
||||
<div className="navbar-item">
|
||||
<a className="button is-small" href="#/logout">
|
||||
<span className="icon">
|
||||
<i className="fas fa-sign-out-alt"></i>
|
||||
</span>
|
||||
<span>Se déconnecter</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
}
|
30
client/src/components/Page.tsx
Normal file
30
client/src/components/Page.tsx
Normal file
@ -0,0 +1,30 @@
|
||||
import React from 'react';
|
||||
import { Navbar } from './Navbar';
|
||||
|
||||
export interface PageProps {
|
||||
title?: string
|
||||
}
|
||||
|
||||
export class Page extends React.PureComponent<PageProps> {
|
||||
render() {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Navbar />
|
||||
{this.props.children}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.updateTitle();
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
this.updateTitle();
|
||||
}
|
||||
|
||||
updateTitle() {
|
||||
const { title } = this.props;
|
||||
if (title !== undefined) window.document.title = title;
|
||||
}
|
||||
}
|
@ -1,32 +0,0 @@
|
||||
<ga-app>
|
||||
<!-- DOM -->
|
||||
<riot-router>
|
||||
<riot-route path="/">
|
||||
<ga-app />
|
||||
</riot-route>
|
||||
<riot-route path="/logout">
|
||||
<ga-logout />
|
||||
</riot-route>
|
||||
<riot-route path="(.*)">
|
||||
<ga-redirect path="/" />
|
||||
</riot-route>
|
||||
</riot-router>
|
||||
|
||||
<!-- Script -->
|
||||
<script>
|
||||
import { Router, Route, router } from '@riotjs/route';
|
||||
import Redirect from './redirect.riot';
|
||||
import Logout from './logout.riot';
|
||||
import Ticketing from './apps/ticketing/app.riot';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
RiotRoute: Route,
|
||||
RiotRouter: Router,
|
||||
GaRedirect: Redirect,
|
||||
GaLogout: Logout,
|
||||
GaApp: Ticketing,
|
||||
}
|
||||
};
|
||||
</script>
|
||||
</ga-app>
|
@ -1,23 +0,0 @@
|
||||
<ga-ticketing>
|
||||
|
||||
<style>
|
||||
:host { display: block; }
|
||||
</style>
|
||||
|
||||
<div class="container">
|
||||
<ga-navbar></ga-navbar>
|
||||
<div class="columns"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
import Navbar from '../../navbar.riot';
|
||||
export default {
|
||||
|
||||
components: {
|
||||
GaNavbar: Navbar,
|
||||
},
|
||||
|
||||
|
||||
};
|
||||
</script>
|
||||
</ga-ticketing>
|
@ -1,12 +0,0 @@
|
||||
<ga-logout>
|
||||
<style>
|
||||
:host { display: none; }
|
||||
</style>
|
||||
<script>
|
||||
export default {
|
||||
onMounted() {
|
||||
window.location = "/logout";
|
||||
}
|
||||
};
|
||||
</script>
|
||||
</ga-logout>
|
@ -1,20 +0,0 @@
|
||||
<ga-navbar>
|
||||
<style>
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
<div class="level">
|
||||
<div class="level-left"></div>
|
||||
<div class="level-right">
|
||||
<a class="button is-warning level-item" href="/logout">
|
||||
Se déconnecter
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
export default {
|
||||
|
||||
}
|
||||
</script>
|
||||
</ga-navbar>
|
@ -1,14 +0,0 @@
|
||||
<ga-redirect>
|
||||
<style>
|
||||
:host { display: none; }
|
||||
</style>
|
||||
<script>
|
||||
import { router } from '@riotjs/route';
|
||||
export default {
|
||||
onMounted() {
|
||||
console.log(`redirect to ${this.props.path}`);
|
||||
router.push(this.props.path);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
</ga-redirect>
|
5
client/src/custom.d.ts
vendored
Normal file
5
client/src/custom.d.ts
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
declare module "*.svg" {
|
||||
const content: any;
|
||||
export default content;
|
||||
}
|
||||
declare module '@lourenci/react-kanban';
|
@ -3,11 +3,18 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
<title>Gitea Kan</title>
|
||||
<link rel="stylesheet" href="css/main.css">
|
||||
<title>GenGitKan</title>
|
||||
<% for (var css in htmlWebpackPlugin.files.css) { %>
|
||||
<link href="<%= htmlWebpackPlugin.files.css[css] %>" rel="stylesheet">
|
||||
<% } %>
|
||||
<% if (htmlWebpackPlugin.files.favicon) { %>
|
||||
<link rel="shortcut icon" href="<%= htmlWebpackPlugin.files.favicon%>">
|
||||
<% } %>
|
||||
</head>
|
||||
<body>
|
||||
<div id="gitea-kan" class="is-fullheight"></div>
|
||||
<script type="text/javascript" src="main.js"></script>
|
||||
<div id="app" class="is-fullheight"></div>
|
||||
<% for (var chunk in htmlWebpackPlugin.files.chunks) { %>
|
||||
<script type="text/javascript" src="<%= htmlWebpackPlugin.files.chunks[chunk].entry %>"></script>
|
||||
<% } %>
|
||||
</body>
|
||||
</html>
|
@ -1,11 +0,0 @@
|
||||
import { setBase } from '@riotjs/route';
|
||||
|
||||
// const loc = window.location;
|
||||
// setBase(`${loc.protocol}//${loc.host}#/`);
|
||||
|
||||
import { component } from 'riot';
|
||||
import AppLoader from './components/app-loader.riot';
|
||||
import './sass/_all.scss';
|
||||
import './index.html';
|
||||
|
||||
component(AppLoader)(document.getElementById('gitea-kan'));
|
15
client/src/index.tsx
Normal file
15
client/src/index.tsx
Normal file
@ -0,0 +1,15 @@
|
||||
import './sass/_all.scss';
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { App } from './components/App';
|
||||
|
||||
import '@fortawesome/fontawesome-free/js/fontawesome'
|
||||
import '@fortawesome/fontawesome-free/js/solid'
|
||||
import '@fortawesome/fontawesome-free/js/regular'
|
||||
import '@fortawesome/fontawesome-free/js/brands'
|
||||
import './resources/favicon.png';
|
||||
|
||||
ReactDOM.render(
|
||||
<App />,
|
||||
document.getElementById('app')
|
||||
);
|
BIN
client/src/resources/favicon.png
Normal file
BIN
client/src/resources/favicon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 887 B |
1
client/src/resources/logo.svg
Normal file
1
client/src/resources/logo.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" style="height: 512px; width: 512px;"><g class="" transform="translate(0,0)" style="touch-action: none;"><path d="M69.313 18.75c-1.574 2.353-3.376 4.513-4.813 7C29.19 86.9 50.184 165.194 111.344 200.5c.822.475 1.64.92 2.47 1.375 6.942-20.223 16.297-38.746 27.592-54.844-46.972-27.407-73.09-77.42-72.094-128.28zm366.218 0c1 51.074-25.34 101.293-72.686 128.625-.06.035-.128.06-.188.094 11.25 16.126 20.54 34.673 27.438 54.905 1.143-.615 2.275-1.222 3.406-1.875 61.16-35.306 82.154-113.6 46.844-174.75-1.437-2.487-3.24-4.647-4.813-7zm-183.655 83.063c-49.747 0-94.34 38.94-117.188 97.687 34.868-8.738 76.5-12.77 118.282-12.78 40.775-.013 81.443 3.814 115.843 12.124-22.932-58.378-67.38-97.03-116.938-97.03zm1.094 103.593c-61.1.017-122.17 10.173-156.44 27.875v59.69c38.836-8.845 89.384-13.424 140.626-14.158L219.28 395h59.97l-17.875-116.22c55.228.506 109.26 5.38 148.25 14.158V233.28c-34.38-17.77-95.545-27.89-156.656-27.874zm-61.064 94.78c-11.582.002-20.094 8.333-20.094 18.002 0 9.668 8.512 18 20.094 18 11.583 0 20.125-8.332 20.125-18 0-9.67-8.54-18-20.124-18zm114.688.002c-11.583 0-20.094 8.33-20.094 18 0 9.668 8.51 18 20.094 18 11.582 0 20.125-8.332 20.125-18 0-9.67-8.544-18-20.126-18zm44.625 2.625L300.06 493.938l81.844-21.25V306.75c-9.6-1.504-19.885-2.81-30.687-3.938zm-203.25.593c-10.817 1.254-21.174 2.733-30.845 4.438v164.844l81.844 21.25-51-190.532z" fill="#4a4a4a" fill-opacity="1"></path></g></svg>
|
After Width: | Height: | Size: 1.4 KiB |
@ -1 +1,6 @@
|
||||
@import 'bulma/bulma.sass';
|
||||
@import 'bulma/bulma.sass';
|
||||
@import '../../node_modules/@lourenci/react-kanban/dist/styles.css';
|
||||
@import 'bulma-switch/dist/css/bulma-switch.sass';
|
||||
@import '_base.scss';
|
||||
@import '_loader.scss';
|
||||
@import '_kanboard.scss';
|
||||
|
25
client/src/sass/_base.scss
Normal file
25
client/src/sass/_base.scss
Normal file
@ -0,0 +1,25 @@
|
||||
html, body {
|
||||
height: 100%;
|
||||
background-color: #f7f7f7;
|
||||
}
|
||||
|
||||
.is-fullheight {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.has-margin-top-normal {
|
||||
margin-top: $size-normal;
|
||||
}
|
||||
|
||||
.has-padding-small {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
#app {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.mr-1 {
|
||||
margin-right: 5px;
|
||||
}
|
198
client/src/sass/_kanboard.scss
Normal file
198
client/src/sass/_kanboard.scss
Normal file
@ -0,0 +1,198 @@
|
||||
|
||||
.kanboard-container {
|
||||
& > div {
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
|
||||
& > div {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: stretch;
|
||||
justify-content: stretch;
|
||||
|
||||
// Lanes
|
||||
& > div {
|
||||
&:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
flex-grow: 1;
|
||||
flex-basis: 100%;
|
||||
background-color: transparent;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
bottom: 0;
|
||||
height: 100% !important;
|
||||
min-height: 100% !important;
|
||||
|
||||
// Card container
|
||||
& > div > div > div {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[data-react-beautiful-dnd-droppable] {
|
||||
min-height: calc(100vh) !important;
|
||||
}
|
||||
|
||||
.kanboard-card {
|
||||
margin-bottom: $size-small;
|
||||
}
|
||||
|
||||
.kanboard-lane {
|
||||
margin-bottom: $size-small;
|
||||
background: $white;
|
||||
top: 0;
|
||||
|
||||
.expand {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 50% !important;
|
||||
}
|
||||
|
||||
.react-kanban-board {
|
||||
max-height: calc(100vh - calc(52px * 2));
|
||||
overflow: hidden;
|
||||
overflow-x: scroll;
|
||||
|
||||
scrollbar-color: $grey-lighter, #f1f1f1;
|
||||
scrollbar-width: 5px;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
}
|
||||
|
||||
/* Track */
|
||||
&::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
}
|
||||
|
||||
/* Handle */
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: $grey-lighter;
|
||||
}
|
||||
|
||||
/* Handle on hover */
|
||||
&::-webkit-scrollbar-thumb:hover {
|
||||
background: $green;
|
||||
}
|
||||
}
|
||||
|
||||
.react-kanban-column {
|
||||
transition: width ease .2s;
|
||||
max-width: 305px;
|
||||
min-width: 305px;
|
||||
position: relative;
|
||||
|
||||
max-height: 100%;
|
||||
overflow-x: hidden;
|
||||
overflow-y: scroll;
|
||||
|
||||
scrollbar-color: $grey-lighter, #f1f1f1;
|
||||
scrollbar-width: 5px;
|
||||
|
||||
.kanboard-card {
|
||||
display: block;
|
||||
}
|
||||
|
||||
&.minimized {
|
||||
max-width: 70px;
|
||||
min-width: 70px;
|
||||
|
||||
writing-mode: vertical-rl;
|
||||
text-orientation: sideways-right;
|
||||
|
||||
|
||||
|
||||
.level-item {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.level-left {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.level-right.is-show-expand {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.kanboard-lane {
|
||||
/*margin-right: -1em;*/
|
||||
|
||||
h3 {
|
||||
margin-top: .5em;
|
||||
/*margin-right: -.5em;*/
|
||||
}
|
||||
|
||||
.tag {
|
||||
writing-mode: horizontal-tb;
|
||||
}
|
||||
|
||||
.expand {
|
||||
display: block !important;
|
||||
margin-top: .5em;
|
||||
}
|
||||
}
|
||||
|
||||
.kanboard-card {
|
||||
display: none;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 5px;
|
||||
}
|
||||
|
||||
/* Track */
|
||||
&::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
}
|
||||
|
||||
/* Handle */
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: $grey-lighter;
|
||||
}
|
||||
|
||||
/* Handle on hover */
|
||||
&::-webkit-scrollbar-thumb:hover {
|
||||
background: $green;
|
||||
}
|
||||
}
|
||||
|
||||
.react-kanban-card__title {
|
||||
position: sticky;
|
||||
position: -webkit-sticky;
|
||||
}
|
||||
|
||||
.react-kanban-column {
|
||||
background: white !important;
|
||||
}
|
||||
|
||||
|
||||
.kanboard-card {
|
||||
overflow-wrap: break-word;
|
||||
|
||||
a {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.level {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.level-item {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
44
client/src/sass/_loader.scss
Normal file
44
client/src/sass/_loader.scss
Normal file
@ -0,0 +1,44 @@
|
||||
.loader-container {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.lds-ripple {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
transform: scale(2);
|
||||
}
|
||||
|
||||
.lds-ripple div {
|
||||
position: absolute;
|
||||
border: 4px solid $grey;
|
||||
opacity: 1;
|
||||
border-radius: 50%;
|
||||
animation: lds-ripple 1s cubic-bezier(0, 0.2, 0.8, 1) infinite;
|
||||
}
|
||||
|
||||
.lds-ripple div:nth-child(2) {
|
||||
animation-delay: -0.5s;
|
||||
}
|
||||
|
||||
@keyframes lds-ripple {
|
||||
0% {
|
||||
top: 36px;
|
||||
left: 36px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
23
client/src/store/actions/boards.ts
Normal file
23
client/src/store/actions/boards.ts
Normal file
@ -0,0 +1,23 @@
|
||||
export const FETCH_BOARDS_REQUEST = "FETCH_BOARDS_REQUEST";
|
||||
export const FETCH_BOARDS_SUCCESS = "FETCH_BOARDS_SUCCESS";
|
||||
export const FETCH_BOARDS_FAILURE = "FETCH_BOARDS_FAILURE";
|
||||
|
||||
export function fetchBoards() {
|
||||
return { type: FETCH_BOARDS_REQUEST };
|
||||
};
|
||||
|
||||
export const SAVE_BOARD_REQUEST = "SAVE_BOARD_REQUEST";
|
||||
export const SAVE_BOARD_SUCCESS = "SAVE_BOARD_SUCCESS";
|
||||
export const SAVE_BOARD_FAILURE = "SAVE_BOARD_FAILURE";
|
||||
|
||||
export function saveBoard(board: any) {
|
||||
return { type: SAVE_BOARD_REQUEST, board };
|
||||
};
|
||||
|
||||
export const DELETE_BOARD_REQUEST = "DELETE_BOARD_REQUEST";
|
||||
export const DELETE_BOARD_SUCCESS = "DELETE_BOARD_SUCCESS";
|
||||
export const DELETE_BOARD_FAILURE = "DELETE_BOARD_FAILURE";
|
||||
|
||||
export function deleteBoard(id: any) {
|
||||
return { type: DELETE_BOARD_REQUEST, id };
|
||||
};
|
31
client/src/store/actions/issues.ts
Normal file
31
client/src/store/actions/issues.ts
Normal file
@ -0,0 +1,31 @@
|
||||
export const FETCH_ISSUES_REQUEST = "FETCH_ISSUES_REQUEST";
|
||||
export const FETCH_ISSUES_SUCCESS = "FETCH_ISSUES_SUCCESS";
|
||||
export const FETCH_ISSUES_FAILURE = "FETCH_ISSUES_FAILURE";
|
||||
|
||||
export function fetchIssues(project: any, milestones: string) {
|
||||
return { type: FETCH_ISSUES_REQUEST, project, milestones };
|
||||
};
|
||||
|
||||
export const ADD_LABEL_REQUEST = "ADD_LABEL_REQUEST";
|
||||
export const ADD_LABEL_SUCCESS = "ADD_LABEL_SUCCESS";
|
||||
export const ADD_LABEL_FAILURE = "ADD_LABEL_FAILURE";
|
||||
|
||||
export function addLabel(project: any, issueNumber: any, label: string) {
|
||||
return { type: ADD_LABEL_REQUEST, project, issueNumber, label };
|
||||
}
|
||||
|
||||
export const REMOVE_LABEL_REQUEST = "REMOVE_LABEL_REQUEST";
|
||||
export const REMOVE_LABEL_SUCCESS = "REMOVE_LABEL_SUCCESS";
|
||||
export const REMOVE_LABEL_FAILURE = "REMOVE_LABEL_FAILURE";
|
||||
|
||||
export function removeLabel(project: any, issueNumber: any, label: string) {
|
||||
return { type: REMOVE_LABEL_REQUEST, project, issueNumber, label };
|
||||
}
|
||||
|
||||
export const CREATE_ISSUE_REQUEST = "CREATE_ISSUE_REQUEST";
|
||||
export const CREATE_ISSUE_SUCCESS = "CREATE_ISSUE_SUCCESS";
|
||||
export const CREATE_ISSUE_FAILURE = "CREATE_ISSUE_FAILURE";
|
||||
|
||||
export function createIssue(project: any, title: string, body: any, label: string) {
|
||||
return { type: CREATE_ISSUE_REQUEST, project, title, body, label };
|
||||
};
|
13
client/src/store/actions/kanboards.ts
Normal file
13
client/src/store/actions/kanboards.ts
Normal file
@ -0,0 +1,13 @@
|
||||
export const BUILD_KANBOARD_REQUEST = "BUILD_KANBOARD_REQUEST";
|
||||
export const BUILD_KANBOARD_SUCCESS = "BUILD_KANBOARD_SUCCESS";
|
||||
export const BUILD_KANBOARD_FAILURE = "BUILD_KANBOARD_FAILURE";
|
||||
|
||||
export function buildKanboard(board: string, milestones: string) {
|
||||
return { type: BUILD_KANBOARD_REQUEST, board, milestones };
|
||||
};
|
||||
|
||||
export const MOVE_CARD = "MOVE_CARD";
|
||||
|
||||
export function moveCard(boardID: string, fromLaneID: string, fromPosition: any, toLaneID: any, toPosition: any) {
|
||||
return { type: MOVE_CARD, boardID, fromLaneID, fromPosition, toLaneID, toPosition };
|
||||
};
|
7
client/src/store/actions/logout.ts
Normal file
7
client/src/store/actions/logout.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export const LOGOUT_REQUEST = "LOGOUT_REQUEST";
|
||||
export const LOGOUT_SUCCESS = "LOGOUT_SUCCESS";
|
||||
export const LOGOUT_FAILURE = "LOGOUT_FAILURE";
|
||||
|
||||
export function logout() {
|
||||
return { type: LOGOUT_REQUEST };
|
||||
};
|
15
client/src/store/actions/projects.ts
Normal file
15
client/src/store/actions/projects.ts
Normal file
@ -0,0 +1,15 @@
|
||||
export const FETCH_PROJECTS_REQUEST = "FETCH_PROJECTS_REQUEST";
|
||||
export const FETCH_PROJECTS_SUCCESS = "FETCH_PROJECTS_SUCCESS";
|
||||
export const FETCH_PROJECTS_FAILURE = "FETCH_PROJECTS_FAILURE";
|
||||
|
||||
export const FETCH_PROJECT_MILESTONES_REQUEST = "FETCH_PROJECTS_MILESTONES_REQUEST";
|
||||
export const FETCH_PROJECT_MILESTONES_SUCCESS = "FETCH_PROJECTS_MILESTONES_SUCCESS";
|
||||
export const FETCH_PROJECT_MILESTONES_FAILURE = "FETCH_PROJECTS_MILESTONES_FAILURE";
|
||||
|
||||
export function fetchProjects() {
|
||||
return { type: FETCH_PROJECTS_REQUEST };
|
||||
};
|
||||
|
||||
export function fetchProjectsMilestones(projects: any) {
|
||||
return { type: FETCH_PROJECT_MILESTONES_REQUEST, projects };
|
||||
};
|
42
client/src/store/reducers/boards.ts
Normal file
42
client/src/store/reducers/boards.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import { SAVE_BOARD_SUCCESS, FETCH_BOARDS_SUCCESS } from "../actions/boards";
|
||||
|
||||
export const defaultState = {
|
||||
byID: {},
|
||||
};
|
||||
|
||||
export function boardsReducer(state = defaultState, action: any) {
|
||||
switch(action.type) {
|
||||
case SAVE_BOARD_SUCCESS:
|
||||
return handleSaveBoardSuccess(state, action);
|
||||
case FETCH_BOARDS_SUCCESS:
|
||||
return handleFetchBoardsSuccess(state, action);
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
function handleSaveBoardSuccess(state: any, action: any) {
|
||||
const { board } = action;
|
||||
return {
|
||||
...state,
|
||||
byID: {
|
||||
...state.byID,
|
||||
[board.id]: {
|
||||
...board,
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function handleFetchBoardsSuccess(state: any, action: any) {
|
||||
const boardsByID = action.boards.reduce((byID: any, board: any) => {
|
||||
byID[board.id] = board;
|
||||
return byID;
|
||||
}, {});
|
||||
return {
|
||||
...state,
|
||||
byID: {
|
||||
...boardsByID,
|
||||
}
|
||||
};
|
||||
}
|
22
client/src/store/reducers/flags.ts
Normal file
22
client/src/store/reducers/flags.ts
Normal file
@ -0,0 +1,22 @@
|
||||
const defaultState = {
|
||||
actions: {}
|
||||
};
|
||||
|
||||
export function flagsReducer(state = defaultState, action: any) {
|
||||
const matches = (/^(.*)_((SUCCESS)|(FAILURE)|(REQUEST))$/).exec(action.type);
|
||||
|
||||
if(!matches) return state;
|
||||
|
||||
const actionPrefix = matches[1];
|
||||
|
||||
return {
|
||||
...state,
|
||||
actions: {
|
||||
...state.actions,
|
||||
[actionPrefix]: {
|
||||
isLoading: matches[2] === 'REQUEST'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
}
|
42
client/src/store/reducers/issues.ts
Normal file
42
client/src/store/reducers/issues.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import { FETCH_ISSUES_SUCCESS, CREATE_ISSUE_SUCCESS } from "../actions/issues";
|
||||
|
||||
const defaultState = {
|
||||
byProject: {}
|
||||
};
|
||||
|
||||
export function issuesReducer(state = defaultState, action: any) {
|
||||
switch(action.type) {
|
||||
case FETCH_ISSUES_SUCCESS:
|
||||
return handleFetchIssuesSuccess(state, action);
|
||||
case CREATE_ISSUE_SUCCESS:
|
||||
return handleCreateIssueSuccess(state, action);
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function handleFetchIssuesSuccess(state: any, action: any) {
|
||||
return {
|
||||
...state,
|
||||
byProject: {
|
||||
...state.byProject,
|
||||
[action.project]: [
|
||||
...action.issues,
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleCreateIssueSuccess(state: any, action: any) {
|
||||
return {
|
||||
...state,
|
||||
byProject: {
|
||||
...state.byProject,
|
||||
[action.project]: [
|
||||
...state.byProject[action.project],
|
||||
action.issue
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
74
client/src/store/reducers/kanboards.ts
Normal file
74
client/src/store/reducers/kanboards.ts
Normal file
@ -0,0 +1,74 @@
|
||||
import { BUILD_KANBOARD_SUCCESS, MOVE_CARD } from "../actions/kanboards";
|
||||
import { CREATE_ISSUE_SUCCESS } from "../actions/issues";
|
||||
|
||||
export const defaultState = {
|
||||
byID: {},
|
||||
};
|
||||
|
||||
export function kanboardsReducer(state = defaultState, action: any) {
|
||||
switch(action.type) {
|
||||
case BUILD_KANBOARD_SUCCESS:
|
||||
return handleBuildKanboardSuccess(state, action);
|
||||
case MOVE_CARD:
|
||||
return handleMoveCard(state, action);
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
function handleBuildKanboardSuccess(state: any, action: any) {
|
||||
return {
|
||||
...state,
|
||||
byID: {
|
||||
...state.byID,
|
||||
[action.kanboard.id]: {
|
||||
...action.kanboard,
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function handleMoveCard(state: any, action: any) {
|
||||
const {
|
||||
boardID, fromLaneID,
|
||||
fromPosition, toLaneID,
|
||||
toPosition
|
||||
} = action;
|
||||
|
||||
const kanboard = state.byID[boardID];
|
||||
|
||||
const columns = [ ...kanboard.columns ];
|
||||
const fromLane = columns[fromLaneID];
|
||||
const toLane = columns[toLaneID];
|
||||
const card = fromLane.cards[fromPosition];
|
||||
|
||||
const fromCards = [ ...fromLane.cards ];
|
||||
if (fromLaneID !== toLaneID) {
|
||||
fromCards.splice(fromPosition, 1);
|
||||
columns[fromLaneID] = {
|
||||
...fromLane,
|
||||
cards: fromCards,
|
||||
};
|
||||
|
||||
const toCards = [ ...toLane.cards ];
|
||||
toCards.splice(toPosition, 0, card);
|
||||
columns[toLaneID] = {
|
||||
...toLane,
|
||||
cards: toCards,
|
||||
};
|
||||
} else {
|
||||
fromCards.splice(fromPosition, 1);
|
||||
fromCards.splice(toPosition, 0, card);
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
byID: {
|
||||
...state.byID,
|
||||
[boardID]: {
|
||||
...state.byID[boardID],
|
||||
columns,
|
||||
},
|
||||
}
|
||||
};
|
||||
}
|
40
client/src/store/reducers/projects.ts
Normal file
40
client/src/store/reducers/projects.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import { FETCH_PROJECTS_SUCCESS, FETCH_PROJECT_MILESTONES_SUCCESS } from "../actions/projects";
|
||||
|
||||
export const defaultState = {
|
||||
byName: {},
|
||||
milestones: {}
|
||||
};
|
||||
|
||||
export function projectsReducer(state = defaultState, action: any) {
|
||||
switch (action.type) {
|
||||
case FETCH_PROJECTS_SUCCESS:
|
||||
return handleFetchProjectsSuccess(state, action);
|
||||
|
||||
case FETCH_PROJECT_MILESTONES_SUCCESS:
|
||||
return handleFetchProjectMilestonesSuccess(state, action);
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
function handleFetchProjectsSuccess(state: any, action: any) {
|
||||
const projectsByName = action.projects.reduce((byName: any, project: any) => {
|
||||
byName[project.full_name] = project;
|
||||
return byName;
|
||||
}, {});
|
||||
return {
|
||||
...state,
|
||||
byName: {
|
||||
...projectsByName,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function handleFetchProjectMilestonesSuccess(state: any, action: any) {
|
||||
console.log(action.milestones);
|
||||
return {
|
||||
...state,
|
||||
milestones: action.milestones,
|
||||
};
|
||||
}
|
14
client/src/store/reducers/root.ts
Normal file
14
client/src/store/reducers/root.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { combineReducers } from 'redux';
|
||||
import { issuesReducer } from './issues';
|
||||
import { boardsReducer } from './boards';
|
||||
import { flagsReducer } from './flags';
|
||||
import { projectsReducer } from './projects';
|
||||
import { kanboardsReducer } from './kanboards';
|
||||
|
||||
export const rootReducer = combineReducers({
|
||||
issues: issuesReducer,
|
||||
boards: boardsReducer,
|
||||
kanboards: kanboardsReducer,
|
||||
flags: flagsReducer,
|
||||
projects: projectsReducer
|
||||
});
|
47
client/src/store/sagas/boards.ts
Normal file
47
client/src/store/sagas/boards.ts
Normal file
@ -0,0 +1,47 @@
|
||||
import { put, call } from 'redux-saga/effects';
|
||||
import {
|
||||
FETCH_BOARDS_SUCCESS, SAVE_BOARD_SUCCESS,
|
||||
SAVE_BOARD_FAILURE, FETCH_BOARDS_FAILURE,
|
||||
DELETE_BOARD_FAILURE, DELETE_BOARD_SUCCESS
|
||||
} from '../actions/boards';
|
||||
import { api } from '../../util/api';
|
||||
|
||||
export function* fetchBoardsSaga() {
|
||||
let boards;
|
||||
|
||||
try {
|
||||
boards = yield call(api.fetchBoards)
|
||||
} catch(error) {
|
||||
yield put({ type: FETCH_BOARDS_FAILURE, error });
|
||||
return
|
||||
}
|
||||
|
||||
yield put({ type: FETCH_BOARDS_SUCCESS, boards });
|
||||
}
|
||||
|
||||
export function* saveBoardSaga(action: any) {
|
||||
let { board } = action;
|
||||
|
||||
try {
|
||||
board = yield call(api.saveBoard, board)
|
||||
} catch(error) {
|
||||
yield put({ type: SAVE_BOARD_FAILURE, error });
|
||||
return
|
||||
}
|
||||
|
||||
yield put({ type: SAVE_BOARD_SUCCESS, board });
|
||||
}
|
||||
|
||||
|
||||
export function* deleteBoardSaga(action: any) {
|
||||
let { id } = action;
|
||||
|
||||
try {
|
||||
yield call(api.deleteBoard, id)
|
||||
} catch(error) {
|
||||
yield put({ type: DELETE_BOARD_FAILURE, error });
|
||||
return
|
||||
}
|
||||
|
||||
yield put({ type: DELETE_BOARD_SUCCESS, id });
|
||||
}
|
11
client/src/store/sagas/failure.ts
Normal file
11
client/src/store/sagas/failure.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { GiteaUnauthorizedError } from "../../util/gitea";
|
||||
import { put } from 'redux-saga/effects';
|
||||
import { logout } from '../actions/logout';
|
||||
import { saveReferer } from "../../util/referer";
|
||||
|
||||
export function* failuresSaga(action) {
|
||||
if (action.error instanceof GiteaUnauthorizedError) {
|
||||
saveReferer();
|
||||
yield put(logout());
|
||||
}
|
||||
}
|
89
client/src/store/sagas/issues.ts
Normal file
89
client/src/store/sagas/issues.ts
Normal file
@ -0,0 +1,89 @@
|
||||
import { put, call, retry } from 'redux-saga/effects';
|
||||
import {
|
||||
FETCH_ISSUES_SUCCESS, FETCH_ISSUES_FAILURE,
|
||||
ADD_LABEL_FAILURE, ADD_LABEL_SUCCESS,
|
||||
REMOVE_LABEL_FAILURE, REMOVE_LABEL_SUCCESS,
|
||||
CREATE_ISSUE_FAILURE, CREATE_ISSUE_SUCCESS
|
||||
} from '../actions/issues';
|
||||
import { gitea } from '../../util/gitea';
|
||||
|
||||
export function* fetchIssuesSaga(action: any) {
|
||||
const { project, milestones } = action;
|
||||
|
||||
let issues = [];
|
||||
try {
|
||||
let page = 1;
|
||||
while (true) {
|
||||
let pageIssues = yield call(gitea.fetchIssues.bind(gitea), project, page, milestones);
|
||||
if (pageIssues.length === 0) {
|
||||
break;
|
||||
}
|
||||
issues.push(...pageIssues.filter(issue => issue.pull_request === null));
|
||||
page++;
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
yield put({ type: FETCH_ISSUES_FAILURE, project, error });
|
||||
return;
|
||||
}
|
||||
|
||||
yield put({ type: FETCH_ISSUES_SUCCESS, project, issues });
|
||||
}
|
||||
|
||||
export function* addLabelSaga(action: any) {
|
||||
const { project, issueNumber, label } = action;
|
||||
const labels = yield call(gitea.fetchProjectLabels.bind(gitea), project);
|
||||
const giteaLabel = labels.find((l: any) => l.name === label)
|
||||
|
||||
if (!giteaLabel) {
|
||||
yield put({ type: ADD_LABEL_FAILURE, error: new Error(`Label "${label}" not found !`) });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
yield retry(5, 250, gitea.addIssueLabel.bind(gitea), project, issueNumber, giteaLabel.id);
|
||||
} catch (error) {
|
||||
yield put({ type: ADD_LABEL_FAILURE, error });
|
||||
return;
|
||||
}
|
||||
|
||||
yield put({ type: ADD_LABEL_SUCCESS, project, issueNumber, label });
|
||||
}
|
||||
|
||||
export function* removeLabelSaga(action: any) {
|
||||
const { project, issueNumber, label } = action;
|
||||
const labels = yield call(gitea.fetchProjectLabels.bind(gitea), project);
|
||||
const giteaLabel = labels.find((l: any) => l.name === label)
|
||||
|
||||
if (!giteaLabel) {
|
||||
yield put({ type: REMOVE_LABEL_FAILURE, error: new Error(`Label "${label}" not found !`) });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
yield retry(5, 250, gitea.removeIssueLabel.bind(gitea), project, issueNumber, giteaLabel.id);
|
||||
} catch (error) {
|
||||
yield put({ type: REMOVE_LABEL_FAILURE, error });
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
yield put({ type: REMOVE_LABEL_SUCCESS, project, issueNumber, label });
|
||||
}
|
||||
|
||||
export function* createIssueSaga(action: any) {
|
||||
const { project, title, label, body } = action;
|
||||
const labels = yield call(gitea.fetchProjectLabels.bind(gitea), project);
|
||||
const giteaLabel = labels.find((l: any) => l.name === label)
|
||||
|
||||
let issue;
|
||||
try {
|
||||
issue = yield call(gitea.createIssue.bind(gitea), project, title, body, giteaLabel ? giteaLabel.id : null);
|
||||
} catch (error) {
|
||||
yield put({ type: CREATE_ISSUE_FAILURE, error });
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
yield put({ type: CREATE_ISSUE_SUCCESS, project, title, label, body, issue });
|
||||
}
|
152
client/src/store/sagas/kanboards.ts
Normal file
152
client/src/store/sagas/kanboards.ts
Normal file
@ -0,0 +1,152 @@
|
||||
import { select, put } from 'redux-saga/effects';
|
||||
import { fetchIssues, addLabel, removeLabel } from '../actions/issues';
|
||||
import { fetchIssuesSaga } from './issues';
|
||||
import { BUILD_KANBOARD_SUCCESS, buildKanboard, BUILD_KANBOARD_FAILURE } from '../actions/kanboards';
|
||||
import { Project, Issue } from '../../types/gitea';
|
||||
import { Board, BoardLane } from '../../types/board';
|
||||
import { KanboardLane, Kanboard, KanboardCard } from '../../types/kanboard';
|
||||
|
||||
export function* moveCardSaga(action: any) {
|
||||
const {
|
||||
boardID, fromLaneID,
|
||||
fromPosition, toLaneID,
|
||||
toPosition,
|
||||
} = action;
|
||||
|
||||
if (fromLaneID === toLaneID) return;
|
||||
|
||||
const { board, kanboard } = yield select(state => {
|
||||
return {
|
||||
kanboard: state.kanboards.byID[boardID],
|
||||
board: state.boards.byID[boardID]
|
||||
}
|
||||
});
|
||||
|
||||
const toLane = kanboard.columns[toLaneID];
|
||||
const card = toLane.cards[toPosition];
|
||||
|
||||
if (!card) return;
|
||||
|
||||
yield put(addLabel(card.project, card.issue.number, board.lanes[toLaneID].issueLabel));
|
||||
yield put(removeLabel(card.project, card.issue.number, board.lanes[fromLaneID].issueLabel));
|
||||
|
||||
}
|
||||
|
||||
export function* buildKanboardSaga(action: any) {
|
||||
const { board, milestones } = action;
|
||||
console.log("milestones", milestones);
|
||||
let kanboard;
|
||||
try {
|
||||
|
||||
for (let p, i = 0; (p = board.projects[i]); i++) {
|
||||
const { project } = yield fetchIssues(p, milestones);
|
||||
yield fetchIssuesSaga({ project: project, milestones: milestones });
|
||||
}
|
||||
|
||||
const issues = yield select(state => state.issues);
|
||||
|
||||
kanboard = createKanboard(board, issues);
|
||||
|
||||
} catch (error) {
|
||||
yield put({ type: BUILD_KANBOARD_FAILURE, error });
|
||||
return
|
||||
}
|
||||
|
||||
yield put({ type: BUILD_KANBOARD_SUCCESS, kanboard });
|
||||
}
|
||||
|
||||
export function* refreshKanboardSaga(action: any) {
|
||||
const { project, milestones } = action;
|
||||
const boards = yield select(state => state.boards);
|
||||
const boardValues = Object.values(boards.byID);
|
||||
|
||||
for (let b: any, i = 0; (b = boardValues[i]); i++) {
|
||||
const hasProject = b.projects.indexOf(project) !== -1;
|
||||
if (!hasProject) continue;
|
||||
yield put(buildKanboard(b, milestones));
|
||||
}
|
||||
}
|
||||
|
||||
function createCards(projects: Project[], issues: any, lane: BoardLane, rest: Set<KanboardCard>) {
|
||||
const cards: KanboardCard[] = projects.reduce((laneCards, p) => {
|
||||
|
||||
const projectIssues = p in issues.byProject ? issues.byProject[p] : [];
|
||||
|
||||
return projectIssues.reduce((projectCards: KanboardCard[], issue: any) => {
|
||||
|
||||
const hasLabel = issue.labels.some((l: any) => l.name === lane.issueLabel);
|
||||
const { card, memoized } = getMemoizedKanboardCard(issue.id, issue.title, p, issue);
|
||||
|
||||
if (hasLabel) {
|
||||
projectCards.push(card);
|
||||
rest.delete(card);
|
||||
} else {
|
||||
if (!memoized) rest.add(card);
|
||||
}
|
||||
|
||||
return projectCards;
|
||||
|
||||
}, laneCards);
|
||||
|
||||
}, []);
|
||||
|
||||
return cards;
|
||||
}
|
||||
|
||||
const kanboardCardMemo: { [key: string]: KanboardCard } = {};
|
||||
|
||||
function getKanboardCardMemoizationKey(id: number, project: Project, issue: Issue) {
|
||||
return `${project.id}-${issue.id}-${id}`;
|
||||
}
|
||||
|
||||
function isKanboardCardMemoized(key: string) {
|
||||
return kanboardCardMemo.hasOwnProperty(key)
|
||||
}
|
||||
|
||||
function getMemoizedKanboardCard(id: number, title: string, project: Project, issue: Issue) {
|
||||
const key = getKanboardCardMemoizationKey(id, project, issue);
|
||||
if (isKanboardCardMemoized(key)) return { card: kanboardCardMemo[key], memoized: true };
|
||||
kanboardCardMemo[key] = { id, title, project, issue };
|
||||
return { card: kanboardCardMemo[key], memoized: false };
|
||||
}
|
||||
|
||||
function resetKandboarCardMemo() {
|
||||
Object.keys(kanboardCardMemo).forEach(k => delete kanboardCardMemo[k]);
|
||||
}
|
||||
|
||||
function createKanboardLanes(board: Board, issues: any): KanboardLane[] {
|
||||
const lanes: KanboardLane[] = [];
|
||||
const rest = new Set<KanboardCard>();
|
||||
|
||||
resetKandboarCardMemo();
|
||||
|
||||
board.lanes.forEach((l: BoardLane, i: number) => {
|
||||
const cards = createCards(board.projects, issues, l, rest);
|
||||
lanes.push({
|
||||
id: i,
|
||||
title: l.title,
|
||||
cards,
|
||||
});
|
||||
});
|
||||
|
||||
// Assign remaining issues
|
||||
board.lanes.forEach((l: BoardLane, i: number) => {
|
||||
if (!l.collectRemainingIssues) return;
|
||||
lanes[i].cards.push(...Array.from(rest.values()));
|
||||
});
|
||||
|
||||
resetKandboarCardMemo();
|
||||
|
||||
return lanes;
|
||||
}
|
||||
|
||||
function createKanboard(board: Board, issues: any) {
|
||||
if (!board) return null;
|
||||
|
||||
const kanboard = {
|
||||
id: board.id,
|
||||
columns: createKanboardLanes(board, issues),
|
||||
};
|
||||
|
||||
return kanboard;
|
||||
}
|
17
client/src/store/sagas/logout.ts
Normal file
17
client/src/store/sagas/logout.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { call, put } from 'redux-saga/effects';
|
||||
import { LOGOUT_FAILURE, LOGOUT_SUCCESS } from '../actions/logout';
|
||||
|
||||
export function* logoutSaga() {
|
||||
try {
|
||||
yield call(fetch, '/logout', { mode: 'no-cors', credentials: 'include' });
|
||||
} catch(err) {
|
||||
yield put({ type: LOGOUT_FAILURE, error: err });
|
||||
return;
|
||||
}
|
||||
|
||||
yield put({ type: LOGOUT_SUCCESS });
|
||||
}
|
||||
|
||||
export function* logoutSuccessSaga() {
|
||||
window.location.reload();
|
||||
}
|
36
client/src/store/sagas/projects.ts
Normal file
36
client/src/store/sagas/projects.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import { put, call } from 'redux-saga/effects';
|
||||
import { FETCH_PROJECTS_SUCCESS, FETCH_PROJECTS_FAILURE, FETCH_PROJECT_MILESTONES_FAILURE, FETCH_PROJECT_MILESTONES_SUCCESS } from '../actions/projects';
|
||||
import { gitea } from '../../util/gitea';
|
||||
|
||||
export function* fetchProjectsSaga() {
|
||||
|
||||
let projects;
|
||||
try {
|
||||
projects = yield call(gitea.fetchUserProjects.bind(gitea))
|
||||
} catch (error) {
|
||||
yield put({ type: FETCH_PROJECTS_FAILURE, error });
|
||||
return;
|
||||
}
|
||||
|
||||
yield put({ type: FETCH_PROJECTS_SUCCESS, projects });
|
||||
}
|
||||
|
||||
export function* fetchProjectsMilestonesSaga(action: any) {
|
||||
const { projects } = action;
|
||||
let milestones = [];
|
||||
try {
|
||||
for (var i = 0; i < projects.length; i++) {
|
||||
console.log("PROJECT ", projects[i])
|
||||
milestones.push({
|
||||
project: projects[i],
|
||||
milestones: yield call(gitea.fetchMilestones.bind(gitea), projects[i])
|
||||
})
|
||||
}
|
||||
console.log("MILESTONES", milestones)
|
||||
} catch (error) {
|
||||
yield put({ type: FETCH_PROJECT_MILESTONES_FAILURE, error });
|
||||
return;
|
||||
}
|
||||
|
||||
yield put({ type: FETCH_PROJECT_MILESTONES_SUCCESS, milestones });
|
||||
}
|
9
client/src/store/sagas/referer.ts
Normal file
9
client/src/store/sagas/referer.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { hasReferer, getReferer, clearReferer } from '../../util/referer';
|
||||
|
||||
export function* navigateToRefererSaga() {
|
||||
if (!hasReferer()) return;
|
||||
const referer = getReferer();
|
||||
console.log("Redirecting to referer", referer);
|
||||
clearReferer();
|
||||
window.location.hash = referer;
|
||||
}
|
40
client/src/store/sagas/root.ts
Normal file
40
client/src/store/sagas/root.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import { all, takeEvery, takeLatest } from 'redux-saga/effects';
|
||||
import { failuresSaga } from './failure';
|
||||
import { FETCH_BOARDS_REQUEST, SAVE_BOARD_REQUEST, DELETE_BOARD_REQUEST } from '../actions/boards';
|
||||
import { fetchBoardsSaga, saveBoardSaga, deleteBoardSaga } from './boards';
|
||||
import { FETCH_ISSUES_REQUEST, ADD_LABEL_REQUEST, REMOVE_LABEL_REQUEST, CREATE_ISSUE_REQUEST, CREATE_ISSUE_SUCCESS } from '../actions/issues';
|
||||
import { fetchIssuesSaga, addLabelSaga, removeLabelSaga, createIssueSaga } from './issues';
|
||||
import { FETCH_PROJECTS_REQUEST, FETCH_PROJECT_MILESTONES_REQUEST } from '../actions/projects';
|
||||
import { fetchProjectsMilestonesSaga, fetchProjectsSaga } from './projects';
|
||||
import { LOGOUT_REQUEST, LOGOUT_SUCCESS } from '../actions/logout';
|
||||
import { logoutSaga, logoutSuccessSaga } from './logout';
|
||||
import { BUILD_KANBOARD_REQUEST, MOVE_CARD } from '../actions/kanboards';
|
||||
import { buildKanboardSaga, moveCardSaga, refreshKanboardSaga } from './kanboards';
|
||||
import { navigateToRefererSaga } from './referer';
|
||||
|
||||
export function* rootSaga() {
|
||||
yield all([
|
||||
navigateToRefererSaga(),
|
||||
takeEvery(patternFromRegExp(/^.*_FAILURE/), failuresSaga),
|
||||
takeLatest(FETCH_BOARDS_REQUEST, fetchBoardsSaga),
|
||||
takeLatest(BUILD_KANBOARD_REQUEST, buildKanboardSaga),
|
||||
takeLatest(SAVE_BOARD_REQUEST, saveBoardSaga),
|
||||
takeLatest(DELETE_BOARD_REQUEST, deleteBoardSaga),
|
||||
takeLatest(FETCH_ISSUES_REQUEST, fetchIssuesSaga),
|
||||
takeLatest(FETCH_PROJECTS_REQUEST, fetchProjectsSaga),
|
||||
takeLatest(FETCH_PROJECT_MILESTONES_REQUEST, fetchProjectsMilestonesSaga),
|
||||
takeEvery(MOVE_CARD, moveCardSaga),
|
||||
takeEvery(ADD_LABEL_REQUEST, addLabelSaga),
|
||||
takeEvery(REMOVE_LABEL_REQUEST, removeLabelSaga),
|
||||
takeLatest(CREATE_ISSUE_REQUEST, createIssueSaga),
|
||||
takeLatest(CREATE_ISSUE_SUCCESS, refreshKanboardSaga),
|
||||
takeLatest(LOGOUT_REQUEST, logoutSaga),
|
||||
takeLatest(LOGOUT_SUCCESS, logoutSuccessSaga)
|
||||
]);
|
||||
}
|
||||
|
||||
export function patternFromRegExp(re: any) {
|
||||
return (action: any) => {
|
||||
return re.test(action.type);
|
||||
};
|
||||
}
|
11
client/src/store/selectors/boards.ts
Normal file
11
client/src/store/selectors/boards.ts
Normal file
@ -0,0 +1,11 @@
|
||||
export function selectBoardByUserProjects(boardsByID: any, projectsByName: any) {
|
||||
const userProjects = Object.keys(projectsByName);
|
||||
return Object.keys(boardsByID).reduce((filteredBoardsByID: any, boardID: string) => {
|
||||
const board = boardsByID[boardID];
|
||||
const hasProject = board.projects.length === 0 || board.projects.some((p: any) => userProjects.indexOf(p) !== -1);
|
||||
if (hasProject) {
|
||||
filteredBoardsByID[boardID] = board;
|
||||
}
|
||||
return filteredBoardsByID;
|
||||
}, {});
|
||||
}
|
7
client/src/store/selectors/flags.ts
Normal file
7
client/src/store/selectors/flags.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export function selectFlagsIsLoading(state: any, ...actionPrefixes: any[]) {
|
||||
const { actions } = state.flags;
|
||||
return actionPrefixes.reduce((isLoading, prefix) => {
|
||||
if (!(prefix in actions)) return isLoading;
|
||||
return isLoading || actions[prefix].isLoading;
|
||||
}, false);
|
||||
};
|
30
client/src/store/store.ts
Normal file
30
client/src/store/store.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import { createStore, applyMiddleware, compose } from 'redux'
|
||||
import createSagaMiddleware from 'redux-saga'
|
||||
import { rootReducer } from './reducers/root'
|
||||
import { rootSaga } from './sagas/root'
|
||||
|
||||
let reduxMiddlewares = [];
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
const createLogger = require('redux-logger').createLogger;
|
||||
const loggerMiddleware = createLogger({
|
||||
collapsed: true,
|
||||
diff: true
|
||||
});
|
||||
reduxMiddlewares.push(loggerMiddleware);
|
||||
}
|
||||
|
||||
const composeEnhancers = (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
|
||||
|
||||
// create the saga middleware
|
||||
const sagaMiddleware = createSagaMiddleware()
|
||||
reduxMiddlewares.push(sagaMiddleware);
|
||||
|
||||
// mount it on the Store
|
||||
export const store = createStore(
|
||||
rootReducer,
|
||||
composeEnhancers(applyMiddleware(...reduxMiddlewares)),
|
||||
)
|
||||
|
||||
// then run the saga
|
||||
sagaMiddleware.run(rootSaga);
|
19
client/src/types/board.ts
Normal file
19
client/src/types/board.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { Project } from "./gitea"
|
||||
|
||||
export type BoardLaneId = string
|
||||
export type BoardId = string
|
||||
|
||||
export interface Board {
|
||||
id: BoardId
|
||||
title: string
|
||||
description: string
|
||||
lanes: BoardLane[]
|
||||
projects: Project[]
|
||||
};
|
||||
|
||||
export interface BoardLane {
|
||||
id: BoardLaneId
|
||||
title: string
|
||||
issueLabel: string
|
||||
collectRemainingIssues: boolean
|
||||
};
|
2
client/src/types/gitea.ts
Normal file
2
client/src/types/gitea.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export type Project = any
|
||||
export type Issue = any
|
21
client/src/types/kanboard.ts
Normal file
21
client/src/types/kanboard.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import { Project, Issue } from "./gitea";
|
||||
import { BoardId } from "./board";
|
||||
|
||||
export interface Kanboard {
|
||||
id: BoardId
|
||||
lanes: KanboardLane[]
|
||||
}
|
||||
|
||||
export interface KanboardLane {
|
||||
id: number
|
||||
title: string
|
||||
cards: KanboardCard[]
|
||||
};
|
||||
|
||||
|
||||
export interface KanboardCard {
|
||||
id: number
|
||||
title: string
|
||||
project: Project
|
||||
issue: Issue
|
||||
}
|
30
client/src/util/api.ts
Normal file
30
client/src/util/api.ts
Normal file
@ -0,0 +1,30 @@
|
||||
|
||||
export class APIClient {
|
||||
|
||||
saveBoard(board) {
|
||||
return fetch(`/api/boards`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(board)
|
||||
})
|
||||
.then(res => res.json())
|
||||
;
|
||||
}
|
||||
|
||||
deleteBoard(id) {
|
||||
return fetch(`/api/boards/${id}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
}
|
||||
|
||||
fetchBoards() {
|
||||
return fetch(`/api/boards`)
|
||||
.then(res => res.json())
|
||||
;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export const api = new APIClient();
|
93
client/src/util/gitea.ts
Normal file
93
client/src/util/gitea.ts
Normal file
@ -0,0 +1,93 @@
|
||||
|
||||
export class GiteaUnauthorizedError extends Error {
|
||||
constructor(...args: any[]) {
|
||||
super(...args)
|
||||
Object.setPrototypeOf(this, GiteaUnauthorizedError.prototype);
|
||||
}
|
||||
}
|
||||
|
||||
export class GiteaClient {
|
||||
|
||||
fetchIssues(project: any, page = "", milestones = "") {
|
||||
return fetch(`/gitea/api/v1/repos/${project}/issues?page=${page}&milestones=${milestones}`)
|
||||
.then(this.assertAuthorization)
|
||||
.then(this.assertOk)
|
||||
.then(res => res.json())
|
||||
;
|
||||
}
|
||||
|
||||
fetchMilestones(project: any) {
|
||||
return fetch(`/gitea/api/v1/repos/${project}/milestones`)
|
||||
.then(this.assertAuthorization)
|
||||
.then(this.assertOk)
|
||||
.then(res => res.json())
|
||||
;
|
||||
}
|
||||
|
||||
fetchUserProjects() {
|
||||
return fetch(`/gitea/api/v1/user/repos`)
|
||||
.then(this.assertAuthorization)
|
||||
.then(this.assertOk)
|
||||
.then(res => res.json())
|
||||
;
|
||||
}
|
||||
|
||||
addIssueLabel(project: any, issueNumber: any, labelID: any) {
|
||||
return fetch(`/gitea/api/v1/repos/${project}/issues/${issueNumber}/labels`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ labels: [labelID] }),
|
||||
})
|
||||
.then(this.assertAuthorization)
|
||||
.then(this.assertOk)
|
||||
.then(res => res.json())
|
||||
}
|
||||
|
||||
fetchProjectLabels(project: any) {
|
||||
return fetch(`/gitea/api/v1/repos/${project}/labels`)
|
||||
.then(this.assertAuthorization)
|
||||
.then(this.assertOk)
|
||||
.then(res => res.json())
|
||||
;
|
||||
}
|
||||
|
||||
removeIssueLabel(project: any, issueNumber: any, labelID: any) {
|
||||
return fetch(`/gitea/api/v1/repos/${project}/issues/${issueNumber}/labels/${labelID}`, {
|
||||
method: 'DELETE'
|
||||
})
|
||||
.then(this.assertAuthorization)
|
||||
.then(this.assertOk)
|
||||
}
|
||||
|
||||
createIssue(project: any, title: any, body: any, labelID: any) {
|
||||
return fetch(`/gitea/api/v1/repos/${project}/issues`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
title,
|
||||
body,
|
||||
labels: labelID ? [labelID] : undefined,
|
||||
}),
|
||||
})
|
||||
.then(this.assertAuthorization)
|
||||
.then(this.assertOk)
|
||||
.then(res => res.json())
|
||||
}
|
||||
|
||||
assertOk(res: any) {
|
||||
if (!res.ok) return Promise.reject(new Error('Request failed'));
|
||||
return res;
|
||||
}
|
||||
|
||||
assertAuthorization(res: any) {
|
||||
if (res.status === 401 || res.status === 404) return Promise.reject(new GiteaUnauthorizedError());
|
||||
return res;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export const gitea = new GiteaClient();
|
19
client/src/util/referer.ts
Normal file
19
client/src/util/referer.ts
Normal file
@ -0,0 +1,19 @@
|
||||
const localStorage = window.localStorage;
|
||||
const refererKey = 'referer';
|
||||
|
||||
export function getReferer() {
|
||||
return localStorage.getItem(refererKey);
|
||||
}
|
||||
|
||||
export function saveReferer() {
|
||||
console.log("Saving referer", window.location.hash);
|
||||
localStorage.setItem(refererKey, window.location.hash);
|
||||
}
|
||||
|
||||
export function hasReferer() {
|
||||
return !!getReferer();
|
||||
}
|
||||
|
||||
export function clearReferer() {
|
||||
localStorage.removeItem(refererKey);
|
||||
}
|
16
client/tsconfig.json
Normal file
16
client/tsconfig.json
Normal file
@ -0,0 +1,16 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"module": "es6",
|
||||
"lib": ["dom", "es6"],
|
||||
"moduleResolution": "node",
|
||||
"jsx": "react",
|
||||
"strict": false,
|
||||
"sourceMap": true,
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.tsx"],
|
||||
"files": [
|
||||
"./src/custom.d.ts"
|
||||
]
|
||||
}
|
@ -2,19 +2,24 @@ const path = require('path');
|
||||
|
||||
// Plugins
|
||||
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
const WebpackCleanupPlugin = require('webpack-cleanup-plugin');
|
||||
|
||||
const env = process.env;
|
||||
|
||||
module.exports = {
|
||||
mode: `${env.NODE_ENV ? env.NODE_ENV : 'production'}`,
|
||||
entry: './src/index.js',
|
||||
entry: './src/index.tsx',
|
||||
devtool: 'inline-source-map',
|
||||
output: {
|
||||
path: path.join(__dirname, 'dist')
|
||||
},
|
||||
resolve: {
|
||||
extensions: [".ts", ".tsx", ".js", ".jsx"]
|
||||
},
|
||||
module: {
|
||||
rules: [{
|
||||
test: /\.(scss)|(sass)$/,
|
||||
test: /\.s(a|c)ss$/,
|
||||
use: [
|
||||
MiniCssExtractPlugin.loader,
|
||||
{
|
||||
@ -34,52 +39,32 @@ module.exports = {
|
||||
}
|
||||
]
|
||||
},{
|
||||
test: /\.(woff(2)?|ttf|eot|svg)(\?v=\d+\.\d+\.\d+)?$/,
|
||||
test: /\.(woff(2)?|ttf|eot|svg|png)(\?v=\d+\.\d+\.\d+)?$/,
|
||||
use: [{
|
||||
loader: 'file-loader',
|
||||
options: {
|
||||
name: '[name].[ext]',
|
||||
outputPath: '/fonts/'
|
||||
outputPath: '/resources/'
|
||||
}
|
||||
}]
|
||||
},{
|
||||
test: /\.(html)$/,
|
||||
use: [{
|
||||
loader: 'file-loader',
|
||||
options: {
|
||||
name: '[name].[ext]'
|
||||
},
|
||||
},
|
||||
'extract-loader',
|
||||
{
|
||||
loader: 'html-loader',
|
||||
options: {}
|
||||
}
|
||||
]
|
||||
},{
|
||||
test: /\.riot$/,
|
||||
test: /\.(t|j)sx?$/,
|
||||
exclude: /node_modules/,
|
||||
use: [{
|
||||
loader: '@riotjs/webpack-loader',
|
||||
options: {
|
||||
hot: false,
|
||||
}
|
||||
}]
|
||||
},{
|
||||
test: /\.js$/,
|
||||
exclude: /node_modules/,
|
||||
use: {
|
||||
loader: 'babel-loader',
|
||||
options: {
|
||||
presets: ['@babel/preset-env']
|
||||
}
|
||||
}
|
||||
loaders: ['ts-loader']
|
||||
}]
|
||||
},
|
||||
plugins: [
|
||||
new MiniCssExtractPlugin({
|
||||
filename: "css/[name].css",
|
||||
chunkFilename: "css/[id].css"
|
||||
}),
|
||||
new HtmlWebpackPlugin({
|
||||
template: './src/index.html',
|
||||
inject: false,
|
||||
favicon: "./src/resources/favicon.png"
|
||||
}),
|
||||
new WebpackCleanupPlugin({
|
||||
exclude: ['resources/logo.svg']
|
||||
})
|
||||
]
|
||||
}
|
@ -1,12 +1,16 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"forge.cadoles.com/wpetit/gitea-kan/internal/config"
|
||||
"forge.cadoles.com/wpetit/gengitkan/internal/config"
|
||||
"forge.cadoles.com/wpetit/gengitkan/internal/repository"
|
||||
stormRepo "forge.cadoles.com/wpetit/gengitkan/internal/repository/storm"
|
||||
"github.com/asdine/storm"
|
||||
"github.com/gorilla/sessions"
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/service"
|
||||
"gitlab.com/wpetit/goweb/service/session"
|
||||
"gitlab.com/wpetit/goweb/session/gorilla"
|
||||
"go.etcd.io/bbolt"
|
||||
)
|
||||
|
||||
func getServiceContainer(conf *config.Config) (*service.Container, error) {
|
||||
@ -24,11 +28,31 @@ func getServiceContainer(conf *config.Config) (*service.Container, error) {
|
||||
|
||||
ctn.Provide(
|
||||
session.ServiceName,
|
||||
gorilla.ServiceProvider("gitea-kan", cookieStore),
|
||||
gorilla.ServiceProvider("gengitkan", cookieStore),
|
||||
)
|
||||
|
||||
// Create and expose config service provider
|
||||
ctn.Provide(config.ServiceName, config.ServiceProvider(conf))
|
||||
|
||||
// Load Storm database
|
||||
db, err := storm.Open(conf.Data.DBPath, storm.BoltOptions(
|
||||
0660,
|
||||
&bbolt.Options{},
|
||||
))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not open database")
|
||||
}
|
||||
|
||||
boardsRepository := stormRepo.NewBoardRepository(db)
|
||||
if err := boardsRepository.Init(); err != nil {
|
||||
return nil, errors.Wrap(err, "could not init boards repository")
|
||||
}
|
||||
|
||||
ctn.Provide(repository.ServiceName, repository.ServiceProvider(
|
||||
repository.NewRepository(
|
||||
boardsRepository,
|
||||
),
|
||||
))
|
||||
|
||||
return ctn, nil
|
||||
}
|
||||
|
@ -6,8 +6,8 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"forge.cadoles.com/wpetit/gitea-kan/internal/config"
|
||||
"forge.cadoles.com/wpetit/gitea-kan/internal/route"
|
||||
"forge.cadoles.com/wpetit/gengitkan/internal/config"
|
||||
"forge.cadoles.com/wpetit/gengitkan/internal/route"
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/go-chi/chi/middleware"
|
||||
"github.com/pkg/errors"
|
||||
|
13
go.mod
13
go.mod
@ -1,24 +1,23 @@
|
||||
module forge.cadoles.com/wpetit/gitea-kan
|
||||
module forge.cadoles.com/wpetit/gengitkan
|
||||
|
||||
go 1.13
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1
|
||||
github.com/asdine/storm v2.1.2+incompatible
|
||||
github.com/cortesi/modd v0.0.0-20210222043654-cbdcc23af7d5 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/go-chi/chi v4.0.2+incompatible
|
||||
github.com/google/uuid v1.1.1 // indirect
|
||||
github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c // indirect
|
||||
github.com/gorilla/sessions v1.2.0
|
||||
github.com/kr/pretty v0.1.0 // indirect
|
||||
github.com/pborman/uuid v1.2.0
|
||||
github.com/pkg/errors v0.8.1
|
||||
github.com/smartystreets/assertions v1.0.1 // indirect
|
||||
github.com/smartystreets/goconvey v0.0.0-20190731233626-505e41936337 // indirect
|
||||
github.com/stretchr/testify v1.4.0 // indirect
|
||||
github.com/zserge/lorca v0.1.8 // indirect
|
||||
gitlab.com/wpetit/goweb v0.0.0-20190728111123-bbcb57177273
|
||||
golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297 // indirect
|
||||
go.etcd.io/bbolt v1.3.3
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45
|
||||
golang.org/x/text v0.3.2 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
|
||||
gopkg.in/ini.v1 v1.49.0
|
||||
gopkg.in/yaml.v2 v2.2.5 // indirect
|
||||
)
|
||||
|
72
go.sum
72
go.sum
@ -1,13 +1,33 @@
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM=
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20210208195552-ff826a37aa15 h1:AUNCr9CiJuwrRYS3XieqF+Z9B9gNxo/eANAJCF2eiN4=
|
||||
github.com/alecthomas/units v0.0.0-20210208195552-ff826a37aa15/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE=
|
||||
github.com/asdine/storm v2.1.2+incompatible h1:dczuIkyqwY2LrtXPz8ixMrU/OFgZp71kbKTHGrXYt/Q=
|
||||
github.com/asdine/storm v2.1.2+incompatible/go.mod h1:RarYDc9hq1UPLImuiXK3BIWPJLdIygvV3PsInK0FbVQ=
|
||||
github.com/bmatcuk/doublestar v1.3.4 h1:gPypJ5xD31uhX6Tf54sDPUOBXTqKH4c9aPY66CyQrS0=
|
||||
github.com/bmatcuk/doublestar v1.3.4/go.mod h1:wiQtGV+rzVYxB7WIlirSN++5HPtPlXEo9MEoZQC/PmE=
|
||||
github.com/cortesi/modd v0.0.0-20210222043654-cbdcc23af7d5 h1:R2McxbAMq4aEHYUzQ8DGxWPXHSr33LdWS1C2qWCS554=
|
||||
github.com/cortesi/modd v0.0.0-20210222043654-cbdcc23af7d5/go.mod h1:5XXIj61uad0c9FWBHCA1M+pP+xvO0+/OI10xL4Hg68w=
|
||||
github.com/cortesi/moddwatch v0.0.0-20210222043437-a6aaad86a36e h1:vNbhR09qtq9ELJgvhAWng4zl/4CVTPBPVev3R8MlUYc=
|
||||
github.com/cortesi/moddwatch v0.0.0-20210222043437-a6aaad86a36e/go.mod h1:MUkYRZrwFTHATqCI5tDJRPqmBt9xf3q4+Avfut7kCCE=
|
||||
github.com/cortesi/termlog v0.0.0-20210222042314-a1eec763abec h1:v7D8uHsIKsyjfyhhNdY4qivqN558Ejiq+CDXiUljZ+4=
|
||||
github.com/cortesi/termlog v0.0.0-20210222042314-a1eec763abec/go.mod h1:10Fm2kasJmcKf1FSMQGSWb976sfR29hejNtfS9AydB4=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/fatih/color v1.10.0 h1:s36xzo75JdqLaaWoiEHk767eHiwo0598uUxyfiPkDsg=
|
||||
github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
|
||||
github.com/go-chi/chi v4.0.2+incompatible h1:maB6vn6FqCxrpz4FqWdh4+lwpyZIQS7YEAUcHlgXVRs=
|
||||
github.com/go-chi/chi v4.0.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
|
||||
github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM=
|
||||
github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY=
|
||||
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
|
||||
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
@ -19,46 +39,98 @@ github.com/gorilla/sessions v1.2.0 h1:S7P+1Hm5V/AT9cjEcUD5uDaQSX0OE577aCXgoaKpYb
|
||||
github.com/gorilla/sessions v1.2.0/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
|
||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw=
|
||||
github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8=
|
||||
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c h1:rp5dCmg/yLR3mgFuSOe4oEnDDmGLROTvMragMUXpTQw=
|
||||
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c/go.mod h1:X07ZCGwUbLaax7L0S3Tw4hpejzu63ZrrQiUe6W0hcy0=
|
||||
github.com/pborman/uuid v1.2.0 h1:J7Q5mO4ysT1dv8hyrUGHb9+ooztCXu1D8MY8DZYsu3g=
|
||||
github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
|
||||
github.com/pkg/diff v0.0.0-20200914180035-5b29258ca4f7/go.mod h1:zO8QMzTeZd5cpnIkz/Gn6iK0jDfGicM1nynOkkPIl28=
|
||||
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rjeczalik/notify v0.0.0-20181126183243-629144ba06a1 h1:FLWDC+iIP9BWgYKvWKKtOUZux35LIQNAuIzp/63RQJU=
|
||||
github.com/rjeczalik/notify v0.0.0-20181126183243-629144ba06a1/go.mod h1:aErll2f0sUX9PXZnVNyeiObbmTlk5jnMoCa4QEjJeqM=
|
||||
github.com/rogpeppe/go-internal v1.6.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
github.com/smartystreets/assertions v1.0.1/go.mod h1:kHHU4qYBaI3q23Pp3VPrmWhuIUrLW/7eUrw0BU5VaoM=
|
||||
github.com/smartystreets/goconvey v0.0.0-20190731233626-505e41936337/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/zserge/lorca v0.1.8 h1:gZwyvesmaoGCwxF5NssI6pdydXkCVHOoHw2nks/PBRs=
|
||||
github.com/zserge/lorca v0.1.8/go.mod h1:gTrVdXKyWxNhc8aUb1Uu3s0mY343arR1T6jUtxmBxR8=
|
||||
gitlab.com/wpetit/goweb v0.0.0-20190728111123-bbcb57177273 h1:YtMGT0pEGTQ5MAglg6rvu8pQVQJEtskoeEw+csUqf2o=
|
||||
gitlab.com/wpetit/goweb v0.0.0-20190728111123-bbcb57177273/go.mod h1:5Y/eVplFvdsd6zMdA3bx8KON6Ab1n90+cQeX5uJ6jIE=
|
||||
go.etcd.io/bbolt v1.3.3 h1:MUGmc65QhB3pIlaQ5bB4LwqSj6GIonVJXpZiaKNyaKk=
|
||||
go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83 h1:/ZScEX8SfEmUGRHs0gxpqteO5nfNW6axyZbBdw9A12g=
|
||||
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181102091132-c10e9556a7bc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297 h1:k7pJ2yAPLPgbskkFdhRCsA77k2fySZ1zf2zCjvQCiIM=
|
||||
golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200202094626-16171245cfb2 h1:CCH4IOTTfewWjGOlSp+zGcjutRKlBEZQ6wTn8ozI/nI=
|
||||
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210220033124-5f55cee0dc0d h1:1aflnvSoWWLI2k/dMUAl5lvU1YO4Mb4hz0gh+1rjcxU=
|
||||
golang.org/x/net v0.0.0-20210220033124-5f55cee0dc0d/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180926160741-c2ed4eda69e7/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201029080932-201ba4db2418/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43 h1:SgQ6LNaYJU0JIuEHv9+s6EbhSCwYeAf5Yvj6lpYlqAE=
|
||||
golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/term v0.0.0-20191110171634-ad39bd3f0407/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d h1:SZxvLBoTP5yHO3Frd4z4vrF+DBX9vMVanchswa69toE=
|
||||
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
|
||||
gopkg.in/go-playground/validator.v9 v9.29.1/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ=
|
||||
gopkg.in/ini.v1 v1.49.0 h1:MW0aLMiezbm/Ray0gJJ+nQFE2uOC9EpK2p5zPN3NqpM=
|
||||
gopkg.in/ini.v1 v1.49.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
mvdan.cc/editorconfig v0.1.1-0.20200121172147-e40951bde157/go.mod h1:Ge4atmRUYqueGppvJ7JNrtqpqokoJEFxYbP0Z+WeKS8=
|
||||
mvdan.cc/sh/v3 v3.2.2 h1:UpH3jtOUEXBWXZg35bnRukUjsB6UR+nNhhXCi2dGKOs=
|
||||
mvdan.cc/sh/v3 v3.2.2/go.mod h1:fPQmabBpREM/XQ9YXSU5ZFZ/Sm+PmKP9/vkFHgYKJEI=
|
||||
|
@ -11,6 +11,7 @@ type Config struct {
|
||||
Debug bool
|
||||
HTTP HTTPConfig
|
||||
Gitea GiteaConfig
|
||||
Data DataConfig
|
||||
}
|
||||
|
||||
type HTTPConfig struct {
|
||||
@ -29,6 +30,10 @@ type GiteaConfig struct {
|
||||
APIBaseURL string
|
||||
}
|
||||
|
||||
type DataConfig struct {
|
||||
DBPath string
|
||||
}
|
||||
|
||||
// NewFromFile retrieves the configuration from the given file
|
||||
func NewFromFile(filepath string) (*Config, error) {
|
||||
config := NewDefault()
|
||||
@ -52,9 +57,12 @@ func NewDefault() *Config {
|
||||
Debug: false,
|
||||
HTTP: HTTPConfig{
|
||||
Address: ":3000",
|
||||
PublicDir: "${GITEA_APP_PUBDIR}",
|
||||
PublicDir: "${GENGITKAN_HTTP_PUBDIR}",
|
||||
},
|
||||
Gitea: GiteaConfig{},
|
||||
Data: DataConfig{
|
||||
DBPath: "${GENGITKAN_DATA_DBPATH}",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -5,7 +5,7 @@ import (
|
||||
|
||||
"github.com/pborman/uuid"
|
||||
|
||||
"forge.cadoles.com/wpetit/gitea-kan/internal/config"
|
||||
"forge.cadoles.com/wpetit/gengitkan/internal/config"
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/middleware/container"
|
||||
"gitlab.com/wpetit/goweb/service"
|
||||
|
27
internal/repository/board.go
Normal file
27
internal/repository/board.go
Normal file
@ -0,0 +1,27 @@
|
||||
package repository
|
||||
|
||||
type BoardRepository interface {
|
||||
List() ([]*Board, error)
|
||||
Get(BoardID) (*Board, error)
|
||||
Save(*Board) error
|
||||
Delete(BoardID) error
|
||||
}
|
||||
|
||||
type BoardID string
|
||||
|
||||
type Board struct {
|
||||
ID BoardID `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
Lanes []*BoardLane `json:"lanes"`
|
||||
Projects []string `json:"projects"`
|
||||
}
|
||||
|
||||
type BoardLaneID string
|
||||
|
||||
type BoardLane struct {
|
||||
ID BoardLaneID `json:"id"`
|
||||
Title string `json:"title"`
|
||||
IssueLabel string `json:"issueLabel"`
|
||||
CollectRemainingIssues bool `json:"collectRemainingIssues"`
|
||||
}
|
9
internal/repository/provider.go
Normal file
9
internal/repository/provider.go
Normal file
@ -0,0 +1,9 @@
|
||||
package repository
|
||||
|
||||
import "gitlab.com/wpetit/goweb/service"
|
||||
|
||||
func ServiceProvider(repository *Repository) service.Provider {
|
||||
return func(ctn *service.Container) (interface{}, error) {
|
||||
return repository, nil
|
||||
}
|
||||
}
|
21
internal/repository/repository.go
Normal file
21
internal/repository/repository.go
Normal file
@ -0,0 +1,21 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNotFound = errors.New("not found")
|
||||
)
|
||||
|
||||
type Repository struct {
|
||||
boards BoardRepository
|
||||
}
|
||||
|
||||
func (r *Repository) Boards() BoardRepository {
|
||||
return r.boards
|
||||
}
|
||||
|
||||
func NewRepository(boards BoardRepository) *Repository {
|
||||
return &Repository{boards}
|
||||
}
|
33
internal/repository/service.go
Normal file
33
internal/repository/service.go
Normal file
@ -0,0 +1,33 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/service"
|
||||
)
|
||||
|
||||
const ServiceName service.Name = "repository"
|
||||
|
||||
// From retrieves the repository service in the given container
|
||||
func From(container *service.Container) (*Repository, error) {
|
||||
service, err := container.Service(ServiceName)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "error while retrieving '%s' service", ServiceName)
|
||||
}
|
||||
|
||||
srv, ok := service.(*Repository)
|
||||
if !ok {
|
||||
return nil, errors.Errorf("retrieved service is not a valid '%s' service", ServiceName)
|
||||
}
|
||||
|
||||
return srv, nil
|
||||
}
|
||||
|
||||
// Must retrieves the repository service in the given container or panic otherwise
|
||||
func Must(container *service.Container) *Repository {
|
||||
srv, err := From(container)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return srv
|
||||
}
|
81
internal/repository/storm/board.go
Normal file
81
internal/repository/storm/board.go
Normal file
@ -0,0 +1,81 @@
|
||||
package storm
|
||||
|
||||
import (
|
||||
"forge.cadoles.com/wpetit/gengitkan/internal/repository"
|
||||
"github.com/asdine/storm"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type BoardRepository struct {
|
||||
db *storm.DB
|
||||
}
|
||||
|
||||
type boardItem struct {
|
||||
ID string `storm:"id"`
|
||||
Board *repository.Board
|
||||
}
|
||||
|
||||
func (r *BoardRepository) Init() error {
|
||||
if err := r.db.Init(&boardItem{}); err != nil {
|
||||
return errors.Wrap(err, "could not init 'boardItem' collection")
|
||||
}
|
||||
|
||||
if err := r.db.ReIndex(&boardItem{}); err != nil {
|
||||
return errors.Wrap(err, "could not reindex 'boardItem' collection")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *BoardRepository) List() ([]*repository.Board, error) {
|
||||
boardItems := make([]*boardItem, 0)
|
||||
|
||||
if err := r.db.All(&boardItems); err != nil {
|
||||
return nil, errors.Wrap(err, "could not retrieve board items")
|
||||
}
|
||||
|
||||
boards := make([]*repository.Board, 0, len(boardItems))
|
||||
|
||||
for _, b := range boardItems {
|
||||
boards = append(boards, b.Board)
|
||||
}
|
||||
|
||||
return boards, nil
|
||||
}
|
||||
|
||||
func (r *BoardRepository) Get(id repository.BoardID) (*repository.Board, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *BoardRepository) Save(board *repository.Board) error {
|
||||
b := &boardItem{
|
||||
ID: string(board.ID),
|
||||
Board: board,
|
||||
}
|
||||
|
||||
if err := r.db.Save(b); err != nil {
|
||||
return errors.Wrap(err, "could not save board item")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *BoardRepository) Delete(id repository.BoardID) error {
|
||||
b := &boardItem{
|
||||
ID: string(id),
|
||||
}
|
||||
|
||||
if err := r.db.DeleteStruct(b); err != nil {
|
||||
if err == storm.ErrNotFound {
|
||||
return repository.ErrNotFound
|
||||
}
|
||||
|
||||
return errors.Wrapf(err, "could not delete board '%s'", id)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewBoardRepository(db *storm.DB) *BoardRepository {
|
||||
return &BoardRepository{db}
|
||||
}
|
70
internal/route/board.go
Normal file
70
internal/route/board.go
Normal file
@ -0,0 +1,70 @@
|
||||
package route
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
|
||||
"forge.cadoles.com/wpetit/gengitkan/internal/repository"
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/middleware/container"
|
||||
)
|
||||
|
||||
func serveBoards(w http.ResponseWriter, r *http.Request) {
|
||||
ctn := container.Must(r.Context())
|
||||
repo := repository.Must(ctn)
|
||||
|
||||
boards, err := repo.Boards().List()
|
||||
if err != nil {
|
||||
panic(errors.Wrap(err, "could not retrieve boards list"))
|
||||
}
|
||||
|
||||
w.Header().Add("Content-Type", "application/json")
|
||||
|
||||
encoder := json.NewEncoder(w)
|
||||
if err := encoder.Encode(boards); err != nil {
|
||||
panic(errors.Wrap(err, "could not encode boards list"))
|
||||
}
|
||||
}
|
||||
|
||||
func saveBoard(w http.ResponseWriter, r *http.Request) {
|
||||
ctn := container.Must(r.Context())
|
||||
repo := repository.Must(ctn)
|
||||
|
||||
decoder := json.NewDecoder(r.Body)
|
||||
board := &repository.Board{}
|
||||
|
||||
if err := decoder.Decode(board); err != nil {
|
||||
panic(errors.Wrap(err, "could not decode board"))
|
||||
}
|
||||
|
||||
if err := repo.Boards().Save(board); err != nil {
|
||||
panic(errors.Wrap(err, "could not save board"))
|
||||
}
|
||||
|
||||
w.Header().Add("Content-Type", "application/json")
|
||||
|
||||
encoder := json.NewEncoder(w)
|
||||
if err := encoder.Encode(board); err != nil {
|
||||
panic(errors.Wrap(err, "could not encode board"))
|
||||
}
|
||||
}
|
||||
|
||||
func deleteBoard(w http.ResponseWriter, r *http.Request) {
|
||||
boardID := repository.BoardID(chi.URLParam(r, "boardID"))
|
||||
|
||||
ctn := container.Must(r.Context())
|
||||
repo := repository.Must(ctn)
|
||||
|
||||
if err := repo.Boards().Delete(boardID); err != nil {
|
||||
if err == repository.ErrNotFound {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
panic(err)
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
@ -3,7 +3,7 @@ package route
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"forge.cadoles.com/wpetit/gitea-kan/internal/config"
|
||||
"forge.cadoles.com/wpetit/gengitkan/internal/config"
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/middleware/container"
|
||||
"gitlab.com/wpetit/goweb/service/session"
|
||||
|
@ -3,7 +3,7 @@ package route
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"forge.cadoles.com/wpetit/gitea-kan/internal/middleware"
|
||||
"forge.cadoles.com/wpetit/gengitkan/internal/middleware"
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/middleware/container"
|
||||
"gitlab.com/wpetit/goweb/service/session"
|
||||
|
@ -6,10 +6,8 @@ import (
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
|
||||
"forge.cadoles.com/wpetit/gitea-kan/internal/config"
|
||||
"forge.cadoles.com/wpetit/gitea-kan/internal/middleware"
|
||||
"forge.cadoles.com/wpetit/gengitkan/internal/config"
|
||||
"forge.cadoles.com/wpetit/gengitkan/internal/middleware"
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/middleware/container"
|
||||
"gitlab.com/wpetit/goweb/service/session"
|
||||
@ -32,12 +30,13 @@ func proxyAPIRequest(w http.ResponseWriter, r *http.Request) {
|
||||
accessToken := sess.Get(middleware.SessionOAuth2AccessToken)
|
||||
|
||||
proxy := httputil.NewSingleHostReverseProxy(apiBaseURL)
|
||||
proxy.Director = func(r *http.Request) {
|
||||
r.Host = apiBaseURL.Host
|
||||
r.URL.Scheme = apiBaseURL.Scheme
|
||||
r.URL.Host = apiBaseURL.Host
|
||||
r.Header.Add("Authorization", fmt.Sprintf("token %s", accessToken))
|
||||
spew.Dump(r)
|
||||
proxy.Director = func(rr *http.Request) {
|
||||
rr.Host = apiBaseURL.Host
|
||||
rr.URL.Scheme = apiBaseURL.Scheme
|
||||
rr.URL.Host = apiBaseURL.Host
|
||||
rr.Method = r.Method
|
||||
rr.Header.Add("Accept", "application/json")
|
||||
rr.Header.Add("Authorization", fmt.Sprintf("token %s", accessToken))
|
||||
}
|
||||
|
||||
proxy.ServeHTTP(w, r)
|
||||
|
@ -4,8 +4,8 @@ import (
|
||||
"net/http"
|
||||
"path"
|
||||
|
||||
"forge.cadoles.com/wpetit/gitea-kan/internal/config"
|
||||
"forge.cadoles.com/wpetit/gitea-kan/internal/middleware"
|
||||
"forge.cadoles.com/wpetit/gengitkan/internal/config"
|
||||
"forge.cadoles.com/wpetit/gengitkan/internal/middleware"
|
||||
"github.com/go-chi/chi"
|
||||
"gitlab.com/wpetit/goweb/middleware/container"
|
||||
"gitlab.com/wpetit/goweb/static"
|
||||
@ -19,7 +19,10 @@ func Mount(r *chi.Mux, config *config.Config) {
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(middleware.Authenticate)
|
||||
r.Get("/logout", handleLogout)
|
||||
r.Get("/gitea/api/*", http.StripPrefix("/gitea", http.HandlerFunc(proxyAPIRequest)).ServeHTTP)
|
||||
r.Get("/api/boards", serveBoards)
|
||||
r.Post("/api/boards", saveBoard)
|
||||
r.Delete("/api/boards/{boardID}", deleteBoard)
|
||||
r.Handle("/gitea/api/*", http.StripPrefix("/gitea", http.HandlerFunc(proxyAPIRequest)))
|
||||
r.Get("/*", static.Dir(config.HTTP.PublicDir, "", html5PushStateHandler))
|
||||
})
|
||||
})
|
||||
|
@ -101,8 +101,7 @@ function release_server {
|
||||
|
||||
copy 'server' $os $arch "$PROJECT_DIR/README.md" "README.md"
|
||||
copy 'server' $os $arch "$PROJECT_DIR/LICENSE" "LICENSE"
|
||||
copy 'server' $os $arch "$PROJECT_DIR/cmd/server/public" "public"
|
||||
copy 'server' $os $arch "$PROJECT_DIR/cmd/server/template" "template"
|
||||
copy 'server' $os $arch "$PROJECT_DIR/client/dist" "public"
|
||||
|
||||
compress 'server' $os $arch
|
||||
|
||||
@ -110,6 +109,8 @@ function release_server {
|
||||
|
||||
function main {
|
||||
|
||||
make client-dist
|
||||
|
||||
for os in ${OS_TARGETS[@]}; do
|
||||
for arch in ${ARCH_TARGETS[@]}; do
|
||||
release_server $os $arch
|
||||
|
@ -5,7 +5,10 @@ modd.conf
|
||||
!mage_output_file.go {
|
||||
prep: make build
|
||||
prep: [ -e data/server.conf ] || ( mkdir -p data && bin/server -dump-config > data/server.conf )
|
||||
daemon: GITEA_APP_PUBDIR=./client/dist bin/server -config ./data/server.conf
|
||||
daemon: GENGITKAN_HTTP_PUBDIR=./client/dist \
|
||||
GENGITKAN_DATA_DBPATH=./data/data.db \
|
||||
bin/server \
|
||||
-config ./data/server.conf
|
||||
}
|
||||
|
||||
**/*.go {
|
||||
@ -13,6 +16,8 @@ modd.conf
|
||||
}
|
||||
|
||||
client/webpack.config.js
|
||||
client/tsconfig.json
|
||||
client/package.json
|
||||
{
|
||||
daemon: cd client && NODE_ENV=development npm run watch
|
||||
}
|
@ -1,17 +0,0 @@
|
||||
{{define "base"}}
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
<title>{{block "title" . -}}{{- end}}</title>
|
||||
<link rel="stylesheet" href="css/main.css">
|
||||
{{- block "head_style" . -}}{{end}}
|
||||
{{- block "head_script" . -}}{{end}}
|
||||
</head>
|
||||
<body>
|
||||
{{- block "body" . -}}{{- end -}}
|
||||
{{- block "body_script" . -}}{{end}}
|
||||
</body>
|
||||
</html>
|
||||
{{end}}
|
@ -1,8 +0,0 @@
|
||||
{{define "title"}}Gitea Kan{{end}}
|
||||
{{define "body"}}
|
||||
<div id="gitea-kan" class="is-fullheight"></div>
|
||||
{{end}}
|
||||
{{define "body_scripts"}}
|
||||
<script type="text/javascript" src="main.js"></script>
|
||||
{{end}}
|
||||
{{template "base" .}}
|
Reference in New Issue
Block a user