diff --git a/README.md b/README.md
index 70a69285..6b1d4326 100644
--- a/README.md
+++ b/README.md
@@ -64,11 +64,11 @@ Stage 3 creates the server to handle and distribute everything.
Stage 4 finally adds support for multiple users.
This will enable you to invite other to tasks.
-* [ ] Define creator of project (aka "admin")
-* [ ] Mark own projects
-* [ ] Invite user (only possible by admin)
- * [ ] Enter username and to invite user
- * [ ] Users should also see projects they've invited to
+* [x] Define creator of project (aka "owner")
+* [x] Mark own projects
+* [x] Invite user (only possible by owner)
+ * [x] Enter username and to invite user
+ * [x] Users should also see projects they've invited to
## Stage 5
@@ -103,7 +103,7 @@ Things that would be nice but are not necessary for a prototype.
* [ ] From overpass-query / -result
* [ ] Internal development
* [ ] Use go modules? (may or may not be useful)
- * [ ] Create Docker container for client and server
+ * [x] Create Docker container for client and server
# Development
diff --git a/client/package-lock.json b/client/package-lock.json
index a8ddb368..cdad7ec0 100644
--- a/client/package-lock.json
+++ b/client/package-lock.json
@@ -1,6 +1,6 @@
{
"name": "simple-task-manager",
- "version": "0.3.1",
+ "version": "0.4.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
diff --git a/client/package.json b/client/package.json
index 3502a5b6..3c82c91c 100644
--- a/client/package.json
+++ b/client/package.json
@@ -1,6 +1,6 @@
{
"name": "simple-task-manager",
- "version": "0.3.1",
+ "version": "0.4.0",
"scripts": {
"ng": "ng",
"dev": "ng serve --watch",
diff --git a/client/src/app/app.module.ts b/client/src/app/app.module.ts
index 37339efb..5cb432f7 100644
--- a/client/src/app/app.module.ts
+++ b/client/src/app/app.module.ts
@@ -15,8 +15,11 @@ import { ProjectComponent } from './project/project.component';
import { TaskListComponent } from './task/task-list.component';
import { TaskDetailsComponent } from './task/task-details.component';
import { TaskMapComponent } from './task/task-map.component';
-import { FooterComponent } from './footer.component';
+import { FooterComponent } from './ui/footer.component';
import { ProjectCreationComponent } from './project/project-creation.component';
+import { TabsComponent } from './ui/tabs.component';
+import { UserListComponent } from './user/user-list.component';
+import { UserInvitationComponent } from './user/user-invitation.component';
@NgModule({
declarations: [
@@ -30,7 +33,10 @@ import { ProjectCreationComponent } from './project/project-creation.component';
TaskDetailsComponent,
TaskMapComponent,
FooterComponent,
- ProjectCreationComponent
+ ProjectCreationComponent,
+ TabsComponent,
+ UserListComponent,
+ UserInvitationComponent
],
imports: [
BrowserModule,
diff --git a/client/src/app/auth/auth.component.ts b/client/src/app/auth/auth.component.ts
index 2e07601a..91a33521 100644
--- a/client/src/app/auth/auth.component.ts
+++ b/client/src/app/auth/auth.component.ts
@@ -1,7 +1,7 @@
import { NgZone, Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { AuthService } from './auth.service';
-import { UserService } from './user.service';
+import { UserService } from '../user/user.service';
@Component({
selector: 'app-auth',
diff --git a/client/src/app/auth/auth.service.ts b/client/src/app/auth/auth.service.ts
index 0873f30a..8cc2981b 100644
--- a/client/src/app/auth/auth.service.ts
+++ b/client/src/app/auth/auth.service.ts
@@ -1,6 +1,6 @@
import { Injectable } from '@angular/core';
import { environment } from '../../environments/environment';
-import { UserService } from './user.service';
+import { UserService } from '../user/user.service';
import { Router } from '@angular/router';
@Injectable({
diff --git a/client/src/app/manager/manager.component.html b/client/src/app/manager/manager.component.html
index 39382be1..9ac2424a 100644
--- a/client/src/app/manager/manager.component.html
+++ b/client/src/app/manager/manager.component.html
@@ -7,4 +7,5 @@
+
diff --git a/client/src/app/manager/manager.component.scss b/client/src/app/manager/manager.component.scss
index 7eb26ffd..5e92f9c4 100644
--- a/client/src/app/manager/manager.component.scss
+++ b/client/src/app/manager/manager.component.scss
@@ -14,3 +14,9 @@
.toolbar > p {
color: $very-light-gray;
}
+
+.root-container {
+ max-width: 500px;
+ margin-left: auto;
+ margin-right: auto;
+}
diff --git a/client/src/app/manager/manager.component.ts b/client/src/app/manager/manager.component.ts
index 02a62dc8..70b548bc 100644
--- a/client/src/app/manager/manager.component.ts
+++ b/client/src/app/manager/manager.component.ts
@@ -1,5 +1,5 @@
import { Component, OnInit } from '@angular/core';
-import { UserService } from '../auth/user.service';
+import { UserService } from '../user/user.service';
import { AuthService } from '../auth/auth.service';
import { Router } from '@angular/router';
diff --git a/client/src/app/project/project-creation.component.html b/client/src/app/project/project-creation.component.html
index 67269a27..161d08e9 100644
--- a/client/src/app/project/project-creation.component.html
+++ b/client/src/app/project/project-creation.component.html
@@ -5,16 +5,16 @@
-
-
Divide into squares:
@@ -25,4 +25,7 @@
Divide into squares:
+
diff --git a/client/src/app/project/project-creation.component.scss b/client/src/app/project/project-creation.component.scss
index caeaf1df..34adc6b0 100644
--- a/client/src/app/project/project-creation.component.scss
+++ b/client/src/app/project/project-creation.component.scss
@@ -2,26 +2,30 @@
:host {
height: 100%;
+ display: flex;
+ flex-direction: column;
+ justify-content: stretch;
}
.root-container {
- height: 100%;
display: flex;
flex-direction: row;
justify-content: space-between;
+ flex-grow: 1;
+ margin-top: 0px;
+ min-height: 0px; // important for scrolling (for whatever reason, s. above)
}
.project-properties-container {
display: flex;
flex-direction: column;
}
-.project-properties-container > div {
+.project-properties-container div {
margin-bottom: $space-base
}
.map-container {
- height: 75%;
- width: 50%;
+ width: 65%;
display: flex;
flex-direction: column;
}
diff --git a/client/src/app/project/project-list.component.html b/client/src/app/project/project-list.component.html
index f12b3cac..b7c751d6 100644
--- a/client/src/app/project/project-list.component.html
+++ b/client/src/app/project/project-list.component.html
@@ -1,7 +1,12 @@
Projekte
- {{p.name}}
({{p.id}})
+
+ {{p.name}} ({{p.id}})
+
+
+ Owned by you
+
diff --git a/client/src/app/project/project-list.component.scss b/client/src/app/project/project-list.component.scss
index 84e58952..f07920ab 100644
--- a/client/src/app/project/project-list.component.scss
+++ b/client/src/app/project/project-list.component.scss
@@ -1,9 +1,14 @@
@import "../../styles.scss";
+.list-item {
+ display: flex;
+ justify-content: space-between;
+}
+
.create-project-button {
margin-top: $space-large;
}
-.id-label {
+.light-label {
color: $gray-mid;
}
diff --git a/client/src/app/project/project-list.component.ts b/client/src/app/project/project-list.component.ts
index 165ab62b..21e20b2b 100644
--- a/client/src/app/project/project-list.component.ts
+++ b/client/src/app/project/project-list.component.ts
@@ -1,5 +1,6 @@
import { Component, OnInit } from '@angular/core';
import { ProjectService } from './project.service';
+import { UserService } from './../user/user.service';
import { Project } from './project.material';
import { Router } from '@angular/router';
@@ -13,13 +14,18 @@ export class ProjectListComponent implements OnInit {
constructor(
private projectService: ProjectService,
- private router: Router
+ private router: Router,
+ private userService: UserService
) { }
ngOnInit(): void {
this.projectService.getProjects().subscribe(p => this.projects = p);
}
+ public get currentUser(): string {
+ return this.userService.getUser();
+ }
+
public onProjectListItemClicked(id: string) {
this.router.navigate(['/project', id]);
}
diff --git a/client/src/app/project/project.component.html b/client/src/app/project/project.component.html
index 333bce04..7cb47ddd 100644
--- a/client/src/app/project/project.component.html
+++ b/client/src/app/project/project.component.html
@@ -3,18 +3,30 @@
Project: {{thisProject?.name}}
-
-
-
-
diff --git a/client/src/app/project/project.component.scss b/client/src/app/project/project.component.scss
index 2a35fec4..1355f73e 100644
--- a/client/src/app/project/project.component.scss
+++ b/client/src/app/project/project.component.scss
@@ -1,5 +1,8 @@
@import '../../styles.scss';
+// Making the task list content scrollable was made with the help of this article:
+// https://moduscreate.com/blog/how-to-fix-overflow-issues-in-css-flex-layouts/
+
:host {
height: 100%;
width: 100%;
@@ -8,33 +11,34 @@
justify-content: stretch;
}
+.task-details-container {
+ margin-bottom: $space-large;
+}
+
.root-container {
display: flex;
- height: 100%;
+ flex-grow: 1;
margin-top: 0px;
+ min-height: 0px; // important for scrolling (for whatever reason, s. above)
}
-.root-container-item {
- width: 50%;
-}
-
-.task-list-container {
+.task-list-details-container {
+ width: 35%;
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ margin-top: $space-large;
margin-right: $space-large;
}
-.task-details-map-container {
+.tab-container {
display: flex;
flex-direction: column;
+ min-height: 0px; // important for scrolling (for whatever reason, s. above)
}
.map-container {
- height: 65%;
-}
-.details-container {
- height: 35%;
-}
-
-.map-container {
+ width: 65%;
display: flex;
flex-direction: column;
}
@@ -47,3 +51,7 @@
color: $very-light-gray;
margin-left: $space-base;
}
+
+.tab-container-item {
+ margin-bottom: $space-large;
+}
diff --git a/client/src/app/project/project.material.ts b/client/src/app/project/project.material.ts
index eb41b232..a9d48e6c 100644
--- a/client/src/app/project/project.material.ts
+++ b/client/src/app/project/project.material.ts
@@ -1,6 +1,8 @@
export class Project {
constructor(public id: string,
public name: string,
- public taskIds: string[]
+ public taskIds: string[],
+ public users?: string[],
+ public owner?: string
) { }
}
diff --git a/client/src/app/project/project.service.ts b/client/src/app/project/project.service.ts
index db9bb359..0e347046 100644
--- a/client/src/app/project/project.service.ts
+++ b/client/src/app/project/project.service.ts
@@ -1,6 +1,6 @@
-import { Injectable } from '@angular/core';
+import { Injectable, EventEmitter } from '@angular/core';
import { Observable, throwError } from 'rxjs';
-import { map, flatMap } from 'rxjs/operators';
+import { map, flatMap, tap } from 'rxjs/operators';
import { Project } from './project.material';
import { Task } from './../task/task.material';
import { TaskService } from './../task/task.service';
@@ -11,7 +11,7 @@ import { environment } from './../../environments/environment';
providedIn: 'root'
})
export class ProjectService {
- public projects: Project[] = [];
+ public projectChanged: EventEmitter
= new EventEmitter();
constructor(
private taskService: TaskService,
@@ -44,4 +44,9 @@ export class ProjectService {
return this.http.post(environment.url_projects, JSON.stringify(p));
}));
}
+
+ public inviteUser(user: string, id: string): Observable {
+ return this.http.post(environment.url_projects_users + '?user=' + user + '&project=' + id, '')
+ .pipe(tap(p => this.projectChanged.emit(p)));
+ }
}
diff --git a/client/src/app/task/task-details.component.html b/client/src/app/task/task-details.component.html
index 33697d1a..2efc6c73 100644
--- a/client/src/app/task/task-details.component.html
+++ b/client/src/app/task/task-details.component.html
@@ -2,20 +2,20 @@
Task: {{task?.id}}
-
Assigned to: {{task?.assignedUser}}
+
Assigned to: {{task?.assignedUser}}
-
+
- Points:
+ Points:
/
{{task?.maxProcessPoints}}
- Points:
+ Points:
{{task?.processPoints}}
/
{{task?.maxProcessPoints}}
diff --git a/client/src/app/task/task-details.component.scss b/client/src/app/task/task-details.component.scss
index 3dde99b7..86364969 100644
--- a/client/src/app/task/task-details.component.scss
+++ b/client/src/app/task/task-details.component.scss
@@ -15,6 +15,16 @@
margin-left: $space-large;
}
+.process-point-container {
+ display: flex;
+ align-items: center;
+ height: 40px; // To stay equally high, regardless of what the content is
+}
+
+.points-label {
+ margin-right: 5px;
+}
+
input {
width: 50px;
}
diff --git a/client/src/app/task/task-details.component.ts b/client/src/app/task/task-details.component.ts
index 0d843c4f..b577f752 100644
--- a/client/src/app/task/task-details.component.ts
+++ b/client/src/app/task/task-details.component.ts
@@ -2,7 +2,7 @@ import { Component, OnInit } from '@angular/core';
import { Observable, of } from 'rxjs';
import { TaskService } from './task.service';
import { Task } from './task.material';
-import { UserService } from '../auth/user.service';
+import { UserService } from '../user/user.service';
@Component({
selector: 'app-task-details',
diff --git a/client/src/app/task/task-map.component.ts b/client/src/app/task/task-map.component.ts
index 0e4d4a81..8f2b9c92 100644
--- a/client/src/app/task/task-map.component.ts
+++ b/client/src/app/task/task-map.component.ts
@@ -1,6 +1,6 @@
import { Component, AfterViewInit, Input } from '@angular/core';
import { TaskService } from './task.service';
-import { UserService } from '../auth/user.service';
+import { UserService } from '../user/user.service';
import { Task } from './task.material';
import { Map, View } from 'ol';
import TileLayer from 'ol/layer/Tile';
diff --git a/client/src/app/footer.component.html b/client/src/app/ui/footer.component.html
similarity index 100%
rename from client/src/app/footer.component.html
rename to client/src/app/ui/footer.component.html
diff --git a/client/src/app/footer.component.scss b/client/src/app/ui/footer.component.scss
similarity index 60%
rename from client/src/app/footer.component.scss
rename to client/src/app/ui/footer.component.scss
index 395fd330..f7291793 100644
--- a/client/src/app/footer.component.scss
+++ b/client/src/app/ui/footer.component.scss
@@ -1,4 +1,4 @@
-@import "../styles.scss";
+@import "../../styles.scss";
.version-label {
margin: $space-large;
diff --git a/client/src/app/footer.component.ts b/client/src/app/ui/footer.component.ts
similarity index 86%
rename from client/src/app/footer.component.ts
rename to client/src/app/ui/footer.component.ts
index 37f957c3..4f155770 100644
--- a/client/src/app/footer.component.ts
+++ b/client/src/app/ui/footer.component.ts
@@ -1,5 +1,5 @@
import { Component, OnInit } from '@angular/core';
-import { version } from '../../package.json';
+import { version } from '../../../package.json';
@Component({
selector: 'app-footer',
diff --git a/client/src/app/ui/tabs.component.html b/client/src/app/ui/tabs.component.html
new file mode 100644
index 00000000..a7f75ee2
--- /dev/null
+++ b/client/src/app/ui/tabs.component.html
@@ -0,0 +1,10 @@
+
+
diff --git a/client/src/app/ui/tabs.component.scss b/client/src/app/ui/tabs.component.scss
new file mode 100644
index 00000000..c1742eb6
--- /dev/null
+++ b/client/src/app/ui/tabs.component.scss
@@ -0,0 +1,38 @@
+@import "../../styles.scss";
+
+:host {
+ min-height: 0px;
+ display: flex;
+ flex-direction: column;
+}
+
+button {
+ background-color: white;
+ margin-right: -1px; // get rid of douple sized border between two buttons
+}
+button.selected:hover {
+ border-bottom: 3px solid $color-mid;
+}
+
+.selected {
+ border-bottom: 3px solid $color-mid;
+ background-color: $color-very-light;
+}
+
+.tab-list {
+ display: flex;
+ flex-direction: row;
+}
+
+.tab-content {
+ border: 1px solid $color-light;
+ padding: $space-base;
+ min-height: 0px;
+ display: flex;
+ flex-direction: column;
+ overflow: auto;
+}
+
+.inner-content {
+ padding-bottom: 10px;
+}
diff --git a/client/src/app/ui/tabs.component.spec.ts b/client/src/app/ui/tabs.component.spec.ts
new file mode 100644
index 00000000..96b5fd02
--- /dev/null
+++ b/client/src/app/ui/tabs.component.spec.ts
@@ -0,0 +1,25 @@
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { TabsComponent } from './tabs.component';
+
+describe('TabsComponent', () => {
+ let component: TabsComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ declarations: [ TabsComponent ]
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(TabsComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/client/src/app/ui/tabs.component.ts b/client/src/app/ui/tabs.component.ts
new file mode 100644
index 00000000..dfe4558f
--- /dev/null
+++ b/client/src/app/ui/tabs.component.ts
@@ -0,0 +1,25 @@
+import { Component, OnInit, Input } from '@angular/core';
+
+@Component({
+ selector: 'app-tabs',
+ templateUrl: './tabs.component.html',
+ styleUrls: ['./tabs.component.scss']
+})
+export class TabsComponent implements OnInit {
+ @Input() tabs: string[];
+
+ public tabTitle: string;
+ public tabIndex: number;
+
+ constructor() { }
+
+ ngOnInit(): void {
+ this.tabIndex = 0;
+ this.tabTitle = this.tabs[this.tabIndex];
+ }
+
+ public onTabClicked(tabTitle: string) {
+ this.tabIndex = this.tabs.indexOf(tabTitle);
+ this.tabTitle = tabTitle;
+ }
+}
diff --git a/client/src/app/user/user-invitation.component.html b/client/src/app/user/user-invitation.component.html
new file mode 100644
index 00000000..9e4ffe9f
--- /dev/null
+++ b/client/src/app/user/user-invitation.component.html
@@ -0,0 +1,2 @@
+
+
diff --git a/client/src/app/user/user-invitation.component.scss b/client/src/app/user/user-invitation.component.scss
new file mode 100644
index 00000000..0460b8c8
--- /dev/null
+++ b/client/src/app/user/user-invitation.component.scss
@@ -0,0 +1,5 @@
+@import "../../styles.scss";
+
+#userInput {
+ margin-right: $space-base;
+}
diff --git a/client/src/app/user/user-invitation.component.spec.ts b/client/src/app/user/user-invitation.component.spec.ts
new file mode 100644
index 00000000..d316b281
--- /dev/null
+++ b/client/src/app/user/user-invitation.component.spec.ts
@@ -0,0 +1,25 @@
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { UserInvitationComponent } from './user-invitation.component';
+
+describe('UserInvitationComponent', () => {
+ let component: UserInvitationComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ declarations: [ UserInvitationComponent ]
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(UserInvitationComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/client/src/app/user/user-invitation.component.ts b/client/src/app/user/user-invitation.component.ts
new file mode 100644
index 00000000..97e658c2
--- /dev/null
+++ b/client/src/app/user/user-invitation.component.ts
@@ -0,0 +1,26 @@
+import { Component, OnInit, Input } from '@angular/core';
+import { ProjectService } from '../project/project.service';
+import { Project } from '../project/project.material';
+
+@Component({
+ selector: 'app-user-invitation',
+ templateUrl: './user-invitation.component.html',
+ styleUrls: ['./user-invitation.component.scss']
+})
+export class UserInvitationComponent implements OnInit {
+ @Input() project: Project;
+
+ constructor(
+ private projectService: ProjectService
+ ) { }
+
+ ngOnInit(): void {
+ }
+
+ public onInvitationButtonClicked(userName: string) {
+ this.projectService.inviteUser(userName, this.project.id)
+ .subscribe(p => {
+ this.project = p;
+ });
+ }
+}
diff --git a/client/src/app/user/user-list.component.html b/client/src/app/user/user-list.component.html
new file mode 100644
index 00000000..07a8a5bd
--- /dev/null
+++ b/client/src/app/user/user-list.component.html
@@ -0,0 +1,3 @@
+
+ {{u}}
+
diff --git a/client/src/app/user/user-list.component.scss b/client/src/app/user/user-list.component.scss
new file mode 100644
index 00000000..e69de29b
diff --git a/client/src/app/user/user-list.component.spec.ts b/client/src/app/user/user-list.component.spec.ts
new file mode 100644
index 00000000..9d521807
--- /dev/null
+++ b/client/src/app/user/user-list.component.spec.ts
@@ -0,0 +1,25 @@
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { UserListComponent } from './user-list.component';
+
+describe('UserListComponent', () => {
+ let component: UserListComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async(() => {
+ TestBed.configureTestingModule({
+ declarations: [ UserListComponent ]
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(UserListComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/client/src/app/user/user-list.component.ts b/client/src/app/user/user-list.component.ts
new file mode 100644
index 00000000..fa946786
--- /dev/null
+++ b/client/src/app/user/user-list.component.ts
@@ -0,0 +1,19 @@
+import { Component, OnInit, Input } from '@angular/core';
+import { ProjectService } from '../project/project.service';
+
+@Component({
+ selector: 'app-user-list',
+ templateUrl: './user-list.component.html',
+ styleUrls: ['./user-list.component.scss']
+})
+export class UserListComponent implements OnInit {
+ @Input() users: string[];
+
+ constructor(
+ private projectService: ProjectService
+ ) { }
+
+ ngOnInit(): void {
+ this.projectService.projectChanged.subscribe(p => this.users = p.users);
+ }
+}
diff --git a/client/src/app/auth/user.service.spec.ts b/client/src/app/user/user.service.spec.ts
similarity index 85%
rename from client/src/app/auth/user.service.spec.ts
rename to client/src/app/user/user.service.spec.ts
index 3f804c9f..0ec68ffc 100644
--- a/client/src/app/auth/user.service.spec.ts
+++ b/client/src/app/user/user.service.spec.ts
@@ -1,6 +1,6 @@
import { TestBed } from '@angular/core/testing';
-import { UserService } from './user.service';
+import { UserService } from '../user/user.service';
describe('UserService', () => {
let service: UserService;
diff --git a/client/src/app/auth/user.service.ts b/client/src/app/user/user.service.ts
similarity index 100%
rename from client/src/app/auth/user.service.ts
rename to client/src/app/user/user.service.ts
diff --git a/client/src/environments/environment.ts b/client/src/environments/environment.ts
index c97619b9..b7fecaf8 100644
--- a/client/src/environments/environment.ts
+++ b/client/src/environments/environment.ts
@@ -6,6 +6,7 @@ export const environment = {
url_auth: baseUrl + '/oauth_login',
url_projects: baseUrl + '/projects',
+ url_projects_users: baseUrl + '/projects/users',
url_tasks: baseUrl + '/tasks',
url_task_assignedUser: baseUrl + '/task/assignedUser',
url_task_processPoints: baseUrl + '/task/processPoints'
diff --git a/client/src/styles.scss b/client/src/styles.scss
index e9382e31..d6879193 100644
--- a/client/src/styles.scss
+++ b/client/src/styles.scss
@@ -1,6 +1,7 @@
@import "colors.scss";
// Global distances etc.
+$space-small: 5px;
$space-base: 10px;
$space-large: $space-base * 2;
$space-huge: $space-base * 4;
@@ -48,6 +49,9 @@ button {
button:hover {
border: 1px solid $color-mid;
}
+button::-moz-focus-inner { // remove dotted line in Firefoc
+ border: 0
+}
a {
color: $color-very-dark;
@@ -86,3 +90,11 @@ a:hover {
display: flex;
align-items: center;
}
+
+input {
+ border: 1px solid $color-light;
+ padding: $space-small;
+}
+input:focus {
+ border: 1px solid $color-mid;
+}
diff --git a/deploy.sh b/deploy.sh
new file mode 100755
index 00000000..5d5a30d4
--- /dev/null
+++ b/deploy.sh
@@ -0,0 +1,12 @@
+#!/bin/bash
+
+# Some logging to see how long building an deployment takes plus the deployment itself.
+
+clear
+date
+echo -e "\n\n\n"
+
+docker-compose up -d --build
+
+echo -e "\n\n\n"
+date
diff --git a/server/main.go b/server/main.go
index 1216645c..b89b014e 100644
--- a/server/main.go
+++ b/server/main.go
@@ -14,7 +14,7 @@ import (
"github.com/hauke96/sigolo"
)
-const VERSION string = "0.3.1"
+const VERSION string = "0.4.0"
var (
app = kingpin.New("Simple Task Manager", "A tool dividing an area of the map into smaller tasks.")
@@ -60,6 +60,7 @@ func main() {
router.HandleFunc("/oauth_callback", oauthCallback).Methods(http.MethodGet)
router.HandleFunc("/projects", authenticatedHandler(getProjects)).Methods(http.MethodGet)
router.HandleFunc("/projects", authenticatedHandler(addProject)).Methods(http.MethodPost)
+ router.HandleFunc("/projects/users", authenticatedHandler(addUserToTask)).Methods(http.MethodPost)
router.HandleFunc("/tasks", authenticatedHandler(getTasks)).Methods(http.MethodGet)
router.HandleFunc("/tasks", authenticatedHandler(addTask)).Methods(http.MethodPost)
router.HandleFunc("/task/assignedUser", authenticatedHandler(assignUser)).Methods(http.MethodPost)
@@ -146,10 +147,33 @@ func addProject(w http.ResponseWriter, r *http.Request, token *Token) {
json.Unmarshal(bodyBytes, &project)
// TODO check wether all neccessary fields are set
- project = AddProject(project, token.User)
+ updatedProject := AddProject(&project, token.User)
encoder := json.NewEncoder(w)
- encoder.Encode(project)
+ encoder.Encode(updatedProject)
+}
+
+func addUserToTask(w http.ResponseWriter, r *http.Request, token *Token) {
+ userName, err := getParam("user", r)
+ if err != nil {
+ responseBadRequest(w, err.Error())
+ return
+ }
+
+ projectId, err := getParam("project", r)
+ if err != nil {
+ responseBadRequest(w, err.Error())
+ return
+ }
+
+ updatedProject, err := AddUser(userName, projectId, token.User)
+ if err != nil {
+ responseInternalError(w, err.Error())
+ return
+ }
+
+ encoder := json.NewEncoder(w)
+ encoder.Encode(updatedProject)
}
func getTasks(w http.ResponseWriter, r *http.Request, token *Token) {
diff --git a/server/project.go b/server/project.go
index 79b09379..c928acf8 100644
--- a/server/project.go
+++ b/server/project.go
@@ -1,40 +1,49 @@
package main
+import (
+ "errors"
+ "fmt"
+)
+
type Project struct {
Id string `json:"id"`
Name string `json:"name"`
TaskIDs []string `json:"taskIds"`
Users []string `json:"users"`
+ Owner string `json:"owner"`
}
var (
- projects []Project
+ projects []*Project
)
func InitProjects() {
- projects = make([]Project, 0)
- projects = append(projects, Project{
+ projects = make([]*Project, 0)
+ projects = append(projects, &Project{
Id: "p-" + GetId(),
Name: "First project",
TaskIDs: []string{"t-3", "t-4"},
Users: []string{"hauke-stieler"},
+ Owner: "hauke-stieler",
})
- projects = append(projects, Project{
+ projects = append(projects, &Project{
Id: "p-" + GetId(),
Name: "Foo",
TaskIDs: []string{"t-5"},
Users: []string{"hauke-stieler", "hauke-stieler-dev"},
+ Owner: "hauke-stieler",
})
- projects = append(projects, Project{
+ projects = append(projects, &Project{
Id: "p-" + GetId(),
Name: "Bar",
TaskIDs: []string{"t-6", "t-7", "t-8", "t-9", "t-10"},
Users: []string{"hauke-stieler-dev"},
+ Owner: "hauke-stieler-dev",
})
}
-func GetProjects(user string) []Project {
- result := make([]Project, 0)
+func GetProjects(user string) []*Project {
+ result := make([]*Project, 0)
for _, p := range projects {
for _, u := range p.Users {
@@ -47,9 +56,10 @@ func GetProjects(user string) []Project {
return result
}
-func AddProject(project Project, user string) Project {
+func AddProject(project *Project, user string) *Project {
project.Id = "p-" + GetId()
project.Users = []string{user}
+ project.Owner = user
projects = append(projects, project)
return project
}
@@ -88,3 +98,36 @@ func VerifyOwnership(user string, taskIds []string) bool {
return true
}
+
+func GetProject(id string) (*Project, error) {
+ for _, p := range projects {
+ if p.Id == id {
+ return p, nil
+ }
+ }
+
+ return nil, errors.New(fmt.Sprintf("Project with ID '%s' not found", id))
+}
+
+func AddUser(user, id, potentialOwner string) (*Project, error) {
+ project, err := GetProject(id)
+ if err != nil {
+ return nil, err
+ }
+
+ // Only the owner is allowed to invite
+ if project.Owner != potentialOwner {
+ return nil, errors.New(fmt.Sprintf("User '%s' is not allowed to add another user", potentialOwner))
+ }
+
+ // Check if user is already in project. If so, just do nothing and return
+ for _, u := range project.Users {
+ if u == user {
+ return project, nil
+ }
+ }
+
+ project.Users = append(project.Users, user)
+
+ return project, nil
+}
diff --git a/server/project_test.go b/server/project_test.go
index afdc0829..fbc0c51d 100644
--- a/server/project_test.go
+++ b/server/project_test.go
@@ -5,24 +5,27 @@ import (
)
func prepare() {
- projects = make([]Project, 0)
- projects = append(projects, Project{
+ projects = make([]*Project, 0)
+ projects = append(projects, &Project{
Id: "p-0",
Name: "First project",
TaskIDs: []string{"t-3", "t-4"},
Users: []string{"Peter"},
+ Owner: "Peter",
})
- projects = append(projects, Project{
+ projects = append(projects, &Project{
Id: "p-1",
Name: "Foo",
TaskIDs: []string{"t-5"},
Users: []string{"Peter", "Maria"},
+ Owner: "Peter",
})
- projects = append(projects, Project{
+ projects = append(projects, &Project{
Id: "p-2",
Name: "Bar",
TaskIDs: []string{"t-6", "t-7", "t-8", "t-9", "t-10"},
Users: []string{"Maria"},
+ Owner: "Maria",
})
}
@@ -66,8 +69,8 @@ func TestGetProjects(t *testing.T) {
}
}
-func TestAddProject(t *testing.T) {
- projects = make([]Project, 0)
+func TestAddAndGetProject(t *testing.T) {
+ projects = make([]*Project, 0)
nextId = 100 // the new project should then have the ID "p-100"
p := Project{
@@ -75,8 +78,9 @@ func TestAddProject(t *testing.T) {
Name: "Test name",
TaskIDs: []string{"t-11"},
Users: []string{"noname-user"},
+ Owner: "noname-user",
}
- AddProject(p, "Maria")
+ AddProject(&p, "Maria")
// Check parameter of the just added Project
newProject := GetProjects("Maria")[0]
@@ -96,9 +100,52 @@ func TestAddProject(t *testing.T) {
t.Errorf("Name is not the same")
t.Fail()
}
+ if newProject.Owner != "Maria" {
+ t.Errorf("Owner does not match")
+ t.Fail()
+ }
+}
+
+func TestAddUser(t *testing.T) {
+ prepare()
+
+ newUser := "new user"
+
+ p, err := AddUser(newUser, "p-0", "Peter")
+ if err != nil {
+ t.Error("This should work")
+ t.Error(err.Error())
+ t.Fail()
+ }
+
+ containsUser := false
+ for _, u := range p.Users {
+ if u == newUser {
+ containsUser = true
+ break
+ }
+ }
+ if !containsUser {
+ t.Error("Project should contain new user")
+ t.Fail()
+ }
+
+ p, err = AddUser(newUser, "p-2346", "Peter")
+ if err == nil {
+ t.Error("This should not work: The project does not exist")
+ t.Error(err.Error())
+ t.Fail()
+ }
+
+ p, err = AddUser(newUser, "p-0", "Not-Owner-User")
+ if err == nil {
+ t.Error("This should not work: A non-owner user tries to add a user")
+ t.Error(err.Error())
+ t.Fail()
+ }
}
-func contains(projectIdToFind string, projectsToCheck []Project) bool {
+func contains(projectIdToFind string, projectsToCheck []*Project) bool {
for _, p := range projectsToCheck {
if p.Id == projectIdToFind {
return true