Add edit route for custom content blocks

This commit is contained in:
Ramon Wenger 2022-02-10 12:06:03 +01:00
parent fdb408de84
commit f563483b79
10 changed files with 490 additions and 290 deletions

View File

@ -85,6 +85,7 @@
import {CONTENT_TYPE} from '@/consts/types';
import PopoverLink from '@/components/ui/PopoverLink';
import {removeAtIndex} from '@/graphql/immutable-operations';
import {EDIT_CONTENT_BLOCK_PAGE} from '@/router/module.names';
const ContentComponent = () => import(/* webpackChunkName: "content-components" */'@/components/content-blocks/ContentComponent');
const instruments = {
@ -207,7 +208,13 @@
},
methods: {
editContentBlock(contentBlock) {
this.$store.dispatch('editContentBlock', contentBlock.id);
const route = {
name: EDIT_CONTENT_BLOCK_PAGE,
params: {
id: contentBlock.id
}
};
this.$router.push(route);
},
deleteContentBlock(contentBlock) {
const parent = this.parent;

View File

@ -0,0 +1,308 @@
<template>
<div class="content-block-form content-list__parent">
<div class="content-block-form__content">
<h1
class="heading-1 content-block-form__heading"
data-cy="content-block-form-heading"
>
{{ title }}
</h1>
<!-- Assignment toggle / checkbox -->
<toggle
:bordered="false"
:checked="localContentBlock.isAssignment"
class="content-block-form__task-toggle"
label="Inhaltsblock als Auftrag formatieren"
@input="localContentBlock.isAssignment=$event"
/>
<!-- Form for title of content block -->
<content-form-section title="Titel">
<input-with-label
:value="localContentBlock.title"
label="Name"
placeholder="z.B. Auftrag 3"
@input="localContentBlock.title=$event"
/>
</content-form-section>
<!-- Add content at top of content block -->
<add-content-link
class="content-block-form__segment"
@click="addBlock(-1)"
/>
<!-- Loop for outer contents layer -->
<div
class="content-list content-list--creator content-block-form__segment"
v-for="(block, outer) in localContentBlock.contents"
:key="block.id"
>
<!-- If the block is a content list -->
<ol
class="content-list__item content-block-form__segment"
data-cy="content-list-item"
v-if="block.type === 'content_list_item'"
>
<li
class="content-block-form__segment"
v-for="(content, index) in block.contents"
:key="content.id"
>
<content-element
:element="content"
class="content-block-form__segment"
@update="update(index, $event, outer)"
@remove="remove(outer, index)"
@up="up(outer, index)"
@down="down(outer, index)"
/>
<add-content-link
class="content-block-form__add-button"
@click="addBlock(outer, index)"
/>
</li>
</ol>
<!-- If the block is a single element -->
<content-element
:element="block"
class="content-block-form__segment"
v-else
@update="update(outer, $event)"
@remove="remove(outer)"
@up="up(outer)"
@down="down(outer)"
/>
<!-- Add element after the looped item -->
<add-content-link
class="content-block-form__segment"
@click="addBlock(outer)"
/>
</div>
<!-- Save and Cancel buttons -->
<footer class="content-block-form__footer">
<button
:disabled="!isValid"
class="button button--primary"
@click="save(localContentBlock)"
>
Speichern
</button>
<a
class="button"
@click="$emit('back')"
>Abbrechen</a>
</footer>
</div><!-- -->
</div>
</template>
<script>
import Toggle from '@/components/ui/Toggle';
import ContentFormSection from '@/components/content-block-form/ContentFormSection';
import InputWithLabel from '@/components/ui/InputWithLabel';
import AddContentLink from '@/components/content-block-form/AddContentLink';
import ContentElement from '@/components/content-block-form/ContentElement';
import {removeAtIndex, replaceAtIndex, swapElements} from '@/graphql/immutable-operations';
import {CHOOSER, transformInnerContents} from '@/components/content-block-form/helpers';
export default {
props: {
title: {
type: String,
default: ''
},
contentBlock: {
type: Object,
required: true
}
},
components: {
ContentElement,
AddContentLink,
InputWithLabel,
ContentFormSection,
Toggle,
},
data() {
return {
localContentBlock: Object.assign({}, {
title: this.contentBlock.title,
// contents: [...this.contentBlock.contents],
contents: transformInnerContents([...this.contentBlock.contents]),
id: this.contentBlock.id || undefined,
isAssignment: this.contentBlock.type && this.contentBlock.type.toLowerCase() === 'task',
}),
};
},
computed: {
isValid() {
return this.localContentBlock.title > '';
}
},
methods: {
update(index, element, parent) {
if (parent === undefined) {
// element is top level
this.localContentBlock.contents = [
...this.localContentBlock.contents.slice(0, index),
element,
...this.localContentBlock.contents.slice(index + 1)
];
} else {
const parentBlock = this.localContentBlock.contents[parent];
this.localContentBlock.contents = [
...this.localContentBlock.contents.slice(0, parent),
{
...parentBlock,
contents: [
...parentBlock.contents.slice(0, index),
element,
...parentBlock.contents.slice(index + 1),
]
},
...this.localContentBlock.contents.slice(parent + 1)
];
}
},
addBlock(afterOuterIndex, innerIndex) {
if (innerIndex !== undefined) {
const block = this.localContentBlock.contents[afterOuterIndex];
this.localContentBlock.contents = [
...this.localContentBlock.contents.slice(0, afterOuterIndex),
{
...block,
contents: [
...block.contents.slice(0, innerIndex + 1),
{
id: -1,
type: CHOOSER,
},
...block.contents.slice(innerIndex + 1)
]
},
...this.localContentBlock.contents.slice(afterOuterIndex + 1)
];
} else {
this.localContentBlock.contents = [
...this.localContentBlock.contents.slice(0, afterOuterIndex+1),
{
id: -1,
type: CHOOSER,
includeListOption: true
},
...this.localContentBlock.contents.slice(afterOuterIndex+1)
];
}
},
remove(outer, inner) {
this.$modal.open('confirm')
.then(() => {
this.executeRemoval(outer, inner);
})
.catch(() => {
});
},
shift(outer, inner, distance) {
if (inner === undefined) {
this.localContentBlock.contents = swapElements(this.localContentBlock.contents, outer, outer + distance);
} else {
const {contents} = this.localContentBlock;
const outerElement = contents[outer];
const newOuterElement = {
...outerElement,
contents: swapElements(outerElement.contents, inner, inner + distance)
};
this.localContentBlock.contents = replaceAtIndex(contents, outer, newOuterElement);
}
},
up(outer, inner){
this.shift(outer, inner, -1);
},
down(outer, inner){
this.shift(outer, inner, 1);
},
executeRemoval(outer, inner) {
if (inner === undefined) {
// not a list item container, just remove the element from the outer array
this.localContentBlock.contents = removeAtIndex(this.localContentBlock.contents, outer);
} else {
let prevInnerContents = this.localContentBlock.contents[outer].contents;
let innerContents = removeAtIndex(prevInnerContents, inner);
if (innerContents.length) {
/*
there is still an element inside the outer element after removal,
so we replace the previous element in the outer array with the new one with fewer contents
*/
let element = {
...this.localContentBlock.contents[outer],
contents: innerContents
};
this.localContentBlock.contents = replaceAtIndex(this.localContentBlock.contents, outer, element);
} else {
// inner contents is now empty, remove the whole element from the outer array
this.localContentBlock.contents = removeAtIndex(this.localContentBlock.contents, outer);
}
}
},
save(contentBlock) {
this.$emit('save', contentBlock);
},
}
};
</script>
<style scoped lang="scss">
@import '~styles/helpers';
.content-block-form {
width: 800px;
max-width: 100%;
display: grid;
grid-template-columns: 800px;
grid-template-rows: 1fr auto;
grid-template-areas:
'content'
'footer';
&__heading {
@include heading-1;
}
&__task-toggle {
margin-bottom: $large-spacing;
}
&__add-button {
}
&__segment {
margin-bottom: $large-spacing;
:last-child {
margin-bottom: 0;
}
}
&__content {
grid-area: content;
overflow-x: visible;
overflow-y: auto;
padding: 10px;
}
&__footer {
margin-top: auto;
grid-area: footer;
}
}
</style>

View File

@ -12,7 +12,7 @@
import store from '@/store/index';
import EDIT_CONTENT_BLOCK_MUTATION from '@/graphql/gql/mutations/mutateContentBlock.gql';
import EDIT_CONTENT_BLOCK_MUTATION from 'gql/mutations/editContentBlock.gql';
import MODULE_DETAILS_QUERY from '@/graphql/gql/queries/modules/moduleDetailsQuery.gql';
import CONTENT_BLOCK_QUERY from '@/graphql/gql/queries/contentBlockQuery.gql';
import { setUserBlockType } from '@/helpers/content-block';

View File

@ -0,0 +1,40 @@
import {setUserBlockType} from '@/helpers/content-block';
import NEW_CONTENT_BLOCK_MUTATION from '@/graphql/gql/mutations/addContentBlock.gql';
import MODULE_DETAILS_QUERY from '@/graphql/gql/queries/modules/moduleDetailsQuery.gql';
export const CHOOSER = 'content-block-element-chooser-widget';
export const chooserFilter = value => value.type !== CHOOSER;
export const cleanUpContents = (contents) => {
let filteredContents = contents
.filter(chooserFilter); // only use items that are not chooser elements
return filteredContents.map(content => {
// if the element has a contents property, it's a list of contents, filter them
if (content.contents) {
return {
...content,
contents: content.contents.filter(chooserFilter)
};
}
// else just return it
return content;
});
};
// transform value prop to contents, to better handle the input type on the server
export const transformInnerContents = (contents) => {
let ret = [];
for (let content of contents) {
if (Array.isArray(content.value)) {
const {value, ...contentWithoutValue} = content;
ret.push({
...contentWithoutValue,
contents: value
});
} else {
ret.push(content);
}
}
return ret;
};

View File

@ -5,7 +5,7 @@ import {
OBJECTIVE_GROUP_TYPE,
OBJECTIVE_TYPE
} from '@/consts/types';
import CHANGE_CONTENT_BLOCK_MUTATION from '@/graphql/gql/mutations/mutateContentBlock.gql';
import EDIT_CONTENT_BLOCK_MUTATION from '@/graphql/gql/mutations/editContentBlock.gql';
import UPDATE_OBJECTIVE_VISIBILITY_MUTATION from '@/graphql/gql/mutations/updateObjectiveVisibility.gql';
import UPDATE_OBJECTIVE_GROUP_VISIBILITY_MUTATION from '@/graphql/gql/mutations/updateObjectiveGroupVisibility.gql';
import UPDATE_CHAPTER_VISIBILITY_MUTATION from '@/graphql/gql/mutations/updateChapterVisibility.gql';
@ -14,7 +14,7 @@ export const createVisibilityMutation = (type, id, visibility) => {
let mutation, variables;
switch (type) {
case CONTENT_TYPE:
mutation = CHANGE_CONTENT_BLOCK_MUTATION;
mutation = EDIT_CONTENT_BLOCK_MUTATION;
variables = {
input: {
id,

View File

@ -1,108 +1,20 @@
<template>
<div class="create-content-block content-list__parent">
<div class="create-content-block__content">
<h1 class="heading-1 create-content-block__heading">
Inhaltsblock erfassen
</h1>
<toggle
:bordered="false"
:checked="contentBlock.isAssignment"
class="create-content-block__task-toggle"
label="Inhaltsblock als Auftrag formatieren"
@input="contentBlock.isAssignment=$event"
/>
<content-form-section title="Titel">
<input-with-label
:value="contentBlock.title"
label="Name"
placeholder="z.B. Auftrag 3"
@input="contentBlock.title=$event"
/>
</content-form-section>
<add-content-link
class="create-content-block__segment"
@click="addBlock(-1)"
/>
<div
class="content-list content-list--creator create-content-block__segment"
v-for="(block, outer) in contentBlock.contents"
:key="block.id"
>
<ol
class="content-list__item create-content-block__segment"
v-if="block.type === 'content_list_item'"
>
<li
class="create-content-block__segment"
v-for="(content, index) in block.contents"
:key="content.id"
>
<content-element
:element="content"
class="create-content-block__segment"
@update="update(index, $event, outer)"
@remove="remove(outer, index)"
@up="up(outer, index)"
@down="down(outer, index)"
/>
<add-content-link
class="create-content-block__add-button"
@click="addBlock(outer, index)"
/>
</li>
</ol>
<content-element
:element="block"
class="create-content-block__segment"
v-else
@update="update(outer, $event)"
@remove="remove(outer)"
@up="up(outer)"
@down="down(outer)"
/>
<add-content-link
class="create-content-block__segment"
@click="addBlock(outer)"
/>
</div>
<footer class="create-content-block__footer">
<button
:disabled="!isValid"
class="button button--primary"
@click="save(contentBlock)"
>
Speichern
</button>
<a
class="button"
@click="goToModule"
>Abbrechen</a>
</footer>
</div>
</div>
<content-block-form
title="Inhaltsblock erfassen"
:content-block="contentBlock"
@back="goToModule"
@save="save"
/>
</template>
<script>
import Vue from 'vue';
import Toggle from '@/components/ui/Toggle';
import ContentFormSection from '@/components/content-block-form/ContentFormSection';
import InputWithLabel from '@/components/ui/InputWithLabel';
import AddContentLink from '@/components/content-block-form/AddContentLink';
import ContentElement from '@/components/content-block-form/ContentElement';
import NEW_CONTENT_BLOCK_MUTATION from '@/graphql/gql/mutations/addContentBlock.gql';
import ContentBlockForm from '@/components/content-block-form/ContentBlockForm';
import {setUserBlockType} from '@/helpers/content-block';
import NEW_CONTENT_BLOCK_MUTATION from '@/graphql/gql/mutations/addContentBlock.gql';
import MODULE_DETAILS_QUERY from '@/graphql/gql/queries/modules/moduleDetailsQuery.gql';
import {insertAtIndex, removeAtIndex, replaceAtIndex, swapElements} from '@/graphql/immutable-operations';
const CHOOSER = 'content-block-element-chooser-widget';
const chooserFilter = value => value.type !== CHOOSER;
import {cleanUpContents} from '@/components/content-block-form/helpers';
export default Vue.extend({
props: {
@ -117,161 +29,32 @@
},
components: {
ContentElement,
AddContentLink,
InputWithLabel,
ContentFormSection,
Toggle,
ContentBlockForm,
},
data: () => ({
contentBlock: {
title: '',
isAssignment: false,
contents: [
{}
]},
contents: [
]},
}),
computed: {
isValid() {
return this.contentBlock.title > '';
}
},
methods: {
update(index, element, parent) {
if (parent === undefined) {
this.contentBlock.contents = [
...this.contentBlock.contents.slice(0, index),
element,
...this.contentBlock.contents.slice(index + 1)
];
} else {
const parentBlock = this.contentBlock.contents[parent];
this.contentBlock.contents = [
...this.contentBlock.contents.slice(0, parent),
{
...parentBlock,
contents: [
...parentBlock.contents.slice(0, index),
element,
...parentBlock.contents.slice(index + 1),
]
},
...this.contentBlock.contents.slice(parent + 1)
];
}
},
addBlock(afterOuterIndex, innerIndex) {
if (innerIndex !== undefined) {
const block = this.contentBlock.contents[afterOuterIndex];
this.contentBlock.contents = [
...this.contentBlock.contents.slice(0, afterOuterIndex),
{
...block,
contents: [
...block.contents.slice(0, innerIndex + 1),
{
id: -1,
type: CHOOSER,
},
...block.contents.slice(innerIndex + 1)
]
},
...this.contentBlock.contents.slice(afterOuterIndex + 1)
];
} else {
this.contentBlock.contents = [
...this.contentBlock.contents.slice(0, afterOuterIndex+1),
{
id: -1,
type: CHOOSER,
includeListOption: true
},
...this.contentBlock.contents.slice(afterOuterIndex+1)
];
}
},
remove(outer, inner) {
this.$modal.open('confirm')
.then(() => {
this.executeRemoval(outer, inner);
})
.catch(() => {
});
},
shift(outer, inner, distance) {
if (inner === undefined) {
this.contentBlock.contents = swapElements(this.contentBlock.contents, outer, outer + distance);
} else {
const {contents} = this.contentBlock;
const outerElement = contents[outer];
const newOuterElement = {
...outerElement,
contents: swapElements(outerElement.contents, inner, inner + distance)
};
this.contentBlock.contents = replaceAtIndex(contents, outer, newOuterElement);
}
},
up(outer, inner){
this.shift(outer, inner, -1);
},
down(outer, inner){
this.shift(outer, inner, 1);
},
executeRemoval(outer, inner) {
if (inner === undefined) {
// not a list item container, just remove the element from the outer array
this.contentBlock.contents = removeAtIndex(this.contentBlock.contents, outer);
} else {
let prevInnerContents = this.contentBlock.contents[outer].contents;
let innerContents = removeAtIndex(prevInnerContents, inner);
if (innerContents.length) {
/*
there is still an element inside the outer element after removal,
so we replace the previous element in the outer array with the new one with fewer contents
*/
let element = {
...this.contentBlock.contents[outer],
contents: innerContents
};
this.contentBlock.contents = replaceAtIndex(this.contentBlock.contents, outer, element);
} else {
// inner contents is now empty, remove the whole element from the outer array
this.contentBlock.contents = removeAtIndex(this.contentBlock.contents, outer);
}
}
},
save({title, contents, isAssignment}) {
let filteredContents = contents
.filter(chooserFilter); // only use items that are not chooser elements
let mappedContents = filteredContents.map(content => {
// if the element has a contents property, it's a list of contents, filter them
if (content.contents) {
return {
...content,
contents: content.contents.filter(chooserFilter)
};
}
// else just return it
return content;
});
let cleanedContents = cleanUpContents(contents);
const contentBlock = {
title: title,
contents: mappedContents,
contents: cleanedContents,
type: setUserBlockType(isAssignment),
};
let input;
const { parent, after, slug} = this.$route.params;
const { parent, after, slug } = this.$route.params;
if(after) {
input = {
contentBlock,
after
};
};
} else {
input = {
contentBlock,
@ -295,51 +78,8 @@
// use the history, so the scroll position is preserved
this.$router.go(-1);
}
},
}
});
</script>
<style scoped lang="scss">
@import '~styles/helpers';
.create-content-block {
width: 800px;
max-width: 100%;
display: grid;
grid-template-columns: 800px;
grid-template-rows: 1fr auto;
grid-template-areas:
'content'
'footer';
&__heading {
@include heading-1;
}
&__task-toggle {
margin-bottom: $large-spacing;
}
&__add-button {
}
&__segment {
margin-bottom: $large-spacing;
:last-child {
margin-bottom: 0;
}
}
&__content {
grid-area: content;
overflow-x: visible;
overflow-y: auto;
padding: 10px;
}
&__footer {
margin-top: auto;
grid-area: footer;
}
}
</style>

View File

@ -0,0 +1,94 @@
<template>
<component
:is="component"
v-bind="componentProps"
@save="save"
@back="goToModule"
/>
</template>
<script>
import CONTENT_BLOCK_QUERY from '@/graphql/gql/queries/contentBlockQuery.gql';
import ContentBlockForm from '@/components/content-block-form/ContentBlockForm';
import EDIT_CONTENT_BLOCK_MUTATION from '@/graphql/gql/mutations/editContentBlock.gql';
import MODULE_DETAILS_QUERY from '@/graphql/gql/queries/modules/moduleDetailsQuery.gql';
import {setUserBlockType} from '@/helpers/content-block';
import {cleanUpContents} from '@/components/content-block-form/helpers';
// import ContentBlockForm from '@/components/content-block-form/ContentBlockForm';
export default {
props: {
id: {
type: String,
required: true
},
},
components: {ContentBlockForm},
data: ()=>({
contentBlock: null,
}),
computed: {
// only display component once the prop is loaded
component() {
if (this.contentBlock) {
return 'content-block-form';
}
return '';
},
componentProps() {
return {
title: 'Inhalte bearbeiten',
contentBlock: this.contentBlock
};
}
},
apollo: {
contentBlock: {
query: CONTENT_BLOCK_QUERY,
variables() {
return {
id: this.id
};
}
}
},
methods: {
save({title, contents, isAssignment, id}) {
let cleanedContents = cleanUpContents(contents);
const contentBlock = {
title: title,
contents: cleanedContents,
type: setUserBlockType(isAssignment),
};
const { slug } = this.$route.params;
const input = {
id,
contentBlock
};
this.$apollo.mutate({
mutation: EDIT_CONTENT_BLOCK_MUTATION,
variables: {
input
},
refetchQueries: [{
query: MODULE_DETAILS_QUERY,
variables: { slug }
}]
}).then(this.goToModule);
},
goToModule() {
// use the history, so the scroll position is preserved
this.$router.go(-1);
}
}
};
</script>

View File

@ -6,3 +6,4 @@ export const SNAPSHOT_LIST = 'snapshot-list';
export const SNAPSHOT_DETAIL = 'snapshot-detail';
export const CREATE_CONTENT_BLOCK_AFTER_PAGE = 'create-content-block-after';
export const CREATE_CONTENT_BLOCK_UNDER_PARENT_PAGE = 'create-content-block-under-parent';
export const EDIT_CONTENT_BLOCK_PAGE = 'edit-content-block';

View File

@ -5,10 +5,12 @@ import {
SNAPSHOT_LIST,
SUBMISSIONS_PAGE,
VISIBILITY_PAGE,
CREATE_CONTENT_BLOCK_AFTER_PAGE, CREATE_CONTENT_BLOCK_UNDER_PARENT_PAGE,
CREATE_CONTENT_BLOCK_AFTER_PAGE,
CREATE_CONTENT_BLOCK_UNDER_PARENT_PAGE, EDIT_CONTENT_BLOCK_PAGE,
} from '@/router/module.names';
import {LAYOUT_SIMPLE} from '@/router/core.constants';
import createContentBlock from '@/pages/createContentBlock';
import editContentBlock from '@/pages/editContentBlock';
const moduleBase = () => import(/* webpackChunkName: "modules" */'@/pages/module/module-base');
const module = () => import(/* webpackChunkName: "modules" */'@/pages/module/module');
@ -18,14 +20,15 @@ const settingsPage = () => import(/* webpackChunkName: "modules" */'@/pages/modu
const snapshots = () => import(/* webpackChunkName: "modules" */'@/pages/snapshot/snapshots');
const snapshot = () => import(/* webpackChunkName: "modules" */'@/pages/snapshot/snapshot');
const contentBlockFormMeta = {
// layout: LAYOUT_SIMPLE,
hideFooter: true,
hideHeader: true,
showSubNavigation: true
};
const createContentBlockRouteFragment = {
component: createContentBlock,
meta: {
// layout: LAYOUT_SIMPLE,
hideFooter: true,
hideHeader: true,
showSubNavigation: true
},
meta: contentBlockFormMeta,
props: true
};
@ -85,6 +88,13 @@ export default [
fullWidth: true,
},
},
{
path: 'edit/:id',
meta: contentBlockFormMeta,
props: true,
component: editContentBlock,
name: EDIT_CONTENT_BLOCK_PAGE
},
{
...createContentBlockRouteFragment,
path: 'add-after/:after',