From ba52642f0634f211432e877d9dff0d2dec75d806 Mon Sep 17 00:00:00 2001 From: Ron Lucke <lucke@elan-ev.de> Date: Mon, 18 Dec 2023 12:03:38 +0000 Subject: [PATCH] StEP #3255 Merge request studip/studip!2355 --- ....5.11_courseware_add_optional_comments.php | 21 +++ .../JsonApi/Routes/Courseware/Authority.php | 4 +- .../Routes/Courseware/BlocksCreate.php | 1 + .../Routes/Courseware/BlocksUpdate.php | 4 + .../Courseware/StructuralElementsCreate.php | 3 +- .../Courseware/StructuralElementsUpdate.php | 4 + .../JsonApi/Routes/Courseware/UnitsCreate.php | 3 +- .../JsonApi/Schemas/Courseware/Block.php | 1 + .../Schemas/Courseware/StructuralElement.php | 1 + lib/models/Courseware/Block.php | 4 + lib/models/Courseware/StructuralElement.php | 9 +- .../assets/stylesheets/scss/courseware.scss | 3 +- .../scss/courseware/blocks/default-block.scss | 37 +---- .../stylesheets/scss/courseware/comments.scss | 26 ++- .../containers/default-container.scss | 18 -- .../courseware/layouts/call-to-action.scss | 19 +++ .../scss/courseware/layouts/talk-bubble.scss | 133 ++++++++++----- .../scss/courseware/structural-element.scss | 41 +---- .../blocks/CoursewareBlockActions.vue | 33 ++++ .../blocks/CoursewareBlockComments.vue | 10 +- .../blocks/CoursewareBlockDiscussion.vue | 116 +++++++++---- .../blocks/CoursewareBlockFeedback.vue | 33 ++-- .../blocks/CoursewareDefaultBlock.vue | 61 +++++-- .../layouts/CoursewareCallToActionBox.vue | 70 ++++++++ .../layouts/CoursewareTalkBubble.vue | 58 +++++-- .../CoursewareStructuralElement.vue | 157 ++++++++++++++++-- .../CoursewareStructuralElementComments.vue | 10 +- .../CoursewareStructuralElementFeedback.vue | 52 +++--- .../widgets/CoursewareViewWidget.vue | 14 -- .../vue/store/courseware/courseware.module.js | 52 ++++++ 30 files changed, 713 insertions(+), 285 deletions(-) create mode 100644 db/migrations/5.5.11_courseware_add_optional_comments.php create mode 100644 resources/assets/stylesheets/scss/courseware/layouts/call-to-action.scss create mode 100644 resources/vue/components/courseware/layouts/CoursewareCallToActionBox.vue diff --git a/db/migrations/5.5.11_courseware_add_optional_comments.php b/db/migrations/5.5.11_courseware_add_optional_comments.php new file mode 100644 index 00000000000..7c12a2ef892 --- /dev/null +++ b/db/migrations/5.5.11_courseware_add_optional_comments.php @@ -0,0 +1,21 @@ +<?php + +final class CoursewareAddOptionalComments extends Migration +{ + public function description() + { + return 'Add column commentable to cw_blocks and cw_structural_elements'; + } + + protected function up() + { + DBManager::get()->exec("ALTER TABLE `cw_blocks` ADD `commentable` TINYINT(1) NOT NULL AFTER `visible`"); + DBManager::get()->exec("ALTER TABLE `cw_structural_elements` ADD `commentable` TINYINT(1) NOT NULL AFTER `public`"); + } + + protected function down() + { + DBManager::get()->exec("ALTER TABLE `cw_blocks` DROP `commentable`"); + DBManager::get()->exec("ALTER TABLE `cw_structural_elements` DROP `commentable`"); + } +} diff --git a/lib/classes/JsonApi/Routes/Courseware/Authority.php b/lib/classes/JsonApi/Routes/Courseware/Authority.php index 3df103d81a2..88eb3df36b8 100644 --- a/lib/classes/JsonApi/Routes/Courseware/Authority.php +++ b/lib/classes/JsonApi/Routes/Courseware/Authority.php @@ -400,7 +400,7 @@ class Authority return $perm; } - public static function canUpdateStructuralElementFeedback(User $user, StructuralElementComment $resource) + public static function canUpdateStructuralElementFeedback(User $user, StructuralElementFeedback $resource) { return self::canCreateStructuralElementFeedback($user, $resource->structural_element); } @@ -410,7 +410,7 @@ class Authority return $resource->user_id === $user->id || self::canUpdateStructuralElement($user, $resource->structural_element); } - public static function canDeleteStructuralElementFeedback(User $user, StructuralElementComment $resource) + public static function canDeleteStructuralElementFeedback(User $user, StructuralElementFeedback $resource) { return self::canUpdateStructuralElementFeedback($user, $resource); } diff --git a/lib/classes/JsonApi/Routes/Courseware/BlocksCreate.php b/lib/classes/JsonApi/Routes/Courseware/BlocksCreate.php index 04c7d928b1f..d1d3afbe105 100644 --- a/lib/classes/JsonApi/Routes/Courseware/BlocksCreate.php +++ b/lib/classes/JsonApi/Routes/Courseware/BlocksCreate.php @@ -102,6 +102,7 @@ class BlocksCreate extends JsonApiController 'block_type' => $get('data.attributes.block-type'), 'payload' => '', 'visible' => 1, + 'commentable' => 0 ]); $payload = $get('data.attributes.payload'); diff --git a/lib/classes/JsonApi/Routes/Courseware/BlocksUpdate.php b/lib/classes/JsonApi/Routes/Courseware/BlocksUpdate.php index fcec2bb3697..6cf06ff9b7b 100644 --- a/lib/classes/JsonApi/Routes/Courseware/BlocksUpdate.php +++ b/lib/classes/JsonApi/Routes/Courseware/BlocksUpdate.php @@ -84,6 +84,10 @@ class BlocksUpdate extends JsonApiController $resource->visible = $get('data.attributes.visible'); } + if (is_bool($get('data.attributes.commentable'))) { + $resource->commentable = $get('data.attributes.commentable'); + } + if ($get('data.relationships.container.data.id')) { $resource->container_id = $get('data.relationships.container.data.id'); } diff --git a/lib/classes/JsonApi/Routes/Courseware/StructuralElementsCreate.php b/lib/classes/JsonApi/Routes/Courseware/StructuralElementsCreate.php index 496a8f7001f..c038c45f847 100644 --- a/lib/classes/JsonApi/Routes/Courseware/StructuralElementsCreate.php +++ b/lib/classes/JsonApi/Routes/Courseware/StructuralElementsCreate.php @@ -83,7 +83,8 @@ class StructuralElementsCreate extends JsonApiController 'payload' => self::arrayGet($json, 'data.attributes.payload', ''), 'read_approval' => $parent->read_approval, 'write_approval' => $parent->write_approval, - 'position' => $parent->countChildren() + 'position' => $parent->countChildren(), + 'commentable' => 0 ]); $struct->store(); diff --git a/lib/classes/JsonApi/Routes/Courseware/StructuralElementsUpdate.php b/lib/classes/JsonApi/Routes/Courseware/StructuralElementsUpdate.php index 6bf0e79b7af..455aacc3c06 100644 --- a/lib/classes/JsonApi/Routes/Courseware/StructuralElementsUpdate.php +++ b/lib/classes/JsonApi/Routes/Courseware/StructuralElementsUpdate.php @@ -140,6 +140,10 @@ class StructuralElementsUpdate extends JsonApiController $resource->withdraw_date = $json['data']['attributes']['withdraw-date']; } + if (isset($json['data']['attributes']['commentable'])) { + $resource->commentable = $json['data']['attributes']['commentable']; + } + // update parent if (self::arrayHas($json, 'data.relationships.parent')) { $parent = $this->getParentFromJson($json); diff --git a/lib/classes/JsonApi/Routes/Courseware/UnitsCreate.php b/lib/classes/JsonApi/Routes/Courseware/UnitsCreate.php index a6159e3f416..5909f2aa473 100644 --- a/lib/classes/JsonApi/Routes/Courseware/UnitsCreate.php +++ b/lib/classes/JsonApi/Routes/Courseware/UnitsCreate.php @@ -96,7 +96,8 @@ class UnitsCreate extends JsonApiController 'title' => self::arrayGet($json, 'data.attributes.title', ''), 'purpose' => self::arrayGet($json, 'data.attributes.purpose', ''), 'payload' => self::arrayGet($json, 'data.attributes.payload', ''), - 'position' => 0 + 'position' => 0, + 'commentable' => 0 ]); $unit = \Courseware\Unit::create([ diff --git a/lib/classes/JsonApi/Schemas/Courseware/Block.php b/lib/classes/JsonApi/Schemas/Courseware/Block.php index 03eb56b8a4f..e608188f86b 100644 --- a/lib/classes/JsonApi/Schemas/Courseware/Block.php +++ b/lib/classes/JsonApi/Schemas/Courseware/Block.php @@ -40,6 +40,7 @@ class Block extends SchemaProvider 'block-type' => (string) $resource->getBlockType(), 'title' => (string) $resource->type->getTitle(), 'visible' => (bool) $resource['visible'], + 'commentable' => (bool) $resource['commentable'], 'payload' => $resource->type->getPayload(), 'mkdate' => date('c', $resource['mkdate']), 'chdate' => date('c', $resource['chdate']), diff --git a/lib/classes/JsonApi/Schemas/Courseware/StructuralElement.php b/lib/classes/JsonApi/Schemas/Courseware/StructuralElement.php index 4335e892417..ab1dd0f5047 100644 --- a/lib/classes/JsonApi/Schemas/Courseware/StructuralElement.php +++ b/lib/classes/JsonApi/Schemas/Courseware/StructuralElement.php @@ -54,6 +54,7 @@ class StructuralElement extends SchemaProvider 'can-edit' => $resource->canEdit($user), 'can-visit' => $resource->canVisit($user), 'is-link' => (int) $resource['is_link'], + 'commentable' => (bool) $resource['commentable'], 'target-id' => (int) $resource['target_id'], 'external-relations' => $resource['external_relations']->getIterator(), 'mkdate' => date('c', $resource['mkdate']), diff --git a/lib/models/Courseware/Block.php b/lib/models/Courseware/Block.php index b7948323438..8b3d6f43349 100644 --- a/lib/models/Courseware/Block.php +++ b/lib/models/Courseware/Block.php @@ -22,6 +22,7 @@ use User; * @property int $position database column * @property string|null $block_type database column * @property int $visible database column + * @property int $commentable database column * @property string $payload database column * @property int $mkdate database column * @property int $chdate database column @@ -172,6 +173,7 @@ class Block extends \SimpleORMap implements \PrivacyObject 'block-type'=> $this->type->getType(), 'title'=> $this->type->getTitle(), 'visible'=> $this->visible, + 'commentable' => $this->commentable, 'payload'=> $this->type->getPayload(), 'mkdate'=> $this->mkdate, 'chdate'=> $this->chdate @@ -204,6 +206,7 @@ class Block extends \SimpleORMap implements \PrivacyObject 'block_type' => $this->type->getType(), 'payload' => json_encode($this->type->copyPayload($rangeId)), 'visible' => 1, + 'commentable' => 0 ]); //update Container payload @@ -227,6 +230,7 @@ class Block extends \SimpleORMap implements \PrivacyObject 'block_type' => $data->attributes->{'block-type'}, 'payload' => json_encode($data->attributes->payload), 'visible' => 1, + 'commentable' => 0 ]); $block->payload = json_encode($block->type->copyPayload($rangeId)); diff --git a/lib/models/Courseware/StructuralElement.php b/lib/models/Courseware/StructuralElement.php index 34cef8067af..a63c73f7f9f 100644 --- a/lib/models/Courseware/StructuralElement.php +++ b/lib/models/Courseware/StructuralElement.php @@ -31,6 +31,7 @@ use User; * @property string|null $purpose database column * @property \JSONArrayObject $payload database column * @property int $public database column + * @property int $commentable database column * @property int $release_date database column * @property int $withdraw_date database column * @property \JSONArrayObject $read_approval database column @@ -758,6 +759,7 @@ class StructuralElement extends \SimpleORMap implements \PrivacyObject 'owner_id' => $user->id, 'editor_id' => $user->id, 'title' => _('neue Seite'), + 'commentable' => 0 ]); $struct->store(); @@ -841,6 +843,7 @@ SQL; 'purpose' => $purpose ?: $this->purpose, 'position' => 0, 'payload' => $this->payload, + 'commentable' => 0 ]); $element->store(); @@ -892,7 +895,8 @@ SQL; 'image_id' => $image_id, 'image_type' => $this->image_type, 'read_approval' => $parent->read_approval, - 'write_approval' => $parent->write_approval + 'write_approval' => $parent->write_approval, + 'commentable' => 0 ]); $element->store(); @@ -1032,7 +1036,8 @@ SQL; 'position' => $parent->countChildren(), 'payload' => $this->payload, 'read_approval' => $parent->read_approval, - 'write_approval' => $parent->write_approval + 'write_approval' => $parent->write_approval, + 'commentable' => 0 ]); $element->store(); diff --git a/resources/assets/stylesheets/scss/courseware.scss b/resources/assets/stylesheets/scss/courseware.scss index 6c3d7ea620e..d83a23e0313 100644 --- a/resources/assets/stylesheets/scss/courseware.scss +++ b/resources/assets/stylesheets/scss/courseware.scss @@ -20,7 +20,8 @@ @import './courseware/containers/tabs.scss'; @import './courseware/blocks/default-block.scss'; -@import './courseware/layouts/collapsible.scss'; + +@import './courseware/layouts/call-to-action.scss'; @import './courseware/layouts/companion.scss'; @import './courseware/layouts/import-zip.scss'; @import './courseware/layouts/input-file.scss'; diff --git a/resources/assets/stylesheets/scss/courseware/blocks/default-block.scss b/resources/assets/stylesheets/scss/courseware/blocks/default-block.scss index 21e583e50a8..d65927bb06d 100644 --- a/resources/assets/stylesheets/scss/courseware/blocks/default-block.scss +++ b/resources/assets/stylesheets/scss/courseware/blocks/default-block.scss @@ -1,6 +1,7 @@ .cw-default-block { display: flex; - flex-flow: row; + flex-flow: column nowrap; + .cw-default-block-invisible-info { img { vertical-align: text-bottom; @@ -95,21 +96,6 @@ } } - -.cw-container-wrapper-discuss { - .cw-container-colspan-full { - .cw-content-wrapper { - max-width: $max-content-width; - } - } - .cw-container-colspan-half, - .cw-container-colspan-half-center { - .cw-content-wrapper { - max-width: 540px; - } - } -} - .cw-block-title { padding: 4px; background-color: var(--content-color-20); @@ -132,22 +118,3 @@ padding-top: 106px; } } - -.cw-call-to-action { - border: solid thin var(--content-color-40); - border-top: none; - - button { - width: 100%; - background-color: var(--activity-color-20); - border: none; - text-align: left; - padding: 1em; - cursor: pointer; - - img { - margin: 0 1em; - vertical-align: middle; - } - } -} \ No newline at end of file diff --git a/resources/assets/stylesheets/scss/courseware/comments.scss b/resources/assets/stylesheets/scss/courseware/comments.scss index f953e86d2ad..cec94b03233 100644 --- a/resources/assets/stylesheets/scss/courseware/comments.scss +++ b/resources/assets/stylesheets/scss/courseware/comments.scss @@ -1,17 +1,11 @@ -.cw-structural-element-feedback, -.cw-structural-element-comments { - padding: 0 1em; -} - .cw-structural-element-feedback-items, .cw-structural-element-comments-items, .cw-block-feedback-items, .cw-block-comments-items { min-height: 1em; - max-height: 225px; + max-height: 270px; overflow-y: auto; overflow-x: hidden; - margin: -1em -1em 0em 0em; padding: 0em 1em 0em 0em; scroll-behavior: smooth; } @@ -30,7 +24,7 @@ .cw-block-feedback-create, .cw-block-comment-create { border-top: solid thin var(--content-color-40); - padding-top: 1em; + padding: 8px 1em 0 1em; textarea { width: calc(100% - 6px); resize: none; @@ -54,4 +48,20 @@ .cw-comments-overview-dialog-comments-context { margin: 0 0 1.5em 0; +} + +.cw-block-discussion { + .cw-call-to-action:not(:first-child) { + border-top: none; + } +} + +.cw-default-block-active { + .cw-block-discussion { + margin: 0 -2px 0 0px; + .cw-call-to-action { + border-top: none; + } + } + } \ No newline at end of file diff --git a/resources/assets/stylesheets/scss/courseware/containers/default-container.scss b/resources/assets/stylesheets/scss/courseware/containers/default-container.scss index 7798d502dd9..a959d86b95c 100644 --- a/resources/assets/stylesheets/scss/courseware/containers/default-container.scss +++ b/resources/assets/stylesheets/scss/courseware/containers/default-container.scss @@ -104,24 +104,6 @@ form.cw-container-dialog-edit-form { } } -.cw-container-wrapper-discuss { - flex-direction: column; - - .cw-container-colspan-full { - max-width: unset; - } - .cw-container-colspan-half-center, - .cw-container-colspan-half { - max-width: 1050px; - } - .cw-container-colspan-half-center { - width: 100%; - .cw-container-content { - width: 1050px; - } - } -} - .cw-radioset { display: flex; flex-direction: row; diff --git a/resources/assets/stylesheets/scss/courseware/layouts/call-to-action.scss b/resources/assets/stylesheets/scss/courseware/layouts/call-to-action.scss new file mode 100644 index 00000000000..2e5e3b9cc32 --- /dev/null +++ b/resources/assets/stylesheets/scss/courseware/layouts/call-to-action.scss @@ -0,0 +1,19 @@ +@use '../../../mixins.scss' as *; + +.cw-call-to-action { + border: solid thin var(--content-color-40); + + .action-button { + width: 100%; + background-color: var(--activity-color-20); + border: none; + text-align: left; + padding: 1em; + cursor: pointer; + + img { + margin: 0 1em; + vertical-align: middle; + } + } +} \ No newline at end of file diff --git a/resources/assets/stylesheets/scss/courseware/layouts/talk-bubble.scss b/resources/assets/stylesheets/scss/courseware/layouts/talk-bubble.scss index 06062b692ee..c3d30c2f84f 100644 --- a/resources/assets/stylesheets/scss/courseware/layouts/talk-bubble.scss +++ b/resources/assets/stylesheets/scss/courseware/layouts/talk-bubble.scss @@ -1,58 +1,99 @@ -.cw-talk-bubble { - margin: 10px 20px; - position: relative; - width: 85%; - height: auto; - background-color: var(--dark-gray-color-10); - float: left; - - .cw-talk-bubble-talktext { - padding: 1em; - text-align: left; - line-height: 1.5em; - - .cw-talk-bubble-talktext-time { - color: var(--dark-gray-color-80); - text-align: right; - font-size: 0.8em; - margin-bottom: -0.5em; - } - } +$color: var(--base-color-20); +$ownColor: var(--petrol-40); - &.cw-talk-bubble-own-post { - float: right; - } +.cw-talk-bubble-wrapper { + display: flex; + flex-direction: row; + justify-content: start; - &:after { - content: ' '; - position: absolute; - width: 0; - height: 0; - top: 0px; - bottom: auto; - border: 22px solid; - border-color: var(--dark-gray-color-10) transparent transparent transparent; - left: -20px; - right: auto; + .cw-talk-bubble-avatar { + padding: 8px; } - &.cw-talk-bubble-own-post:after { - left: auto; - right: -20px; + .cw-talk-bubble { + margin: 10px 20px; + position: relative; + max-width: 80%; + height: auto; + background-color: $color; + border-radius: 10px; + + .cw-talk-bubble-content { + padding: 8px 1em; + + .cw-talk-bubble-header { + margin-bottom: 8px; + + a { + font-weight: 700; + } + } + + .cw-talk-bubble-talktext { + margin-bottom: 4px; + text-align: left; + line-height: 1.5em; + + .cw-talk-bubble-footer { + float: right; + margin-top: 4px; + padding-bottom: 4px; + + &:before { + content: " "; + width: 1em; + display: inline-block; + } + + .cw-talk-bubble-talktext-time { + text-align: right; + font-size: 0.8em; + margin-bottom: -0.5em; + } + + button { + border: none; + background-color: transparent; + vertical-align: middle; + cursor: pointer; + } + } + + } + } + + &:after { + content: ' '; + position: absolute; + width: 0; + height: 0; + top: 0px; + bottom: auto; + border: 16px solid; + border-color: $color transparent transparent transparent; + border-radius: 4px; + left: -14px; + right: auto; + } } - .cw-talk-bubble-user { - padding: 1em 1em 0 1em; + &.cw-talk-bubble-own-post { + justify-content: end; + + .cw-talk-bubble { + flex-direction: row-reverse; + background-color: $ownColor; - .cw-talk-bubble-avatar { - display: inline-block; + &:after { + border-color: $ownColor transparent transparent transparent; + left: auto; + right: -14px; + } } - span { - padding-left: 4px; - color: var(--dark-gray-color-45); - font-weight: 600; - vertical-align: top; + + .cw-talk-bubble-header { + flex-direction: row-reverse; } } } \ No newline at end of file diff --git a/resources/assets/stylesheets/scss/courseware/structural-element.scss b/resources/assets/stylesheets/scss/courseware/structural-element.scss index ed1ba385749..4609f8cebf0 100644 --- a/resources/assets/stylesheets/scss/courseware/structural-element.scss +++ b/resources/assets/stylesheets/scss/courseware/structural-element.scss @@ -49,8 +49,9 @@ } } - .cw-structural-element-discussion { - max-width: 1606px; + .cw-structural-element-feedback-wrapper, + .cw-structural-element-comments-wrapper { + max-width: calc(1095px - 2px); width: 100%; margin-bottom: 1em; } @@ -68,10 +69,6 @@ margin: 0 auto; padding: 91px 15px 15px 15px; } - - &.cw-container-wrapper-discuss { - max-width: 1606px; - } } .cw-structural-element-description { @@ -238,36 +235,4 @@ } } } -} - -@media only screen and (max-width: 1820px) { - .cw-structural-element .cw-container-wrapper.cw-container-wrapper-discuss { - max-width: $max-content-width; - .cw-container.cw-container-list.cw-container-item.cw-container-colspan-full { - .cw-default-block { - flex-flow: column; - .cw-discuss-wrapper { - margin-left: 0; - margin-top: 8px; - } - } - } - } -} - -@media only screen and (max-width: 1200px) { - .cw-structural-element .cw-container-wrapper.cw-container-wrapper-discuss { - max-width: $max-content-width; - .cw-container.cw-container-list.cw-container-item.cw-container-colspan-half, - .cw-container.cw-container-list.cw-container-item.cw-container-colspan-half-center { - .cw-default-block { - flex-flow: column; - .cw-discuss-wrapper { - margin-left: 0; - margin-top: 8px; - max-width: 540px; - } - } - } - } } \ No newline at end of file diff --git a/resources/vue/components/courseware/blocks/CoursewareBlockActions.vue b/resources/vue/components/courseware/blocks/CoursewareBlockActions.vue index 3ffff182f0c..89beb0a0886 100644 --- a/resources/vue/components/courseware/blocks/CoursewareBlockActions.vue +++ b/resources/vue/components/courseware/blocks/CoursewareBlockActions.vue @@ -9,6 +9,9 @@ @deleteBlock="deleteBlock" @removeLock="removeLock" @copyToClipboard="copyToClipboard" + @deactivateComments="deactivateComments" + @activateComments="activateComments" + @showFeedback="showFeedback" /> </div> </template> @@ -34,6 +37,7 @@ export default { ...mapGetters({ userId: 'userId', userIsTeacher: 'userIsTeacher', + getRelatedFeedback: 'courseware-block-feedback/related', }), blocked() { return this.block?.relationships?.['edit-blocker']?.data !== null; @@ -47,6 +51,16 @@ export default { blockedByAnotherUser() { return this.blocked && this.userId !== this.blockerId; }, + hasFeedback() { + const { id, type } = this.block; + const feedback = this.getRelatedFeedback({ parent: { id, type }, relationship: 'feedback' }); + + if (feedback === null || feedback.length === 0) { + return false; + } + + return true; + }, menuItems() { let menuItems = []; if (this.canEdit) { @@ -61,6 +75,16 @@ export default { icon: this.block.attributes.visible ? 'visibility-visible' : 'visibility-invisible', // do we change the icons ? emit: 'setVisibility', }); + if (this.userIsTeacher) { + menuItems.push({ + id: 4, + label: this.block.attributes.commentable + ? this.$gettext('Kommentare abschalten') + : this.$gettext('Kommentare aktivieren'), + icon: 'comment2', + emit: this.block.attributes.commentable ? 'deactivateComments' : 'activateComments', + }); + } } if (this.blocked && this.blockedByAnotherUser && this.userIsTeacher) { menuItems.push({ @@ -145,6 +169,15 @@ export default { }, copyToClipboard() { this.$emit('copyToClipboard'); + }, + activateComments() { + this.$emit('activateComments') + }, + deactivateComments() { + this.$emit('deactivateComments') + }, + showFeedback() { + this.$emit('showFeedback'); } }, }; diff --git a/resources/vue/components/courseware/blocks/CoursewareBlockComments.vue b/resources/vue/components/courseware/blocks/CoursewareBlockComments.vue index 68dc648b1c0..9b1cc44623c 100644 --- a/resources/vue/components/courseware/blocks/CoursewareBlockComments.vue +++ b/resources/vue/components/courseware/blocks/CoursewareBlockComments.vue @@ -7,6 +7,7 @@ v-for="comment in comments" :key="comment.id" :payload="buildPayload(comment)" + @delete="deleteComment(comment)" /> </div> <div class="cw-block-comment-create"> @@ -61,7 +62,8 @@ export default { methods: { ...mapActions({ createComments: 'courseware-block-comments/create', - loadRelatedComments: 'courseware-block-comments/loadRelated' + loadRelatedComments: 'courseware-block-comments/loadRelated', + deleteBlockComment: 'courseware-block-comments/delete' }), async loadComments() { const parent = { @@ -96,6 +98,9 @@ export default { this.loadComments(); this.createComment = ''; }, + deleteComment(comment) { + this.deleteBlockComment({id: comment.id, type: comment.type }); + }, buildPayload(comment) { const commenter = this.relatedUser({ parent: { id: comment.id, type: comment.type }, @@ -109,7 +114,8 @@ export default { chdate: comment.attributes.chdate, mkdate: comment.attributes.mkdate, user_id: commenter.id, - user_name: commenter.attributes['formatted-name'], + user_formatted_name: commenter.attributes['formatted-name'], + username: commenter?.attributes?.username ?? '', user_avatar: commenter.meta.avatar.small, }; diff --git a/resources/vue/components/courseware/blocks/CoursewareBlockDiscussion.vue b/resources/vue/components/courseware/blocks/CoursewareBlockDiscussion.vue index 716e6ee0b09..e29e15a1407 100644 --- a/resources/vue/components/courseware/blocks/CoursewareBlockDiscussion.vue +++ b/resources/vue/components/courseware/blocks/CoursewareBlockDiscussion.vue @@ -1,31 +1,37 @@ <template> <div class="cw-block-discussion"> - <courseware-collapsible-box - :title="text.comments" - :open="hasComments" + <courseware-call-to-action-box + v-if="commentable" + iconShape="chat" + :actionTitle="callToActionTitleComments" + :titleClosed="text.comments.titleClosed" + :titleOpen="text.comments.titleOpen" + :foldable="true" + :open="false" > - <courseware-block-comments - :block="block" - @hasComments="hasComments = true" - /> - </courseware-collapsible-box> + <template #content> + <courseware-block-comments :block="block" /> + </template> + </courseware-call-to-action-box> - <courseware-collapsible-box - v-if="canEdit || userIsTeacher" - :title="text.feedback" - :open="hasFeedback" + <courseware-call-to-action-box + v-if="showFeedback" + iconShape="exclaim-circle" + :actionTitle="callToActionTitleFeedback" + :titleClosed="text.feedback.titleClosed" + :titleOpen="text.feedback.titleOpen" + :foldable="true" + :open="displayFeedback" > - <courseware-block-feedback - :block="block" - :canEdit="canEdit" - @hasFeedback="hasFeedback = true" - /> - </courseware-collapsible-box> + <template #content> + <courseware-block-feedback :block="block" :canEdit="canEdit" /> + </template> + </courseware-call-to-action-box> </div> </template> <script> -import CoursewareCollapsibleBox from '../layouts/CoursewareCollapsibleBox.vue'; +import CoursewareCallToActionBox from '../layouts/CoursewareCallToActionBox.vue'; import CoursewareBlockComments from './CoursewareBlockComments.vue'; import CoursewareBlockFeedback from './CoursewareBlockFeedback.vue'; import { mapGetters } from 'vuex'; @@ -33,28 +39,80 @@ import { mapGetters } from 'vuex'; export default { name: 'courseware-block-discussion', components: { - CoursewareCollapsibleBox, + CoursewareCallToActionBox, CoursewareBlockComments, CoursewareBlockFeedback, }, props: { block: Object, - canEdit: Boolean + canEdit: Boolean, + commentable: Boolean, + displayFeedback: Boolean }, data() { return { - hasComments: false, - hasFeedback: false, text: { - comments: this.$gettext('Kommentare'), - feedback: this.$gettext('Feedback') - } - } + comments: { + titleClosed: this.$gettext('Kommentare anzeigen'), + titleOpen: this.$gettext('Kommentare ausblenden'), + }, + feedback: { + titleClosed: this.$gettext('Anmerkungen anzeigen'), + titleOpen: this.$gettext('Anmerkungen ausblenden'), + }, + }, + }; }, computed: { ...mapGetters({ + getRelatedFeedback: 'courseware-block-feedback/related', + getRelatedComments: 'courseware-block-comments/related', userIsTeacher: 'userIsTeacher', }), - } -} + feedback() { + const { id, type } = this.block; + + return this.getRelatedFeedback({ parent: { id, type }, relationship: 'feedback' }); + }, + feedbackCounter() { + return this.feedback?.length ?? 0; + }, + hasFeedback() { + if (this.feedback === null || this.feedbackCounter === 0) { + return false; + } + + return true; + }, + showFeedback() { + return ((this.canEdit || this.userIsTeacher) && this.hasFeedback) || this.displayFeedback; + }, + callToActionTitleFeedback() { + return this.$gettextInterpolate( + this.$ngettext( + '%{length} Anmerkung (Nur für Nutzende mit Schreibrechten sichtbar)', + '%{length} Anmerkungen (Nur für Nutzende mit Schreibrechten sichtbar)', + this.feedbackCounter + ), + { length: this.feedbackCounter }); + }, + comments() { + const { id, type } = this.block; + + return this.getRelatedComments({ parent: { id, type }, relationship: 'comments' }); + }, + commentsCounter() { + return this.comments?.length ?? 0; + }, + callToActionTitleComments() { + return this.$gettextInterpolate( + this.$ngettext( + '%{length} Kommentar', + '%{length} Kommentare', + this.commentsCounter + ), + { length: this.commentsCounter }); + }, + }, +}; </script> diff --git a/resources/vue/components/courseware/blocks/CoursewareBlockFeedback.vue b/resources/vue/components/courseware/blocks/CoursewareBlockFeedback.vue index dfc95dc4515..c13d500de7f 100644 --- a/resources/vue/components/courseware/blocks/CoursewareBlockFeedback.vue +++ b/resources/vue/components/courseware/blocks/CoursewareBlockFeedback.vue @@ -11,16 +11,19 @@ v-for="feedback in feedback" :key="feedback.id" :payload="buildPayload(feedback)" + @delete="deleteFeedback(feedback)" /> </div> <courseware-companion-box v-if="!userIsTeacher && feedback.length === 0" - :msgCompanion="$gettext('Es wurde noch kein Feedback abgegeben.')" + :msgCompanion="$gettext('Es wurde noch keine Anmerkungen abgegeben.')" mood="pointing" /> <div v-if="userIsTeacher" class="cw-block-feedback-create"> <textarea v-model="feedbackText" :placeholder="placeHolder" spellcheck="true"></textarea> - <button class="button" @click="postFeedback">{{ $gettext('Senden') }}</button> + <button class="button" @click="postFeedback"> + {{ $gettext('Senden') }} + </button> </div> </div> </section> @@ -31,7 +34,6 @@ import CoursewareCompanionBox from '../layouts/CoursewareCompanionBox.vue'; import CoursewareTalkBubble from '../layouts/CoursewareTalkBubble.vue'; import { mapActions, mapGetters } from 'vuex'; - export default { name: 'courseware-block-feedback', components: { @@ -45,8 +47,8 @@ export default { data() { return { feedbackText: '', - placeHolder: this.$gettext('Schreiben Sie ein Feedback...'), - srMessage: '' + placeHolder: this.$gettext('Schreiben Sie eine Anmerkung...'), + srMessage: '', }; }, computed: { @@ -67,12 +69,13 @@ export default { } return false; - } + }, }, methods: { ...mapActions({ createFeedback: 'courseware-block-feedback/create', loadRelatedFeedback: 'courseware-block-feedback/loadRelated', + deleteBlockFeedback: 'courseware-block-feedback/delete', }), buildPayload(feedback) { const { id, type } = feedback; @@ -83,7 +86,8 @@ export default { content: feedback.attributes.feedback, chdate: feedback.attributes.chdate, mkdate: feedback.attributes.mkdate, - user_name: user?.attributes?.['formatted-name'] ?? '', + user_formatted_name: user?.attributes?.['formatted-name'] ?? '', + username: user?.attributes?.username ?? '', user_avatar: user?.meta?.avatar.small, }; }, @@ -119,23 +123,16 @@ export default { this.feedbackText = ''; this.loadFeedback(); }, + deleteFeedback(feedback) { + this.deleteBlockFeedback({ id: feedback.id, type: feedback.type }); + }, updateSrMessage(message) { this.srMessage = ''; this.srMessage = message; - } - }, - async mounted() { - await this.loadFeedback(this.block.id); + }, }, updated() { this.$refs.feedbacks.scrollTop = this.$refs.feedbacks.scrollHeight; }, - watch: { - feedback() { - if (this.feedback && this.feedback.length > 0) { - this.$emit('hasFeedback'); - } - } - } }; </script> diff --git a/resources/vue/components/courseware/blocks/CoursewareDefaultBlock.vue b/resources/vue/components/courseware/blocks/CoursewareDefaultBlock.vue index d11a53834dc..2fabdda16f5 100644 --- a/resources/vue/components/courseware/blocks/CoursewareDefaultBlock.vue +++ b/resources/vue/components/courseware/blocks/CoursewareDefaultBlock.vue @@ -1,5 +1,5 @@ <template> - <div v-if="block.attributes.visible || canEdit" class="cw-default-block"> + <div v-if="block.attributes.visible || canEdit" class="cw-default-block" :class="[showEditMode ? 'cw-default-block-active' : '']"> <div class="cw-content-wrapper" :class="[showEditMode ? 'cw-content-wrapper-active' : '']"> <header v-if="showEditMode" class="cw-block-header"> <a href="#" class="cw-block-header-toggle" :aria-expanded="isOpen" @click.prevent="isOpen = !isOpen"> @@ -24,6 +24,9 @@ @deleteBlock="displayDeleteDialog()" @removeLock="displayRemoveLockDialog()" @copyToClipboard="copyToClipboard" + @deactivateComments="deactivateComments" + @activateComments="activateComments" + @showFeedback="showFeedback" /> </header> <div v-show="isOpen"> @@ -55,12 +58,12 @@ </div> </div> </div> - <div v-if="discussView" class="cw-discuss-wrapper"> - <courseware-block-discussion - :block="block" - :canEdit="canEdit" - /> - </div> + <courseware-block-discussion + :block="block" + :canEdit="canEdit" + :commentable="commentable" + :displayFeedback="displayFeedback" + /> <studip-dialog v-if="showDeleteDialog" :title="textDeleteTitle" @@ -138,6 +141,7 @@ export default { textRemoveLockTitle: this.$gettext('Sperre aufheben'), textRemoveLockAlert: this.$gettext('Möchten Sie die Sperre dieses Blocks wirklich aufheben?'), isOpen: true, + displayFeedback: false, }; }, computed: { @@ -148,7 +152,7 @@ export default { userId: 'userId', userById: 'users/byId', viewMode: 'viewMode', - currentElementisLink: 'currentElementisLink' + currentElementisLink: 'currentElementisLink', }), showEditMode() { let show = (this.viewMode === 'edit' || this.blockedByThisUser) && !this.currentElementisLink; @@ -157,9 +161,6 @@ export default { } return show; }, - discussView() { - return this.viewMode === 'discuss'; - }, blocked() { return this.block?.relationships?.['edit-blocker']?.data !== null; }, @@ -189,7 +190,10 @@ export default { }, public() { return this.context.type === 'public'; - } + }, + commentable() { + return this.block?.attributes?.commentable ?? false; + }, }, mounted() { if (this.blocked) { @@ -200,6 +204,9 @@ export default { if (!this.public && this.userProgress && this.userProgress.attributes.grade === 0 && this.defaultGrade) { this.userProgress = 1; } + if (this.canEdit) { + this.loadFeedback(this.block.id); + } }, methods: { ...mapActions({ @@ -212,7 +219,10 @@ export default { loadContainer: 'loadContainer', loadBlock: 'courseware-blocks/loadById', updateContainer: 'updateContainer', - createClipboard: 'courseware-clipboards/create' + createClipboard: 'courseware-clipboards/create', + activateBlockComments: 'activateBlockComments', + deactivateBlockComments: 'deactivateBlockComments', + loadRelatedFeedback: 'courseware-block-feedback/loadRelated', }), async displayFeature(element) { if (this.showEdit && element === 'Edit') { @@ -377,7 +387,30 @@ export default { await this.createClipboard(clipboard, { root: true }); this.companionSuccess({ info: this.$gettext('Block wurde in Merkliste abgelegt.') }); - } + }, + activateComments() { + this.activateBlockComments({ block: this.block }); + }, + deactivateComments() { + this.deactivateBlockComments({ block: this.block }); + }, + showFeedback() { + console.log('displayFeedback'); + this.displayFeedback = true; + }, + async loadFeedback() { + const parent = { + type: this.block.type, + id: this.block.id, + }; + await this.loadRelatedFeedback({ + parent, + relationship: 'feedback', + options: { + include: 'user', + }, + }); + }, }, watch: { diff --git a/resources/vue/components/courseware/layouts/CoursewareCallToActionBox.vue b/resources/vue/components/courseware/layouts/CoursewareCallToActionBox.vue new file mode 100644 index 00000000000..601a339e1a5 --- /dev/null +++ b/resources/vue/components/courseware/layouts/CoursewareCallToActionBox.vue @@ -0,0 +1,70 @@ +<template> + <div class="cw-call-to-action"> + <button class="action-button" :title="unfold ? titleOpen : titleClosed" @click="buttonAction"> + <studip-icon :shape="unfold ? 'arr_1down' : iconShape" :size="24"/> + {{ actionTitle }} + </button> + <div v-if="unfold" class="cw-call-to-action-content"> + <slot name="content"></slot> + </div> + </div> + +</template> + +<script> +import StudipIcon from '../../StudipIcon.vue'; + +export default { + name: 'courseware-call-to-action-box', + components: { + StudipIcon + }, + props: { + iconShape: { + type: String, + default: 'arr_1right' + }, + titleClosed: { + type: String, + required: true + }, + titleOpen: { + type: String, + required: true + }, + actionTitle: { + type: String, + required: true + }, + foldable: { + type: Boolean, + default: false + }, + open: { + type: Boolean, + default: true + } + }, + data() { + return { + unfold: true + } + }, + methods: { + buttonAction() { + this.$emit('click'); + if (this.foldable) { + this.unfold = !this.unfold; + } + } + }, + mounted() { + this.unfold = this.open; + }, + watch: { + open(newState) { + this.unfold = newState; + } + } +} +</script> \ No newline at end of file diff --git a/resources/vue/components/courseware/layouts/CoursewareTalkBubble.vue b/resources/vue/components/courseware/layouts/CoursewareTalkBubble.vue index cd76aaa1a9b..007a72ef6e3 100644 --- a/resources/vue/components/courseware/layouts/CoursewareTalkBubble.vue +++ b/resources/vue/components/courseware/layouts/CoursewareTalkBubble.vue @@ -1,20 +1,35 @@ <template> - <div :class="{ 'cw-talk-bubble-own-post': payload.own }" class="cw-talk-bubble"> - <div class="cw-talk-bubble-user" v-if="!payload.own"> - <div class="cw-talk-bubble-avatar"> - <img :src="payload.user_avatar" /> - </div> - <span>{{ payload.user_name }}</span> + <div :class="{ 'cw-talk-bubble-own-post': payload.own }" class="cw-talk-bubble-wrapper"> + <div v-if="!payload.own" class="cw-talk-bubble-avatar"> + <img :src="payload.user_avatar" /> </div> - <div class="cw-talk-bubble-talktext"> - <p>{{ payload.content }}</p> - <p class="cw-talk-bubble-talktext-time"><iso-date :date="payload.chdate" /></p> + <div class="cw-talk-bubble"> + <div class="cw-talk-bubble-content"> + <header v-if="!payload.own" class="cw-talk-bubble-header"> + <a :href="userProfileUrl">{{ payload.user_formatted_name }}</a> + </header> + <div class="cw-talk-bubble-talktext"> + <span>{{ payload.content }}</span> + <div class="cw-talk-bubble-footer"> + <span class="cw-talk-bubble-talktext-time"><iso-date :date="payload.chdate" /></span> + <button v-if="userIsTeacher || payload.own" :title="$gettext('Löschen')" + @click="showDeleteDialog = true"> + <studip-icon shape="trash" /> + </button> + </div> + </div> + </div> </div> + <studip-dialog v-if="showDeleteDialog" :title="$gettext('Eintrag löschen')" + :question="$gettext('Möchten Sie diesen Eintrag löschen?')" height="180" width="360" @confirm="deletePost" + @close="closeDeleteDialog"> + </studip-dialog> </div> </template> <script> import IsoDate from './IsoDate.vue'; +import { mapGetters } from 'vuex'; export default { name: 'courseware-talk-bubble', @@ -22,5 +37,28 @@ export default { props: { payload: Object, }, + data() { + return { + showDeleteDialog: false + } + }, + computed: { + ...mapGetters({ + userIsTeacher: 'userIsTeacher' + }), + userProfileUrl() { + const username = this.payload.username; + return STUDIP.URLHelper.getURL('dispatch.php/profile', { username }); + } + }, + methods: { + closeDeleteDialog() { + this.showDeleteDialog = false; + }, + deletePost() { + this.closeDeleteDialog(); + this.$emit('delete'); + } + } }; -</script> \ No newline at end of file +</script> diff --git a/resources/vue/components/courseware/structural-element/CoursewareStructuralElement.vue b/resources/vue/components/courseware/structural-element/CoursewareStructuralElement.vue index dcb396984dd..4f70f4894fe 100644 --- a/resources/vue/components/courseware/structural-element/CoursewareStructuralElement.vue +++ b/resources/vue/components/courseware/structural-element/CoursewareStructuralElement.vue @@ -77,11 +77,30 @@ @linkElement="menuAction('linkElement')" @removeLock="menuAction('removeLock')" @activateFullscreen="menuAction('activateFullscreen')" + @activateComments="menuAction('activateComments')" + @deactivateComments="menuAction('deactivateComments')" + @showFeedback="menuAction('showFeedback')" /> </template> </courseware-ribbon> <div class="cw-page-wrapper"> <div class="cw-page-content"> + <courseware-call-to-action-box + v-if="canEdit && (hasFeedback || displayFeedback)" + class="cw-structural-element-feedback-wrapper" + iconShape="exclaim-circle" + :actionTitle="callToActionTitleFeedback" + :titleClosed="$gettext('Anmerkungen anzeigen')" + :titleOpen="$gettext('Anmerkungen ausblenden')" + :foldable="true" + > + <template #content> + <courseware-structural-element-feedback + :structuralElement="structuralElement" + :canEdit="canEdit" + /> + </template> + </courseware-call-to-action-box> <div v-if="structuralElementLoaded && !isLink" class="cw-companion-box-wrapper"> <courseware-companion-box v-if="!canVisit" @@ -113,14 +132,8 @@ class="cw-container-wrapper" :class="{ 'cw-container-wrapper-consume': consumeMode, - 'cw-container-wrapper-discuss': discussView, }" > - <courseware-structural-element-discussion - v-if="!noContainers && discussView" - :structuralElement="structuralElement" - :canEdit="canEdit" - /> <component v-for="container in containers" :key="container.id" @@ -138,14 +151,8 @@ class="cw-container-wrapper" :class="{ 'cw-container-wrapper-consume': consumeMode, - 'cw-container-wrapper-discuss': discussView, }" > - <courseware-structural-element-discussion - v-if="discussView" - :structuralElement="structuralElement" - :canEdit="canEdit" - /> <div v-if="editView" class="cw-companion-box-wrapper"> <courseware-companion-box :msgCompanion="$gettextInterpolate($gettext('Dieser Inhalt ist aus den persönlichen Lernmaterialien von %{ ownerName } verlinkt und kann nur dort bearbeitet werden.'), { ownerName: ownerName })" @@ -211,6 +218,22 @@ </div> <courseware-toolbar v-if="canVisit && canEdit && editView && !isLink" /> </div> + <courseware-call-to-action-box + v-if="commentable" + class="cw-structural-element-comments-wrapper" + iconShape="chat" + :actionTitle="callToActionTitleComments" + :titleClosed="$gettext('Kommentare anzeigen')" + :titleOpen="$gettext('Kommentare ausblenden')" + :foldable="true" + :open="false" + > + <template #content> + <courseware-structural-element-comments + :structuralElement="structuralElement" + /> + </template> + </courseware-call-to-action-box> </div> <studip-dialog v-if="showEditDialog" @@ -593,6 +616,8 @@ import StructuralElementComponents from './structural-element-components.js'; import CoursewarePluginComponents from '../plugin-components.js'; import CoursewareRootContent from './CoursewareRootContent.vue'; +import CoursewareStructuralElementComments from './CoursewareStructuralElementComments.vue'; +import CoursewareStructuralElementFeedback from './CoursewareStructuralElementFeedback.vue'; import CoursewareStructuralElementDialogAdd from './CoursewareStructuralElementDialogAdd.vue'; import CoursewareStructuralElementDialogAddChooser from './CoursewareStructuralElementDialogAddChooser.vue'; import CoursewareStructuralElementDialogCopy from './CoursewareStructuralElementDialogCopy.vue'; @@ -609,6 +634,7 @@ import CoursewareExport from '@/vue/mixins/courseware/export.js'; import CoursewareOerMessage from '@/vue/mixins/courseware/oermessage.js'; import colorMixin from '@/vue/mixins/courseware/colors.js'; import wizardMixin from '@/vue/mixins/courseware/wizard.js'; +import CoursewareCallToActionBox from '../layouts/CoursewareCallToActionBox.vue'; import CoursewareDateInput from '../layouts/CoursewareDateInput.vue'; import StockImageSelector from '../../stock-images/SelectorDialog.vue'; import StudipDialog from '../../StudipDialog.vue'; @@ -620,6 +646,8 @@ export default { name: 'courseware-structural-element', components: Object.assign(StructuralElementComponents, { CoursewareRootContent, + CoursewareStructuralElementComments, + CoursewareStructuralElementFeedback, CoursewareStructuralElementDialogAdd, CoursewareStructuralElementDialogAddChooser, CoursewareStructuralElementDialogCopy, @@ -632,6 +660,7 @@ export default { CoursewareStructuralElementPermissions, CoursewareContentPermissions, CoursewareWelcomeScreen, + CoursewareCallToActionBox, CoursewareDateInput, StockImageSelector, StudipDialog, @@ -699,6 +728,7 @@ export default { uploadImageURL: null, showStockImageSelector: false, selectedStockImage: null, + displayFeedback: false, }; }, @@ -710,6 +740,8 @@ export default { containerById: 'courseware-containers/byId', relatedContainers: 'courseware-containers/related', relatedStructuralElements: 'courseware-structural-elements/related', + getRelatedFeedback: 'courseware-structural-element-feedback/related', + getRelatedComments: 'courseware-structural-element-comments/related', relatedTaskGroups: 'courseware-task-groups/related', relatedUsers: 'users/related', structuralElementById: 'courseware-structural-elements/byId', @@ -1037,6 +1069,24 @@ export default { icon: 'settings', emit: 'editCurrentElement', }); + if (this.userIsTeacher) { + menu.push({ + id: 2, + label: this.commentable + ? this.$gettext('Kommentare abschalten') + : this.$gettext('Kommentare aktivieren'), + icon: 'comment2', + emit: this.commentable ? 'deactivateComments' : 'activateComments', + }); + if (!this.hasFeedback && !this.displayFeedback) { + menu.push({ + id: 3, + label: this.$gettext('Anmerkungen aktivieren'), + icon: 'exclaim-circle', + emit: 'showFeedback' + }); + } + } } if (this.blockedByAnotherUser && this.userIsTeacher) { menu.push({ @@ -1085,9 +1135,6 @@ export default { blockingUserName() { return this.blockingUser ? this.blockingUser.attributes['formatted-name'] : ''; }, - discussView() { - return this.viewMode === 'discuss'; - }, editView() { return this.viewMode === 'edit'; }, @@ -1211,7 +1258,57 @@ export default { 'dispatch.php/course/courseware/courseware/' + this.context.unit, {cid: this.context.id} ); - } + }, + commentable() { + return this.currentElement?.attributes?.commentable ?? false; + }, + feedback() { + const parent = { + type: this.currentElement.type, + id: this.currentElement.id, + }; + + return this.getRelatedFeedback({ parent, relationship: 'feedback' }); + }, + feedbackCounter() { + return this.feedback?.length ?? 0; + }, + hasFeedback() { + if (this.feedback === null || this.feedbackCounter === 0) { + return false; + } + + return true; + }, + callToActionTitleFeedback() { + return this.$gettextInterpolate( + this.$ngettext( + '%{length} Anmerkung zur Seite (Nur für Nutzende mit Schreibrechten sichtbar)', + '%{length} Anmerkungen zur Seite (Nur für Nutzende mit Schreibrechten sichtbar)', + this.feedbackCounter + ), + { length: this.feedbackCounter }); + }, + comments() { + const parent = { + type: this.currentElement.type, + id: this.currentElement.id, + }; + + return this.getRelatedComments({ parent, relationship: 'comments' }); + }, + commentsCounter() { + return this.comments?.length ?? 0; + }, + callToActionTitleComments() { + return this.$gettextInterpolate( + this.$ngettext( + '%{length} Kommentar zur Seite', + '%{length} Kommentare zur Seite', + this.commentsCounter + ), + { length: this.commentsCounter }); + }, }, methods: { @@ -1246,7 +1343,10 @@ export default { loadStructuralElement: 'loadStructuralElement', createLink: 'createLink', setCurrentElementId: 'coursewareCurrentElement', - loadProgresses: 'loadProgresses' + loadProgresses: 'loadProgresses', + activateStructuralElementComments: 'activateStructuralElementComments', + deactivateStructuralElementComments: 'deactivateStructuralElementComments', + loadRelatedFeedback: 'courseware-structural-element-feedback/loadRelated', }), initCurrent() { @@ -1254,6 +1354,7 @@ export default { this.uploadFileError = ''; this.deletingPreviewImage = false; this.uploadImageURL = null; + this.loadFeedback(); }, async menuAction(action) { switch (action) { @@ -1315,6 +1416,15 @@ export default { case 'activateFullscreen': STUDIP.Fullscreen.activate(); break; + case 'activateComments': + this.activateStructuralElementComments({ element: this.currentElement }); + break; + case 'deactivateComments': + this.deactivateStructuralElementComments({ element: this.currentElement }); + break; + case 'showFeedback': + this.displayFeedback = true; + break; } }, async closeEditDialog() { @@ -1558,6 +1668,19 @@ export default { ref.initCurrentData(); } }, + async loadFeedback() { + const parent = { + type: this.currentElement.type, + id: this.currentElement.id, + }; + await this.loadRelatedFeedback({ + parent, + relationship: 'feedback', + options: { + include: 'user', + }, + }); + }, keyHandler(e, containerId) { switch (e.keyCode) { case 27: // esc diff --git a/resources/vue/components/courseware/structural-element/CoursewareStructuralElementComments.vue b/resources/vue/components/courseware/structural-element/CoursewareStructuralElementComments.vue index 42c7f5f6707..e78377f4f20 100644 --- a/resources/vue/components/courseware/structural-element/CoursewareStructuralElementComments.vue +++ b/resources/vue/components/courseware/structural-element/CoursewareStructuralElementComments.vue @@ -9,6 +9,7 @@ v-for="comment in comments" :key="comment.id" :payload="buildPayload(comment)" + @delete="deleteComment(comment)" /> </div> <div class="cw-structural-element-comment-create"> @@ -62,7 +63,8 @@ export default { methods: { ...mapActions({ createComments: 'courseware-structural-element-comments/create', - loadRelatedComments: 'courseware-structural-element-comments/loadRelated' + loadRelatedComments: 'courseware-structural-element-comments/loadRelated', + deleteElementComment: 'courseware-structural-element-comments/delete' }), async loadComments() { const parent = { @@ -110,12 +112,16 @@ export default { chdate: comment.attributes.chdate, mkdate: comment.attributes.mkdate, user_id: commenter.id, - user_name: commenter.attributes['formatted-name'], + user_formatted_name: commenter.attributes['formatted-name'], + username: commenter?.attributes?.username ?? '', user_avatar: commenter.meta.avatar.small, }; return payload; }, + deleteComment(comment) { + this.deleteElementComment({ id: comment.id, type: comment.type }); + }, updateSrMessage(message) { this.srMessage = ''; this.srMessage = message; diff --git a/resources/vue/components/courseware/structural-element/CoursewareStructuralElementFeedback.vue b/resources/vue/components/courseware/structural-element/CoursewareStructuralElementFeedback.vue index fd950bc1625..acecf630448 100644 --- a/resources/vue/components/courseware/structural-element/CoursewareStructuralElementFeedback.vue +++ b/resources/vue/components/courseware/structural-element/CoursewareStructuralElementFeedback.vue @@ -10,16 +10,19 @@ v-for="feedback in feedback" :key="feedback.id" :payload="buildPayload(feedback)" + @delete="deleteFeedback(feedback)" /> </div> <courseware-companion-box - v-if="!userIsTeacher && feedback.length === 0" - :msgCompanion="$gettext('Es wurde noch kein Feedback abgegeben.')" - mood="pointing" - /> + v-if="!userIsTeacher && feedback.length === 0" + :msgCompanion="$gettext('Es wurde noch keine Anmerkungen abgegeben.')" + mood="pointing" + /> <div v-if="userIsTeacher" class="cw-structural-element-feedback-create"> <textarea v-model="feedbackText" :placeholder="placeHolder" spellcheck="true"></textarea> - <button class="button" @click="postFeedback"><translate>Senden</translate></button> + <button class="button" @click="postFeedback"> + {{ $gettext('Senden') }} + </button> </div> </section> </template> @@ -42,8 +45,8 @@ export default { data() { return { feedbackText: '', - placeHolder: this.$gettext('Schreiben Sie ein Feedback...'), - srMessage: '' + placeHolder: this.$gettext('Schreiben Sie eine Anmerkung...'), + srMessage: '', }; }, computed: { @@ -67,12 +70,13 @@ export default { } return false; - } + }, }, methods: { ...mapActions({ createFeedback: 'courseware-structural-element-feedback/create', loadRelatedFeedback: 'courseware-structural-element-feedback/loadRelated', + deleteElementFeedback: 'courseware-structural-element-feedback/delete', }), buildPayload(feedback) { const { id, type } = feedback; @@ -83,7 +87,8 @@ export default { content: feedback.attributes.feedback, chdate: feedback.attributes.chdate, mkdate: feedback.attributes.mkdate, - user_name: user?.attributes?.['formatted-name'] ?? '', + user_formatted_name: user?.attributes?.['formatted-name'] ?? '', + username: user?.attributes?.username ?? '', user_avatar: user?.meta?.avatar.small, }; }, @@ -101,7 +106,7 @@ export default { }); }, async postFeedback() { - this.updateSrMessage(this.$gettext('Feedback gesendet')); + this.updateSrMessage(this.$gettext('Anmerkung gesendet')); const data = { attributes: { feedback: this.feedbackText, @@ -110,32 +115,25 @@ export default { 'structural-element': { data: { id: this.structuralElement.id, - type: this.structuralElement.type - } - } + type: this.structuralElement.type, + }, + }, }, }; - await this.createFeedback( data, { root: true }); + await this.createFeedback(data, { root: true }); this.feedbackText = ''; this.loadFeedback(); }, + deleteFeedback(feedback) { + this.deleteElementFeedback({ id: feedback.id, type: feedback.type }); + }, updateSrMessage(message) { this.srMessage = ''; this.srMessage = message; - } - }, - async mounted() { - await this.loadFeedback(); + }, }, updated() { this.$refs.feedbacks.scrollTop = this.$refs.feedbacks.scrollHeight; }, - watch: { - feedback() { - if (this.feedback && this.feedback.length > 0) { - this.$emit('hasFeedback'); - } - } - } -} -</script> \ No newline at end of file +}; +</script> diff --git a/resources/vue/components/courseware/widgets/CoursewareViewWidget.vue b/resources/vue/components/courseware/widgets/CoursewareViewWidget.vue index d8a8563ce1a..6f9d787f6bc 100644 --- a/resources/vue/components/courseware/widgets/CoursewareViewWidget.vue +++ b/resources/vue/components/courseware/widgets/CoursewareViewWidget.vue @@ -15,14 +15,6 @@ <translate>Bearbeiten</translate> </button> </li> - <li - v-if="context.type === 'courses' && canVisit" - :class="{ active: discussView }" - > - <button @click="setDiscussView"> - <translate>Kommentieren</translate> - </button> - </li> </ul> </template> </sidebar-widget> @@ -50,9 +42,6 @@ export default { editView() { return this.viewMode === 'edit'; }, - discussView() { - return this.viewMode === 'discuss'; - }, canEdit() { if (!this.structuralElement) { return false; @@ -77,9 +66,6 @@ export default { setEditView() { this.coursewareViewMode('edit'); }, - setDiscussView() { - this.coursewareViewMode('discuss'); - }, }, }; </script> diff --git a/resources/vue/store/courseware/courseware.module.js b/resources/vue/store/courseware/courseware.module.js index 1f85d56b0e7..db13cf16de8 100644 --- a/resources/vue/store/courseware/courseware.module.js +++ b/resources/vue/store/courseware/courseware.module.js @@ -532,6 +532,32 @@ export const actions = { }, + async activateStructuralElementComments({ dispatch }, { element }) { + + element.attributes.commentable = true; + + const updatedElement = await dispatch('setStructuralElementComments', { element: element }); + + return updatedElement; + + }, + async deactivateStructuralElementComments({ dispatch }, { element }) { + + element.attributes.commentable = false; + + const updatedElement = await dispatch('setStructuralElementComments', { element: element }); + + return updatedElement; + }, + + async setStructuralElementComments({ dispatch }, { element }) { + await dispatch('lockObject', { id: element.id, type: 'courseware-structural-elements' }); + const updatedElement = await dispatch('courseware-structural-elements/update', element, { root: true }); + await dispatch('unlockObject', { id: element.id, type: 'courseware-structural-elements' }); + + return updatedElement; + }, + async createBlockInContainer({ dispatch }, { container, blockType }) { const block = { attributes: { @@ -609,6 +635,32 @@ export const actions = { return dispatch('loadContainer', containerId); }, + async activateBlockComments({ dispatch }, { block }) { + + block.attributes.commentable = true; + + const updatedBlock = await dispatch('setBlockComments', { block: block }); + + return updatedBlock; + + }, + async deactivateBlockComments({ dispatch }, { block }) { + + block.attributes.commentable = false; + + const updatedBlock = await dispatch('setBlockComments', { block: block }); + + return updatedBlock; + }, + + async setBlockComments({ dispatch }, { block }) { + await dispatch('lockObject', { id: block.id, type: 'courseware-blocks' }); + const updatedBlock = await dispatch('courseware-blocks/update', block, { root: true }); + await dispatch('unlockObject', { id: block.id, type: 'courseware-blocks' }); + + return updatedBlock; + }, + async storeCoursewareSettings({ dispatch, getters }, { permission, progression, certificateSettings, reminderSettings, resetProgressSettings }) { -- GitLab