├── .env
├── .dockerignore
├── Procfile
├── img
├── sidebar.png
└── main_window.png
├── public
├── icon.png
├── robots.txt
├── favicon.ico
├── logo192.png
├── logo512.png
├── wallpaper.jpg
├── manifest.json
├── electron.js
└── index.html
├── src
├── assets
│ └── ding.mp3
├── routes.js
├── setupTests.js
├── config.sample.js
├── App.test.js
├── nginx.conf
├── variables.scss
├── index.css
├── custom.scss
├── api
│ ├── account.js
│ ├── common.js
│ └── gitlab.js
├── index.js
├── start-react.js
├── components
│ ├── DropMenuItem.js
│ ├── SearchForm.js
│ ├── TodoCommentForm.js
│ ├── TodoListForm.js
│ ├── TodoForm.js
│ ├── Todo.js
│ └── TodoList.js
├── start.js
├── utils.js
├── logo.svg
├── App.scss
├── serviceWorker.js
└── App.js
├── electron-builder.yaml
├── copy.ps1
├── Dockerfile
├── .gitignore
├── Dockerfile.prod
├── dist.sh
├── docker-compose.sample.yaml
├── README.md
├── contributing.md
└── package.json
/.env:
--------------------------------------------------------------------------------
1 | BROWSER=none
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
1 | node_modules
--------------------------------------------------------------------------------
/Procfile:
--------------------------------------------------------------------------------
1 | react: npm run react-start
2 | electron: npm run electron-start
--------------------------------------------------------------------------------
/img/sidebar.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/danobot/gitlab_task_manager/HEAD/img/sidebar.png
--------------------------------------------------------------------------------
/public/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/danobot/gitlab_task_manager/HEAD/public/icon.png
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/img/main_window.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/danobot/gitlab_task_manager/HEAD/img/main_window.png
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/danobot/gitlab_task_manager/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/danobot/gitlab_task_manager/HEAD/public/logo192.png
--------------------------------------------------------------------------------
/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/danobot/gitlab_task_manager/HEAD/public/logo512.png
--------------------------------------------------------------------------------
/public/wallpaper.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/danobot/gitlab_task_manager/HEAD/public/wallpaper.jpg
--------------------------------------------------------------------------------
/src/assets/ding.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/danobot/gitlab_task_manager/HEAD/src/assets/ding.mp3
--------------------------------------------------------------------------------
/electron-builder.yaml:
--------------------------------------------------------------------------------
1 | appId: net.danielbkr.gitlab-task-manager
2 | publish:
3 | provider: s3
4 | bucket: ''
5 | acl: public-read
6 |
--------------------------------------------------------------------------------
/src/routes.js:
--------------------------------------------------------------------------------
1 | import { LIST_SEPARATOR } from "./config"
2 |
3 | export const getAccountPath = id => `/accounts/${id}`
4 | export const getTodoListPath = label => `/list/${label.split(LIST_SEPARATOR)[1]}`
--------------------------------------------------------------------------------
/src/setupTests.js:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import '@testing-library/jest-dom/extend-expect';
6 |
--------------------------------------------------------------------------------
/src/config.sample.js:
--------------------------------------------------------------------------------
1 | export const PROJECT_ID = 1
2 | export const LIST_SEPARATOR = '::'
3 | export const LABEL_STARRED = 'meta::starred'
4 | export const LABEL_MYDAY = 'meta::myday'
5 | export const LABEL_ARCHIVED = 'meta::archived'
6 |
7 | export const GITLAB_HOST = 'http://localhost'
8 | export const GITLAB_TOKEN = 'token'
9 |
--------------------------------------------------------------------------------
/src/App.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render } from '@testing-library/react';
3 | import App from './App';
4 |
5 | test('renders learn react link', () => {
6 | const { getByText } = render( );
7 | const linkElement = getByText(/learn react/i);
8 | expect(linkElement).toBeInTheDocument();
9 | });
10 |
--------------------------------------------------------------------------------
/copy.ps1:
--------------------------------------------------------------------------------
1 | robocopy . C:\Users\danie\Documents\repos\gitlab_task_manager_github /MIR /W:5 /XD node_modules /XD .git /XD dist /XD build /XD .vscode
2 |
3 | $msg = git log -1 --pretty=%B
4 | Push-Location "C:\Users\danie\Documents\repos\gitlab_task_manager_github"
5 |
6 | git add *
7 | git commit -m $msg
8 | git push
9 | Pop-Location
10 |
--------------------------------------------------------------------------------
/src/nginx.conf:
--------------------------------------------------------------------------------
1 | server {
2 |
3 | listen 80;
4 |
5 | location / {
6 | root /usr/share/nginx/html;
7 | index index.html index.htm;
8 | try_files $uri $uri/ /index.html;
9 | }
10 |
11 | error_page 500 502 503 504 /50x.html;
12 |
13 | location = /50x.html {
14 | root /usr/share/nginx/html;
15 | }
16 |
17 | }
--------------------------------------------------------------------------------
/src/variables.scss:
--------------------------------------------------------------------------------
1 |
2 | $base-color: rgba(26, 27, 28, 1);
3 |
4 | $base-color-lifted: rgb(37, 38, 39);
5 | $base-color-highlight: rgb(56, 58, 59);
6 | $base-color-input: rgb(45, 47, 48);
7 | $base-color-hover: rgba(150, 150, 150, 0.3);
8 | $text-color: rgba(217, 217, 217, 1);
9 | $textmuted-color: rgba(217, 217, 217, 0.4);
10 | $heading-color: rgba(120, 140, 222, 1);
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | # base image
2 | FROM node:12.2.0-alpine
3 |
4 | # set working directory
5 | WORKDIR /app
6 |
7 | # add `/app/node_modules/.bin` to $PATH
8 | ENV PATH /app/node_modules/.bin:$PATH
9 |
10 | # install and cache app dependencies
11 | COPY package.json /app/package.json
12 | RUN npm install --silent
13 | RUN npm install react-scripts@3.0.1 -g --silent
14 |
15 | # start app
16 | CMD ["npm", "start"]
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
5 | sans-serif;
6 | -webkit-font-smoothing: antialiased;
7 | -moz-osx-font-smoothing: grayscale;
8 | }
9 |
10 | code {
11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
12 | monospace;
13 | }
14 |
--------------------------------------------------------------------------------
/src/custom.scss:
--------------------------------------------------------------------------------
1 | @import "./variables.scss";
2 | .detail-footer {
3 | margin: 46px 0 0 0;
4 | padding: 10px 0px 0px 0px;
5 | height: 40px;
6 | display: block;
7 | position: fixed;
8 | bottom: 0;
9 | width: 100%;
10 | }
11 |
12 | .todo-p p {
13 | margin: 0;
14 | padding: 0;
15 | }
16 |
17 | .text-muted {
18 | color: $textmuted-color !important;
19 | }
20 |
21 | .search-bar {
22 | padding: 10px;
23 | background-color: $base-color-lifted;
24 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 | dist/*
25 | src/config.js
26 | docker-compose.yaml
27 |
--------------------------------------------------------------------------------
/src/api/account.js:
--------------------------------------------------------------------------------
1 | import {checkStatus, parseJSON, getClient} from './common';
2 |
3 |
4 | function findAll(cb) {
5 | getClient().then(c => {
6 | c.getAccounts().then(checkStatus)
7 | .then(parseJSON).then(cb)
8 | })
9 | // return fetch('/accounts', {
10 | // accept: 'application/json',
11 | // }).then(checkStatus)
12 | // .then(parseJSON)
13 | // .then(cb);
14 | }
15 |
16 |
17 |
18 |
19 | const Account = { findAll };
20 | export default Account;
--------------------------------------------------------------------------------
/Dockerfile.prod:
--------------------------------------------------------------------------------
1 | # build environment
2 | FROM node:12.2.0-alpine as build
3 | WORKDIR /app
4 | ENV PATH /app/node_modules/.bin:$PATH
5 | COPY package.json /app/package.json
6 | RUN npm install --silent
7 | RUN npm install react-scripts@3.0.1 -g --silent
8 | COPY . /app
9 | RUN npm run build
10 |
11 | # production environment
12 | FROM nginx:1.16.0-alpine
13 | COPY --from=build /app/build /usr/share/nginx/html
14 | RUN rm /etc/nginx/conf.d/default.conf
15 | COPY src/nginx.conf /etc/nginx/conf.d
16 | EXPOSE 80
17 | CMD ["nginx", "-g", "daemon off;"]
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "Gitlab Todo",
3 | "name": "Gitlab Todo",
4 | "icons": [
5 | {
6 | "src": "icon.png",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/png"
9 | },
10 | {
11 | "src": "icon.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "icon.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/dist.sh:
--------------------------------------------------------------------------------
1 | docker run --rm -ti --env-file <(env | grep -iE 'DEBUG|NODE_|ELECTRON_|YARN_|NPM_|CI|CIRCLE|TRAVIS_TAG|TRAVIS|TRAVIS_REPO_|TRAVIS_BUILD_|TRAVIS_BRANCH|TRAVIS_PULL_REQUEST_|APPVEYOR_|CSC_|GH_|GITHUB_|BT_|AWS_|STRIP|BUILD_') --env ELECTRON_CACHE="/root/.cache/electron" --env ELECTRON_BUILDER_CACHE="/root/.cache/electron-builder" -v ${PWD}:/project -v ${PWD##*/}-node-modules:/project/node_modules -v ~/.cache/electron:/root/.cache/electron -v ~/.cache/electron-builder:/root/.cache/electron-builder electronuserland/builder:wine /bin/bash /project/build.sh
2 | cp "./dist/gitlab_todo Setup 0.1.0.exe" "/mnt/user/other/Installers"
3 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import './index.css';
4 | import App from './App';
5 | import * as serviceWorker from './serviceWorker';
6 | import { DndProvider } from 'react-dnd'
7 | import Backend from 'react-dnd-html5-backend'
8 |
9 | ReactDOM.render(
10 |
11 | , document.getElementById('root'));
12 |
13 | // If you want your app to work offline and load faster, you can change
14 | // unregister() to register() below. Note this comes with some pitfalls.
15 | // Learn more about service workers: https://bit.ly/CRA-PWA
16 | serviceWorker.unregister();
17 |
--------------------------------------------------------------------------------
/docker-compose.sample.yaml:
--------------------------------------------------------------------------------
1 | version: '3.7'
2 |
3 | services:
4 | gitlab_task_manager:
5 | container_name: gitlab_task_manager
6 | build:
7 | context: .
8 | dockerfile: Dockerfile.prod
9 | volumes:
10 | - '.:/app'
11 | - '/app/node_modules'
12 | ports:
13 | - '3001:3000'
14 | labels:
15 | - "traefik.enable=true"
16 | - "traefik.docker.network=public"
17 | - "traefik.http.routers.todo.rule=Host(`app.example.com`)"
18 | - "traefik.http.routers.todo.entrypoints=https,http"
19 | - "traefik.http.routers.todo.tls.certresolver=myhttpchallenge"
20 | - "traefik.http.routers.todo.middlewares=danielauth@file,redirecthttps@file"
--------------------------------------------------------------------------------
/src/start-react.js:
--------------------------------------------------------------------------------
1 | const net = require('net')
2 | const childProcess = require('child_process')
3 |
4 | const port = process.env.PORT ? process.env.PORT - 100 : 3000
5 |
6 | process.env.ELECTRON_START_URL = `http://localhost:${port}`
7 |
8 | const client = new net.Socket()
9 |
10 | let startedElectron = false
11 | const tryConnection = () => {
12 | client.connect(
13 | { port },
14 | () => {
15 | client.end()
16 | if (!startedElectron) {
17 | console.log('starting electron')
18 | startedElectron = true
19 | const exec = childProcess.exec
20 | exec('npm run electron')
21 | }
22 | }
23 | )
24 | }
25 |
26 | tryConnection()
27 |
28 | client.on('error', () => {
29 | setTimeout(tryConnection, 1000)
30 | })
--------------------------------------------------------------------------------
/src/components/DropMenuItem.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | // import { useDrop } from 'react-dnd'
3 | import { Menu } from 'antd';
4 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
5 |
6 | const DropMenuItem = (props) => {
7 | let { label, icon,right, faicon } = props
8 | // const [{ canDrop, isOver }, drop] = useDrop({
9 | // accept: 'box',
10 | // drop: () => ({ name: 'Dustbin' }),
11 | // collect: monitor => ({
12 | // isOver: monitor.isOver(),
13 | // canDrop: monitor.canDrop(),
14 | // }),
15 | // })
16 | return
{faicon ? faicon : } {label}{right}
17 |
18 | }
19 |
20 | export default DropMenuItem;
--------------------------------------------------------------------------------
/src/start.js:
--------------------------------------------------------------------------------
1 | const electron = require('electron')
2 | const app = electron.app
3 | const BrowserWindow = electron.BrowserWindow
4 |
5 | const path = require('path')
6 | const url = require('url')
7 |
8 | let mainWindow
9 |
10 | function createWindow() {
11 | mainWindow = new BrowserWindow({ width: 800, height: 600 })
12 |
13 | mainWindow.loadURL(
14 | process.env.ELECTRON_START_URL ||
15 | url.format({
16 | pathname: path.join(__dirname, '/../public/index.html'),
17 | protocol: 'file:',
18 | slashes: true
19 | })
20 | )
21 |
22 | mainWindow.on('closed', () => {
23 | mainWindow = null
24 | })
25 | }
26 |
27 | app.on('ready', createWindow)
28 |
29 | app.on('window-all-closed', () => {
30 | if (process.platform !== 'darwin') {
31 | app.quit()
32 | }
33 | })
34 |
35 | app.on('activate', () => {
36 | if (mainWindow === null) {
37 | createWindow()
38 | }
39 | })
--------------------------------------------------------------------------------
/src/api/common.js:
--------------------------------------------------------------------------------
1 | import OpenAPIClientAxios from 'openapi-client-axios';
2 |
3 | export function checkStatus(response) {
4 | if (response.status >= 200 && response.status < 300) {
5 | return response;
6 | }
7 | const error = new Error(`HTTP Error ${response.statusText}`);
8 | error.status = response.statusText;
9 | error.response = response;
10 | console.log(error); // eslint-disable-line no-console
11 | throw error;
12 | }
13 |
14 | export function parseJSON(response) {
15 | return response.data;
16 | }
17 |
18 |
19 | const api = new OpenAPIClientAxios({ definition: 'http://localhost:3000/openapi.json' });
20 | api.init()
21 |
22 | // async function createTodo() {
23 | // const client = await api.getClient();
24 | // console.log(client)
25 | // const res = await client.getAccounts();
26 | // console.log('Accounts queried: ', res.data);
27 | // }
28 |
29 | export async function getClient() {
30 | return await api.getClient();
31 | }
--------------------------------------------------------------------------------
/public/electron.js:
--------------------------------------------------------------------------------
1 | const electron = require('electron');
2 | const autoUpdater = require("electron-updater").autoUpdater
3 | const app = electron.app;
4 | const BrowserWindow = electron.BrowserWindow;
5 | const log = require("electron-log")
6 |
7 | const path = require('path');
8 | const url = require('url');
9 | const isDev = require('electron-is-dev');
10 |
11 | let mainWindow;
12 |
13 | function createWindow() {
14 | log.transports.file.level = "debug"
15 | autoUpdater.logger = log
16 | autoUpdater.checkForUpdatesAndNotify()
17 | mainWindow = new BrowserWindow({width: 900, height: 680});
18 | mainWindow.loadURL(isDev ? 'http://localhost:3000' : `file://${path.join(__dirname, '../build/index.html')}`);
19 | mainWindow.on('closed', () => mainWindow = null);
20 | }
21 |
22 | app.on('ready', createWindow);
23 |
24 | app.on('window-all-closed', () => {
25 | if (process.platform !== 'darwin') {
26 | app.quit();
27 | }
28 | });
29 |
30 | app.on('activate', () => {
31 | if (mainWindow === null) {
32 | createWindow();
33 | }
34 | });
35 |
36 |
37 | autoUpdater.on('update-available', () => {
38 | mainWindow.webContents.send('update_available');
39 | });
40 | autoUpdater.on('update-downloaded', () => {
41 | mainWindow.webContents.send('update_downloaded');
42 | });
--------------------------------------------------------------------------------
/src/components/SearchForm.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Formik } from 'formik';
3 | import { Input, Button, Form, Col, Row } from 'antd';
4 | const SearchForm = ({ onSubmit }) => (
5 |
6 | {
9 | onSubmit(values)
10 | setSubmitting(false)
11 | }}
12 | onChange={c=> {
13 | onSubmit(c)
14 | }}
15 | >
16 | {({
17 | values,
18 | errors,
19 | touched,
20 | handleChange,
21 | handleBlur,
22 | handleSubmit,
23 | isSubmitting,
24 | }) => (
25 |
40 | )}
41 |
42 |
43 | );
44 |
45 | export default SearchForm;
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 | Gitlab Todo
28 |
29 |
30 | You need to enable JavaScript to run this app.
31 |
32 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/src/components/TodoCommentForm.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Formik } from "formik";
3 | import { Input, Form, message, Button } from "antd";
4 | import { addComment } from "../api/gitlab";
5 | const { TextArea } = Input;
6 | const TodoCommentForm = ({ issue, addCommentCb }) => (
7 |
8 | {
11 | if (values !== issue) {
12 | const comment = values.comment;
13 | values.comment = "";
14 | addComment(issue.project_id, issue.iid, comment)
15 | .then(r => {
16 | addCommentCb(r);
17 | setSubmitting(false);
18 | })
19 | .catch(e => {
20 | message.error("Cannor add comment right now.");
21 | console.log(e);
22 | setSubmitting(false);
23 | });
24 | }
25 | }}
26 | >
27 | {({
28 | values,
29 | errors,
30 | touched,
31 | handleChange,
32 | handleBlur,
33 | handleSubmit,
34 | isSubmitting
35 | /* and other goodies */
36 | }) => (
37 |
39 |
48 |
49 |
50 |
56 | Save
57 |
58 |
59 | )}
60 |
61 |
62 | );
63 |
64 | export default TodoCommentForm;
65 |
--------------------------------------------------------------------------------
/src/components/TodoListForm.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Formik } from 'formik';
3 | import { Input, Button, Form } from 'antd';
4 | const TodoListForm = ({ onSubmit }) => (
5 |
6 | {
9 | const errors = {};
10 |
11 | return errors;
12 | }}
13 | onSubmit={(values, { setSubmitting }) => {
14 | // console.log("onSubmit:: ", values)
15 |
16 | onSubmit(`list::${values.name}`, values.color)
17 | setSubmitting(false)
18 | }}
19 | >
20 | {({
21 | values,
22 | errors,
23 | touched,
24 | handleChange,
25 | handleBlur,
26 | handleSubmit,
27 | isSubmitting,
28 | /* and other goodies */
29 | }) => (
30 |
32 |
38 |
39 |
40 |
45 |
46 | Add
47 |
48 | )}
49 |
50 |
51 | );
52 |
53 | export default TodoListForm;
--------------------------------------------------------------------------------
/src/utils.js:
--------------------------------------------------------------------------------
1 | import { LIST_SEPARATOR, LABEL_ARCHIVED } from "./config";
2 | const ignoredLabels = ["ALL", LABEL_ARCHIVED];
3 |
4 | export const hasLabel = (issue, label) => {
5 | // console.log("hasLabel issue" ,issue.labels)
6 | // console.log("hasLabel label" ,label)
7 | const val = issue.labels.filter(l => l === label).length === 1;
8 | // console.log("hasLabel result" ,val)
9 | return val;
10 | };
11 |
12 | export const filterExcluded = issues => {
13 | return issues.filter(i => !i.labels.some(r=> ignoredLabels.indexOf(r) >= 0))
14 | }
15 | export const hasNoListLabel = issue => {
16 | const val =
17 | issue.labels.filter(l => l.indexOf("list") > -1 || l.indexOf("meta") > -1)
18 | .length === 0;
19 | return val;
20 | };
21 | export const removeMetaLabels = labels => {
22 | const val = labels.filter(
23 | l => l.indexOf("list") === -1 && l.indexOf("meta") === -1
24 | );
25 |
26 | return val;
27 | };
28 | export const getActualLabelName = label => {
29 | return label.name.split(LIST_SEPARATOR)[1];
30 | };
31 |
32 | export const titleCase = string => {
33 | var sentence = string.toLowerCase().split(" ");
34 | for (var i = 0; i < sentence.length; i++) {
35 | sentence[i] = sentence[i][0].toUpperCase() + sentence[i].slice(1);
36 | }
37 | return sentence;
38 | };
39 | // problem is that when adding the myday label then all othe labels are removed
40 | export const extractLabels = (title, list) => {
41 | const regex = /#(\w*)/gm;
42 | let m;
43 | let labels = [];
44 | if (ignoredLabels.indexOf(list) === -1) {
45 | labels.push(list);
46 | }
47 | while ((m = regex.exec(title)) !== null) {
48 | console.log(m);
49 | // This is necessary to avoid infinite loops with zero-width matches
50 | if (m.index === regex.lastIndex) {
51 | regex.lastIndex++;
52 | }
53 | m.forEach((match, groupIndex) => {
54 | console.log(`Found match, group ${groupIndex}: ${match}`);
55 | if (groupIndex > 0) {
56 | labels.push(match);
57 | title = title.replace(' #' + match, '');
58 | }
59 | });
60 | }
61 | return { title, labels };
62 | }
--------------------------------------------------------------------------------
/src/components/TodoForm.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Formik } from 'formik';
3 | import { Input,Form, message } from 'antd';
4 | import { editTodo } from '../api/gitlab';
5 | const { TextArea } = Input;
6 | const TodoForm = ({ issue, updateTodo, labels }) => (
7 | {
12 | const errors = {};
13 |
14 | return errors;
15 | }}
16 | onSubmit={(values, { setSubmitting }) => {
17 | if (values !== issue) {
18 | console.log("onSubmit:: ", values)
19 |
20 | editTodo(issue.project_id, issue.iid, values).then(r => {
21 | console.log("Submitted: ", r)
22 | message.success("Todo updated.")
23 | setSubmitting(false);
24 | updateTodo(r)
25 | }).catch(e => {
26 | message.error("Cannor update todo right now.")
27 | console.log(e)
28 | setSubmitting(false);
29 | })
30 | }
31 |
32 | }}
33 | >
34 | {({
35 | values,
36 | errors,
37 | touched,
38 | handleChange,
39 | handleBlur,
40 | handleSubmit,
41 | isSubmitting,
42 | /* and other goodies */
43 | }) => (
44 |
57 | )}
58 |
59 | );
60 |
61 | export default TodoForm;
--------------------------------------------------------------------------------
/src/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
10 | Elegant Microsoft To-Do inspired desktop and web app leveraging Gitlab's Issue Tracker in the backend
11 |
12 |
13 |
14 | Gitlab Task Manager is a task manager application which hooks into Gitlab's Issue Tracker as the backend. Simply create a project on `gitlab.com` or your personal Gitlab instance, generate an API token and you are ready to start using GTM.
15 | ## Features
16 |
17 | * Uses Gitlab labels to manage task lists
18 | * Add tasks to "My Day"
19 | * Star tasks to mark them as important
20 | * Clear all completed tasks
21 | * Leave comments on tasks that appear as comments in Gitlab
22 | * Create new task lists from the app
23 | * Assign task labels using hashtags (e.g. "Get Milk #shopping" )
24 | * View all issues with a particular label by clicking on the label itself
25 |
26 |
31 |
32 | ## Motivation
33 | As I work on many hobby projects at the same time, I felt the need to add comments to a task as a place to note down decisions, research and root cause analysis findings. I really liked the look and feel of Microsoft Todo and the functionality of Gitlab's issue tracker and wanted to combine them into one easy-to-use tool.
34 |
35 | ## Implementation
36 | This application was bootstrapped using Create React App and is distributed as a Docker web application as well as an Electron Desktop App.
37 |
38 |
39 | ## Getting Started
40 | Copy `src/config.sample.js` to `src/config.js` and modify the Gitlab values.
41 |
42 | ```bash
43 | yarn
44 | yarn start
45 | ```
46 |
47 | ## Available Yarn/NPM Scripts
48 |
49 | In the project directory, you can run:
50 |
51 | * Run for development `yarn dev`
52 | * Build the electron app `yarn dist` (If this fails, run `npm install` first.)
53 | * Build and publish Electron App `yarn dist`
54 | * Run Electron build on Linux using Docker `bash dist.sh`
55 | * Build docker image `yarn docker-build`
56 |
57 | ## Docker Deployment
58 | You can deploy the webapplication using docker for use on mobile. Rename `docker-compose.sample.yaml` to `docker-compose.yaml`.
59 |
60 | ```
61 | docker-compose up -d
62 | ```
63 | This will build the image and deploy.
64 |
65 | ## Development
66 |
67 | `yarn start` Runs the app in the development mode. Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
68 |
69 | The page will reload if you make edits.
70 | You will also see any lint errors in the console.
71 |
72 | ## Contributing
73 |
74 | See [contributing.md](contributing.md)
75 |
--------------------------------------------------------------------------------
/contributing.md:
--------------------------------------------------------------------------------
1 | # Contributing to GTM
2 |
3 | Thank you for taking the time to contribute to GTM!
4 |
5 | ## How to contribute
6 |
7 | ### Improve documentation
8 |
9 | Typo corrections, error fixes, better explanations, more examples etc. Open an issue regarding anything that you think it could be improved! You can use the [`docs` label](https://github.com/danobot/gitlab_task_manager/issues/labels) to find out what others have suggested!
10 |
11 | ### Improve issues
12 |
13 | Sometimes reported issues lack information, are not reproducible, or are even plain invalid. Help us out to make them easier to resolve. Handling issues takes a lot of time that we could rather spend on adding features.
14 |
15 | ### Give feedback on issues
16 |
17 | We're always looking for more opinions on discussions in the issue tracker. It's a good opportunity to influence the future direction of the project.
18 |
19 | The [`question` label](https://github.com/danobot/gitlab_task_manager/issues/labels/question) is a good place to find ongoing discussions.
20 |
21 | ### Write code
22 |
23 | You can use issue labels to discover issues you could help us out with!
24 |
25 | - [`enhancement` issues](https://github.com/danobot/gitlab_task_manager/issues/labels/enhancement) are features we are open to including
26 | - [`bug` issues](https://github.com/danobot/gitlab_task_manager/issues/labels/bug) are known bugs we would like to fix
27 | - [`future` issues](https://github.com/danobot/gitlab_task_manager/issues/labels/future) are those that we'd like to get to, but not anytime soon. Please check before working on these since we may not yet want to take on the burden of supporting those features
28 | - on the [`help wanted`](https://github.com/danobot/gitlab_task_manager/issues/labels/help%20wanted) label you can always find something exciting going on
29 |
30 | You may find an issue is assigned. Please double-check before starting on this issue because somebody else is likely already working on it
31 |
32 |
33 | ### Submitting an issue
34 |
35 | - Search the issue tracker before opening an issue
36 | - Ensure you're using the latest version of GTM
37 | - Use a descriptive title
38 | - Include as much information as possible;
39 | - Steps to reproduce the issue
40 | - Error message
41 | - GTM version
42 | - Operating system **etc**
43 |
44 | ### Submitting a pull request
45 |
46 | - Non-trivial changes are often best discussed in an issue first, to prevent you from doing unnecessary work
47 | - Try making the pull request from a [topic branch](https://github.com/dchelimsky/rspec/wiki/Topic-Branches) if it is of crucial importance
48 | - Use a descriptive title for the pull request and commits
49 | - You might be asked to do changes to your pull request, you can do that by just [updating the existing one](https://github.com/RichardLitt/docs/blob/master/amending-a-commit-guide.md)
50 |
51 | > Inspired by project [AVA](https://github.com/avajs/ava/blob/master/contributing.md)'s contributing.md
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "gitlab_todo",
3 | "version": "0.1.0",
4 | "private": true,
5 | "author": "danobot",
6 | "description": "todo app",
7 | "repository": {
8 | "type": "git",
9 | "url": "https://github.com/danobot/notorious.git"
10 | },
11 | "dependencies": {
12 | "@fortawesome/fontawesome-svg-core": "^1.2.27",
13 | "@fortawesome/free-regular-svg-icons": "^5.12.1",
14 | "@fortawesome/free-solid-svg-icons": "^5.12.1",
15 | "@fortawesome/react-fontawesome": "^0.1.8",
16 | "@testing-library/jest-dom": "^4.2.4",
17 | "@testing-library/react": "^9.4.0",
18 | "@testing-library/user-event": "^7.2.1",
19 | "antd": "^3.26.9",
20 | "electron-is-dev": "^1.1.0",
21 | "electron-log": "^4.0.6",
22 | "electron-store": "^5.1.0",
23 | "electron-updater": "^4.2.2",
24 | "formik": "^2.1.4",
25 | "gitlab": "^14.2.2",
26 | "node-sass": "^4.13.1",
27 | "openapi-client-axios": "^3.3.2",
28 | "react": "^16.12.0",
29 | "react-custom-scrollbars": "^4.2.1",
30 | "react-dnd": "^10.0.2",
31 | "react-dnd-html5-backend": "^10.0.2",
32 | "react-dom": "^16.12.0",
33 | "react-moment": "^0.9.7",
34 | "react-router-dom": "^5.1.2",
35 | "react-scripts": "3.4.0",
36 | "react-sidebar": "^3.0.2",
37 | "standard-version": "^7.1.0",
38 | "uifx": "^2.0.7"
39 | },
40 | "scripts": {
41 | "start": "react-scripts start",
42 | "build": "react-scripts build",
43 | "test": "react-scripts test --env=jsdom",
44 | "eject": "react-scripts eject",
45 | "electron": "electron .",
46 | "dev": "concurrently \" yarn start\" \"wait-on http://localhost:3000 && electron .\"",
47 | "react-start": "react-scripts start",
48 | "dist": "yarn build && electron-builder build --win --publish never",
49 | "dist-publish": "yarn build && electron-builder build --win --publish always",
50 | "postinstall": "install-app-deps",
51 | "dockerbuild": "docker build -t danobot/gitlab_todo .",
52 | "release": "standard-version -a --no-verify --prerelease alpha --skip.commit"
53 | },
54 | "eslintConfig": {
55 | "extends": "react-app"
56 | },
57 | "proxy": "http://localhost:3000/",
58 | "browserslist": {
59 | "production": [
60 | ">0.2%",
61 | "not dead",
62 | "not op_mini all"
63 | ],
64 | "development": [
65 | "last 1 chrome version",
66 | "last 1 firefox version",
67 | "last 1 safari version"
68 | ]
69 | },
70 | "devDependencies": {
71 | "concurrently": "^5.1.0",
72 | "electron": "^8.0.1",
73 | "electron-builder": "^22.3.2",
74 | "electron-packager": "^14.2.1",
75 | "openapi-client": "^1.0.5",
76 | "wait-on": "^4.0.0"
77 | },
78 | "homepage": "./",
79 | "main": "public/electron.js",
80 | "build": {
81 | "appId": "net.danielbkr.gitlab_todo",
82 | "files": [
83 | "build/**/*",
84 | "package.json",
85 | "./public/electron.js"
86 | ],
87 | "directories": {
88 | "buildResources": "assets"
89 | },
90 | "win": {}
91 | },
92 | "standard-version": {
93 | "scripts": {
94 | "release": "git add package.json",
95 | "precommit": "echo \"[skip ci]\""
96 | },
97 | "skip": {
98 | "tag": true
99 | }
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/src/api/gitlab.js:
--------------------------------------------------------------------------------
1 | import { ProjectsBundle } from 'gitlab';
2 | import {GITLAB_HOST, GITLAB_TOKEN, LABEL_STARRED, LABEL_MYDAY, LABEL_ARCHIVED } from '../config.js'
3 | export const gitlab = new ProjectsBundle({
4 | host: GITLAB_HOST,
5 | token: GITLAB_TOKEN,
6 | });
7 |
8 |
9 | export async function markDone(project, issue) {
10 | return gitlab.Issues.edit(project,issue, { state_event: 'close'})
11 | }
12 | export async function markTodo(project, issue) {
13 | return gitlab.Issues.edit(project,issue, { state_event: 'reopen'})
14 | }
15 |
16 | export async function editTodo(project, issue, data) {
17 | return gitlab.Issues.edit(project,issue, data)
18 | }
19 | export async function addComment(project, issue, data) {
20 | return gitlab.IssueNotes.create(project,issue, data)
21 | }
22 | export async function deleteIssue(project, issue) {
23 | return gitlab.Issues.remove(project,issue.iid)
24 | }
25 | export async function getComments(project, issue) {
26 | console.log("Getting comments for : issue: " + issue + " on project " + project)
27 | return gitlab.IssueNotes.all(project,issue) // issue id somehow incorrect
28 | }
29 | export async function createIssue(project, data) {
30 | return gitlab.Issues.create(project,data)
31 | }
32 | export async function starIssue(project, issue) {
33 | console.log("starIssue" , issue)
34 | return addLabel(project, issue, LABEL_STARRED)
35 | }
36 | export async function unstarIssue(project, issue) {
37 | console.log("unstarIssue" , issue)
38 | return removeLabel(project, issue, LABEL_STARRED)
39 | }
40 | export async function getWallpaper() {
41 | console.log("getWallpaper")
42 | return fetch("https://www.bing.com/HPImageArchive.aspx?format=js&idx=0&n=1&mkt=en-AU",
43 | {
44 | host: 'www.bing.com',
45 | method: 'GET', // *GET, POST, PUT, DELETE, etc.
46 | mode: 'no-cors', // no-cors, , same-origin
47 | credentials: 'include', // include, *same-origin, omit
48 | accept: '*/*',
49 | headers: {
50 | 'Content-Type': 'application/json; charset=utf-8',
51 | },
52 | redirect: 'follow', // manual, *follow, error
53 | referrerPolicy: 'no-referrer', // no-referrer, *client
54 | userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36'
55 | }).then(r=> {
56 | console.log("Wallpaper response: ", r)
57 | })
58 | }
59 |
60 | export async function addIssueToMyDay(project, issue) {
61 | return addLabel(project, issue, LABEL_MYDAY)
62 | }
63 | export async function removeIssueFromMyDay(project, issue) {
64 |
65 | return removeLabel(project, issue, LABEL_MYDAY)
66 | }
67 | export async function addIssueArchive(project, issue) {
68 | return addLabel(project, issue, LABEL_ARCHIVED)
69 | }
70 | export async function removeIssueArchive(project, issue) {
71 |
72 | return removeLabel(project, issue, LABEL_ARCHIVED)
73 | }
74 |
75 | export async function addLabel(project, issue, label) {
76 | // console.log("addLabel() issue: ", issue)
77 | let c = issue.labels;
78 | // console.log("addLabel() original labels: ", issue.labels)
79 | c.push(label);
80 | // console.log("addLabel() new labels: ", c)
81 | return gitlab.Issues.edit(project, issue.iid, {labels: c})
82 | }
83 | export async function removeLabel(project, issue, label) {
84 | console.log("issue.labels.indexOf(label)", issue.labels.indexOf(label))
85 | let temp = issue.labels
86 | const splicedPart = temp.splice(temp.indexOf(label),1)
87 | console.log("afterSplice",temp)
88 | return gitlab.Issues.edit(project, issue.iid, {labels: temp})
89 | }
90 | // this.state.issue is not updated when the label is added or removed
91 | // when you reselect the same issue, then the side pane is updated
--------------------------------------------------------------------------------
/src/App.scss:
--------------------------------------------------------------------------------
1 | @import "~antd/dist/antd.css";
2 | @import "./variables.scss";
3 |
4 | .App {
5 | text-align: center;
6 | }
7 |
8 | .ant-drawer-body {
9 | padding: 0;
10 | height: 100%;
11 | overflow: hidden;
12 | }
13 | .heading-color {
14 | color: $heading-color;
15 | }
16 | .ant-timeline-item-head {
17 | color: $heading-color;
18 | }
19 | .ant-timeline-item-head-blue {
20 | border-color: $base-color-highlight;
21 | }
22 | .todo-row:hover {
23 | background-color: $base-color-hover;
24 | }
25 | .todo-row {
26 | background: $base-color-lifted;
27 | color: $text-color;
28 | margin: 0px 15px 2px 15px;
29 | padding: 18px 13px;
30 | }
31 | .sidebar-right {
32 | background-color: $base-color-lifted;
33 | }
34 | .ant-tag {
35 | border: none;
36 | background: $base-color-highlight;
37 | color: $heading-color;
38 | &.todo-labels {
39 | cursor: pointer;
40 | }
41 | }
42 | .vertical-center {
43 | margin: 0;
44 | position: absolute;
45 | top: 50%;
46 | -ms-transform: translateY(-50%);
47 | transform: translateY(-50%);
48 | }
49 | .ant-layout {
50 | height: 100%;
51 | background: unset;
52 | color: $text-color;
53 | transition: transform 0.3s cubic-bezier(0.7, 0.3, 0.1, 1), box-shadow 0.3s cubic-bezier(0.7, 0.3, 0.1, 1), -webkit-transform 0.3s cubic-bezier(0.7, 0.3, 0.1, 1), -webkit-box-shadow 0.3s cubic-bezier(0.7, 0.3, 0.1, 1);
54 | }
55 | .sidebar * {
56 | color: $text-color;
57 |
58 | }
59 | .ant-menu-item {
60 | cursor: default;
61 | }
62 | .ant-menu.ant-menu-dark .ant-menu-item-selected, .ant-menu-submenu-popup.ant-menu-dark .ant-menu-item-selected {
63 | background-color: $heading-color;
64 | cursor: default;
65 | }
66 | .ant-btn, .ant-btn:active, .ant-btn.active, .ant-btn:focus {
67 | background: $base-color-hover;
68 | color: $text-color;
69 | border: none;
70 | }
71 | .ant-btn:hover {
72 | background: $base-color-highlight;
73 | }
74 | .ant-input {
75 | background: $base-color-input;
76 | border: none;
77 | color: $text-color;
78 |
79 | }
80 | .ant-popover {
81 | color: $text-color !important;
82 |
83 | .ant-popover-inner {
84 | background-color: $base-color-highlight !important;
85 | }
86 | .ant-popover-title {
87 | color: $heading-color;
88 | }
89 | }
90 | .ant-input:focus,
91 | .ant-input:hover {
92 | background: $base-color-highlight;
93 | border: none;
94 | outline: none;
95 | }
96 | .ant-list-item {
97 | justify-content: unset;
98 | }
99 |
100 | .ant-card-body {
101 | padding-left: 0px;
102 | padding-right: 0px;
103 | }
104 | html,
105 | body,
106 | #root {
107 | height: 100%;
108 | margin: 0px;
109 | overflow: hidden;
110 | }
111 | body,
112 | .ant-card,
113 | .ant-page-header,
114 | .ant-typography,
115 | .ant-menu-dark,
116 | .ant-menu-dark .ant-menu-sub,
117 | .ant-page-header-heading-title,
118 | .ant-statistic-content,
119 | .ant-statistic {
120 | color: $text-color !important;
121 |
122 | background: $base-color;
123 | }
124 | .ant-timeline-item {
125 | padding: 0 0 5px;
126 | }
127 | h2.ant-typography,
128 | .ant-typography h2 {
129 | color: $heading-color !important;
130 | }
131 | .ant-menu-dark,
132 | .ant-menu-dark .ant-menu-sub {
133 | box-shadow: none;
134 | background: $base-color-lifted;
135 | }
136 | .ant-menu-dark .ant-menu-inline.ant-menu-sub {
137 | background: $base-color-lifted;
138 | box-shadow: none;
139 | }
140 | .root {
141 | min-height: 100%;
142 | bottom: 0;
143 | top: 0;
144 | position: absolute;
145 | right: 0;
146 | left: 0;
147 | }
148 |
149 |
150 |
151 |
152 | .main-menu-class {
153 | background: $base-color-lifted;
154 | }
155 | // Custom
156 | .menu-right {
157 | float: right;
158 | }
159 |
160 | .resync .ant-page-header-heading-extra {
161 | padding-top: 12px;
162 | display: block;
163 | width: unset;
164 | float: right;
165 | margin-bottom: 0.5em;
166 | margin-top: -6px;
167 | }
--------------------------------------------------------------------------------
/src/components/Todo.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { markDone, markTodo, unstarIssue, starIssue } from "../api/gitlab";
3 | import { Skeleton, message, Row, Tag } from "antd";
4 | import { useDrag } from "react-dnd";
5 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
6 | import { LIST_SEPARATOR } from "../config";
7 | import {
8 | faCircle,
9 | faStar as faStarRegular
10 | } from "@fortawesome/free-regular-svg-icons";
11 | import {
12 | faStar as faStarSolid,
13 | faCheckCircle
14 | } from "@fortawesome/free-solid-svg-icons";
15 | import { hasLabel, removeMetaLabels } from "../utils";
16 | import UIfx from "uifx";
17 | import tickAudio from "../assets/ding.mp3";
18 |
19 | const Checko = ({ checked, onClick }) => {
20 | return (
21 |
22 | {checked === "closed" && (
23 |
24 | )}
25 | {checked === "opened" && (
26 |
27 | )}
28 | {checked === "hover" && (
29 |
30 | )}
31 |
32 | );
33 | };
34 | const Todo = ({ onUpdate, onSelect, issue, onSelectLabel }) => {
35 | const [{ isDragging }] = useDrag({
36 | item: { name: issue.id, type: "box" },
37 | end: (item, monitor) => {
38 | const dropResult = monitor.getDropResult();
39 | if (item && dropResult) {
40 | alert(`You dropped ${item.name} into ${dropResult.name}!`);
41 | }
42 | },
43 | collect: monitor => ({
44 | isDragging: monitor.isDragging()
45 | })
46 | });
47 |
48 | const handleClick = issue => {
49 | if (issue && issue.state !== "closed") {
50 | onUpdate({ ...issue, state: "closed" });
51 | const beep = new UIfx(tickAudio);
52 | beep.play();
53 | markDone(issue.project_id, issue.iid)
54 | .then(d => {
55 | onUpdate(d);
56 | message.success(`Marked task ${issue.iid} as completed`);
57 | })
58 | .catch(e => {
59 | console.log(e);
60 | onUpdate(issue);
61 | message.error(`Task ${issue.iid} could not be updated`);
62 | });
63 | } else {
64 | onUpdate({ ...issue, state: "opened" });
65 |
66 | markTodo(issue.project_id, issue.iid)
67 | .then(d => {
68 | onUpdate(d);
69 | message.success(`Marked task ${issue.iid} as todo`);
70 | })
71 | .catch(e => {
72 | message.error(`Task ${issue.iid} could not be updated`);
73 | onUpdate(issue);
74 | });
75 | }
76 | };
77 | const starred = hasLabel(issue, "meta" + LIST_SEPARATOR + "starred");
78 | const myDay = hasLabel(issue, "meta" + LIST_SEPARATOR + "myday");
79 |
80 | const handleStarClick = () => {
81 | console.log("Stargging issue");
82 | if (starred) {
83 | unstarIssue(issue.project_id, issue).then(i => {
84 | onUpdate(i);
85 | });
86 | } else {
87 | starIssue(issue.project_id, issue).then(i => {
88 | onUpdate(i);
89 | });
90 | }
91 | };
92 | let todoStyle = {
93 | marginLeft: "40px",
94 | padding: "18px 0px 18px 13px",
95 | position: "absolute",
96 | right: "32px",
97 | left: "5px"
98 | };
99 |
100 | if (issue.state === "closed") {
101 | todoStyle.textDecoration = "line-through";
102 | todoStyle.color = "darkgray";
103 | }
104 | let rowStyle = {
105 | // border: "gray 2px solid",
106 | borderRadius: "4px",
107 | transition: "all 0.3s",
108 | }
109 | if (myDay) {
110 | todoStyle.fontWeight = "bold";
111 | }
112 |
113 | return issue && !isDragging ? (
114 |
118 | handleClick(issue)}
121 | style={{ marginLeft: "3px" }}
122 | >
123 |
124 |
125 | onSelect(issue)}
128 | style={todoStyle}
129 | >
130 |
{issue.title}
131 |
132 | {removeMetaLabels(issue.labels).map(l => (
133 | onSelectLabel(l)}
137 | >
138 | {l}
139 |
140 | ))}
141 |
142 |
143 |
144 | {
146 | handleStarClick();
147 | }}
148 | style={{ fontSize: "12pt", float: "right" }}
149 | >
150 | {starred && }
151 | {!starred && }
152 |
153 |
154 | ) : (
155 |
156 | );
157 | };
158 |
159 | export default Todo;
160 |
--------------------------------------------------------------------------------
/src/serviceWorker.js:
--------------------------------------------------------------------------------
1 | // This optional code is used to register a service worker.
2 | // register() is not called by default.
3 |
4 | // This lets the app load faster on subsequent visits in production, and gives
5 | // it offline capabilities. However, it also means that developers (and users)
6 | // will only see deployed updates on subsequent visits to a page, after all the
7 | // existing tabs open on the page have been closed, since previously cached
8 | // resources are updated in the background.
9 |
10 | // To learn more about the benefits of this model and instructions on how to
11 | // opt-in, read https://bit.ly/CRA-PWA
12 |
13 | const isLocalhost = Boolean(
14 | window.location.hostname === 'localhost' ||
15 | // [::1] is the IPv6 localhost address.
16 | window.location.hostname === '[::1]' ||
17 | // 127.0.0.0/8 are considered localhost for IPv4.
18 | window.location.hostname.match(
19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
20 | )
21 | );
22 |
23 | export function register(config) {
24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
25 | // The URL constructor is available in all browsers that support SW.
26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
27 | if (publicUrl.origin !== window.location.origin) {
28 | // Our service worker won't work if PUBLIC_URL is on a different origin
29 | // from what our page is served on. This might happen if a CDN is used to
30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374
31 | return;
32 | }
33 |
34 | window.addEventListener('load', () => {
35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
36 |
37 | if (isLocalhost) {
38 | // This is running on localhost. Let's check if a service worker still exists or not.
39 | checkValidServiceWorker(swUrl, config);
40 |
41 | // Add some additional logging to localhost, pointing developers to the
42 | // service worker/PWA documentation.
43 | navigator.serviceWorker.ready.then(() => {
44 | console.log(
45 | 'This web app is being served cache-first by a service ' +
46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA'
47 | );
48 | });
49 | } else {
50 | // Is not localhost. Just register service worker
51 | registerValidSW(swUrl, config);
52 | }
53 | });
54 | }
55 | }
56 |
57 | function registerValidSW(swUrl, config) {
58 | navigator.serviceWorker
59 | .register(swUrl)
60 | .then(registration => {
61 | registration.onupdatefound = () => {
62 | const installingWorker = registration.installing;
63 | if (installingWorker == null) {
64 | return;
65 | }
66 | installingWorker.onstatechange = () => {
67 | if (installingWorker.state === 'installed') {
68 | if (navigator.serviceWorker.controller) {
69 | // At this point, the updated precached content has been fetched,
70 | // but the previous service worker will still serve the older
71 | // content until all client tabs are closed.
72 | console.log(
73 | 'New content is available and will be used when all ' +
74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
75 | );
76 |
77 | // Execute callback
78 | if (config && config.onUpdate) {
79 | config.onUpdate(registration);
80 | }
81 | } else {
82 | // At this point, everything has been precached.
83 | // It's the perfect time to display a
84 | // "Content is cached for offline use." message.
85 | console.log('Content is cached for offline use.');
86 |
87 | // Execute callback
88 | if (config && config.onSuccess) {
89 | config.onSuccess(registration);
90 | }
91 | }
92 | }
93 | };
94 | };
95 | })
96 | .catch(error => {
97 | console.error('Error during service worker registration:', error);
98 | });
99 | }
100 |
101 | function checkValidServiceWorker(swUrl, config) {
102 | // Check if the service worker can be found. If it can't reload the page.
103 | fetch(swUrl, {
104 | headers: { 'Service-Worker': 'script' }
105 | })
106 | .then(response => {
107 | // Ensure service worker exists, and that we really are getting a JS file.
108 | const contentType = response.headers.get('content-type');
109 | if (
110 | response.status === 404 ||
111 | (contentType != null && contentType.indexOf('javascript') === -1)
112 | ) {
113 | // No service worker found. Probably a different app. Reload the page.
114 | navigator.serviceWorker.ready.then(registration => {
115 | registration.unregister().then(() => {
116 | window.location.reload();
117 | });
118 | });
119 | } else {
120 | // Service worker found. Proceed as normal.
121 | registerValidSW(swUrl, config);
122 | }
123 | })
124 | .catch(() => {
125 | console.log(
126 | 'No internet connection found. App is running in offline mode.'
127 | );
128 | });
129 | }
130 |
131 | export function unregister() {
132 | if ('serviceWorker' in navigator) {
133 | navigator.serviceWorker.ready.then(registration => {
134 | registration.unregister();
135 | });
136 | }
137 | }
138 |
--------------------------------------------------------------------------------
/src/App.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import "./App.scss";
3 | import "./custom.scss";
4 | import { Layout, Menu, Popover, message, Drawer, Button} from "antd";
5 |
6 | import TodoList from "./components/TodoList";
7 | import { gitlab } from "./api/gitlab";
8 | import TodoListForm from "./components/TodoListForm";
9 | import SearchForm from "./components/SearchForm";
10 | import { LIST_SEPARATOR, PROJECT_ID, LABEL_ARCHIVED } from "./config";
11 | import DropMenuItem from "./components/DropMenuItem";
12 | import { faSquare, faStar, faTasks, faCalendarDay, faBars, faChevronCircleLeft } from "@fortawesome/free-solid-svg-icons";
13 | import { faPlusSquare } from "@fortawesome/free-regular-svg-icons";
14 | import Sidebar from "react-sidebar";
15 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
16 | import { getActualLabelName, hasLabel, hasNoListLabel, filterExcluded } from "./utils";
17 |
18 | const { Content, Sider } = Layout;
19 |
20 | class App extends React.Component {
21 | state = {
22 | accounts: [],
23 | addListVisible: false,
24 | allIssues: [],
25 | search: null,
26 | sidebarVisible: true,
27 | docked: false
28 | };
29 | componentDidMount = () => {
30 | // Get all labels
31 | console.log("compo")
32 | let localLabels = JSON.parse(localStorage.getItem("labels"));
33 | // console.log("Using local labels", localLabels);
34 | if (localLabels != null && localLabels.length > 0) {
35 | // console.log("Using local labels", localLabels);
36 | const selectedList = JSON.parse(localStorage.getItem("selectedList"))
37 | this.setState({
38 | ...this.state,
39 | label: selectedList? selectedList : localLabels[0],
40 | labels: localLabels
41 | });
42 | } else {
43 | if (!this.state.labels ) {
44 | this.updateAllLabels();
45 | }
46 | }
47 |
48 | // get al Tasks
49 | let localTodos = JSON.parse(localStorage.getItem("allIssues"));
50 | // console.log("localTodos: ", localTodos);
51 | if (localTodos != null && localTodos.length > 0) {
52 | // console.log("Using local Todods");
53 | this.setState({
54 | ...this.state,
55 | allIssues: localTodos.sort((a, b) => a.id - b.id)
56 | // .filter(i => hasLabel(i, this.state.label.name))
57 | });
58 | } else {
59 | if (this.state.allIssues.length === 0) {
60 | // console.log("State issues.length is 0");
61 | this.updateTodos(this.state.label);
62 | }
63 | } // when you remove from my day the issue loses all labels except my day.
64 | };
65 | componentDidUpdate = () => {};
66 | createList = (name, color) => {
67 | console.log("createList ", name);
68 | gitlab.Labels.create(PROJECT_ID, name, color).then(e => {
69 | console.log(e);
70 | this.state.labels.push(e);
71 | message.success("Todo list created");
72 | this.hide();
73 | this.setState({ ...this.state, labels: this.state.labels, label: e });
74 | });
75 | };
76 | updateAllLabels = () => {
77 | gitlab.Labels.all(PROJECT_ID).then(labels => {
78 | const filteredLabels = labels.filter(
79 | l => l.name.indexOf("list" + LIST_SEPARATOR) === 0
80 | );
81 | console.log("Labels received: ", filteredLabels);
82 | localStorage.setItem("labels", JSON.stringify(filteredLabels));
83 | this.setState({ ...this.state, labels: filteredLabels });
84 | });
85 | }
86 | hide = () => {
87 | this.setState({
88 | addListVisible: false
89 | });
90 | };
91 | updateTodos = () => {
92 | console.log("Update tasks from Gitlab");
93 | this.setState({ ...this.state, allIssues: [], list: null });
94 |
95 | gitlab.Issues.all({ projectId: PROJECT_ID }).then(e => {
96 | const sortedTasks = e.filter(i=> i.state !== "closed").sort((a, b) => a.id - b.id)
97 | console.log("Received tasks frrom Gitlab", sortedTasks);
98 | localStorage.setItem("allIssues", JSON.stringify(sortedTasks));
99 | this.setState({ ...this.state, allIssues: sortedTasks});
100 | });
101 | this.updateAllLabels()
102 | };
103 | updateTodo = issue => {
104 | console.log("updtaeTood: ", issue);
105 | console.log("issues before update: ", this.state.allIssues);
106 | let issues = this.state.allIssues.filter(i => i.id !== issue.id);
107 | issues.push(issue);
108 | console.log("after update: ", issues);
109 |
110 | this.setState({
111 | ...this.state,
112 | allIssues: issues.sort((a, b) => a.id - b.id)
113 | });
114 | };
115 | removeTodo = issue => {
116 | let issues = this.state.allIssues.filter(i => i.iid !== issue.iid);
117 | message.success("Task was removed");
118 |
119 | this.setState({
120 | ...this.state,
121 | allIssues: issues.sort((a, b) => a.id - b.id)
122 | });
123 | };
124 | handleVisibleChange = () => {
125 | this.setState({ addListVisible: !this.state.addListVisible });
126 | };
127 | // if (this.state.accounts.length === 0) {
128 | // Account.findAll(result => {
129 | // console.log(result)
130 | // this.setState({
131 | // accounts: result.slice(0, MATCHING_ITEM_LIMIT)
132 | // });
133 | // });
134 | // }
135 | onSetSidebarOpen = open => {
136 | this.setState({ sidebarOpen: open });
137 | };
138 | selectTaskList = (list) => {
139 |
140 | this.setState({label: list, search: null})
141 | localStorage.setItem("selectedList", JSON.stringify(list))
142 | }
143 | showDrawer = () => {
144 | this.setState({
145 | sidebarVisible: true,
146 | });
147 | };
148 |
149 | onClose = () => {
150 | this.setState({
151 | sidebarVisible: false,
152 | docked: false
153 | });
154 | };
155 | render() {
156 | const mydaycount = this.state.allIssues.filter(i=> hasLabel(i, "meta::myday")).length
157 | let issuesToDisplay = []
158 | let todoListId = this.state.search ? 'search' : (this.state.label ? this.state.label.id : 'other')
159 | const labelProp = this.state.search ? null : this.state.label
160 | let titleProp = 'test'
161 |
162 | if (this.state.search) {
163 | titleProp = "Search for \"" +this.state.search + "\""
164 | console.log("searching fo r", this.state.search)
165 | const s = this.state.search.toLowerCase()
166 | issuesToDisplay = this.state.allIssues.filter(i=> (i.title && i.title.toLowerCase().indexOf(s) > -1) || (i.description && i.description.toLowerCase().indexOf(s) > -1))
167 | } else {
168 | if (this.state.label) {
169 | titleProp = this.state.label.title ? this.state.label.title : getActualLabelName(this.state.label)
170 | issuesToDisplay = this.state.allIssues.filter(i => {
171 | if (this.state.label.name === "ALL") {
172 | return hasNoListLabel(i)
173 | } else if (this.state.label.name !== LABEL_ARCHIVED && i.labels.indexOf(LABEL_ARCHIVED) > -1) {
174 | return false
175 | } else {
176 | return hasLabel(i, this.state.label.name)
177 | }
178 | }).sort((a, b) => a.id - b.id)
179 | }
180 | }
181 | let mainstyle = {}
182 | const buttonStyle= {
183 | width: "28px",
184 | margin: '10px 0 0 0px',
185 | padding: "2px",
186 | position: "fixed",
187 | top: "-3px",
188 | zIndex: 50,
189 | left: "10px"
190 | }
191 | if (this.state.sidebarVisible) {
192 | buttonStyle.left = "260px";
193 | } else {
194 |
195 |
196 | }
197 | return (
198 |
199 |
200 |
205 |
206 | {
207 | this.setState({search: search.search});
208 | }}/>
209 |
210 |
217 | {
219 | this.selectTaskList({ name: "meta::myday", title: "My Day" } );
220 | this.onClose();
221 | }}
222 | icon={faCalendarDay}
223 | right={mydaycount}
224 | key={"meta::myday"}
225 | label="My Day"
226 | title="My Day"
227 | />
228 | {
230 | this.selectTaskList({ name: "ALL", title: "My Tasks"});
231 | this.onClose();
232 | }}
233 | icon={faTasks}
234 | right={filterExcluded(this.state.allIssues.filter(i=> hasNoListLabel(i))).length}
235 | key="mytasks"
236 | label="My Tasks"
237 | title="My Tasks"
238 | />
239 | {
241 | this.selectTaskList({ name: "meta::starred", title: "Important Tasks" } );
242 | this.onClose();
243 | }}
244 | icon={faStar}
245 | right={filterExcluded(this.state.allIssues.filter(i=> hasLabel(i, "meta::starred"))).length}
246 |
247 | key={"meta::starred"}
248 | label="Important"
249 | title="Important"
250 | />
251 |
252 |
253 |
254 | { this.state.labels &&
255 | this.state.labels.map(l => (
256 | {
258 | this.selectTaskList( l )
259 | this.onClose()
260 | }}
261 | key={l.id}
262 | label={getActualLabelName(l)}
263 | right={filterExcluded(this.state.allIssues.filter(i=> hasLabel(i, l.name))).length}
264 | faicon={ }
265 | />
266 | ))}
267 |
268 | }
270 | visible={this.state.addListVisible}
271 | onVisibleChange={this.handleVisibleChange}
272 | title="Add List"
273 | trigger="click"
274 | >
275 |
276 | this.handleVisibleChange()}/>
277 |
278 |
279 | {
280 | this.selectTaskList({ name: LABEL_ARCHIVED, title: "Archived Tasks" } );
281 | }} style={{padding: "10px 10px 0px 10px"}}>
282 | Archived
283 |
284 |
285 |
286 |
287 | }
288 | open={this.state.sidebarVisible}
289 | onSetOpen={this.showDrawer}
290 | style={{sidebar: {backgroundColor: "rgb(37, 38, 39)"}}}
291 | >
292 |
293 | {this.state.sidebarVisible === false && this.showDrawer()}> }
294 | {this.state.sidebarVisible && this.onClose()}> }
295 |
296 |
297 |
298 |
304 | {this.state.label && (
305 | this.setState({label: {name: label, title: label}}) // not using select task list because we dont want to persist this
312 | } />
313 | )}
314 |
315 |
316 |
317 |
318 | );
319 | }
320 | }
321 |
322 | export default App;
323 |
--------------------------------------------------------------------------------
/src/components/TodoList.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import {
3 | getComments,
4 | createIssue,
5 | addIssueToMyDay,
6 | removeIssueFromMyDay,
7 | addIssueArchive,
8 | deleteIssue
9 | } from "../api/gitlab";
10 | import {extractLabels} from '../utils'
11 | import Todo from "./Todo";
12 | import { PROJECT_ID, LABEL_MYDAY } from "../config";
13 | import { Formik } from "formik";
14 | import {
15 | List,
16 | Button,
17 | Card,
18 | message,
19 | PageHeader,
20 | Row,
21 | Timeline,
22 | Typography,
23 | Input,
24 | Form,
25 | Col
26 | } from "antd";
27 | import Moment from "react-moment";
28 | import TodoForm from "./TodoForm";
29 | import TodoCommentForm from "./TodoCommentForm";
30 | import { Scrollbars } from "react-custom-scrollbars";
31 | import {
32 | faSyncAlt,
33 | faTrashAlt,
34 | faSun,
35 | faArchive
36 | } from "@fortawesome/free-solid-svg-icons";
37 | import { faSun as faSunOutline } from "@fortawesome/free-regular-svg-icons";
38 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
39 | import { hasLabel } from "../utils";
40 |
41 | const { Title } = Typography;
42 | class TodoList extends React.Component {
43 | state = {
44 | comments: -1,
45 | visible: false
46 | };
47 | componentDidMount = () => {
48 | console.log("List to display: ", this.props.title);
49 | };
50 |
51 | addCommentCb = comment => {
52 | let c = this.state.comments;
53 | c.push(comment);
54 | this.setState({ ...this.state, comments: c });
55 | };
56 | onClose = () => {
57 | this.setState({
58 | visible: false
59 | });
60 | };
61 | onSelection = issue => {
62 | getComments(issue.project_id, issue.iid)
63 | .then(r => {
64 | this.setState({
65 | ...this.state,
66 | issue: issue,
67 | visible: true,
68 | comments: r
69 | });
70 | })
71 | .catch(e => {
72 | console.log("That issue does not have comments yet.");
73 | this.setState({ ...this.state, issue: issue, comments: [] });
74 | });
75 | };
76 | addIssue = () => {
77 | this.setState({ addIssueModal: true });
78 | };
79 | clickAddToMyDayButton = () => {
80 | // this.props.updateTodo()
81 | const todo = this.state.issue;
82 | // console.log(
83 | // "hasLabel(this.state.issue, LABEL_MYDAY)",
84 | // hasLabel(this.state.issue, LABEL_MYDAY)
85 | // );
86 | // console.log("hasLabel() issue", this.state.issue.labels);
87 | if (hasLabel(this.state.issue, LABEL_MYDAY)) {
88 | // this part works with state up date
89 | removeIssueFromMyDay(this.state.issue.project_id, this.state.issue)
90 | .then(r => {
91 | // message.success("Removed from My Day");
92 | this.props.updateTodo(r);
93 | })
94 | .catch(e => {
95 | this.props.updateTodo(todo);
96 | // message.error("Can't remove todo from My Day right now.");
97 | console.log(e);
98 | });
99 | } else {
100 | // when this runs, the state is not updated
101 | addIssueToMyDay(this.state.issue.project_id, this.state.issue)
102 | .then(r => {
103 | message.success("Added to My Day");
104 | this.props.updateTodo(r);
105 | })
106 | .catch(e => {
107 | this.props.updateTodo(todo);
108 | message.error("Can't add todo to My Day right now.");
109 | console.log(e);
110 | });
111 | }
112 | };
113 | render() {
114 | return (
115 |
116 |
120 | {this.props.title ? this.props.title : "Untitled"}
121 |
122 | }
123 | extra={[
124 | this.props.updateTodos()} key="2" >
125 |
126 |
127 | ]}
128 | // breadcrumb={{ route's }}
129 | >
130 |
131 |
135 |
136 | {this.props.issues.length > 0 ? (
137 | (
144 | this.onSelection(item)}
147 | onSelectLabel={this.props.onSelectLabel}
148 | onUpdate={this.props.updateTodo}
149 | />
150 | )}
151 | />
152 | ) : (
153 | {this.props.empty ? this.props.empty : ''}
157 | )}
158 |
159 | { this.props.label &&
160 | {
163 | console.log(values)
164 | let title = (' ' + values.title).slice(1);
165 |
166 | const data = extractLabels(title, this.props.label.name);
167 | console.log(data)
168 | values.title = "";
169 |
170 | createIssue(PROJECT_ID, data).then(
171 | i => {
172 | var issues = this.props.issues;
173 | issues.push(i);
174 | this.setState({ issues: issues });
175 | message.success("Todo created");
176 | setSubmitting(false);
177 | }
178 | );
179 | }}
180 | >
181 | {({
182 | values,
183 | handleChange,
184 | handleBlur,
185 | handleSubmit,
186 | isSubmitting
187 | }) => (
188 |
198 |
220 |
221 | )}
222 |
223 | }
224 |
225 |
231 |
232 | {(this.state.issue && this.state.issue !== null) && (
233 |
234 | this.clickAddToMyDayButton()}
243 | >
244 | {hasLabel(this.state.issue, LABEL_MYDAY) ? (
245 |
246 | ) : (
247 |
248 | )}
249 |
250 | ]}
251 | />
252 |
253 | this.setState({ ...this.state, issue: t })}
256 | />
257 | {/*
258 |
259 | {this.state.issue.updated_at && (
260 |
261 |
262 | {this.state.issue.updated_at}
263 |
264 |
265 | )}
266 | {this.state.issue.closed_at && (
267 |
268 |
269 | {this.state.issue.closed_at}
270 |
271 |
272 | )}
273 | */}
274 | Comments
275 |
276 | {this.state.comments.length > 0 ? (
277 | this.state.comments
278 | .sort((a, b) => a.id - b.id)
279 | .map(c => (
280 |
281 | {c.body}
282 |
288 | {c.created_at}
289 |
290 |
291 | ))
292 | ) : No notes yet
}
293 |
294 |
298 | {this.state.issue && (
299 |
300 |
301 | {this.state.issue.created_at}
302 |
303 |
304 |
305 |
306 | addIssueArchive(this.state.issue.project_id, this.state.issue).then(r => {
309 | this.props.updateTodo(r);
310 | this.setState({visible : false});
311 | })}
312 | >
313 |
314 |
315 | {
318 | deleteIssue(this.state.issue.project_id, this.state.issue).then(r=>{
319 | this.props.removeTodo(r)
320 | })
321 | }}
322 | >
323 |
324 |
325 |
326 |
327 |
328 | )}
329 |
330 | )}
331 |
332 |
333 |
334 |
335 | {/*
336 |
337 |
338 | */}
339 |
340 |
341 | );
342 | }
343 |
344 |
345 | }
346 |
347 | export default TodoList;
348 |
--------------------------------------------------------------------------------