Base générale d'UI

This commit is contained in:
2019-12-01 22:12:13 +01:00
parent c6851f3f42
commit 048ef49933
49 changed files with 1913 additions and 88 deletions

View File

@ -0,0 +1,15 @@
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) {
return { type: SAVE_BOARD_REQUEST, board };
};

View File

@ -0,0 +1,23 @@
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) {
return { type: FETCH_ISSUES_REQUEST, project };
};
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, issueNumber, label) {
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, issueNumber, label) {
return { type: REMOVE_LABEL_REQUEST, project, issueNumber, label };
}

View 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) {
return { type: BUILD_KANBOARD_REQUEST, board };
};
export const MOVE_CARD = "MOVE_CARD";
export function moveCard(boardID, fromLaneID, fromPosition, toLaneID, toPosition) {
return { type: MOVE_CARD, boardID, fromLaneID, fromPosition, toLaneID, toPosition };
};

View File

@ -0,0 +1,5 @@
export const LOGOUT = "LOGOUT";
export function logout() {
return { type: LOGOUT };
};

View File

@ -0,0 +1,7 @@
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 function fetchProjects() {
return { type: FETCH_PROJECTS_REQUEST };
};

View File

@ -0,0 +1,41 @@
import { SAVE_BOARD_SUCCESS, FETCH_BOARDS_SUCCESS } from "../actions/boards";
export const defaultState = {
byID: {},
};
export function boardsReducer(state = defaultState, action) {
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, action) {
return {
...state,
byID: {
...state.byID,
[action.board.id.toString()]: {
...action.board,
}
}
};
}
function handleFetchBoardsSuccess(state, action) {
const boardsByID = action.boards.reduce((byID, board) => {
byID[board.id] = board;
return byID;
}, {});
return {
...state,
byID: {
...boardsByID,
}
};
}

View File

@ -0,0 +1,22 @@
const defaultState = {
actions: {}
};
export function flagsReducer(state = defaultState, action) {
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'
}
}
};
}

View File

@ -1,5 +1,27 @@
import { FETCH_ISSUES_SUCCESS } from "../actions/issues";
export function issuesReducer(state = {}, action) {
const defaultState = {
byProject: {}
};
return state;
export function issuesReducer(state = defaultState, action) {
switch(action.type) {
case FETCH_ISSUES_SUCCESS:
return handleFetchIssuesSuccess(state, action);
default:
return state;
}
}
function handleFetchIssuesSuccess(state, action) {
return {
...state,
byProject: {
...state.byProject,
[action.project]: [
...action.issues,
]
}
}
}

View File

@ -0,0 +1,74 @@
import { BUILD_KANBOARD_SUCCESS, MOVE_CARD } from "../actions/kanboards";
export const defaultState = {
byID: {},
};
export function kanboardsReducer(state = defaultState, action) {
switch(action.type) {
case BUILD_KANBOARD_SUCCESS:
return handleBuildKanboardSuccess(state, action);
case MOVE_CARD:
return handleMoveCard(state, action);
default:
return state;
}
}
function handleBuildKanboardSuccess(state, action) {
return {
...state,
byID: {
...state.byID,
[action.kanboard.id]: {
...action.kanboard,
}
}
};
}
function handleMoveCard(state, action) {
const {
boardID, fromLaneID,
fromPosition, toLaneID,
toPosition
} = action;
const kanboard = state.byID[boardID];
const lanes = [ ...kanboard.lanes ];
const fromLane = lanes[fromLaneID];
const toLane = lanes[toLaneID];
const card = fromLane.cards[fromPosition];
const fromCards = [ ...fromLane.cards ];
if (fromLaneID !== toLaneID) {
fromCards.splice(fromPosition, 1);
lanes[fromLaneID] = {
...fromLane,
cards: fromCards,
};
const toCards = [ ...toLane.cards ];
toCards.splice(toPosition, 0, card);
lanes[toLaneID] = {
...toLane,
cards: toCards,
};
} else {
fromCards.splice(fromPosition, 1);
fromCards.splice(toPosition, 0, card);
console.log(fromCards)
}
return {
...state,
byID: {
...state.byID,
[boardID]: {
...state.byID[boardID],
lanes,
},
}
};
}

View File

@ -0,0 +1,27 @@
import { FETCH_PROJECTS_SUCCESS } from "../actions/projects";
export const defaultState = {
byName: {},
};
export function projectsReducer(state = defaultState, action) {
switch(action.type) {
case FETCH_PROJECTS_SUCCESS:
return handleFetchProjectsSuccess(state, action);
default:
return state;
}
}
function handleFetchProjectsSuccess(state, action) {
const projectsByName = action.projects.reduce((byName, project) => {
byName[project.full_name] = project;
return byName;
}, {});
return {
...state,
byName: {
...projectsByName,
}
};
}

View File

@ -1,6 +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
});

View File

@ -0,0 +1,31 @@
import { put, call } from 'redux-saga/effects';
import { FETCH_BOARDS_SUCCESS, SAVE_BOARD_SUCCESS, SAVE_BOARD_FAILURE, FETCH_BOARDS_FAILURE } from '../actions/boards';
import { api } from '../../util/api';
const boardsLocalStorageKey = 'giteakan.boards';
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) {
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 });
}

View File

@ -1,4 +1,10 @@
import { GiteaUnauthorizedError } from "../../util/gitea";
import { LOGOUT } from "../actions/logout";
import { put } from 'redux-saga/effects';
export function* handleFailedActionSaga(action) {
console.error(action.error);
export function* failuresSaga(action) {
const err = action.error;
if (err instanceof GiteaUnauthorizedError) {
yield put({ type: LOGOUT });
}
}

View File

@ -0,0 +1,59 @@
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 } from '../actions/issues';
import { gitea } from '../../util/gitea';
export function* fetchIssuesSaga(action) {
const { project } = action;
let issues;
try {
issues = yield call(gitea.fetchIssues.bind(gitea), action.project);
} catch(error) {
yield put({ type: FETCH_ISSUES_FAILURE, project, error });
return;
}
yield put({ type: FETCH_ISSUES_SUCCESS, project, issues });
}
export function* addLabelSaga(action) {
const { project, issueNumber, label } = action;
const labels = yield call(gitea.fetchProjectLabels.bind(gitea), project);
const giteaLabel = labels.find(l => 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) {
const { project, issueNumber, label } = action;
const labels = yield call(gitea.fetchProjectLabels.bind(gitea), project);
const giteaLabel = labels.find(l => 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 });
}

View File

@ -0,0 +1,98 @@
import { select, put } from 'redux-saga/effects';
import { fetchIssues, addLabel, removeLabel } from '../actions/issues';
import { fetchIssuesSaga } from './issues';
import { BUILD_KANBOARD_SUCCESS } from '../actions/kanboards';
export function* moveCardSaga(action) {
const {
boardID, fromLaneID,
fromPosition, toLaneID,
toPosition,
} = action;
const { board, kanboard} = yield select(state => {
return {
kanboard: state.kanboards.byID[boardID],
board: state.boards.byID[boardID]
}
});
const toLane = kanboard.lanes[toLaneID];
const card = toLane.cards[toPosition];
if (!card) return;
yield put(addLabel(card.project, card.number, board.lanes[toLaneID].issueLabel));
yield put(removeLabel(card.project, card.number, board.lanes[fromLaneID].issueLabel));
}
export function* buildKanboardSaga(action) {
const { board } = action;
let kanboard;
try {
for (let p, i = 0; (p = board.projects[i]); i++) {
yield* fetchIssuesSaga(fetchIssues(p));
}
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 });
}
function createCards(projects, issues, lane) {
return projects.reduce((laneCards, p) => {
const projectIssues = p in issues.byProject ? issues.byProject[p] : [];
return projectIssues.reduce((projectCards, issue) => {
const hasLabel = issue.labels.some(l => l.name === lane.issueLabel);
if (hasLabel) {
projectCards.push({
id: issue.id,
title: `#${issue.number} - ${issue.title}`,
description: "",
project: p,
labels: issue.labels,
assignee: issue.assignee,
number: issue.number
});
}
return projectCards;
}, laneCards);
}, []);
}
function createLane(projects, issues, lane, index) {
return {
id: index,
title: lane.title,
cards: createCards(projects, issues, lane)
}
}
function createKanboard(board, issues) {
if (!board) return null;
const kanboard = {
id: board.id,
lanes: board.lanes.map(createLane.bind(null, board.projects, issues)),
};
return kanboard;
}

View File

@ -0,0 +1,3 @@
export function* logoutSaga() {
window.location = '/logout';
}

View File

@ -0,0 +1,16 @@
import { put, call } from 'redux-saga/effects';
import { FETCH_PROJECTS_SUCCESS, FETCH_PROJECTS_FAILURE } 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 });
}

View File

@ -1,9 +1,28 @@
import { all, takeEvery } from 'redux-saga/effects';
import { handleFailedActionSaga } from './failure';
import { all, takeEvery, takeLatest } from 'redux-saga/effects';
import { failuresSaga } from './failure';
import { FETCH_BOARDS_REQUEST, SAVE_BOARD_REQUEST } from '../actions/boards';
import { fetchBoardsSaga, saveBoardSaga } from './boards';
import { FETCH_ISSUES_REQUEST, ADD_LABEL_REQUEST, REMOVE_LABEL_REQUEST } from '../actions/issues';
import { fetchIssuesSaga, addLabelSaga, removeLabelSaga } from './issues';
import { FETCH_PROJECTS_REQUEST } from '../actions/projects';
import { fetchProjectsSaga } from './projects';
import { LOGOUT } from '../actions/logout';
import { logoutSaga } from './logout';
import { BUILD_KANBOARD_REQUEST, MOVE_CARD } from '../actions/kanboards';
import { buildKanboardSaga, moveCardSaga } from './kanboards';
export function* rootSaga() {
yield all([
takeEvery(patternFromRegExp(/^.*_FAILURE/), handleFailedActionSaga),
takeEvery(patternFromRegExp(/^.*_FAILURE/), failuresSaga),
takeLatest(FETCH_BOARDS_REQUEST, fetchBoardsSaga),
takeLatest(BUILD_KANBOARD_REQUEST, buildKanboardSaga),
takeLatest(SAVE_BOARD_REQUEST, saveBoardSaga),
takeLatest(FETCH_ISSUES_REQUEST, fetchIssuesSaga),
takeLatest(FETCH_PROJECTS_REQUEST, fetchProjectsSaga),
takeEvery(MOVE_CARD, moveCardSaga),
takeEvery(ADD_LABEL_REQUEST, addLabelSaga),
takeEvery(REMOVE_LABEL_REQUEST, removeLabelSaga),
takeLatest(LOGOUT, logoutSaga)
]);
}

View File

@ -0,0 +1,7 @@
export function selectFlagsIsLoading(state, ...actionPrefixes) {
const { actions } = state.flags;
return actionPrefixes.reduce((isLoading, prefix) => {
if (!(prefix in actions)) return isLoading;
return isLoading || actions[prefix].isLoading;
}, false);
};