Move project and room actions to own component

Also adds those actions to the detail pages of both entity types. Also refactors some code.
This commit is contained in:
Ramon Wenger 2019-05-22 17:47:31 +02:00
parent 016f6ce502
commit 590fd180c3
12 changed files with 319 additions and 163 deletions

View File

@ -1,41 +1,9 @@
<template> <template>
<div class="widget-footer"> <div class="widget-footer">
<a @click="toggleMenu" <slot></slot>
class="widget-footer__more-link">
<ellipses></ellipses>
</a>
<widget-popover v-if="showMenu"
@hide-me="showMenu = false">
<slot :hide="toggleMenu"></slot>
</widget-popover>
</div> </div>
</template> </template>
<script>
import Ellipses from '@/components/icons/Ellipses.vue';
import WidgetPopover from '@/components/rooms/WidgetPopover';
export default {
components: {
Ellipses,
WidgetPopover
},
data() {
return {
showMenu: false
}
},
methods: {
toggleMenu: function () {
this.showMenu = !this.showMenu
}
}
}
</script>
<style scoped lang="scss"> <style scoped lang="scss">
@import "@/styles/_variables.scss"; @import "@/styles/_variables.scss";
@import "@/styles/_mixins.scss"; @import "@/styles/_mixins.scss";
@ -58,15 +26,5 @@
* For IE10+ * For IE10+
*/ */
-ms-grid-row: 2; -ms-grid-row: 2;
&__more-link {
cursor: pointer;
}
svg {
width: 30px;
fill: $color-darkgrey-1;
margin-right: 15px;
}
} }
</style> </style>

View File

@ -1,6 +1,6 @@
<template> <template>
<div class="owner-widget"> <div class="owner-widget">
<avatar class="owner-widget__avatar" :avatarUrl="avatarUrl" /> <avatar class="owner-widget__avatar" :avatarUrl="owner.avatarUrl" />
<span> <span>
{{name}} {{name}}
</span> </span>
@ -11,10 +11,16 @@
import Avatar from '@/components/profile/Avatar'; import Avatar from '@/components/profile/Avatar';
export default { export default {
props: ['name', 'avatarUrl'], props: ['owner'],
components: { components: {
Avatar Avatar
},
computed: {
name() {
return `${this.owner.firstName} ${this.owner.lastName}`
}
} }
} }
</script> </script>
@ -26,7 +32,6 @@
.owner-widget { .owner-widget {
display: flex; display: flex;
align-items: center; align-items: center;
margin-top: $small-spacing;
&__avatar { &__avatar {
width: 30px; width: 30px;

View File

@ -0,0 +1,115 @@
<template>
<div class="project-actions">
<a @click="toggleMenu"
class="project-actions__more-link">
<ellipses></ellipses>
</a>
<widget-popover v-if="showMenu"
@hide-me="showMenu = false">
<li class="popover-links__link"><a @click="deleteProject(id)">Projekt löschen</a></li>
<li class="popover-links__link"><a @click="editProject(id)">Projekt bearbeiten</a></li>
<li v-if="!final" class="popover-links__link"><a @click="updateShareState(id, true)">Projekt teilen</a></li>
<li v-if="final" class="popover-links__link"><a @click="updateShareState(id, false)">Projekt nicht mehr teilen</a>
</li>
</widget-popover>
</div>
</template>
<script>
import Ellipses from '@/components/icons/Ellipses.vue';
import WidgetPopover from '@/components/rooms/WidgetPopover';
import DELETE_PROJECT_MUTATION from '@/graphql/gql/mutations/deleteProject.gql';
import PROJECT_QUERY from '@/graphql/gql/projectQuery.gql';
import PROJECTS_QUERY from '@/graphql/gql/allProjects.gql';
import UPDATE_PROJECT_SHARED_STATE_MUTATION from '@/graphql/gql/mutations/updateProjectSharedState.gql';
export default {
props: ['id', 'final'],
components: {
Ellipses,
WidgetPopover
},
data() {
return {
showMenu: false
}
},
methods: {
toggleMenu: function () {
this.showMenu = !this.showMenu
},
editProject(id) {
this.$router.push({name: 'edit-project', params: {id}});
},
deleteProject(id) {
this.$apollo.mutate({
mutation: DELETE_PROJECT_MUTATION,
variables: {
input: {
id
}
},
update(store, {data: {deleteProject: {success}}}) {
if (success) {
const data = store.readQuery({query: PROJECTS_QUERY});
if (data) {
data.projects.edges.splice(data.projects.edges.findIndex(edge => edge.node.id === id), 1);
store.writeQuery({query: PROJECTS_QUERY, data});
}
}
}
}).then(() => {
this.$router.push('/portfolio');
});
},
updateShareState(id, state) {
const project = this;
this.$apollo.mutate({
mutation: UPDATE_PROJECT_SHARED_STATE_MUTATION,
variables: {
input: {
id: this.id,
shared: state
}
},
update(store, {data: {updateProjectSharedState: {shared, errors}}}) {
if (!errors) {
const query = PROJECT_QUERY;
const variables = {
id: project.id
};
const data = store.readQuery({query, variables});
if (data) {
data.project.final = shared;
store.writeQuery({query, variables, data});
}
}
}
})
}
}
}
</script>
<style scoped lang="scss">
@import "@/styles/_variables.scss";
.project-actions {
&__more-link {
cursor: pointer;
}
svg {
width: 30px;
fill: $color-darkgrey-1;
margin-right: 15px;
}
}
</style>

View File

@ -1,24 +1,21 @@
<template> <template>
<div class="project-widget" :class="widgetClass"> <div class="project-widget" :class="widgetClass">
<router-link :to="{name: 'project', params: {slug: slug}}" tag="div" class="project-widget__content" data-cy="project-link"> <router-link :to="{name: 'project', params: {slug: slug}}" tag="div" class="project-widget__content"
data-cy="project-link">
<h3 class="project-widget__title">{{title}}</h3> <h3 class="project-widget__title">{{title}}</h3>
<entry-count-widget :entry-count="entriesCount"></entry-count-widget> <entry-count-widget :entry-count="entriesCount"></entry-count-widget>
<owner-widget :name="owner" :avatarUrl="student.avatarUrl"></owner-widget> <owner-widget class="project-widget__owner" :owner="student"></owner-widget>
</router-link> </router-link>
<widget-footer v-if="isOwner" class="project-widget__footer"> <widget-footer v-if="isOwner" class="project-widget__footer">
<template slot-scope="scope"> <project-actions :id="id" :final="final"></project-actions>
<li class="popover-links__link"><a @click="$emit('delete', id)">Projekt löschen</a></li>
<li class="popover-links__link"><a @click="$emit('edit', id)">Projekt bearbeiten</a></li>
<li v-if="!final" class="popover-links__link"><a @click="share(scope)">Projekt teilen</a></li>
<li v-if="final" class="popover-links__link"><a @click="unshare(scope)">Projekt nicht mehr teilen</a></li>
</template>
</widget-footer> </widget-footer>
</div> </div>
</template> </template>
<script> <script>
import OwnerWidget from '@/components/portfolio/OwnerWidget'; import OwnerWidget from '@/components/portfolio/OwnerWidget';
import ProjectActions from '@/components/portfolio/ProjectActions';
import EntryCountWidget from '@/components/rooms/EntryCountWidget'; import EntryCountWidget from '@/components/rooms/EntryCountWidget';
import WidgetFooter from '@/components/WidgetFooter'; import WidgetFooter from '@/components/WidgetFooter';
@ -28,33 +25,19 @@
components: { components: {
WidgetFooter, WidgetFooter,
EntryCountWidget, EntryCountWidget,
OwnerWidget OwnerWidget,
ProjectActions
}, },
computed: { computed: {
widgetClass () { widgetClass() {
return `project-widget--${this.appearance}`; return `project-widget--${this.appearance}`;
}, },
isOwner () { isOwner() {
return this.student.id === this.userId; return this.student.id === this.userId;
}, },
owner () {
return `${this.student.firstName} ${this.student.lastName}`
}
},
methods: { },
share: function (scope) {
this.updateShare(scope, true);
},
unshare: function (scope) {
this.updateShare(scope, false);
},
updateShare: function (scope, state) {
scope.hide();
this.$emit('updateShare', this.id, state);
}
}
} }
</script> </script>
@ -68,6 +51,7 @@
background-color: $color-accent-4; background-color: $color-accent-4;
@include widget-shadow; @include widget-shadow;
box-sizing: border-box; box-sizing: border-box;
position: relative;
display: -ms-grid; display: -ms-grid;
margin-bottom: $large-spacing; margin-bottom: $large-spacing;
@ -106,6 +90,10 @@
font-weight: 600; font-weight: 600;
} }
&__owner {
margin-top: $small-spacing;
}
&__footer { &__footer {
-ms-grid-row: 2; -ms-grid-row: 2;
} }

View File

@ -0,0 +1,81 @@
<template>
<div class="room-actions">
<a @click="toggleMenu"
class="room-actions__more-link">
<ellipses></ellipses>
</a>
<widget-popover v-if="showMenu"
@hide-me="showMenu = false">
<li class="popover-links__link"><a @click="deleteRoom()">Raum löschen</a></li>
<li class="popover-links__link"><a @click="editRoom()">Raum bearbeiten</a></li>
</widget-popover>
</div>
</template>
<script>
import Ellipses from '@/components/icons/Ellipses.vue';
import WidgetPopover from '@/components/rooms/WidgetPopover';
import DELETE_ROOM_MUTATION from '@/graphql/gql/mutations/deleteRoom.gql';
import ROOMS_QUERY from '@/graphql/gql/roomsQuery.gql';
export default {
props: ['id'],
components: {
Ellipses,
WidgetPopover
},
data() {
return {
showMenu: false
}
},
methods: {
toggleMenu: function () {
this.showMenu = !this.showMenu
},
deleteRoom() {
const theId = this.id;
this.$apollo.mutate({
mutation: DELETE_ROOM_MUTATION,
variables: {
input: {
id: theId
}
},
update(store, {data: {deleteRoom: {success}}}) {
if (success) {
const data = store.readQuery({query: ROOMS_QUERY});
if (data) {
data.rooms.edges.splice(data.rooms.edges.findIndex(edge => edge.node.id === theId), 1);
store.writeQuery({query: ROOMS_QUERY, data});
}
}
}
})
},
editRoom() {
this.$router.push({name: 'edit-room', params: {id: this.id}});
}
}
}
</script>
<style scoped lang="scss">
@import "@/styles/_variables.scss";
.room-actions {
&__more-link {
cursor: pointer;
}
svg {
width: 30px;
fill: $color-darkgrey-1;
margin-right: 15px;
}
}
</style>

View File

@ -6,19 +6,17 @@
<entry-count-widget :entryCount="entryCount"></entry-count-widget> <entry-count-widget :entryCount="entryCount"></entry-count-widget>
</router-link> </router-link>
<widget-footer v-if="canEditRoom"> <widget-footer v-if="canEditRoom">
<li class="popover-links__link"><a @click="deleteRoom()">Raum löschen</a></li> <room-actions :id="this.id"></room-actions>
<li class="popover-links__link"><a @click="editRoom()">Raum bearbeiten</a></li>
</widget-footer> </widget-footer>
</div> </div>
</template> </template>
<script> <script>
import DELETE_ROOM_MUTATION from '@/graphql/gql/mutations/deleteRoom.gql';
import ROOMS_QUERY from '@/graphql/gql/roomsQuery.gql';
import RoomGroupWidget from '@/components/rooms/RoomGroupWidget'; import RoomGroupWidget from '@/components/rooms/RoomGroupWidget';
import EntryCountWidget from '@/components/rooms/EntryCountWidget'; import EntryCountWidget from '@/components/rooms/EntryCountWidget';
import WidgetFooter from '@/components/WidgetFooter'; import WidgetFooter from '@/components/WidgetFooter';
import RoomActions from '@/components/rooms/RoomActions';
import {meQuery} from '@/graphql/queries'; import {meQuery} from '@/graphql/queries';
@ -28,7 +26,8 @@
components: { components: {
EntryCountWidget, EntryCountWidget,
RoomGroupWidget, RoomGroupWidget,
WidgetFooter WidgetFooter,
RoomActions
}, },
data() { data() {
@ -52,32 +51,6 @@
apollo: { apollo: {
me: meQuery me: meQuery
}, },
methods: {
deleteRoom() {
const theId = this.id
this.$apollo.mutate({
mutation: DELETE_ROOM_MUTATION,
variables: {
input: {
id: theId
}
},
update(store, {data: {deleteRoom: {success}}}) {
if (success) {
const data = store.readQuery({query: ROOMS_QUERY});
if (data) {
data.rooms.edges.splice(data.rooms.edges.findIndex(edge => edge.node.id === theId), 1);
store.writeQuery({query: ROOMS_QUERY, data});
}
}
}
})
},
editRoom() {
this.$router.push({name: 'edit-room', params: {id: this.id}});
}
}
} }
</script> </script>

View File

@ -11,7 +11,7 @@
methods: { methods: {
hidePopover() { hidePopover() {
this.$emit('hide-me'); this.$emit('hide-me');
} },
} }
} }
</script> </script>

View File

@ -0,0 +1,7 @@
mutation UpdateProjectShareState($input: UpdateProjectSharedStateInput!) {
updateProjectSharedState(input: $input) {
errors
success
shared
}
}

View File

@ -7,9 +7,6 @@
v-for="project in projects" v-for="project in projects"
v-bind="project" v-bind="project"
:userId="userId" :userId="userId"
@delete="deleteProject"
@updateShare="updateShareState"
@edit="editProject"
:key="project.id" :key="project.id"
class="portfolio__project" class="portfolio__project"
></project-widget> ></project-widget>
@ -23,8 +20,6 @@
import ME_QUERY from '@/graphql/gql/meQuery.gql'; import ME_QUERY from '@/graphql/gql/meQuery.gql';
import PROJECTS_QUERY from '@/graphql/gql/allProjects.gql'; import PROJECTS_QUERY from '@/graphql/gql/allProjects.gql';
import DELETE_PROJECT_MUTATION from '@/graphql/gql/mutations/deleteProject.gql';
import UPDATE_PROJECT_MUTATION from '@/graphql/gql/mutations/updateProject.gql';
export default { export default {
components: { components: {
@ -57,50 +52,6 @@
return this.me.id; return this.me.id;
} }
}, },
methods: {
deleteProject(id) {
this.$apollo.mutate({
mutation: DELETE_PROJECT_MUTATION,
variables: {
input: {
id
}
},
update(store, {data: {deleteProject: {success}}}) {
if (success) {
const data = store.readQuery({query: PROJECTS_QUERY});
if (data) {
data.projects.edges.splice(data.projects.edges.findIndex(edge => edge.node.id === id), 1);
store.writeQuery({query: PROJECTS_QUERY, data});
}
}
}
})
},
editProject(id) {
this.$router.push({name: 'edit-project', params: {id}});
},
updateShareState(id, state) {
const project = this.projects.filter(project => project.id === id)[0];
this.$apollo.mutate({
mutation: UPDATE_PROJECT_MUTATION,
variables: {
input: {
project: {
id: project.id,
title: project.title,
description: project.description,
appearance: project.appearance,
objectives: project.objectives,
final: state
}
}
}
})
}
}
} }
</script> </script>

View File

@ -11,9 +11,16 @@
<li class="project__objective" :key="index" v-for="(objective, index) in objectives">{{objective}}</li> <li class="project__objective" :key="index" v-for="(objective, index) in objectives">{{objective}}</li>
</ul> </ul>
<div class="project__meta">
<project-actions :id="this.project.id"></project-actions>
<owner-widget :owner="this.project.student"></owner-widget>
<entry-count-widget :entry-count="projectEntryCount"></entry-count-widget>
</div>
</div> </div>
<div class="project__content"> <div class="project__content">
<add-project-entry v-if="isOwner" class="project__add-entry" data-cy="add-project-entry" :project="project.id"></add-project-entry> <add-project-entry v-if="isOwner" class="project__add-entry" data-cy="add-project-entry"
:project="project.id"></add-project-entry>
<project-entry v-bind="entry" v-for="(entry, index) in project.entries" :key="index"></project-entry> <project-entry v-bind="entry" v-for="(entry, index) in project.entries" :key="index"></project-entry>
</div> </div>
</div> </div>
@ -21,7 +28,10 @@
<script> <script>
import ProjectEntry from '@/components/portfolio/ProjectEntry'; import ProjectEntry from '@/components/portfolio/ProjectEntry';
import ProjectActions from '@/components/portfolio/ProjectActions';
import AddProjectEntry from '@/components/portfolio/AddProjectEntry'; import AddProjectEntry from '@/components/portfolio/AddProjectEntry';
import EntryCountWidget from '@/components/rooms/EntryCountWidget';
import OwnerWidget from '@/components/portfolio/OwnerWidget';
import ME_QUERY from '@/graphql/gql/meQuery.gql'; import ME_QUERY from '@/graphql/gql/meQuery.gql';
import PROJECT_QUERY from '@/graphql/gql/projectQuery.gql'; import PROJECT_QUERY from '@/graphql/gql/projectQuery.gql';
@ -31,7 +41,10 @@
components: { components: {
AddProjectEntry, AddProjectEntry,
ProjectEntry ProjectEntry,
ProjectActions,
EntryCountWidget,
OwnerWidget
}, },
computed: { computed: {
@ -44,7 +57,10 @@
}, },
isOwner() { isOwner() {
return this.me.id === this.project.student.id; return this.me.id === this.project.student.id;
} },
projectEntryCount() {
return this.project.entries ? this.project.entries.length : 0;
}
}, },
apollo: { apollo: {
@ -129,12 +145,33 @@
line-height: 1.5; line-height: 1.5;
} }
&__content { &__meta {
background-color: $color-grey--lighter;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
max-width: 840px; @include desktop {
align-content: center; flex-direction: row-reverse;
}
justify-content: start;
position: relative;
align-items: center;
& > :first-child {
margin-left: $large-spacing;
}
& > :nth-child(2) {
margin-left: $large-spacing;
}
}
&__content {
background-color: rgba($color-darkgrey-1, 0.18);
display: flex;
flex-direction: column;
/*max-width: 840px;*/
width: 100%;
min-height: 75vh;
align-content: start;
margin: 0 auto; margin: 0 auto;
@supports (display: grid) { @supports (display: grid) {
display: grid; display: grid;

View File

@ -6,6 +6,7 @@
{{room.description}} {{room.description}}
</p> </p>
<div class="room__meta"> <div class="room__meta">
<room-actions :id="this.room.id"></room-actions>
<room-group-widget v-bind="room.schoolClass"></room-group-widget> <room-group-widget v-bind="room.schoolClass"></room-group-widget>
<entry-count-widget :entry-count="roomEntryCount"></entry-count-widget> <entry-count-widget :entry-count="roomEntryCount"></entry-count-widget>
</div> </div>
@ -29,6 +30,7 @@
import RoomEntry from '@/components/rooms/RoomEntry.vue'; import RoomEntry from '@/components/rooms/RoomEntry.vue';
import RoomGroupWidget from '@/components/rooms/RoomGroupWidget'; import RoomGroupWidget from '@/components/rooms/RoomGroupWidget';
import EntryCountWidget from '@/components/rooms/EntryCountWidget'; import EntryCountWidget from '@/components/rooms/EntryCountWidget';
import RoomActions from '@/components/rooms/RoomActions';
export default { export default {
props: ['slug'], props: ['slug'],
@ -37,7 +39,8 @@
EntryCountWidget, EntryCountWidget,
RoomGroupWidget, RoomGroupWidget,
AddRoomEntryButton, AddRoomEntryButton,
RoomEntry RoomEntry,
RoomActions
}, },
beforeDestroy() { beforeDestroy() {
@ -115,9 +118,14 @@
flex-direction: row-reverse; flex-direction: row-reverse;
} }
justify-content: start; justify-content: start;
position: relative;
& > :first-child { & > :first-child {
margin-right: 45px; margin-left: $large-spacing;
}
& > :nth-child(2) {
margin-left: $large-spacing;
} }
} }

View File

@ -143,6 +143,38 @@ class DeleteProjectEntry(relay.ClientIDMutation):
return cls(success=True) return cls(success=True)
class UpdateProjectSharedState(relay.ClientIDMutation):
class Input:
id = graphene.ID()
shared = graphene.Boolean()
success = graphene.Boolean()
shared = graphene.Boolean()
errors = graphene.List(graphene.String)
@classmethod
def mutate_and_get_payload(cls, root, info, **args):
try:
id = args.get('id')
shared = args.get('shared')
user = info.context.user
project = get_object(Project, id)
if project.student != user:
raise PermissionError()
project.final = shared
project.save()
return cls(success=True, shared=shared)
except PermissionError:
errors = ["You don't have the permission to do that."]
except Exception as e:
errors = ['Error: {}'.format(e)]
return cls(success=False, shared=None, errors=errors)
class PortfolioMutations: class PortfolioMutations:
add_project = AddProject.Field() add_project = AddProject.Field()
update_project = UpdateProject.Field() update_project = UpdateProject.Field()
@ -150,3 +182,4 @@ class PortfolioMutations:
add_project_entry = AddProjectEntry.Field() add_project_entry = AddProjectEntry.Field()
update_project_entry = UpdateProjectEntry.Field() update_project_entry = UpdateProjectEntry.Field()
delete_project_entry = DeleteProjectEntry.Field() delete_project_entry = DeleteProjectEntry.Field()
update_project_shared_state = UpdateProjectSharedState.Field()