mirror of
https://github.com/flarum/framework.git
synced 2024-12-02 06:53:47 +08:00
Enforce min/max tag count settings
This commit is contained in:
parent
92e9b5b414
commit
a042f04fa3
|
@ -3,6 +3,7 @@ import DiscussionPage from 'flarum/components/DiscussionPage';
|
||||||
import Button from 'flarum/components/Button';
|
import Button from 'flarum/components/Button';
|
||||||
import highlight from 'flarum/helpers/highlight';
|
import highlight from 'flarum/helpers/highlight';
|
||||||
import classList from 'flarum/utils/classList';
|
import classList from 'flarum/utils/classList';
|
||||||
|
import extractText from 'flarum/utils/extractText';
|
||||||
|
|
||||||
import tagLabel from 'tags/helpers/tagLabel';
|
import tagLabel from 'tags/helpers/tagLabel';
|
||||||
import tagIcon from 'tags/helpers/tagIcon';
|
import tagIcon from 'tags/helpers/tagIcon';
|
||||||
|
@ -24,6 +25,19 @@ export default class TagDiscussionModal extends Modal {
|
||||||
} else if (this.props.discussion) {
|
} else if (this.props.discussion) {
|
||||||
this.props.discussion.tags().map(this.addTag.bind(this));
|
this.props.discussion.tags().map(this.addTag.bind(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.minPrimary = app.forum.attribute('minPrimaryTags');
|
||||||
|
this.maxPrimary = app.forum.attribute('maxPrimaryTags');
|
||||||
|
this.minSecondary = app.forum.attribute('minSecondaryTags');
|
||||||
|
this.maxSecondary = app.forum.attribute('maxSecondaryTags');
|
||||||
|
}
|
||||||
|
|
||||||
|
primaryCount() {
|
||||||
|
return this.selected.filter(tag => tag.isPrimary()).length;
|
||||||
|
}
|
||||||
|
|
||||||
|
secondaryCount() {
|
||||||
|
return this.selected.filter(tag => !tag.isPrimary()).length;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -75,17 +89,46 @@ export default class TagDiscussionModal extends Modal {
|
||||||
: app.trans('tags.tag_new_discussion_title');
|
: app.trans('tags.tag_new_discussion_title');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getInstruction(primaryCount, secondaryCount) {
|
||||||
|
if (primaryCount < this.minPrimary) {
|
||||||
|
return app.trans('tags.choose_primary_tags', {count: this.minPrimary - primaryCount});
|
||||||
|
} else if (secondaryCount < this.minSecondary) {
|
||||||
|
return app.trans('tags.choose_secondary_tags', {count: this.minSecondary - secondaryCount});
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
content() {
|
content() {
|
||||||
let tags = this.tags;
|
let tags = this.tags;
|
||||||
const filter = this.filter().toLowerCase();
|
const filter = this.filter().toLowerCase();
|
||||||
|
const primaryCount = this.primaryCount();
|
||||||
|
const secondaryCount = this.secondaryCount();
|
||||||
|
|
||||||
|
// Filter out all child tags whose parents have not been selected. This
|
||||||
|
// makes it impossible to select a child if its parent hasn't been selected.
|
||||||
|
tags = tags.filter(tag => {
|
||||||
|
const parent = tag.parent();
|
||||||
|
return parent === false || this.selected.indexOf(parent) !== -1;
|
||||||
|
});
|
||||||
|
|
||||||
|
// If the number of selected primary/secondary tags is at the maximum, then
|
||||||
|
// we'll filter out all other tags of that type.
|
||||||
|
if (primaryCount >= app.forum.attribute('maxPrimaryTags')) {
|
||||||
|
tags = tags.filter(tag => !tag.isPrimary() || this.selected.indexOf(tag) !== -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (secondaryCount >= app.forum.attribute('maxSecondaryTags')) {
|
||||||
|
tags = tags.filter(tag => tag.isPrimary() || this.selected.indexOf(tag) !== -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the user has entered text in the filter input, then filter by tags
|
||||||
|
// whose name matches what they've entered.
|
||||||
if (filter) {
|
if (filter) {
|
||||||
tags = tags.filter(tag => tag.name().substr(0, filter.length).toLowerCase() === filter);
|
tags = tags.filter(tag => tag.name().substr(0, filter.length).toLowerCase() === filter);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tags.indexOf(this.index) === -1) {
|
if (tags.indexOf(this.index) === -1) this.index = tags[0];
|
||||||
this.index = tags[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
<div className="Modal-body">
|
<div className="Modal-body">
|
||||||
|
@ -103,7 +146,7 @@ export default class TagDiscussionModal extends Modal {
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
<input className="FormControl"
|
<input className="FormControl"
|
||||||
placeholder={!this.selected.length ? app.trans('tags.discussion_tags_placeholder') : ''}
|
placeholder={extractText(this.getInstruction(primaryCount, secondaryCount))}
|
||||||
value={this.filter()}
|
value={this.filter()}
|
||||||
oninput={m.withAttr('value', this.filter)}
|
oninput={m.withAttr('value', this.filter)}
|
||||||
onkeydown={this.onkeydown.bind(this)}
|
onkeydown={this.onkeydown.bind(this)}
|
||||||
|
@ -115,7 +158,7 @@ export default class TagDiscussionModal extends Modal {
|
||||||
{Button.component({
|
{Button.component({
|
||||||
type: 'submit',
|
type: 'submit',
|
||||||
className: 'Button Button--primary',
|
className: 'Button Button--primary',
|
||||||
disabled: !this.selected.length,
|
disabled: primaryCount < this.minPrimary || secondaryCount < this.minSecondary,
|
||||||
icon: 'check',
|
icon: 'check',
|
||||||
children: app.trans('tags.confirm')
|
children: app.trans('tags.confirm')
|
||||||
})}
|
})}
|
||||||
|
@ -125,12 +168,9 @@ export default class TagDiscussionModal extends Modal {
|
||||||
|
|
||||||
<div className="Modal-footer">
|
<div className="Modal-footer">
|
||||||
<ul className="TagDiscussionModal-list SelectTagList">
|
<ul className="TagDiscussionModal-list SelectTagList">
|
||||||
{tags.map(tag => {
|
{tags
|
||||||
if (!filter && tag.parent() && this.selected.indexOf(tag.parent()) === -1) {
|
.filter(tag => filter || !tag.parent() || this.selected.indexOf(tag.parent()) !== -1)
|
||||||
return '';
|
.map(tag => (
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<li data-index={tag.id()}
|
<li data-index={tag.id()}
|
||||||
className={classList({
|
className={classList({
|
||||||
pinned: tag.position() !== null,
|
pinned: tag.position() !== null,
|
||||||
|
@ -154,8 +194,7 @@ export default class TagDiscussionModal extends Modal {
|
||||||
</span>
|
</span>
|
||||||
) : ''}
|
) : ''}
|
||||||
</li>
|
</li>
|
||||||
);
|
))}
|
||||||
})}
|
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
];
|
];
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import Model from 'flarum/Model';
|
import Model from 'flarum/Model';
|
||||||
import mixin from 'flarum/utils/mixin';
|
import mixin from 'flarum/utils/mixin';
|
||||||
|
import computed from 'flarum/utils/computed';
|
||||||
|
|
||||||
export default class Tag extends mixin(Model, {
|
export default class Tag extends mixin(Model, {
|
||||||
name: Model.attribute('name'),
|
name: Model.attribute('name'),
|
||||||
|
@ -21,5 +22,7 @@ export default class Tag extends mixin(Model, {
|
||||||
lastDiscussion: Model.hasOne('lastDiscussion'),
|
lastDiscussion: Model.hasOne('lastDiscussion'),
|
||||||
|
|
||||||
isRestricted: Model.attribute('isRestricted'),
|
isRestricted: Model.attribute('isRestricted'),
|
||||||
canStartDiscussion: Model.attribute('canStartDiscussion')
|
canStartDiscussion: Model.attribute('canStartDiscussion'),
|
||||||
|
|
||||||
|
isPrimary: computed('position', 'parent', (position, parent) => position !== null && parent === false)
|
||||||
}) {}
|
}) {}
|
||||||
|
|
|
@ -58,8 +58,12 @@
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
.TagsInput-selected {
|
.TagsInput-selected {
|
||||||
.TagLabel {
|
.TagsInput-tag {
|
||||||
margin-right: 5px;
|
margin-right: 5px;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,12 @@ tags:
|
||||||
tag_new_discussion_title: Choose Tags for Your Discussion
|
tag_new_discussion_title: Choose Tags for Your Discussion
|
||||||
edit_discussion_tags_title: "Edit Tags for {title}"
|
edit_discussion_tags_title: "Edit Tags for {title}"
|
||||||
edit_discussion_tags_link: Edit Tags
|
edit_discussion_tags_link: Edit Tags
|
||||||
discussion_tags_placeholder: Choose one or more topics
|
choose_primary_tags:
|
||||||
|
one: Choose a primary tag
|
||||||
|
other: "Choose {count} primary tags"
|
||||||
|
choose_secondary_tags:
|
||||||
|
one: Choose 1 more tag
|
||||||
|
other: "Choose {count} more tags"
|
||||||
confirm: Confirm
|
confirm: Confirm
|
||||||
more: More...
|
more: More...
|
||||||
tag_cloud_title: Tags
|
tag_cloud_title: Tags
|
||||||
|
|
|
@ -70,6 +70,15 @@ class AddApiAttributes
|
||||||
if ($event->serializer instanceof DiscussionSerializer) {
|
if ($event->serializer instanceof DiscussionSerializer) {
|
||||||
$event->attributes['canTag'] = $event->model->can($event->actor, 'tag');
|
$event->attributes['canTag'] = $event->model->can($event->actor, 'tag');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($event->serializer instanceof ForumSerializer) {
|
||||||
|
$settings = app('Flarum\Core\Settings\SettingsRepository');
|
||||||
|
|
||||||
|
$event->attributes['minPrimaryTags'] = $settings->get('tags.min_primary_tags');
|
||||||
|
$event->attributes['maxPrimaryTags'] = $settings->get('tags.max_primary_tags');
|
||||||
|
$event->attributes['minSecondaryTags'] = $settings->get('tags.min_secondary_tags');
|
||||||
|
$event->attributes['maxSecondaryTags'] = $settings->get('tags.max_secondary_tags');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function addRoutes(RegisterApiRoutes $event)
|
public function addRoutes(RegisterApiRoutes $event)
|
||||||
|
|
|
@ -35,7 +35,8 @@ class AddClientAssets
|
||||||
'tags.tag_new_discussion_title',
|
'tags.tag_new_discussion_title',
|
||||||
'tags.edit_discussion_tags_title',
|
'tags.edit_discussion_tags_title',
|
||||||
'tags.edit_discussion_tags_link',
|
'tags.edit_discussion_tags_link',
|
||||||
'tags.discussion_tags_placeholder',
|
'tags.choose_primary_tags',
|
||||||
|
'tags.choose_secondary_tags',
|
||||||
'tags.confirm',
|
'tags.confirm',
|
||||||
'tags.more',
|
'tags.more',
|
||||||
'tags.tag_cloud_title',
|
'tags.tag_cloud_title',
|
||||||
|
|
|
@ -5,9 +5,18 @@ use Flarum\Tags\Events\DiscussionWasTagged;
|
||||||
use Flarum\Events\DiscussionWillBeSaved;
|
use Flarum\Events\DiscussionWillBeSaved;
|
||||||
use Flarum\Core\Discussions\Discussion;
|
use Flarum\Core\Discussions\Discussion;
|
||||||
use Flarum\Core\Exceptions\PermissionDeniedException;
|
use Flarum\Core\Exceptions\PermissionDeniedException;
|
||||||
|
use Flarum\Core\Settings\SettingsRepository;
|
||||||
|
use Flarum\Tags\TagCountException;
|
||||||
|
|
||||||
class PersistData
|
class PersistData
|
||||||
{
|
{
|
||||||
|
protected $settings;
|
||||||
|
|
||||||
|
public function __construct(SettingsRepository $settings)
|
||||||
|
{
|
||||||
|
$this->settings = $settings;
|
||||||
|
}
|
||||||
|
|
||||||
public function subscribe($events)
|
public function subscribe($events)
|
||||||
{
|
{
|
||||||
$events->listen(DiscussionWillBeSaved::class, [$this, 'whenDiscussionWillBeSaved']);
|
$events->listen(DiscussionWillBeSaved::class, [$this, 'whenDiscussionWillBeSaved']);
|
||||||
|
@ -25,13 +34,25 @@ class PersistData
|
||||||
$newTagIds[] = (int) $link['id'];
|
$newTagIds[] = (int) $link['id'];
|
||||||
}
|
}
|
||||||
|
|
||||||
$newTags = Tag::whereIn('id', $newTagIds);
|
$newTags = Tag::whereIn('id', $newTagIds)->get();
|
||||||
|
$primaryCount = 0;
|
||||||
|
$secondaryCount = 0;
|
||||||
|
|
||||||
foreach ($newTags as $tag) {
|
foreach ($newTags as $tag) {
|
||||||
if (! $tag->can($actor, 'startDiscussion')) {
|
if (! $tag->can($actor, 'startDiscussion')) {
|
||||||
throw new PermissionDeniedException;
|
throw new PermissionDeniedException;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($tag->position !== null && $tag->parent_id === null) {
|
||||||
|
$primaryCount++;
|
||||||
|
} else {
|
||||||
|
$secondaryCount++;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$this->validatePrimaryTagCount($primaryCount);
|
||||||
|
$this->validateSecondaryTagCount($secondaryCount);
|
||||||
|
|
||||||
$oldTags = [];
|
$oldTags = [];
|
||||||
|
|
||||||
if ($discussion->exists) {
|
if ($discussion->exists) {
|
||||||
|
@ -50,4 +71,24 @@ class PersistData
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function validatePrimaryTagCount($count)
|
||||||
|
{
|
||||||
|
$min = $this->settings->get('tags.min_primary_tags');
|
||||||
|
$max = $this->settings->get('tags.max_primary_tags');
|
||||||
|
|
||||||
|
if ($count < $min || $count > $max) {
|
||||||
|
throw new TagCountException(['tags' => sprintf('Discussion must have between %d and %d primary tags.', $min, $max)]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function validateSecondaryTagCount($count)
|
||||||
|
{
|
||||||
|
$min = $this->settings->get('tags.min_secondary_tags');
|
||||||
|
$max = $this->settings->get('tags.max_secondary_tags');
|
||||||
|
|
||||||
|
if ($count < $min || $count > $max) {
|
||||||
|
throw new TagCountException(['tags' => sprintf('Discussion must have between %d and %d secondary tags.', $min, $max)]);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
7
extensions/tags/src/TagCountException.php
Normal file
7
extensions/tags/src/TagCountException.php
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
<?php namespace Flarum\Tags;
|
||||||
|
|
||||||
|
use Flarum\Core\Exceptions\ValidationException;
|
||||||
|
|
||||||
|
class TagCountException extends ValidationException
|
||||||
|
{
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user