mirror of
https://github.com/humhub/humhub.git
synced 2025-01-17 06:08:21 +01:00
Added Content State (#6076)
* Added new Content State * Added Content SoftDelete * Added Draft option on WallEntryCreate * Draft Stream Handling * Fix show drafts on dashboard * Reset draft state in Content Form * Remove Notifications/Activities on soft delete * Hide new content notifications on draft content * Added ActivityHelper * Added possibility to publish draft content * Added missing message text [skip ci] * Handle search for non published content * Mark default delete implementation as deprecated * Make sure files of deleted content are not longer accessible [skip ci] * Show badge for deleted content * Added State Filter for Content Queries * Added doc * Added ContentContainerStreamTest * Added Acceptance Tests * Fixed UserReleated exception for Guest users * Fixed popover less * Minor improvements
This commit is contained in:
parent
dbe3eb919c
commit
032aea3dd3
@ -8,7 +8,9 @@
|
||||
|
||||
namespace humhub\modules\activity;
|
||||
|
||||
use humhub\components\ActiveRecord;
|
||||
use humhub\modules\activity\components\MailSummary;
|
||||
use humhub\modules\activity\helpers\ActivityHelper;
|
||||
use humhub\modules\activity\jobs\SendMailSummary;
|
||||
use humhub\modules\activity\models\Activity;
|
||||
use humhub\modules\admin\permissions\ManageSettings;
|
||||
@ -19,7 +21,7 @@ use Yii;
|
||||
use yii\base\ActionEvent;
|
||||
use yii\base\BaseObject;
|
||||
use yii\base\Event;
|
||||
use yii\db\ActiveRecord;
|
||||
use yii\base\InvalidArgumentException;
|
||||
use yii\db\IntegrityException;
|
||||
|
||||
/**
|
||||
@ -66,23 +68,10 @@ class Events extends BaseObject
|
||||
public static function onActiveRecordDelete(Event $event)
|
||||
{
|
||||
if (!($event->sender instanceof ActiveRecord)) {
|
||||
throw new \LogicException('The handler can be applied only to the \yii\db\ActiveRecord.');
|
||||
throw new InvalidArgumentException('The handler can be applied only to the \humhub\components\ActiveRecord.');
|
||||
}
|
||||
|
||||
/** @var \yii\db\ActiveRecord $activeRecordModel */
|
||||
$activeRecordModel = $event->sender;
|
||||
$pk = $activeRecordModel->getPrimaryKey();
|
||||
|
||||
// Check if primary key exists and is not array (multiple pk)
|
||||
if ($pk !== null && !is_array($pk)) {
|
||||
$modelsActivity = Activity::find()->where([
|
||||
'object_id' => $pk,
|
||||
'object_model' => get_class($activeRecordModel)
|
||||
])->each();
|
||||
foreach ($modelsActivity as $activity) {
|
||||
$activity->delete();
|
||||
}
|
||||
}
|
||||
ActivityHelper::deleteActivitiesForRecord($event->sender);
|
||||
}
|
||||
|
||||
public static function onAccountMenuInit($event)
|
||||
|
32
protected/humhub/modules/activity/helpers/ActivityHelper.php
Normal file
32
protected/humhub/modules/activity/helpers/ActivityHelper.php
Normal file
@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace humhub\modules\activity\helpers;
|
||||
|
||||
use humhub\components\ActiveRecord;
|
||||
use humhub\modules\activity\models\Activity;
|
||||
use Yii;
|
||||
|
||||
class ActivityHelper
|
||||
{
|
||||
|
||||
public static function deleteActivitiesForRecord(ActiveRecord $record)
|
||||
{
|
||||
$pk = $record->getPrimaryKey();
|
||||
|
||||
// Check if primary key exists and is not array (multiple pk)
|
||||
if ($pk !== null && !is_array($pk)) {
|
||||
|
||||
$modelsActivity = Activity::find()->where([
|
||||
'object_id' => $pk,
|
||||
'object_model' => get_class($record)
|
||||
])->each();
|
||||
|
||||
foreach ($modelsActivity as $activity) {
|
||||
$activity->delete();
|
||||
}
|
||||
|
||||
Yii::debug('Deleted activities for ' . get_class($record) . " with PK " . $pk, 'activity');
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -15,7 +15,6 @@ use yii\base\Exception;
|
||||
use yii\base\InvalidConfigException;
|
||||
use yii\db\ActiveRecord;
|
||||
use humhub\modules\content\components\ContentActiveRecord;
|
||||
use humhub\modules\activity\components\ActivityWebRenderer;
|
||||
use humhub\components\behaviors\PolymorphicRelation;
|
||||
use yii\db\IntegrityException;
|
||||
use humhub\modules\activity\widgets\Activity as ActivityStreamEntryWidget;
|
||||
|
@ -9,6 +9,7 @@
|
||||
namespace humhub\modules\content;
|
||||
|
||||
use humhub\commands\IntegrityController;
|
||||
use humhub\components\Event;
|
||||
use humhub\modules\content\components\ContentActiveRecord;
|
||||
use humhub\modules\content\models\Content;
|
||||
use humhub\modules\search\interfaces\Searchable;
|
||||
@ -92,7 +93,7 @@ class Events extends BaseObject
|
||||
/**
|
||||
* On init of the WallEntryAddonWidget, attach the wall entry links widget.
|
||||
*
|
||||
* @param CEvent $event
|
||||
* @param Event $event
|
||||
*/
|
||||
public static function onWallEntryAddonInit($event)
|
||||
{
|
||||
@ -105,13 +106,13 @@ class Events extends BaseObject
|
||||
/**
|
||||
* On rebuild of the search index, rebuild all user records
|
||||
*
|
||||
* @param type $event
|
||||
* @param Event $event
|
||||
*/
|
||||
public static function onSearchRebuild($event)
|
||||
{
|
||||
foreach (Content::find()->each() as $content) {
|
||||
$contentObject = $content->getPolymorphicRelation();
|
||||
if ($contentObject instanceof Searchable) {
|
||||
if ($contentObject instanceof Searchable && $content->state === Content::STATE_PUBLISHED) {
|
||||
Yii::$app->search->add($contentObject);
|
||||
}
|
||||
}
|
||||
@ -126,7 +127,10 @@ class Events extends BaseObject
|
||||
{
|
||||
/** @var ContentActiveRecord $record */
|
||||
$record = $event->sender;
|
||||
SearchHelper::queueUpdate($record);
|
||||
|
||||
if ($record->content->state === Content::STATE_PUBLISHED) {
|
||||
SearchHelper::queueUpdate($record);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -141,4 +145,14 @@ class Events extends BaseObject
|
||||
SearchHelper::queueDelete($record);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback on daily cron job run
|
||||
*
|
||||
* @param \yii\base\Event $event
|
||||
*/
|
||||
public static function onCronDailyRun($event): void
|
||||
{
|
||||
Yii::$app->queue->push(new jobs\PurgeDeletedContents());
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -8,12 +8,14 @@
|
||||
|
||||
namespace humhub\modules\content\components;
|
||||
|
||||
use humhub\modules\content\models\Content;
|
||||
use humhub\modules\content\models\ContentTag;
|
||||
use humhub\modules\content\models\ContentTagRelation;
|
||||
use humhub\modules\space\models\Space;
|
||||
use humhub\modules\user\helpers\AuthHelper;
|
||||
use humhub\modules\user\models\User;
|
||||
use Yii;
|
||||
use yii\db\ActiveQuery;
|
||||
use yii\db\Expression;
|
||||
|
||||
/**
|
||||
@ -23,9 +25,8 @@ use yii\db\Expression;
|
||||
*
|
||||
* @author luke
|
||||
*/
|
||||
class ActiveQueryContent extends \yii\db\ActiveQuery
|
||||
class ActiveQueryContent extends ActiveQuery
|
||||
{
|
||||
|
||||
/**
|
||||
* Own content scope for userRelated
|
||||
* @see ActiveQueryContent::userRelated
|
||||
@ -36,6 +37,21 @@ class ActiveQueryContent extends \yii\db\ActiveQuery
|
||||
const USER_RELATED_SCOPE_FOLLOWED_USERS = 4;
|
||||
const USER_RELATED_SCOPE_OWN_PROFILE = 5;
|
||||
|
||||
/**
|
||||
* State filter that is used for queries. By default, only Published content is returned.
|
||||
*
|
||||
* Example to include drafts:
|
||||
* ```
|
||||
* $query = Post::find();
|
||||
* $query->stateFilterCondition[] = ['content.state' => Content::STATE_DRAFT];
|
||||
* $posts = $query->readable()->all();
|
||||
* ```
|
||||
*
|
||||
* @since 1.14
|
||||
* @var array
|
||||
*/
|
||||
public $stateFilterCondition = ['OR', ['content.state' => Content::STATE_PUBLISHED]];
|
||||
|
||||
/**
|
||||
* Only returns user readable records
|
||||
*
|
||||
@ -49,8 +65,9 @@ class ActiveQueryContent extends \yii\db\ActiveQuery
|
||||
$user = Yii::$app->user->getIdentity();
|
||||
}
|
||||
|
||||
$this->joinWith(['content', 'content.contentContainer', 'content.createdBy']);
|
||||
$this->andWhere($this->stateFilterCondition);
|
||||
|
||||
$this->joinWith(['content', 'content.contentContainer', 'content.createdBy']);
|
||||
$this->leftJoin('space', 'contentcontainer.pk=space.id AND contentcontainer.class=:spaceClass', [':spaceClass' => Space::class]);
|
||||
$this->leftJoin('user cuser', 'contentcontainer.pk=cuser.id AND contentcontainer.class=:userClass', [':userClass' => User::class]);
|
||||
$conditionSpace = '';
|
||||
@ -72,7 +89,7 @@ class ActiveQueryContent extends \yii\db\ActiveQuery
|
||||
}
|
||||
|
||||
// Build Access Check based on Space Content Container
|
||||
$conditionSpace = 'space.id IS NOT NULL' . $conditionSpaceMembershipRestriction; // space content
|
||||
$conditionSpace = 'space.id IS NOT NULL' . $conditionSpaceMembershipRestriction;
|
||||
|
||||
// Build Access Check based on User Content Container
|
||||
$conditionUser = 'cuser.id IS NOT NULL AND ('; // user content
|
||||
@ -139,7 +156,7 @@ class ActiveQueryContent extends \yii\db\ActiveQuery
|
||||
$contentTagQuery = ContentTagRelation::find()->select('content_id');
|
||||
$contentTagQuery->andWhere(['content_tag_relation.tag_id' => $contentTag->id]);
|
||||
$contentTagQuery->andWhere('content_tag_relation.content_id=content.id');
|
||||
$this->andWhere(['content.id' =>$contentTagQuery]);
|
||||
$this->andWhere(['content.id' => $contentTagQuery]);
|
||||
}
|
||||
} else if ($mode == 'OR') {
|
||||
$names = array_map(function ($v) {
|
||||
@ -182,6 +199,10 @@ class ActiveQueryContent extends \yii\db\ActiveQuery
|
||||
public function userRelated($scopes = [], $user = null)
|
||||
{
|
||||
if ($user === null) {
|
||||
if ( Yii::$app->user->isGuest) {
|
||||
return $this->andWhere('false');
|
||||
}
|
||||
|
||||
$user = Yii::$app->user->getIdentity();
|
||||
}
|
||||
|
||||
|
@ -474,6 +474,13 @@ class ContentActiveRecord extends ActiveRecord implements ContentOwner, Movable
|
||||
return static::class;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprected Please use `$this->content->softDelete()` instead of the default delete implementation!
|
||||
*/
|
||||
public function delete() {
|
||||
return parent::delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
|
@ -1,8 +1,8 @@
|
||||
<?php
|
||||
|
||||
use humhub\commands\CronController;
|
||||
use humhub\modules\content\Events;
|
||||
use humhub\commands\IntegrityController;
|
||||
use humhub\modules\content\widgets\WallEntryControls;
|
||||
use humhub\modules\content\widgets\WallEntryAddons;
|
||||
use humhub\modules\user\models\User;
|
||||
use humhub\modules\space\models\Space;
|
||||
@ -23,6 +23,7 @@ return [
|
||||
['class' => ContentActiveRecord::class, 'event' => ContentActiveRecord::EVENT_AFTER_INSERT, 'callback' => [Events::class, 'onContentActiveRecordSave']],
|
||||
['class' => ContentActiveRecord::class, 'event' => ContentActiveRecord::EVENT_AFTER_UPDATE, 'callback' => [Events::class, 'onContentActiveRecordSave']],
|
||||
['class' => ContentActiveRecord::class, 'event' => ContentActiveRecord::EVENT_AFTER_DELETE, 'callback' => [Events::class, 'onContentActiveRecordDelete']],
|
||||
['class' => CronController::class, 'event' => CronController::EVENT_ON_DAILY_RUN, 'callback' => [Events::class, 'onCronDailyRun']]
|
||||
],
|
||||
];
|
||||
?>
|
||||
?>
|
||||
|
@ -98,7 +98,7 @@ class ContentController extends Controller
|
||||
}
|
||||
|
||||
$json = [
|
||||
'success' => $contentObj->delete(),
|
||||
'success' => $contentObj->softDelete(),
|
||||
'uniqueId' => $contentObj->getUniqueId(),
|
||||
'model' => $model,
|
||||
'pk' => $id
|
||||
@ -211,7 +211,7 @@ class ContentController extends Controller
|
||||
throw new BadRequestHttpException();
|
||||
}
|
||||
|
||||
if($form->notify) {
|
||||
if ($form->notify) {
|
||||
$contentDeleted = ContentDeleted::instance()
|
||||
->from(Yii::$app->user->getIdentity())
|
||||
->payload(['contentTitle' => (new ContentDeleted)->getContentPlainTextInfo($content), 'reason' => $form->message]);
|
||||
@ -223,7 +223,7 @@ class ContentController extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
return $this->asJson(['success' => $content->delete()]);
|
||||
return $this->asJson(['success' => $content->softDelete()]);
|
||||
}
|
||||
|
||||
public function actionReload($id)
|
||||
@ -402,6 +402,25 @@ class ContentController extends Controller
|
||||
return $this->asJson($json);
|
||||
}
|
||||
|
||||
public function actionPublishDraft()
|
||||
{
|
||||
$this->forcePostRequest();
|
||||
|
||||
$json = [];
|
||||
$json['success'] = false;
|
||||
|
||||
$content = Content::findOne(['id' => Yii::$app->request->get('id', '')]);
|
||||
if ($content !== null && $content->canEdit() && $content->state === Content::STATE_DRAFT) {
|
||||
$content->state = Content::STATE_PUBLISHED;
|
||||
$content->save();
|
||||
$json['message'] = Yii::t('ContentModule.base', 'The content has been successfully published.');
|
||||
$json['success'] = true;
|
||||
|
||||
}
|
||||
|
||||
return $this->asJson($json);
|
||||
}
|
||||
|
||||
public function actionNotificationSwitch()
|
||||
{
|
||||
$this->forcePostRequest();
|
||||
|
@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @link https://www.humhub.org/
|
||||
* @copyright Copyright (c) 2023 HumHub GmbH & Co. KG
|
||||
* @license https://www.humhub.com/licences
|
||||
*/
|
||||
|
||||
namespace humhub\modules\content\jobs;
|
||||
|
||||
use humhub\modules\content\models\Content;
|
||||
use humhub\modules\queue\ActiveJob;
|
||||
|
||||
class PurgeDeletedContents extends ActiveJob
|
||||
{
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function run()
|
||||
{
|
||||
foreach (Content::findAll(['content.state' => Content::STATE_DELETED]) as $content) {
|
||||
$content->delete();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
use yii\db\Migration;
|
||||
|
||||
/**
|
||||
* Class m230127_195245_content_state
|
||||
*/
|
||||
class m230127_195245_content_state extends Migration
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function safeUp()
|
||||
{
|
||||
$this->addColumn(
|
||||
'content',
|
||||
'state',
|
||||
$this->tinyInteger()->defaultValue(1)->notNull()->after('visibility')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function safeDown()
|
||||
{
|
||||
echo "m230127_195245_content_state cannot be reverted.\n";
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/*
|
||||
// Use up()/down() to run migration code without a transaction.
|
||||
public function up()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
echo "m230127_195245_content_state cannot be reverted.\n";
|
||||
|
||||
return false;
|
||||
}
|
||||
*/
|
||||
}
|
@ -12,6 +12,7 @@ use humhub\components\ActiveRecord;
|
||||
use humhub\components\behaviors\GUID;
|
||||
use humhub\components\behaviors\PolymorphicRelation;
|
||||
use humhub\components\Module;
|
||||
use humhub\modules\activity\helpers\ActivityHelper;
|
||||
use humhub\modules\admin\permissions\ManageUsers;
|
||||
use humhub\modules\content\components\ContentActiveRecord;
|
||||
use humhub\modules\content\components\ContentContainerActiveRecord;
|
||||
@ -21,6 +22,7 @@ use humhub\modules\content\live\NewContent;
|
||||
use humhub\modules\content\permissions\CreatePrivateContent;
|
||||
use humhub\modules\content\permissions\CreatePublicContent;
|
||||
use humhub\modules\content\permissions\ManageContent;
|
||||
use humhub\modules\notification\models\Notification;
|
||||
use humhub\modules\search\libs\SearchHelper;
|
||||
use humhub\modules\space\models\Space;
|
||||
use humhub\modules\user\components\PermissionManager;
|
||||
@ -29,7 +31,6 @@ use humhub\modules\user\models\User;
|
||||
use Yii;
|
||||
use yii\base\Exception;
|
||||
use yii\base\InvalidArgumentException;
|
||||
use yii\db\Expression;
|
||||
use yii\db\IntegrityException;
|
||||
use yii\helpers\Url;
|
||||
|
||||
@ -59,6 +60,7 @@ use yii\helpers\Url;
|
||||
* @property integer $visibility
|
||||
* @property integer $pinned
|
||||
* @property integer $archived
|
||||
* @property integer $state
|
||||
* @property integer $locked_comments
|
||||
* @property string $created_at
|
||||
* @property integer $created_by
|
||||
@ -103,6 +105,13 @@ class Content extends ActiveRecord implements Movable, ContentOwner
|
||||
*/
|
||||
const VISIBILITY_OWNER = 2;
|
||||
|
||||
/**
|
||||
* Content States - By default, only content with the "Published" state is returned.
|
||||
*/
|
||||
const STATE_PUBLISHED = 1;
|
||||
const STATE_DRAFT = 10;
|
||||
const STATE_DELETED = 100;
|
||||
|
||||
/**
|
||||
* @var ContentContainerActiveRecord the Container (e.g. Space or User) where this content belongs to.
|
||||
*/
|
||||
@ -191,21 +200,13 @@ class Content extends ActiveRecord implements Movable, ContentOwner
|
||||
throw new Exception("Could not save content with object_model or object_id!");
|
||||
}
|
||||
|
||||
// Set some default values
|
||||
if (!$this->archived) {
|
||||
$this->archived = 0;
|
||||
}
|
||||
if (!$this->visibility) {
|
||||
$this->visibility = self::VISIBILITY_PRIVATE;
|
||||
}
|
||||
if (!$this->pinned) {
|
||||
$this->pinned = 0;
|
||||
}
|
||||
$this->archived ??= 0;
|
||||
$this->visibility ??= self::VISIBILITY_PRIVATE;
|
||||
$this->pinned ??= 0;
|
||||
$this->state ??= Content::STATE_PUBLISHED;
|
||||
|
||||
if ($insert) {
|
||||
if ($this->created_by == "") {
|
||||
$this->created_by = Yii::$app->user->id;
|
||||
}
|
||||
$this->created_by ??= Yii::$app->user->id;
|
||||
}
|
||||
|
||||
$this->stream_sort_date = date('Y-m-d G:i:s');
|
||||
@ -222,15 +223,38 @@ class Content extends ActiveRecord implements Movable, ContentOwner
|
||||
*/
|
||||
public function afterSave($insert, $changedAttributes)
|
||||
{
|
||||
/* @var $contentSource ContentActiveRecord */
|
||||
$contentSource = $this->getModel();
|
||||
if (// New Content with State Published:
|
||||
($insert && $this->state == Content::STATE_PUBLISHED) ||
|
||||
// Content Updated from Draft to Published
|
||||
(array_key_exists('state', $changedAttributes) &&
|
||||
$this->state == Content::STATE_PUBLISHED &&
|
||||
$changedAttributes['state'] == Content::STATE_DRAFT
|
||||
)) {
|
||||
$this->processNewContent();
|
||||
}
|
||||
|
||||
if ($this->state === static::STATE_PUBLISHED) {
|
||||
SearchHelper::queueUpdate($this->getModel());
|
||||
} else {
|
||||
SearchHelper::queueDelete($this->getModel());
|
||||
}
|
||||
|
||||
parent::afterSave($insert, $changedAttributes);
|
||||
}
|
||||
|
||||
private function processNewContent()
|
||||
{
|
||||
$record = $this->getModel();
|
||||
|
||||
Yii::debug('Process new content: ' . get_class($record) . ' ID: ' . $record->getPrimaryKey(), 'content');
|
||||
|
||||
foreach ($this->notifyUsersOfNewContent as $user) {
|
||||
$contentSource->follow($user->id);
|
||||
$record->follow($user->id);
|
||||
}
|
||||
|
||||
// TODO: handle ContentCreated notifications and live events for global content
|
||||
if ($insert && !$this->isMuted()) {
|
||||
|
||||
if (!$this->isMuted()) {
|
||||
$this->notifyContentCreated();
|
||||
}
|
||||
|
||||
@ -241,20 +265,17 @@ class Content extends ActiveRecord implements Movable, ContentOwner
|
||||
'originator' => $this->createdBy->guid,
|
||||
'contentContainerId' => $this->container->contentContainerRecord->id,
|
||||
'visibility' => $this->visibility,
|
||||
'sourceClass' => get_class($contentSource),
|
||||
'sourceId' => $contentSource->getPrimaryKey(),
|
||||
'sourceClass' => get_class($record),
|
||||
'sourceId' => $record->getPrimaryKey(),
|
||||
'silent' => $this->isMuted(),
|
||||
'streamChannel' => $this->stream_channel,
|
||||
'contentId' => $this->id,
|
||||
'insert' => $insert
|
||||
'insert' => true
|
||||
]));
|
||||
}
|
||||
|
||||
SearchHelper::queueUpdate($contentSource);
|
||||
|
||||
parent::afterSave($insert, $changedAttributes);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return bool checks if the given content allows content creation notifications and activities
|
||||
* @throws IntegrityException
|
||||
@ -859,6 +880,11 @@ class Content extends ActiveRecord implements Movable, ContentOwner
|
||||
return $this->checkGuestAccess();
|
||||
}
|
||||
|
||||
// If content is draft, in trash, unapproved - restrict view access to editors
|
||||
if ($this->state !== static::STATE_PUBLISHED) {
|
||||
return $this->canEdit();
|
||||
}
|
||||
|
||||
// Public visible content
|
||||
if ($this->isPublic()) {
|
||||
return true;
|
||||
@ -948,4 +974,29 @@ class Content extends ActiveRecord implements Movable, ContentOwner
|
||||
{
|
||||
return $this->created_at !== $this->updated_at && !empty($this->updated_at) && is_string($this->updated_at);
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks the content as deleted.
|
||||
*
|
||||
* Content which are marked as deleted will not longer returned in queries/stream/search.
|
||||
* A cron job will remove these content permanently.
|
||||
* If installed, such content can also be restored using the `recycle-bin` module.
|
||||
*
|
||||
* @return bool
|
||||
* @since 1.14
|
||||
*/
|
||||
public function softDelete(): bool
|
||||
{
|
||||
ActivityHelper::deleteActivitiesForRecord($this->getModel());
|
||||
|
||||
Notification::deleteAll([
|
||||
'source_class' => get_class($this),
|
||||
'source_pk' => $this->getPrimaryKey(),
|
||||
]);
|
||||
|
||||
$this->state = self::STATE_DELETED;
|
||||
$this->save();
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -95,6 +95,7 @@ humhub.module('content.form', function(module, require, $) {
|
||||
this.setDefaultVisibility();
|
||||
this.resetFilePreview();
|
||||
this.resetFileUpload();
|
||||
this.resetDraftState();
|
||||
|
||||
$('#public').attr('checked', false);
|
||||
$('#contentFormBody').find('.humhub-ui-richtext').trigger('clear');
|
||||
@ -185,6 +186,21 @@ humhub.module('content.form', function(module, require, $) {
|
||||
}
|
||||
};
|
||||
|
||||
CreateForm.prototype.resetDraftState = function() {
|
||||
$('#contentForm_draft').prop("checked", false);
|
||||
$('.label-draft').addClass('hidden');
|
||||
};
|
||||
|
||||
CreateForm.prototype.changeDraftState = function() {
|
||||
if ($('#contentForm_draft').prop("checked")) {
|
||||
$('.label-draft').addClass('hidden');
|
||||
$('#contentForm_draft').prop("checked", false);
|
||||
} else {
|
||||
$('.label-draft').removeClass('hidden');
|
||||
$('#contentForm_draft').prop("checked", true);
|
||||
}
|
||||
};
|
||||
|
||||
const CreateFormMenu = Widget.extend();
|
||||
|
||||
CreateFormMenu.prototype.init = function() {
|
||||
|
@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
use content\AcceptanceTester;
|
||||
|
||||
class DraftCest
|
||||
{
|
||||
public function testCreateDraftPost(AcceptanceTester $I)
|
||||
{
|
||||
$I->amSpaceAdmin(false, 3);
|
||||
|
||||
$I->wantTo('create a draft post.');
|
||||
$I->waitForText('What\'s on your mind?');
|
||||
$I->click('#contentFormBody .humhub-ui-richtext[contenteditable]');
|
||||
$I->fillField('#contentFormBody .humhub-ui-richtext[contenteditable]', 'Some Schabernack');
|
||||
$I->click('#contentFormBody ul.preferences');
|
||||
$I->waitForText('Create as draft');
|
||||
$I->click('Create as draft');
|
||||
$I->waitForText('DRAFT', '10', '.label-container');
|
||||
$I->click('#post_submit_button', '#contentFormBody');
|
||||
|
||||
$I->wantTo('ensure draft has a draft badge.');
|
||||
$I->waitForText('DRAFT', '5', '//div[@class="wall-entry"][1]');
|
||||
|
||||
$I->wantTo('ensure draft is not visible for other users.');
|
||||
$I->amUser2(true);
|
||||
$I->amOnSpace3();
|
||||
$I->dontSee('Schabernack');
|
||||
|
||||
$I->wantTo('publish draft');
|
||||
$I->amSpaceAdmin(true, 3);
|
||||
$I->waitForText('Schabernack');
|
||||
$I->click('//div[@class="wall-entry"][1]//ul[contains(@class, "preferences")]');
|
||||
$I->waitForText('Publish draft', '5');
|
||||
$I->click('Publish draft');
|
||||
$I->waitForText('The content has been successfully published.');
|
||||
$I->dontSee('DRAFT');
|
||||
|
||||
$I->wantTo('ensure published draft is now visible for other users.');
|
||||
$I->amUser2(true);
|
||||
$I->amOnSpace3();
|
||||
$I->waitForText('Schabernack');
|
||||
}
|
||||
}
|
@ -95,6 +95,40 @@ class ContentContainerStreamTest extends HumHubDbTestCase
|
||||
$this->assertTrue(in_array($w2, $ids));
|
||||
}
|
||||
|
||||
public function testDraftContent()
|
||||
{
|
||||
$this->becomeUser('User2');
|
||||
$draft1Id = $this->createPost('Some Draft', Content::VISIBILITY_PRIVATE, Content::STATE_DRAFT);
|
||||
$regular1Id = $this->createPost('Regular 1 by U2', Content::VISIBILITY_PRIVATE,);
|
||||
$this->becomeUser('Admin');
|
||||
$regular2Id = $this->createPost('Regular 2 by Admin', Content::VISIBILITY_PRIVATE);
|
||||
|
||||
$this->becomeUser('User2');
|
||||
$ids = $this->getStreamActionIds($this->space, 2);
|
||||
|
||||
// Check draft is first for Author
|
||||
$this->assertTrue($ids[0] === $draft1Id);
|
||||
|
||||
// Check draft is not visible for other users
|
||||
$this->becomeUser('Admin');
|
||||
$ids = $this->getStreamActionIds($this->space, 5);
|
||||
$this->assertTrue(!in_array($draft1Id, $ids));
|
||||
}
|
||||
|
||||
public function testDeletedContent()
|
||||
{
|
||||
$this->becomeUser('User2');
|
||||
$deleteId = $this->createPost('Something to delete', Content::VISIBILITY_PRIVATE);
|
||||
|
||||
$post = Post::findOne(['id' => $deleteId]);
|
||||
$post->content->softDelete();
|
||||
|
||||
$ids = $this->getStreamActionIds($this->space, 3);
|
||||
|
||||
// Deleted Content should not appear in stream
|
||||
$this->assertTrue(!in_array($deleteId, $ids));
|
||||
}
|
||||
|
||||
private function getStreamActionIds($container, $limit = 4)
|
||||
{
|
||||
$action = new ContentContainerStream('stream', Yii::$app->controller, [
|
||||
@ -107,7 +141,9 @@ class ContentContainerStreamTest extends HumHubDbTestCase
|
||||
|
||||
$wallEntries = $action->getStreamQuery()->all();
|
||||
|
||||
$wallEntryIds = array_map(static function($entry) {return $entry->id; }, $wallEntries);
|
||||
$wallEntryIds = array_map(static function ($entry) {
|
||||
return $entry->id;
|
||||
}, $wallEntries);
|
||||
|
||||
return $wallEntryIds;
|
||||
}
|
||||
@ -122,12 +158,13 @@ class ContentContainerStreamTest extends HumHubDbTestCase
|
||||
return $this->createPost('Public Post', Content::VISIBILITY_PUBLIC);
|
||||
}
|
||||
|
||||
private function createPost($message, $visibility)
|
||||
private function createPost($message, $visibility, $state = Content::STATE_PUBLISHED)
|
||||
{
|
||||
$post = new Post;
|
||||
$post->message = $message;
|
||||
$post->content->setContainer($this->space);
|
||||
$post->content->visibility = $visibility;
|
||||
$post->content->state = $state;
|
||||
$post->save();
|
||||
|
||||
return $post->content->id;
|
||||
|
@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace humhub\modules\content\widgets;
|
||||
|
||||
use humhub\components\Widget;
|
||||
use humhub\libs\Html;
|
||||
use humhub\modules\content\models\Content;
|
||||
use Yii;
|
||||
use yii\helpers\Url;
|
||||
|
||||
class PublishDraftLink extends Widget
|
||||
{
|
||||
/**
|
||||
* @var \humhub\modules\content\components\ContentActiveRecord
|
||||
*/
|
||||
public $content;
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
public function run()
|
||||
{
|
||||
if ($this->content->content->state !== Content::STATE_DRAFT ||
|
||||
!$this->content->content->canEdit()) {
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
$publishUrl = Url::to(['/content/content/publish-draft', 'id' => $this->content->content->id]);
|
||||
|
||||
return Html::tag('li',
|
||||
Html::a(
|
||||
'<i class="fa fa-mail-reply-all"></i> '
|
||||
. Yii::t('ContentModule.base', 'Publish draft'),
|
||||
'#', ['data-action-click' => 'publishDraft', 'data-action-url' => $publishUrl])
|
||||
);
|
||||
}
|
||||
|
||||
}
|
40
protected/humhub/modules/content/widgets/StateBadge.php
Normal file
40
protected/humhub/modules/content/widgets/StateBadge.php
Normal file
@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace humhub\modules\content\widgets;
|
||||
|
||||
|
||||
use humhub\components\Widget;
|
||||
use humhub\libs\Html;
|
||||
use humhub\modules\content\components\ContentActiveRecord;
|
||||
use humhub\modules\content\models\Content;
|
||||
use Yii;
|
||||
|
||||
/**
|
||||
* Can be used to render an archive icon for archived content.
|
||||
* @package humhub\modules\content\widgets
|
||||
* @since 1.14
|
||||
*/
|
||||
class StateBadge extends Widget
|
||||
{
|
||||
public ?ContentActiveRecord $model;
|
||||
|
||||
public function run()
|
||||
{
|
||||
if ($this->model === null) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if ($this->model->content->state === Content::STATE_DRAFT) {
|
||||
return Html::tag(
|
||||
'span', Yii::t('ContentModule.base', 'Draft'),
|
||||
['class' => 'label label-danger label-state-draft']
|
||||
);
|
||||
} elseif ($this->model->content->state === Content::STATE_DELETED) {
|
||||
return Html::tag(
|
||||
'span', Yii::t('ContentModule.base', 'Deleted'),
|
||||
['class' => 'label label-danger label-state-deleted']
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -115,6 +115,10 @@ abstract class WallCreateContentForm extends Widget
|
||||
$record->content->visibility = $visibility;
|
||||
$record->content->container = $contentContainer;
|
||||
|
||||
if ((bool)Yii::$app->request->post('isDraft', false)) {
|
||||
$record->content->state = Content::STATE_DRAFT;
|
||||
}
|
||||
|
||||
// Handle Notify User Features of ContentFormWidget
|
||||
// ToDo: Check permissions of user guids
|
||||
$userGuids = Yii::$app->request->post('notifyUserInput');
|
||||
@ -129,7 +133,7 @@ abstract class WallCreateContentForm extends Widget
|
||||
|
||||
if ($record->save()) {
|
||||
$topics = Yii::$app->request->post('postTopicInput');
|
||||
if(!empty($topics)) {
|
||||
if (!empty($topics)) {
|
||||
Topic::attach($record->content, $topics);
|
||||
}
|
||||
|
||||
|
@ -6,6 +6,7 @@ namespace humhub\modules\content\widgets\stream;
|
||||
use Exception;
|
||||
use humhub\libs\Html;
|
||||
use humhub\modules\content\components\ContentActiveRecord;
|
||||
use humhub\modules\content\models\Content;
|
||||
use humhub\modules\content\widgets\ArchiveLink;
|
||||
use humhub\modules\content\widgets\DeleteLink;
|
||||
use humhub\modules\content\widgets\LockCommentsLink;
|
||||
@ -14,6 +15,7 @@ use humhub\modules\content\widgets\MoveContentLink;
|
||||
use humhub\modules\content\widgets\NotificationSwitchLink;
|
||||
use humhub\modules\content\widgets\PermaLink;
|
||||
use humhub\modules\content\widgets\PinLink;
|
||||
use humhub\modules\content\widgets\PublishDraftLink;
|
||||
use humhub\modules\content\widgets\VisibilityLink;
|
||||
use humhub\modules\dashboard\controllers\DashboardController;
|
||||
use humhub\modules\space\models\Space;
|
||||
@ -324,6 +326,10 @@ abstract class WallStreamEntryWidget extends StreamEntryWidget
|
||||
*/
|
||||
public function getControlsMenuEntries()
|
||||
{
|
||||
if ($this->model->content->state === Content::STATE_DELETED) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if($this->renderOptions->isViewContext([WallStreamEntryOptions::VIEW_CONTEXT_SEARCH])) {
|
||||
return [
|
||||
[PermaLink::class, ['content' => $this->model], ['sortOrder' => 200]]
|
||||
@ -331,6 +337,7 @@ abstract class WallStreamEntryWidget extends StreamEntryWidget
|
||||
}
|
||||
|
||||
$result = [
|
||||
[PublishDraftLink::class, ['content' => $this->model], ['sortOrder' => 100]],
|
||||
[PermaLink::class, ['content' => $this->model], ['sortOrder' => 200]],
|
||||
[DeleteLink::class, ['content' => $this->model], ['sortOrder' => 300]],
|
||||
new DropdownDivider(['sortOrder' => 350]),
|
||||
|
@ -2,8 +2,10 @@
|
||||
|
||||
use humhub\libs\Html;
|
||||
use humhub\modules\content\components\ContentActiveRecord;
|
||||
use humhub\modules\content\models\Content;
|
||||
use humhub\modules\content\widgets\ArchivedIcon;
|
||||
use humhub\modules\content\widgets\LockCommentsIcon;
|
||||
use humhub\modules\content\widgets\StateBadge;
|
||||
use humhub\modules\content\widgets\stream\WallStreamEntryOptions;
|
||||
use humhub\modules\content\widgets\UpdatedIcon;
|
||||
use humhub\modules\content\widgets\VisibilityIcon;
|
||||
@ -29,6 +31,7 @@ $container = $model->content->container;
|
||||
<?php elseif ($renderOptions->isPinned($model)) : ?>
|
||||
<?= Icon::get('map-pin', ['htmlOptions' => ['class' => 'icon-pin tt', 'title' => Yii::t('ContentModule.base', 'Pinned')]]) ?>
|
||||
<?php endif; ?>
|
||||
<?= StateBadge::widget(['model' => $model]); ?>
|
||||
</div>
|
||||
|
||||
<!-- since v1.2 -->
|
||||
|
@ -64,25 +64,31 @@ use yii\helpers\Html;
|
||||
<?= FileHandlerButtonDropdown::widget(['primaryButton' => $uploadButton, 'handlers' => $fileHandlers, 'cssButtonClass' => 'btn-default']); ?>
|
||||
|
||||
<!-- public checkbox -->
|
||||
<?= Html::checkbox('visibility', '', ['id' => 'contentForm_visibility', 'class' => 'contentForm hidden', 'aria-hidden' => 'true', 'title' => Yii::t('ContentModule.base', 'Content visibility') ]); ?>
|
||||
<?= Html::checkbox('visibility', '', ['id' => 'contentForm_visibility', 'class' => 'contentForm hidden', 'aria-hidden' => 'true']); ?>
|
||||
|
||||
<!-- draft checkbox -->
|
||||
<?= Html::checkbox('isDraft', '', ['id' => 'contentForm_draft', 'class' => 'contentForm hidden', 'aria-hidden' => 'true']); ?>
|
||||
|
||||
<!-- content sharing -->
|
||||
<div class="pull-right">
|
||||
|
||||
<span class="label label-info label-public hidden"><?= Yii::t('ContentModule.base', 'Public'); ?></span>
|
||||
<span class="label-container">
|
||||
<span class="label label-info label-public hidden"><?= Yii::t('ContentModule.base', 'Public'); ?></span>
|
||||
<span class="label label-warning label-draft hidden"><?= Yii::t('ContentModule.base', 'Draft'); ?></span>
|
||||
</span>
|
||||
|
||||
<ul class="nav nav-pills preferences" style="right:0;top:5px">
|
||||
<li class="dropdown">
|
||||
<a class="dropdown-toggle" style="padding:5px 10px" data-toggle="dropdown" href="#" aria-label="<?= Yii::t('base', 'Toggle post menu'); ?>" aria-haspopup="true">
|
||||
<?= Icon::get('cogs')?>
|
||||
<a class="dropdown-toggle" style="padding:5px 10px" data-toggle="dropdown" href="#"
|
||||
aria-label="<?= Yii::t('base', 'Toggle post menu'); ?>" aria-haspopup="true">
|
||||
<?= Icon::get('cogs') ?>
|
||||
</a>
|
||||
<ul class="dropdown-menu pull-right">
|
||||
<li>
|
||||
<?= Link::withAction(Yii::t('ContentModule.base', 'Notify members'), 'notifyUser')->icon('bell')?>
|
||||
<?= Link::withAction(Yii::t('ContentModule.base', 'Notify members'), 'notifyUser')->icon('bell') ?>
|
||||
</li>
|
||||
<?php if (TopicPicker::showTopicPicker($contentContainer)) : ?>
|
||||
<li>
|
||||
<?= Link::withAction(Yii::t('ContentModule.base', 'Topics'), 'setTopics')->icon(Yii::$app->getModule('topic')->icon) ?>
|
||||
<?= Link::withAction(Yii::t('ContentModule.base', 'Topics'), 'setTopics')->icon(Yii::$app->getModule('topic')->icon) ?>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
<?php if ($canSwitchVisibility): ?>
|
||||
@ -91,6 +97,10 @@ use yii\helpers\Html;
|
||||
->id('contentForm_visibility_entry')->icon('unlock') ?>
|
||||
</li>
|
||||
<?php endif; ?>
|
||||
<li>
|
||||
<?= Link::withAction(Yii::t('ContentModule.base', 'Create as draft'), 'changeDraftState')
|
||||
->id('contentForm_visibility_entry')->icon('edit') ?>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
@ -99,4 +109,4 @@ use yii\helpers\Html;
|
||||
|
||||
<?= UploadProgress::widget(['id' => 'contentFormFiles_progress']) ?>
|
||||
<?= FilePreview::widget(['id' => 'contentFormFiles_preview', 'edit' => true, 'options' => ['style' => 'margin-top:10px;']]); ?>
|
||||
</div><!-- /contentForm_Options -->
|
||||
</div><!-- /contentForm_Options -->
|
||||
|
@ -23,10 +23,10 @@ return [
|
||||
['space_id' => '1', 'user_id' => '1', 'originator_user_id' => null, 'status' => '3', 'request_message' => null, 'last_visit' => '2014-08-08 06:49:57', 'group_id' => 'admin', 'created_at' => '2014-08-08 05:36:05', 'created_by' => '1', 'updated_at' => '2014-08-08 05:36:05', 'updated_by' => '1'],
|
||||
['space_id' => '1', 'user_id' => '3', 'originator_user_id' => null, 'status' => '3', 'request_message' => null, 'last_visit' => null, 'group_id' => 'member', 'created_at' => '2014-08-10 16:55:41', 'created_by' => null, 'updated_at' => null, 'updated_by' => null],
|
||||
|
||||
// User 2 is Member/Admin of Space 2
|
||||
// User 1 is Member/Admin of Space 2
|
||||
['space_id' => '2', 'user_id' => '2', 'originator_user_id' => null, 'status' => '3', 'request_message' => null, 'last_visit' => '2014-08-08 06:49:57', 'group_id' => 'admin', 'created_at' => '2014-08-08 05:36:05', 'created_by' => '1', 'updated_at' => '2014-08-08 05:36:05', 'updated_by' => '1'],
|
||||
|
||||
// User 1 is admin of space 3 and user 2 & 3 are members
|
||||
// Admin is admin of space 3 and user 1 & 2 are members
|
||||
['space_id' => '3', 'user_id' => '1', 'originator_user_id' => null, 'status' => '3', 'request_message' => null, 'last_visit' => '2014-08-08 06:49:57', 'group_id' => 'admin', 'created_at' => '2014-08-08 05:36:05', 'created_by' => '1', 'updated_at' => '2014-08-08 05:36:05', 'updated_by' => '1'],
|
||||
['space_id' => '3', 'user_id' => '2', 'originator_user_id' => null, 'status' => '3', 'send_notifications' => '1', 'request_message' => null, 'last_visit' => null, 'group_id' => 'member', 'created_at' => '2014-08-10 16:55:41', 'created_by' => null, 'updated_at' => null, 'updated_by' => null],
|
||||
['space_id' => '3', 'user_id' => '3', 'originator_user_id' => null, 'status' => '3', 'request_message' => null, 'last_visit' => '2014-08-08 06:49:57', 'group_id' => 'moderator', 'created_at' => '2014-08-08 05:36:05', 'created_by' => '1', 'updated_at' => '2014-08-08 05:36:05', 'updated_by' => '1'],
|
||||
|
@ -30,7 +30,10 @@ class Module extends \humhub\components\Module
|
||||
/**
|
||||
* @var array default content classes which are not suppressed when in a row
|
||||
*/
|
||||
public $defaultStreamSuppressQueryIgnore = [\humhub\modules\post\models\Post::class, \humhub\modules\activity\models\Activity::class];
|
||||
public $defaultStreamSuppressQueryIgnore = [
|
||||
\humhub\modules\post\models\Post::class,
|
||||
\humhub\modules\activity\models\Activity::class
|
||||
];
|
||||
|
||||
/**
|
||||
* @var int number of contents from which "Show more" appears in the stream
|
||||
|
@ -22,7 +22,6 @@ use yii\base\InvalidConfigException;
|
||||
*/
|
||||
class ContentContainerStream extends Stream
|
||||
{
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
@ -40,6 +39,7 @@ class ContentContainerStream extends Stream
|
||||
protected function initQuery($options = [])
|
||||
{
|
||||
$options['container'] = $this->contentContainer;
|
||||
|
||||
return parent::initQuery($options);
|
||||
}
|
||||
}
|
||||
|
@ -124,9 +124,8 @@ abstract class Stream extends Action
|
||||
public $limit = 4;
|
||||
|
||||
/**
|
||||
* Filters
|
||||
*
|
||||
* @var array
|
||||
* Filters - A list of active filter id's (e.g. `visibility_private`)
|
||||
* @var string[]
|
||||
*/
|
||||
public $filters = [];
|
||||
|
||||
|
@ -7,6 +7,7 @@ namespace humhub\modules\stream\models;
|
||||
use humhub\modules\content\components\ContentContainerActiveRecord;
|
||||
use humhub\modules\stream\models\filters\ContentContainerStreamFilter;
|
||||
use humhub\modules\stream\models\filters\PinnedContentStreamFilter;
|
||||
use humhub\modules\stream\models\filters\StreamQueryFilter;
|
||||
use yii\base\InvalidConfigException;
|
||||
|
||||
/**
|
||||
@ -26,11 +27,6 @@ class ContentContainerStreamQuery extends WallStreamQuery
|
||||
*/
|
||||
public $pinnedContentSupport = true;
|
||||
|
||||
/**
|
||||
* @var PinnedContentStreamFilter
|
||||
*/
|
||||
private $pinnedContentStreamFilter;
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
* @throws InvalidConfigException
|
||||
@ -39,25 +35,15 @@ class ContentContainerStreamQuery extends WallStreamQuery
|
||||
{
|
||||
parent::beforeApplyFilters();
|
||||
|
||||
$this->addFilterHandler(new ContentContainerStreamFilter(['container' => $this->container]));
|
||||
$this->addFilterHandler(
|
||||
new ContentContainerStreamFilter(['container' => $this->container]),
|
||||
true,
|
||||
true
|
||||
);
|
||||
|
||||
$this->pinnedContentStreamFilter = new PinnedContentStreamFilter(['container' => $this->container]);
|
||||
|
||||
if($this->pinnedContentSupport) {
|
||||
$this->addFilterHandler($this->pinnedContentStreamFilter);
|
||||
if ($this->pinnedContentSupport) {
|
||||
$this->addFilterHandler(new PinnedContentStreamFilter(['container' => $this->container]));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public function all()
|
||||
{
|
||||
$result = parent::all();
|
||||
|
||||
if(!empty($this->pinnedContentStreamFilter->pinnedContent)) {
|
||||
$result = array_merge($this->pinnedContentStreamFilter->pinnedContent, $result);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ namespace humhub\modules\stream\models;
|
||||
|
||||
use humhub\modules\stream\models\filters\BlockedUsersStreamFilter;
|
||||
use humhub\modules\stream\models\filters\DateStreamFilter;
|
||||
use humhub\modules\stream\models\filters\DraftContentStreamFilter;
|
||||
use humhub\modules\stream\models\filters\StreamQueryFilter;
|
||||
use Yii;
|
||||
use yii\base\InvalidConfigException;
|
||||
@ -136,9 +137,18 @@ class StreamQuery extends Model
|
||||
ContentTypeStreamFilter::class,
|
||||
OriginatorStreamFilter::class,
|
||||
BlockedUsersStreamFilter::class,
|
||||
DateStreamFilter::class
|
||||
DateStreamFilter::class,
|
||||
DraftContentStreamFilter::class
|
||||
];
|
||||
|
||||
/**
|
||||
* Per default only content with published state are returned.
|
||||
*
|
||||
* @note Used, for example, by the Recycle Bin module to display deleted content in the stream.
|
||||
* @var array
|
||||
*/
|
||||
public $stateFilterCondition = ['OR', ['content.state' => Content::STATE_PUBLISHED]];
|
||||
|
||||
/**
|
||||
* The content query.
|
||||
*
|
||||
@ -386,7 +396,30 @@ class StreamQuery extends Model
|
||||
*/
|
||||
public function all()
|
||||
{
|
||||
return $this->query(!$this->_built)->all();
|
||||
return $this->postProcessAll(
|
||||
$this->query(!$this->_built)->all()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Content[] $result
|
||||
* @return Content[]
|
||||
* @since 1.14
|
||||
*/
|
||||
protected function postProcessAll(array $result)
|
||||
{
|
||||
/**
|
||||
* Allow FilterHandler to directly modify the stream content result
|
||||
* e.g. (e.g. Pinned/Drafts) can inject additional content on the first stream batch
|
||||
*/
|
||||
foreach ($this->filterHandlers as $filterHandler) {
|
||||
if ($filterHandler instanceof StreamQueryFilter) {
|
||||
$filterHandler->postProcessStreamResult($result);
|
||||
} else {
|
||||
Yii::warning('StreamQuery::postProcessAll - invalid FilterHandler: ' . var_export($filterHandler, true), 'content');
|
||||
}
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -402,6 +435,8 @@ class StreamQuery extends Model
|
||||
$this->setupCriteria();
|
||||
$this->setupFilters();
|
||||
|
||||
$this->_query->andWhere($this->stateFilterCondition);
|
||||
|
||||
if (!empty($this->channel)) {
|
||||
$this->channel($this->channel);
|
||||
}
|
||||
@ -529,8 +564,8 @@ class StreamQuery extends Model
|
||||
{
|
||||
$this->beforeApplyFilters();
|
||||
|
||||
foreach ($this->filterHandlers as $handler) {
|
||||
$this->prepareHandler($handler)->apply();
|
||||
foreach (array_keys($this->filterHandlers) as $i) {
|
||||
$this->prepareHandler($this->filterHandlers[$i])->apply();
|
||||
}
|
||||
|
||||
$this->afterApplyFilters();
|
||||
@ -592,24 +627,35 @@ class StreamQuery extends Model
|
||||
* @throws InvalidConfigException
|
||||
* @since 1.6
|
||||
*/
|
||||
public function addFilterHandler($handler, $overwrite = true)
|
||||
public function addFilterHandler($handler, $overwrite = true, $prepend = false)
|
||||
{
|
||||
if($overwrite) {
|
||||
if ($overwrite) {
|
||||
$this->removeFilterHandler($handler);
|
||||
}
|
||||
|
||||
$handler = $this->prepareHandler($handler);
|
||||
return $this->filterHandlers[] = $handler;
|
||||
|
||||
/**
|
||||
* Some Filters must be prepended, when other filters rely on them.
|
||||
* E.g. `ContentContainerStreamFilter` is required for `DraftContentStreamFilter` which clones
|
||||
* the current query to get all drafts (for ContentContainer or Dashboard)
|
||||
*/
|
||||
if ($prepend) {
|
||||
array_unshift($this->filterHandlers, $handler);
|
||||
} else {
|
||||
$this->filterHandlers[] = $handler;
|
||||
}
|
||||
return $handler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Can be used to add multiple filter handlers at once.
|
||||
*
|
||||
* @see self::addFilterHandler
|
||||
* @param $handlers
|
||||
* @param bool $overwrite
|
||||
* @return string[]|StreamQueryFilter[]
|
||||
* @throws InvalidConfigException
|
||||
* @see self::addFilterHandler
|
||||
*/
|
||||
public function addFilterHandlers($handlers, $overwrite = true)
|
||||
{
|
||||
@ -634,7 +680,7 @@ class StreamQuery extends Model
|
||||
$result = [];
|
||||
$handlerToRemoveClass = is_string($handlerToRemove) ? $handlerToRemove : get_class($handlerToRemove);
|
||||
foreach ($this->filterHandlers as $handler) {
|
||||
if(!is_a($handler, $handlerToRemoveClass, true)) {
|
||||
if (!is_a($handler, $handlerToRemoveClass, true)) {
|
||||
$result[] = $handler;
|
||||
}
|
||||
}
|
||||
@ -650,11 +696,11 @@ class StreamQuery extends Model
|
||||
* @throws InvalidConfigException
|
||||
* @since 1.6
|
||||
*/
|
||||
public function getFilterHandler($handlerToRemove)
|
||||
public function getFilterHandler($class): ?StreamQueryFilter
|
||||
{
|
||||
$handlerToRemoveClass = is_string($handlerToRemove) ? $handlerToRemove : get_class($handlerToRemove);
|
||||
$handlerToRemoveClass = is_string($class) ? $class : get_class($class);
|
||||
foreach ($this->filterHandlers as $handler) {
|
||||
if(is_a($handler, $handlerToRemoveClass, true)) {
|
||||
if (is_a($handler, $handlerToRemoveClass, true)) {
|
||||
return $this->prepareHandler($handler);
|
||||
}
|
||||
}
|
||||
@ -668,7 +714,7 @@ class StreamQuery extends Model
|
||||
* @throws InvalidConfigException
|
||||
* @since 1.6
|
||||
*/
|
||||
private function prepareHandler($handler)
|
||||
private function prepareHandler(&$handler)
|
||||
{
|
||||
if (is_string($handler)) {
|
||||
$handler = Yii::createObject([
|
||||
|
@ -139,7 +139,7 @@ class StreamSuppressQuery extends StreamQuery
|
||||
$this->_query->limit = $originalLimit;
|
||||
$this->isQueryExecuted = true;
|
||||
|
||||
return $results;
|
||||
return $this->postProcessAll($results);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace humhub\modules\stream\models\filters;
|
||||
|
||||
use humhub\modules\activity\stream\ActivityStreamQuery;
|
||||
use humhub\modules\content\models\Content;
|
||||
use humhub\modules\stream\models\StreamQuery;
|
||||
use Yii;
|
||||
|
||||
/**
|
||||
* @since 1.14
|
||||
*/
|
||||
class DraftContentStreamFilter extends StreamQueryFilter
|
||||
{
|
||||
/**
|
||||
* @var Content[]
|
||||
*/
|
||||
private array $draftContent = [];
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function apply()
|
||||
{
|
||||
if ($this->streamQuery instanceof ActivityStreamQuery && $this->streamQuery->activity) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->streamQuery->isInitialQuery()) {
|
||||
$this->fetchDraftContent();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
private function fetchDraftContent(): void
|
||||
{
|
||||
$draftQuery = clone $this->query;
|
||||
$draftQuery->andWhere([
|
||||
'AND', ['content.state' => Content::STATE_DRAFT],
|
||||
['content.created_by' => Yii::$app->user->id]]
|
||||
);
|
||||
$draftQuery->limit(100);
|
||||
$this->draftContent = $draftQuery->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function postProcessStreamResult(array &$results): void
|
||||
{
|
||||
$results = array_merge($this->draftContent, $results);
|
||||
}
|
||||
|
||||
}
|
@ -33,7 +33,7 @@ class PinnedContentStreamFilter extends StreamQueryFilter
|
||||
/**
|
||||
* @var Content[]
|
||||
*/
|
||||
public $pinnedContent = [];
|
||||
private $pinnedContent = [];
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
@ -41,34 +41,44 @@ class PinnedContentStreamFilter extends StreamQueryFilter
|
||||
public function apply()
|
||||
{
|
||||
// Currently we only support pinned entries on container streams
|
||||
if(!$this->container) {
|
||||
if (!$this->container) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->streamQuery->isInitialQuery()) {
|
||||
$pinnedContentIds = $this->fetchPinnedContent();
|
||||
if ($this->streamQuery->isInitialQuery()) {
|
||||
$pinnedContentIds = $this->fetchPinnedContent();
|
||||
|
||||
// Exclude pinned content from result, we've already fetched and cached them
|
||||
if(!empty($pinnedContentIds)) {
|
||||
// Exclude pinned content from result, we've already fetched and cached them
|
||||
if (!empty($pinnedContentIds)) {
|
||||
$this->query->andWhere((['NOT IN', 'content.id', $pinnedContentIds]));
|
||||
}
|
||||
} else if(!$this->streamQuery->isSingleContentQuery()) {
|
||||
} else if (!$this->streamQuery->isSingleContentQuery()) {
|
||||
// All pinned entries of this container were loaded within the initial request, so don't include them here!
|
||||
$this->query->andWhere(['OR', ['content.pinned' => 0], ['<>', 'content.contentcontainer_id', $this->container->contentcontainer_id]]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function postProcessStreamResult(array &$results): void
|
||||
{
|
||||
$results = array_merge($this->pinnedContent, $results);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads pinned content entries into [[pinnedContent]] by means of a cloned stream query.
|
||||
* @return array array of pinned content ids
|
||||
*/
|
||||
private function fetchPinnedContent()
|
||||
private function fetchPinnedContent(): array
|
||||
{
|
||||
$pinnedQuery = clone $this->query;
|
||||
$pinnedQuery->andWhere(['AND', ['content.pinned' => 1], ['content.contentcontainer_id' => $this->container->contentcontainer_id]]);
|
||||
$pinnedQuery->andWhere(['AND', [
|
||||
'content.pinned' => 1],
|
||||
['content.contentcontainer_id' => $this->container->contentcontainer_id]]);
|
||||
$pinnedQuery->limit(1000);
|
||||
$this->pinnedContent = $pinnedQuery->all();
|
||||
return array_map(function($content) {
|
||||
return array_map(function ($content) {
|
||||
return $content->id;
|
||||
}, $this->pinnedContent);
|
||||
}
|
||||
|
@ -9,6 +9,7 @@
|
||||
namespace humhub\modules\stream\models\filters;
|
||||
|
||||
|
||||
use humhub\modules\content\models\Content;
|
||||
use humhub\modules\stream\models\ContentContainerStreamQuery;
|
||||
use humhub\modules\stream\models\StreamQuery;
|
||||
use humhub\modules\ui\filter\models\QueryFilter;
|
||||
@ -16,7 +17,7 @@ use humhub\modules\ui\filter\models\QueryFilter;
|
||||
abstract class StreamQueryFilter extends QueryFilter
|
||||
{
|
||||
/**
|
||||
* @var StreamQuery | ContentContainerStreamQuery
|
||||
* @var StreamQuery|ContentContainerStreamQuery
|
||||
*/
|
||||
public $streamQuery;
|
||||
|
||||
@ -45,4 +46,14 @@ abstract class StreamQueryFilter extends QueryFilter
|
||||
return $this->formName ?: 'StreamQuery';
|
||||
}
|
||||
|
||||
/**
|
||||
* This method allows the stream filter direct access to returned Content[] array.
|
||||
* e.g. additional entries can be injected
|
||||
*
|
||||
* @param Content[] $results
|
||||
*/
|
||||
public function postProcessStreamResult(array &$results): void
|
||||
{
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -322,6 +322,22 @@ humhub.module('stream.StreamEntry', function (module, require, $) {
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Publish draft of this entry from the top of the stream.
|
||||
* @param evt
|
||||
*/
|
||||
StreamEntry.prototype.publishDraft = function (evt) {
|
||||
var that = this;
|
||||
this.loader();
|
||||
client.post(evt.url).then(function (data) {
|
||||
that.stream().init();
|
||||
module.log.info(data.message, true);
|
||||
}).catch(function (e) {
|
||||
module.log.error(e, true);
|
||||
that.loader(false);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Replaces this entries dom element.
|
||||
|
@ -10,6 +10,7 @@
|
||||
namespace humhub\modules\topic\widgets;
|
||||
|
||||
use humhub\modules\content\components\ContentActiveRecord;
|
||||
use humhub\modules\content\models\Content;
|
||||
use humhub\widgets\ModalButton;
|
||||
use humhub\modules\topic\models\Topic;
|
||||
use humhub\modules\content\widgets\WallEntryControlLink;
|
||||
@ -26,6 +27,10 @@ class ContentTopicButton extends WallEntryControlLink
|
||||
|
||||
public function renderLink()
|
||||
{
|
||||
if ($this->record->content->state === Content::STATE_DELETED) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return ModalButton::asLink(Yii::t('TopicModule.base', 'Topics'))->icon(Topic::getIcon())
|
||||
->load(Url::to(['/topic/content-topic', 'contentId' => $this->record->content->id]));
|
||||
}
|
||||
|
@ -62,6 +62,7 @@ class LayoutHeader extends Widget
|
||||
navigator.serviceWorker.register('$serviceWorkUrl', { scope: '$rootPath' })
|
||||
.then(function (registration) {
|
||||
if (typeof afterServiceWorkerRegistration === "function") {
|
||||
// Allow Modules like `fcm-push` to register after registration
|
||||
afterServiceWorkerRegistration(registration);
|
||||
}
|
||||
})
|
||||
|
@ -1,195 +1,25 @@
|
||||
// Content create form
|
||||
.contentForm_options {
|
||||
margin-top: 10px;
|
||||
min-height: 29px;
|
||||
|
||||
.btn_container {
|
||||
position: relative;
|
||||
|
||||
.label-public {
|
||||
position: absolute;
|
||||
right: 40px;
|
||||
top: 11px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#content-topic-bar {
|
||||
margin-top: 5px;
|
||||
text-align: right;
|
||||
|
||||
.label {
|
||||
margin-left: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
#contentFormBody {
|
||||
.form-group, .help-block-error {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Empty stream info
|
||||
.placeholder-empty-stream {
|
||||
background-image: url("../img/placeholder-postform-arrow.png");
|
||||
background-repeat: no-repeat;
|
||||
padding: 37px 0 0 70px;
|
||||
margin-left: 90px;
|
||||
}
|
||||
|
||||
#streamUpdateBadge {
|
||||
text-align: center;
|
||||
z-index: 9999;
|
||||
margin-bottom: 15px;
|
||||
margin-top: 15px;
|
||||
|
||||
.label {
|
||||
border-radius: 10px;
|
||||
font-size: 0.8em !important;
|
||||
padding: 5px 10px;
|
||||
}
|
||||
}
|
||||
|
||||
#wallStream {
|
||||
.back_button_holder {
|
||||
padding-bottom: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
@wallEntryInnerPadding: 10px;
|
||||
@wallEntryContentLeftPadding: 50px;
|
||||
@wallEntryContentLeftPaddingMobile: 0;
|
||||
|
||||
// Wall-Entries
|
||||
.wall-entry {
|
||||
position: relative;
|
||||
|
||||
.panel .panel-body {
|
||||
padding: @wallEntryInnerPadding;
|
||||
}
|
||||
|
||||
.wall-entry-header {
|
||||
//
|
||||
// popover
|
||||
// --------------------------------------------------
|
||||
.popover {
|
||||
border: 1px solid rgba(0, 0, 0, 0.15);
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
|
||||
|
||||
.popover-title {
|
||||
background: none;
|
||||
border-bottom: none;
|
||||
color: @text-color-highlight;
|
||||
position: relative;
|
||||
padding-bottom: @wallEntryInnerPadding;
|
||||
margin-bottom: @wallEntryInnerPadding;
|
||||
border-bottom: 1px solid #eeeeee;
|
||||
|
||||
.img-space {
|
||||
top: 25px;
|
||||
left: 25px;
|
||||
}
|
||||
|
||||
.wall-entry-container-link {
|
||||
color: @link;
|
||||
}
|
||||
|
||||
.stream-entry-icon-list {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 25px;
|
||||
display: inline-block;
|
||||
padding-top: 2px;
|
||||
|
||||
i {
|
||||
padding-right: 5px;
|
||||
}
|
||||
|
||||
.icon-pin {
|
||||
color: @danger;
|
||||
}
|
||||
|
||||
.fa-archive {
|
||||
color: @warning;
|
||||
}
|
||||
}
|
||||
|
||||
.wall-entry-header-image {
|
||||
display: table-cell;
|
||||
width: 40px;
|
||||
padding-right: @wallEntryInnerPadding;
|
||||
|
||||
.fa {
|
||||
font-size: 2.39em;
|
||||
color: @info;
|
||||
margin-top: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.wall-entry-header-info {
|
||||
display: table-cell;
|
||||
padding-right: 30px;
|
||||
width: 100%;
|
||||
|
||||
.media-heading {
|
||||
font-size: 15px;
|
||||
padding-top: 1px;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
||||
i.archived {
|
||||
color: @warning;
|
||||
}
|
||||
}
|
||||
|
||||
.preferences {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.media-subheading {
|
||||
i.fa-caret-right {
|
||||
margin: 0 2px;
|
||||
}
|
||||
|
||||
.wall-entry-icons {
|
||||
display: inline-block;
|
||||
|
||||
i {
|
||||
margin-right: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.time, i, span {
|
||||
font-size: 11px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
}
|
||||
font-weight: 300;
|
||||
font-size: 16px;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.wall-entry-body {
|
||||
padding-left: @wallEntryContentLeftPadding;
|
||||
padding-right: @wallEntryContentLeftPadding;
|
||||
.popover-content {
|
||||
font-size: 13px;
|
||||
padding: 5px 15px;
|
||||
color: @text-color-highlight;
|
||||
|
||||
.wall-entry-content {
|
||||
margin-bottom: 5px;
|
||||
|
||||
.post-short-text {
|
||||
font-size: 1.6em;
|
||||
|
||||
.emoji {
|
||||
width: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
audio, video {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.wall-stream-footer {
|
||||
.wall-stream-addons {
|
||||
.files {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
a {
|
||||
color: @link;
|
||||
}
|
||||
@ -199,273 +29,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
.media {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.well {
|
||||
margin-bottom: 0;
|
||||
|
||||
.comment {
|
||||
.show-all-link {
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.media-heading {
|
||||
font-size: 14px;
|
||||
padding-top: 1px;
|
||||
margin-bottom: 3px;
|
||||
|
||||
.labels {
|
||||
padding-right: 32px;
|
||||
}
|
||||
|
||||
.viaLink {
|
||||
font-size: 13px;
|
||||
|
||||
i {
|
||||
color: @text-color-soft;
|
||||
padding-left: 4px;
|
||||
padding-right: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.media-subheading {
|
||||
color: @text-color-soft;
|
||||
font-size: 12px;
|
||||
|
||||
.time {
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
[data-ui-richtext] {
|
||||
h1 {
|
||||
font-size: 1.45em;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.3em;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.2em;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 1.1em;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: 1.0em;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
h6 {
|
||||
font-size: .85em;
|
||||
font-weight: normal;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.wall-entry .wall-entry-body {
|
||||
padding-left: @wallEntryContentLeftPaddingMobile;
|
||||
padding-right: @wallEntryContentLeftPaddingMobile;
|
||||
}
|
||||
|
||||
#wallStream {
|
||||
.back_button_holder {
|
||||
padding-bottom: 5px;
|
||||
text-align:center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.wall-entry-controls a {
|
||||
font-size: 11px;
|
||||
color: @link !important;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
#wall-stream-filter-nav {
|
||||
font-size: 12px;
|
||||
margin-bottom: 10px;
|
||||
padding-top: 2px;
|
||||
border-radius: 0 0 4px 4px;
|
||||
|
||||
.wall-stream-filter-root {
|
||||
margin: 0;
|
||||
border: 0 !important;
|
||||
}
|
||||
|
||||
.filter-panel {
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.wall-stream-filter-head {
|
||||
padding: 5px 5px 10px 5px;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.wall-stream-filter-body {
|
||||
overflow: hidden;
|
||||
background-color: @background-color-secondary;
|
||||
border: 1px solid #ddd;
|
||||
border-top: 0;
|
||||
border-radius: 0 0 4px 4px;
|
||||
}
|
||||
|
||||
hr {
|
||||
margin: 5px 0 0 0;
|
||||
}
|
||||
|
||||
.topic-remove-label {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.topic-remove-label, .content-type-remove-label {
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
.select2 {
|
||||
width: 260px !important;
|
||||
margin-bottom: 5px;
|
||||
margin-top: 2px;
|
||||
|
||||
.select2-search__field {
|
||||
height: 25px !important;
|
||||
}
|
||||
|
||||
.select2-selection__choice {
|
||||
height: 23px !important;
|
||||
|
||||
span, i {
|
||||
line-height: 19px !important;
|
||||
}
|
||||
|
||||
.img-rounded {
|
||||
width: 18px !important;
|
||||
height: 18px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.wall-stream-filter-bar {
|
||||
display: inline;
|
||||
float: right;
|
||||
white-space: normal;
|
||||
|
||||
.label {
|
||||
height: 18px;
|
||||
padding-top: 4px;
|
||||
background-color: @background-color-main;
|
||||
}
|
||||
|
||||
.btn, .label {
|
||||
box-shadow: 0 0 2px @text-color-secondary;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
#wall-stream-filter-nav {
|
||||
.wall-stream-filter-root {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.wall-stream-filter-body {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-root {
|
||||
margin: 15px;
|
||||
|
||||
.row {
|
||||
display: table !important;
|
||||
}
|
||||
|
||||
.filter-panel {
|
||||
padding: 0 5px;
|
||||
display: table-cell !important;
|
||||
float: none;
|
||||
|
||||
.filter-block {
|
||||
strong {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
ul.filter-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0 0 5px;
|
||||
|
||||
li {
|
||||
font-size: 12px;
|
||||
padding: 2px;
|
||||
|
||||
a {
|
||||
color: @text-color-highlight;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
div.filter-block:last-of-type {
|
||||
ul.filter-list {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.filter-panel + .filter-panel {
|
||||
border-left: 2px solid @background-color-page;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.stream-entry-loader {
|
||||
float: right;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.load-suppressed {
|
||||
margin-top: -17px;
|
||||
margin-bottom: 15px;
|
||||
text-align: center;
|
||||
|
||||
a {
|
||||
display: inline-block;
|
||||
background-color: white;
|
||||
padding: 5px;
|
||||
border-radius: 0 0 4px 4px;
|
||||
border: 1px solid #ddd;
|
||||
font-size: 11px
|
||||
}
|
||||
}
|
||||
|
||||
@media print {
|
||||
.wall-entry {
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
|
||||
#wall-stream-filter-nav,
|
||||
#contentFormBody {
|
||||
display: none;
|
||||
.popover-navigation {
|
||||
padding: 15px;
|
||||
}
|
||||
}
|
||||
|
@ -6,7 +6,7 @@
|
||||
.btn_container {
|
||||
position: relative;
|
||||
|
||||
.label-public {
|
||||
.label-container {
|
||||
position: absolute;
|
||||
right: 40px;
|
||||
top: 11px;
|
||||
|
File diff suppressed because one or more lines are too long
Loading…
x
Reference in New Issue
Block a user