aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRon Lucke <lucke@elan-ev.de>2023-12-18 12:03:38 +0000
committerRon Lucke <lucke@elan-ev.de>2023-12-18 12:03:38 +0000
commitba52642f0634f211432e877d9dff0d2dec75d806 (patch)
treea8da0636d714b4f29bc00439dc2f17c5fa995618
parente09c643e2ed3719b081ce3ee3b55fb15cbd04274 (diff)
StEP #3255
Merge request studip/studip!2355
-rw-r--r--db/migrations/5.5.11_courseware_add_optional_comments.php21
-rw-r--r--lib/classes/JsonApi/Routes/Courseware/Authority.php4
-rw-r--r--lib/classes/JsonApi/Routes/Courseware/BlocksCreate.php1
-rw-r--r--lib/classes/JsonApi/Routes/Courseware/BlocksUpdate.php4
-rw-r--r--lib/classes/JsonApi/Routes/Courseware/StructuralElementsCreate.php3
-rw-r--r--lib/classes/JsonApi/Routes/Courseware/StructuralElementsUpdate.php4
-rw-r--r--lib/classes/JsonApi/Routes/Courseware/UnitsCreate.php3
-rw-r--r--lib/classes/JsonApi/Schemas/Courseware/Block.php1
-rw-r--r--lib/classes/JsonApi/Schemas/Courseware/StructuralElement.php1
-rw-r--r--lib/models/Courseware/Block.php4
-rw-r--r--lib/models/Courseware/StructuralElement.php9
-rw-r--r--resources/assets/stylesheets/scss/courseware.scss3
-rw-r--r--resources/assets/stylesheets/scss/courseware/blocks/default-block.scss37
-rw-r--r--resources/assets/stylesheets/scss/courseware/comments.scss26
-rw-r--r--resources/assets/stylesheets/scss/courseware/containers/default-container.scss18
-rw-r--r--resources/assets/stylesheets/scss/courseware/layouts/call-to-action.scss19
-rw-r--r--resources/assets/stylesheets/scss/courseware/layouts/talk-bubble.scss133
-rw-r--r--resources/assets/stylesheets/scss/courseware/structural-element.scss41
-rw-r--r--resources/vue/components/courseware/blocks/CoursewareBlockActions.vue33
-rw-r--r--resources/vue/components/courseware/blocks/CoursewareBlockComments.vue10
-rw-r--r--resources/vue/components/courseware/blocks/CoursewareBlockDiscussion.vue116
-rw-r--r--resources/vue/components/courseware/blocks/CoursewareBlockFeedback.vue33
-rw-r--r--resources/vue/components/courseware/blocks/CoursewareDefaultBlock.vue61
-rw-r--r--resources/vue/components/courseware/layouts/CoursewareCallToActionBox.vue70
-rw-r--r--resources/vue/components/courseware/layouts/CoursewareTalkBubble.vue58
-rw-r--r--resources/vue/components/courseware/structural-element/CoursewareStructuralElement.vue157
-rw-r--r--resources/vue/components/courseware/structural-element/CoursewareStructuralElementComments.vue10
-rw-r--r--resources/vue/components/courseware/structural-element/CoursewareStructuralElementFeedback.vue52
-rw-r--r--resources/vue/components/courseware/widgets/CoursewareViewWidget.vue14
-rw-r--r--resources/vue/store/courseware/courseware.module.js52
30 files changed, 713 insertions, 285 deletions
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 0000000..7c12a2e
--- /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 3df103d..88eb3df 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 04c7d92..d1d3afb 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 fcec2bb..6cf06ff 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 496a8f7..c038c45 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 6bf0e79..455aacc 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 a6159e3..5909f2a 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 03eb56b..e608188 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 4335e89..ab1dd0f 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 b794832..8b3d6f4 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 34cef80..a63c73f 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 6c3d7ea..d83a23e 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 21e583e..d65927b 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 f953e86..cec94b0 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 7798d50..a959d86 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 0000000..2e5e3b9
--- /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 06062b6..c3d30c2 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 ed1ba38..4609f8c 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 3ffff18..89beb0a 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 68dc648..9b1cc44 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 716e6ee..e29e15a 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 dfc95dc..c13d500 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 d11a538..2fabdda 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 0000000..601a339
--- /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 cd76aaa..007a72e 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 dcb3969..4f70f48 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 42c7f5f..e78377f 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 fd950bc..acecf63 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 d8a8563..6f9d787 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 1f85d56..db13cf1 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 }) {