Merged in feature/MS-928-custom-solutions-form (pull request #149)
Feature/MS-928 custom solutions form
This commit is contained in:
commit
722439ca9d
|
|
@ -1,3 +1,4 @@
|
||||||
|
<!-- Element used to display Blocks in a ContentForm -->
|
||||||
<template>
|
<template>
|
||||||
<div class="content-element">
|
<div class="content-element">
|
||||||
<!-- Element Chooser if element has chooser type or no type -->
|
<!-- Element Chooser if element has chooser type or no type -->
|
||||||
|
|
@ -38,65 +39,35 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script lang="ts">
|
||||||
|
import { defineAsyncComponent } from 'vue';
|
||||||
import ContentFormSection from '@/components/content-block-form/ContentFormSection.vue';
|
import ContentFormSection from '@/components/content-block-form/ContentFormSection.vue';
|
||||||
import ContentElementActions from '@/components/content-block-form/ContentElementActions.vue';
|
import ContentElementActions from '@/components/content-block-form/ContentElementActions.vue';
|
||||||
import { defineAsyncComponent } from 'vue';
|
|
||||||
|
|
||||||
const TrashIcon = defineAsyncComponent(() => import('@/components/icons/TrashIcon.vue'));
|
const TrashIcon = defineAsyncComponent(() => import('@/components/icons/TrashIcon.vue'));
|
||||||
const ContentBlockElementChooserWidget = defineAsyncComponent(() =>
|
const ContentBlockElementChooserWidget = defineAsyncComponent(
|
||||||
import('@/components/content-forms/ContentBlockElementChooserWidget.vue')
|
() => import('@/components/content-forms/ContentBlockElementChooserWidget.vue')
|
||||||
);
|
);
|
||||||
const LinkForm = defineAsyncComponent(() =>
|
const LinkForm = defineAsyncComponent(() => import('@/components/content-forms/LinkForm.vue'));
|
||||||
import('@/components/content-forms/LinkForm.vue')
|
const VideoForm = defineAsyncComponent(() => import('@/components/content-forms/VideoForm.vue'));
|
||||||
);
|
const ImageForm = defineAsyncComponent(() => import('@/components/content-forms/ImageForm.vue'));
|
||||||
const VideoForm = defineAsyncComponent(() =>
|
const DocumentForm = defineAsyncComponent(() => import('@/components/content-forms/DocumentForm.vue'));
|
||||||
import('@/components/content-forms/VideoForm.vue')
|
const AssignmentForm = defineAsyncComponent(() => import('@/components/content-forms/AssignmentForm.vue'));
|
||||||
);
|
const TextForm = defineAsyncComponent(
|
||||||
const ImageForm = defineAsyncComponent(() =>
|
() => import(/* webpackChunkName: "content-forms" */ '@/components/content-forms/TipTap.vue')
|
||||||
import('@/components/content-forms/ImageForm.vue')
|
|
||||||
);
|
|
||||||
const DocumentForm = defineAsyncComponent(() =>
|
|
||||||
import('@/components/content-forms/DocumentForm.vue')
|
|
||||||
);
|
|
||||||
const AssignmentForm = defineAsyncComponent(() =>
|
|
||||||
import('@/components/content-forms/AssignmentForm.vue')
|
|
||||||
);
|
|
||||||
const TextForm = defineAsyncComponent(() =>
|
|
||||||
import(/* webpackChunkName: "content-forms" */ '@/components/content-forms/TipTap.vue')
|
|
||||||
);
|
|
||||||
const SubtitleForm = defineAsyncComponent(() =>
|
|
||||||
import('@/components/content-forms/SubtitleForm.vue')
|
|
||||||
);
|
);
|
||||||
|
const SubtitleForm = defineAsyncComponent(() => import('@/components/content-forms/SubtitleForm.vue'));
|
||||||
|
const SolutionForm = defineAsyncComponent(() => import('@/components/content-forms/SolutionForm.vue'));
|
||||||
// readonly blocks
|
// readonly blocks
|
||||||
const Assignment = defineAsyncComponent(() =>
|
const Assignment = defineAsyncComponent(() => import('@/components/content-blocks/assignment/Assignment.vue'));
|
||||||
import('@/components/content-blocks/assignment/Assignment.vue')
|
const SurveyBlock = defineAsyncComponent(() => import('@/components/content-blocks/SurveyBlock.vue'));
|
||||||
);
|
const Solution = defineAsyncComponent(() => import('@/components/content-blocks/Solution.vue'));
|
||||||
const SurveyBlock = defineAsyncComponent(() =>
|
const ImageBlock = defineAsyncComponent(() => import('@/components/content-blocks/ImageBlock.vue'));
|
||||||
import('@/components/content-blocks/SurveyBlock.vue')
|
const Instruction = defineAsyncComponent(() => import('@/components/content-blocks/Instruction.vue'));
|
||||||
);
|
const ModuleRoomSlug = defineAsyncComponent(() => import('@/components/content-blocks/ModuleRoomSlug.vue'));
|
||||||
const Solution = defineAsyncComponent(() =>
|
const CmsDocumentBlock = defineAsyncComponent(() => import('@/components/content-blocks/CmsDocumentBlock.vue'));
|
||||||
import('@/components/content-blocks/Solution.vue')
|
const ThinglinkBlock = defineAsyncComponent(() => import('@/components/content-blocks/ThinglinkBlock.vue'));
|
||||||
);
|
const InfogramBlock = defineAsyncComponent(() => import('@/components/content-blocks/InfogramBlock.vue'));
|
||||||
const ImageBlock = defineAsyncComponent(() =>
|
|
||||||
import('@/components/content-blocks/ImageBlock.vue')
|
|
||||||
);
|
|
||||||
const Instruction = defineAsyncComponent(() =>
|
|
||||||
import('@/components/content-blocks/Instruction.vue')
|
|
||||||
);
|
|
||||||
const ModuleRoomSlug = defineAsyncComponent(() =>
|
|
||||||
import('@/components/content-blocks/ModuleRoomSlug.vue')
|
|
||||||
);
|
|
||||||
const CmsDocumentBlock = defineAsyncComponent(() =>
|
|
||||||
import('@/components/content-blocks/CmsDocumentBlock.vue')
|
|
||||||
);
|
|
||||||
const ThinglinkBlock = defineAsyncComponent(() =>
|
|
||||||
import('@/components/content-blocks/ThinglinkBlock.vue')
|
|
||||||
);
|
|
||||||
const InfogramBlock = defineAsyncComponent(() =>
|
|
||||||
import('@/components/content-blocks/InfogramBlock.vue')
|
|
||||||
);
|
|
||||||
|
|
||||||
const CHOOSER = 'content-block-element-chooser-widget';
|
const CHOOSER = 'content-block-element-chooser-widget';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|
@ -141,8 +112,8 @@ export default {
|
||||||
InfogramBlock,
|
InfogramBlock,
|
||||||
ThinglinkBlock,
|
ThinglinkBlock,
|
||||||
Assignment,
|
Assignment,
|
||||||
|
SolutionForm,
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
actions() {
|
actions() {
|
||||||
return {
|
return {
|
||||||
|
|
@ -220,7 +191,7 @@ export default {
|
||||||
};
|
};
|
||||||
case 'solution':
|
case 'solution':
|
||||||
return {
|
return {
|
||||||
component: 'solution',
|
component: 'solution-form',
|
||||||
title: 'Lösung',
|
title: 'Lösung',
|
||||||
};
|
};
|
||||||
case 'image_block':
|
case 'image_block':
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
type: string;
|
type: string;
|
||||||
icon: string;
|
icon: string;
|
||||||
|
|
@ -29,15 +29,15 @@ const props = withDefaults(defineProps<Props>(), {
|
||||||
|
|
||||||
const emit = defineEmits(['select']);
|
const emit = defineEmits(['select']);
|
||||||
|
|
||||||
const defaultTitle = (type:string) => {
|
const defaultTitle = (type: string) => {
|
||||||
return type.replace(/^\w/, (c :string) => c.toUpperCase());
|
return type.replace(/^\w/, (c: string) => c.toUpperCase());
|
||||||
}
|
};
|
||||||
|
|
||||||
const defaultIcon = (type:string) => {
|
const defaultIcon = (type: string) => {
|
||||||
return `${type}-icon`;
|
return `${type}-icon`;
|
||||||
}
|
};
|
||||||
|
|
||||||
const subclass =`chooser-element--${props.type}`;
|
const subclass = `chooser-element--${props.type}`;
|
||||||
const cy = `choose-${props.type}-widget`;
|
const cy = `choose-${props.type}-widget`;
|
||||||
|
|
||||||
const realTitle = computed(() => {
|
const realTitle = computed(() => {
|
||||||
|
|
@ -55,7 +55,7 @@ export default {
|
||||||
components: {
|
components: {
|
||||||
...formElementIcons,
|
...formElementIcons,
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
|
|
@ -65,19 +65,20 @@ export default {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border: 1px solid $color-silver;
|
border: 1px solid $color-silver;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
height: 105px;
|
height: 120px;
|
||||||
width: 105px;
|
flex-direction: column;
|
||||||
|
flex: 0 0 calc(25% - $medium-spacing * 3 / 4);
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
display: grid;
|
display: flex;
|
||||||
grid-template-rows: 1fr 45px;
|
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
justify-items: center;
|
justify-items: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
align-content: center;
|
||||||
|
gap: $small-spacing;
|
||||||
|
|
||||||
&__icon {
|
&__icon {
|
||||||
width: 40px;
|
width: 40px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
align-self: end;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script lang="ts">
|
||||||
import Checkbox from '@/components/ui/Checkbox.vue';
|
import Checkbox from '@/components/ui/Checkbox.vue';
|
||||||
|
|
||||||
import formElementIcons from '@/components/ui/form-element-icons';
|
import formElementIcons from '@/components/ui/form-element-icons';
|
||||||
|
|
@ -112,6 +112,12 @@ export default {
|
||||||
title: 'Dokument',
|
title: 'Dokument',
|
||||||
show: hasDefaultFeatures,
|
show: hasDefaultFeatures,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
type: 'solution',
|
||||||
|
block: 'solution',
|
||||||
|
title: 'Lösung',
|
||||||
|
icon: 'tick-circle-icon',
|
||||||
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
@ -143,14 +149,11 @@ export default {
|
||||||
@import 'styles/helpers';
|
@import 'styles/helpers';
|
||||||
|
|
||||||
.content-block-element-chooser-widget {
|
.content-block-element-chooser-widget {
|
||||||
display: -ms-grid;
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
@supports (display: grid) {
|
-ms-grid-columns: 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr;
|
||||||
display: grid;
|
gap: $medium-spacing;
|
||||||
}
|
|
||||||
grid-template-columns: repeat(7, 1fr);
|
|
||||||
-ms-grid-columns: 1fr 1fr 1fr 1fr 1fr 1fr 1fr;
|
|
||||||
grid-column-gap: 0;
|
|
||||||
font-family: $sans-serif-font-family;
|
font-family: $sans-serif-font-family;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
|
||||||
|
|
@ -3,137 +3,24 @@
|
||||||
class="document-form"
|
class="document-form"
|
||||||
ref="documentform"
|
ref="documentform"
|
||||||
>
|
>
|
||||||
<div
|
<document-input
|
||||||
v-if="!value.url"
|
:url="value.url"
|
||||||
ref="uploadcare-panel"
|
@update="updateUrl"
|
||||||
/>
|
/>
|
||||||
<div
|
|
||||||
class="document-form__spinner"
|
|
||||||
v-if="loading"
|
|
||||||
>
|
|
||||||
<loading-icon class="document-form__loading-icon" />
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="document-form__uploaded"
|
|
||||||
v-if="value.url"
|
|
||||||
>
|
|
||||||
<document-icon class="document-form__icon" />
|
|
||||||
<a
|
|
||||||
:href="previewUrl"
|
|
||||||
class="document-form__link"
|
|
||||||
target="_blank"
|
|
||||||
>{{ previewLink }}</a
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup lang="ts">
|
||||||
import { uploadcare } from '@/helpers/uploadcare';
|
import DocumentInput from '@/components/content-forms/DocumentInput.vue';
|
||||||
import LoadingIcon from '@/components/icons/LoadingIcon.vue';
|
export interface Props {
|
||||||
import { defineAsyncComponent } from 'vue';
|
value: any;
|
||||||
|
index: number;
|
||||||
|
}
|
||||||
|
|
||||||
const DocumentIcon = defineAsyncComponent(() =>
|
const props = defineProps<Props>();
|
||||||
import('@/components/icons/DocumentIcon.vue')
|
const emit = defineEmits(['change-url']);
|
||||||
);
|
|
||||||
|
|
||||||
export default {
|
const updateUrl = (url: string) => {
|
||||||
props: ['value', 'index'],
|
emit('change-url', url, props.index);
|
||||||
|
|
||||||
components: {
|
|
||||||
LoadingIcon,
|
|
||||||
DocumentIcon,
|
|
||||||
},
|
|
||||||
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
loading: false,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
computed: {
|
|
||||||
previewUrl() {
|
|
||||||
if (this.value && this.value.url) {
|
|
||||||
return this.value.url;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
previewLink() {
|
|
||||||
if (this.value && this.value.url) {
|
|
||||||
const parts = this.value.url.split('/');
|
|
||||||
return parts[parts.length - 1];
|
|
||||||
}
|
|
||||||
return '';
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
mounted() {
|
|
||||||
uploadcare(
|
|
||||||
this,
|
|
||||||
(url) => {
|
|
||||||
this.$emit('change-url', url, this.index);
|
|
||||||
this.loading = false;
|
|
||||||
},
|
|
||||||
() => {
|
|
||||||
this.loading = true;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
|
||||||
@import 'styles/helpers';
|
|
||||||
|
|
||||||
.document-form {
|
|
||||||
&__uploaded {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__link {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__spinner {
|
|
||||||
width: 100%;
|
|
||||||
height: 150px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__loading-icon {
|
|
||||||
@include spin;
|
|
||||||
fill: $color-silver-dark;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__icon {
|
|
||||||
width: 30px;
|
|
||||||
height: 30px;
|
|
||||||
margin-right: $small-spacing;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__file-input {
|
|
||||||
width: 0.1px;
|
|
||||||
height: 0.1px;
|
|
||||||
overflow: hidden;
|
|
||||||
opacity: 0;
|
|
||||||
position: absolute;
|
|
||||||
z-index: -1;
|
|
||||||
|
|
||||||
& + label {
|
|
||||||
cursor: pointer;
|
|
||||||
background-color: $color-silver-light;
|
|
||||||
height: 150px;
|
|
||||||
display: flex;
|
|
||||||
width: 100%;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
font-family: $sans-serif-font-family;
|
|
||||||
font-weight: $font-weight-regular;
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,141 @@
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="document-input"
|
||||||
|
ref="documentform"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="!url"
|
||||||
|
ref="uploadcarePanel"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="document-input__spinner"
|
||||||
|
v-if="loading"
|
||||||
|
>
|
||||||
|
<loading-icon class="document-input__loading-icon" />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="document-input__uploaded"
|
||||||
|
v-if="url"
|
||||||
|
>
|
||||||
|
<document-icon class="document-input__icon" />
|
||||||
|
<a
|
||||||
|
:href="previewUrl"
|
||||||
|
class="document-input__link"
|
||||||
|
target="_blank"
|
||||||
|
>{{ previewLink }}</a
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import LoadingIcon from '@/components/icons/LoadingIcon.vue';
|
||||||
|
import { uploadcare } from '@/helpers/uploadcare';
|
||||||
|
import { computed, onMounted, ref } from 'vue';
|
||||||
|
import { defineAsyncComponent } from 'vue';
|
||||||
|
const DocumentIcon = defineAsyncComponent(() => import('@/components/icons/DocumentIcon.vue'));
|
||||||
|
const loading = ref(false);
|
||||||
|
const documentform = ref(null);
|
||||||
|
const uploadcarePanel = ref(null);
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
|
||||||
|
const previewUrl = computed(() => {
|
||||||
|
return props.url ? props.url : '';
|
||||||
|
});
|
||||||
|
const previewLink = computed(() => {
|
||||||
|
const url = props.url;
|
||||||
|
if (url > '') {
|
||||||
|
const parts = url.split('/');
|
||||||
|
return parts[parts.length - 1];
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['update']);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
uploadcare(
|
||||||
|
{
|
||||||
|
$refs: {
|
||||||
|
'uploadcare-panel': uploadcarePanel.value,
|
||||||
|
documentform: documentform.value,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
(url: string) => {
|
||||||
|
emit('update', url);
|
||||||
|
loading.value = false;
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
loading.value = true;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="postcss">
|
||||||
|
/* todo: do this with a mixin, but in postcss (we have one in scss) */
|
||||||
|
@keyframes spin {
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.document-input {
|
||||||
|
&__uploaded {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
&__link {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
&__spinner {
|
||||||
|
width: 100%;
|
||||||
|
height: 150px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__loading-icon {
|
||||||
|
/* todo: make this work with postcss
|
||||||
|
// @include spin;
|
||||||
|
*/
|
||||||
|
animation: spin 2.5s linear infinite;
|
||||||
|
fill: var(--color-silver-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__icon {
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
margin-right: var(--small-spacing);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__file-input {
|
||||||
|
/* this element should not be visible for the user, but the width can't be 0, otherwise it does not exist in the
|
||||||
|
* DOM. With this hack, the element still works */
|
||||||
|
width: 0.1px;
|
||||||
|
height: 0.1px;
|
||||||
|
overflow: hidden;
|
||||||
|
opacity: 0;
|
||||||
|
position: absolute;
|
||||||
|
z-index: -1;
|
||||||
|
|
||||||
|
& + label {
|
||||||
|
cursor: pointer;
|
||||||
|
background-color: var(--color-silver-light);
|
||||||
|
height: 150px;
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
font-family: var(--sans-serif-font-family);
|
||||||
|
font-weight: var(--regular-font-weight);
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -28,7 +28,7 @@ export default {
|
||||||
.link-form {
|
.link-form {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-auto-rows: auto;
|
grid-auto-rows: auto;
|
||||||
grid-row-gap: 11px;
|
grid-row-gap: $small-spacing;
|
||||||
|
|
||||||
&__text,
|
&__text,
|
||||||
&__url {
|
&__url {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
<template>
|
||||||
|
<div class="solution-form">
|
||||||
|
<textarea
|
||||||
|
placeholder="Lösung erfassen"
|
||||||
|
class="skillbox-textarea"
|
||||||
|
:value="text"
|
||||||
|
@input="$emit('change-text', $event.target.value, index)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
value: any;
|
||||||
|
index: number;
|
||||||
|
}
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
value: null,
|
||||||
|
index: -1,
|
||||||
|
});
|
||||||
|
defineEmits(['change-text']);
|
||||||
|
|
||||||
|
const text = computed(() => {
|
||||||
|
// todo: refactor this and use the helper, just copied it from TextForm.vue
|
||||||
|
return props.value.text ? props.value.text.replace(/<br(\/)?>/, '\n').replace(/(<([^>]+)>)/gi, '') : '';
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="postcss" scoped>
|
||||||
|
.solution-form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--small-spacing);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -3,29 +3,16 @@ const LinkIcon = defineAsyncComponent(() => import('@/components/icons/LinkIcon.
|
||||||
const VideoIcon = defineAsyncComponent(() => import('@/components/icons/VideoIcon.vue'));
|
const VideoIcon = defineAsyncComponent(() => import('@/components/icons/VideoIcon.vue'));
|
||||||
const ImageIcon = defineAsyncComponent(() => import('@/components/icons/ImageIcon.vue'));
|
const ImageIcon = defineAsyncComponent(() => import('@/components/icons/ImageIcon.vue'));
|
||||||
const TextIcon = defineAsyncComponent(() => import('@/components/icons/TextIcon.vue'));
|
const TextIcon = defineAsyncComponent(() => import('@/components/icons/TextIcon.vue'));
|
||||||
const SpeechBubbleIcon = defineAsyncComponent(() =>
|
const SpeechBubbleIcon = defineAsyncComponent(() => import('@/components/icons/SpeechBubbleIcon.vue'));
|
||||||
import('@/components/icons/SpeechBubbleIcon.vue')
|
const DocumentIcon = defineAsyncComponent(() => import('@/components/icons/DocumentIcon.vue'));
|
||||||
);
|
|
||||||
const DocumentIcon = defineAsyncComponent(() =>
|
|
||||||
import('@/components/icons/DocumentIcon.vue')
|
|
||||||
);
|
|
||||||
const TitleIcon = defineAsyncComponent(() => import('@/components/icons/TitleIcon.vue'));
|
const TitleIcon = defineAsyncComponent(() => import('@/components/icons/TitleIcon.vue'));
|
||||||
const DocumentWithLinesIcon = defineAsyncComponent(() =>
|
const DocumentWithLinesIcon = defineAsyncComponent(() => import('@/components/icons/DocumentWithLinesIcon.vue'));
|
||||||
import('@/components/icons/DocumentWithLinesIcon.vue')
|
const ArrowThinBottom = defineAsyncComponent(() => import('@/components/icons/ArrowThinBottom.vue'));
|
||||||
);
|
const ArrowThinDown = defineAsyncComponent(() => import('@/components/icons/ArrowThinDown.vue'));
|
||||||
const ArrowThinBottom = defineAsyncComponent(() =>
|
const ArrowThinTop = defineAsyncComponent(() => import('@/components/icons/ArrowThinTop.vue'));
|
||||||
import('@/components/icons/ArrowThinBottom.vue')
|
const ArrowThinUp = defineAsyncComponent(() => import('@/components/icons/ArrowThinUp.vue'));
|
||||||
);
|
|
||||||
const ArrowThinDown = defineAsyncComponent(() =>
|
|
||||||
import('@/components/icons/ArrowThinDown.vue')
|
|
||||||
);
|
|
||||||
const ArrowThinTop = defineAsyncComponent(() =>
|
|
||||||
import('@/components/icons/ArrowThinTop.vue')
|
|
||||||
);
|
|
||||||
const ArrowThinUp = defineAsyncComponent(() =>
|
|
||||||
import('@/components/icons/ArrowThinUp.vue')
|
|
||||||
);
|
|
||||||
const TrashIcon = defineAsyncComponent(() => import('@/components/icons/TrashIcon.vue'));
|
const TrashIcon = defineAsyncComponent(() => import('@/components/icons/TrashIcon.vue'));
|
||||||
|
const TickCircleIcon = defineAsyncComponent(() => import('@/components/icons/TickCircleIcon.vue'));
|
||||||
|
|
||||||
/*
|
/*
|
||||||
for icons with a single word, leave the *-icon name, to prevent conflicts
|
for icons with a single word, leave the *-icon name, to prevent conflicts
|
||||||
|
|
@ -45,4 +32,5 @@ export default {
|
||||||
ArrowThinTop,
|
ArrowThinTop,
|
||||||
ArrowThinUp,
|
ArrowThinUp,
|
||||||
TrashIcon,
|
TrashIcon,
|
||||||
|
TickCircleIcon,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,6 @@ const isSafe = (attr) => {
|
||||||
|
|
||||||
const removeAttributes = (node) => {
|
const removeAttributes = (node) => {
|
||||||
let attributes = [...node.attributes];
|
let attributes = [...node.attributes];
|
||||||
console.log(attributes);
|
|
||||||
for (const attr of attributes) {
|
for (const attr of attributes) {
|
||||||
if (isSafe(attr)) {
|
if (isSafe(attr)) {
|
||||||
continue;
|
continue;
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ class InputTypes(graphene.Enum):
|
||||||
document_block = "document_block"
|
document_block = "document_block"
|
||||||
content_list_item = "content_list_item"
|
content_list_item = "content_list_item"
|
||||||
subtitle = "subtitle"
|
subtitle = "subtitle"
|
||||||
|
solution = "solution"
|
||||||
readonly = "readonly"
|
readonly = "readonly"
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,11 @@ import bleach
|
||||||
from api.utils import get_object
|
from api.utils import get_object
|
||||||
from assignments.models import Assignment
|
from assignments.models import Assignment
|
||||||
from books.models import ContentBlock
|
from books.models import ContentBlock
|
||||||
|
from core.logger import get_logger
|
||||||
from wagtail.blocks import StreamValue
|
from wagtail.blocks import StreamValue
|
||||||
|
|
||||||
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class AssignmentParameterException(Exception):
|
class AssignmentParameterException(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
@ -46,6 +49,7 @@ ALLOWED_BLOCKS = (
|
||||||
"document_block",
|
"document_block",
|
||||||
"content_list_item",
|
"content_list_item",
|
||||||
"subtitle",
|
"subtitle",
|
||||||
|
"solution",
|
||||||
"readonly",
|
"readonly",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -75,6 +79,7 @@ def handle_content_block(
|
||||||
# todo: add all the content blocks
|
# todo: add all the content blocks
|
||||||
# todo: sanitize user inputs!
|
# todo: sanitize user inputs!
|
||||||
if content["type"] not in allowed_blocks:
|
if content["type"] not in allowed_blocks:
|
||||||
|
logger.error(f"{content['type']} not in allowed blocks")
|
||||||
return
|
return
|
||||||
|
|
||||||
id = content.get("id")
|
id = content.get("id")
|
||||||
|
|
@ -91,6 +96,7 @@ def handle_content_block(
|
||||||
id=id,
|
id=id,
|
||||||
value=value,
|
value=value,
|
||||||
)
|
)
|
||||||
|
|
||||||
elif content["type"] == "assignment":
|
elif content["type"] == "assignment":
|
||||||
if module is None:
|
if module is None:
|
||||||
raise AssignmentParameterException(
|
raise AssignmentParameterException(
|
||||||
|
|
@ -136,6 +142,12 @@ def handle_content_block(
|
||||||
content_type = "document_block"
|
content_type = "document_block"
|
||||||
value = {"url": bleach.clean(content["value"]["url"])}
|
value = {"url": bleach.clean(content["value"]["url"])}
|
||||||
return get_content_dict(content_type=content_type, id=id, value=value)
|
return get_content_dict(content_type=content_type, id=id, value=value)
|
||||||
|
elif content["type"] == "solution":
|
||||||
|
content_type = "solution"
|
||||||
|
value = {
|
||||||
|
"text": handle_text(bleach.clean(content["value"]["text"], strip=True)),
|
||||||
|
}
|
||||||
|
return get_content_dict(content_type=content_type, id=id, value=value)
|
||||||
elif content["type"] == "subtitle":
|
elif content["type"] == "subtitle":
|
||||||
content_type = "subtitle"
|
content_type = "subtitle"
|
||||||
value = {"text": bleach.clean(content["value"]["text"])}
|
value = {"text": bleach.clean(content["value"]["text"])}
|
||||||
|
|
|
||||||
|
|
@ -273,6 +273,18 @@ class TestCreateCustomContentBlock:
|
||||||
assert subtitle_block.get("id") is not None
|
assert subtitle_block.get("id") is not None
|
||||||
assert subtitle_block.get("value").get("text") == value.get("text")
|
assert subtitle_block.get("value").get("text") == value.get("text")
|
||||||
|
|
||||||
|
def test_add_solution(self, teacher, get_client):
|
||||||
|
content_type = "solution"
|
||||||
|
value = {"text": "some solution"}
|
||||||
|
client = get_client(teacher)
|
||||||
|
input = create_block_input(content_type=content_type, value=value)
|
||||||
|
new_content_block = self._add_content_block(client=client, input=input)
|
||||||
|
subtitle_block = new_content_block.contents.raw_data[0]
|
||||||
|
assert subtitle_block.get("id") is not None
|
||||||
|
assert subtitle_block.get("value").get("text") == "<p>{}</p>".format(
|
||||||
|
value.get("text")
|
||||||
|
)
|
||||||
|
|
||||||
# def test_add_readonly(self):
|
# def test_add_readonly(self):
|
||||||
# content_type = "readonly"
|
# content_type = "readonly"
|
||||||
# value = {"text": "some subtitle"}
|
# value = {"text": "some subtitle"}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue