[CONTROLLER][UserPanel][COMPONENT][Tag] Re-add way of adding self tags, but in a more reusable (and less buggy) way
This commit is contained in:
parent
072caad845
commit
95783d6109
@ -6,8 +6,17 @@ namespace Component\Tag\Controller;
|
||||
|
||||
use App\Core\Cache;
|
||||
use App\Core\Controller;
|
||||
use App\Core\DB\DB;
|
||||
use function App\Core\I18n\_m;
|
||||
use App\Entity as E;
|
||||
use App\Util\Common;
|
||||
use App\Util\Exception\ClientException;
|
||||
use App\Util\Exception\RedirectException;
|
||||
use App\Util\Formatting;
|
||||
use Component\Tag\Form\SelfTagsForm;
|
||||
use Component\Tag\Tag as CompTag;
|
||||
use Symfony\Component\Form\SubmitButton;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
class Tag extends Controller
|
||||
{
|
||||
@ -76,4 +85,90 @@ class Tag extends Controller
|
||||
template: 'actor_tag_feed.html.twig',
|
||||
);
|
||||
}
|
||||
|
||||
public static function settingsSelfTags(Request $request, E\Actor $target, string $details_id)
|
||||
{
|
||||
$actor = Common::actor();
|
||||
if (!$actor->canAdmin($target)) {
|
||||
throw new ClientException(_m('You don\'t have enough permissions to edit {nickname}\'s settings', ['{nickname}' => $target->getNickname()]));
|
||||
}
|
||||
|
||||
$actor_tags = $target->getSelfTags();
|
||||
|
||||
[$add_form, $existing_form] = SelfTagsForm::handleTags(
|
||||
$request,
|
||||
$actor_tags,
|
||||
handle_new: function ($form) use ($request, $target, $details_id) {
|
||||
$data = $form->getData();
|
||||
$tags = $data['new-tags'];
|
||||
$language = $target->getTopLanguage()->getLocale();
|
||||
foreach ($tags as $tag) {
|
||||
$tag = CompTag::ensureValid($tag);
|
||||
[$at, ] = E\ActorTag::createOrUpdate([
|
||||
'tagger' => $target->getId(),
|
||||
'tagged' => $target->getId(),
|
||||
'tag' => $tag,
|
||||
'canonical' => CompTag::canonicalTag($tag, language: $language),
|
||||
'use_canonical' => $data['new-tags-use-canon'],
|
||||
]);
|
||||
DB::persist($at);
|
||||
}
|
||||
DB::flush();
|
||||
Cache::delete(E\Actor::cacheKeys($target->getId(), $target->getId())['tags']);
|
||||
throw new RedirectException($request->get('_route'), ['open' => $details_id]);
|
||||
},
|
||||
handle_existing: function ($form, array $form_definition) use ($request, $target, $details_id) {
|
||||
$data = $form->getData();
|
||||
$changed = false;
|
||||
foreach (array_chunk($form_definition, 3) as $entry) {
|
||||
$tag = Formatting::removePrefix($entry[0][2]['data'], '#');
|
||||
$use_canon = $entry[1][2]['attr']['data'];
|
||||
|
||||
/** @var SubmitButton $remove */
|
||||
$remove = $form->get($entry[2][0]);
|
||||
if ($remove->isClicked()) {
|
||||
$changed = true;
|
||||
DB::removeBy(
|
||||
'actor_tag',
|
||||
[
|
||||
'tagger' => $target->getId(),
|
||||
'tagged' => $target->getId(),
|
||||
'tag' => $tag,
|
||||
'use_canonical' => $use_canon,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/** @var SubmitButton $toggle_canon */
|
||||
$toggle_canon = $form->get($entry[1][0]);
|
||||
if ($toggle_canon->isSubmitted()) {
|
||||
$changed = true;
|
||||
$at = DB::find(
|
||||
'actor_tag',
|
||||
[
|
||||
'tagger' => $target->getId(),
|
||||
'tagged' => $target->getId(),
|
||||
'tag' => $tag,
|
||||
'use_canonical' => $use_canon,
|
||||
],
|
||||
);
|
||||
DB::persist($at->setUseCanonical(!$use_canon));
|
||||
}
|
||||
}
|
||||
if ($changed) {
|
||||
DB::flush();
|
||||
Cache::delete(E\Actor::cacheKeys($target->getId(), $target->getId())['tags']);
|
||||
throw new RedirectException($request->get('_route'), ['open' => $details_id]);
|
||||
}
|
||||
},
|
||||
remove_label: _m('Remove self tag'),
|
||||
add_label: _m('Add self tag'),
|
||||
);
|
||||
|
||||
return [
|
||||
'_template' => 'self_tags_settings.fragment.html.twig',
|
||||
'add_self_tags_form' => $add_form->createView(),
|
||||
'existing_self_tags_form' => $existing_form?->createView(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
63
components/Tag/Form/SelfTagsForm.php
Normal file
63
components/Tag/Form/SelfTagsForm.php
Normal file
@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace Component\Tag\Form;
|
||||
|
||||
use App\Core\Form;
|
||||
use function App\Core\I18n\_m;
|
||||
use App\Entity as E;
|
||||
use App\Util\Form\ArrayTransformer;
|
||||
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
abstract class SelfTagsForm
|
||||
{
|
||||
/**
|
||||
* @param E\ActorTag[]|E\ActorTagBlock[]|E\NoteTagBlock[] $tags
|
||||
*
|
||||
* @return array [Form (add), ?Form (existing)]
|
||||
*/
|
||||
public static function handleTags(
|
||||
Request $request,
|
||||
array $tags,
|
||||
callable $handle_new,
|
||||
callable $handle_existing,
|
||||
string $remove_label,
|
||||
string $add_label,
|
||||
): array {
|
||||
$form_definition = [];
|
||||
foreach ($tags as $tag) {
|
||||
$canon = $tag->getCanonical();
|
||||
$form_definition[] = ["{$canon}:old-tag", TextType::class, ['data' => '#' . $tag->getTag(), 'label' => ' ', 'disabled' => true]];
|
||||
$form_definition[] = ["{$canon}:toggle-canon", SubmitType::class, ['attr' => ['data' => $tag->getUseCanonical()], 'label' => $tag->getUseCanonical() ? _m('Set non-canonical') : _m('Set canonical')]];
|
||||
$form_definition[] = [$existing_form_name = "{$canon}:remove", SubmitType::class, ['label' => $remove_label]];
|
||||
}
|
||||
|
||||
$existing_form = !empty($form_definition) ? Form::create($form_definition) : null;
|
||||
|
||||
$add_form = Form::create([
|
||||
['new-tags', TextType::class, ['label' => ' ', 'data' => [], 'required' => false, 'help' => _m('Tags for yourself (letters, numbers, -, ., and _), comma- or space-separated.'), 'transformer' => ArrayTransformer::class]],
|
||||
['new-tags-use-canon', CheckboxType::class, ['label' => _m('Use canonical'), 'help' => _m('Assume this tag is the same as similar tags'), 'required' => false, 'data' => true]],
|
||||
[$add_form_name = 'new-tags-add', SubmitType::class, ['label' => $add_label]],
|
||||
]);
|
||||
|
||||
if ($request->getMethod() === 'POST' && $request->request->has($add_form_name)) {
|
||||
$add_form->handleRequest($request);
|
||||
if ($add_form->isSubmitted() && $add_form->isValid()) {
|
||||
$handle_new($add_form);
|
||||
}
|
||||
}
|
||||
|
||||
if (!\is_null($existing_form) && $request->getMethod() === 'POST' && $request->request->has($existing_form_name ?? '')) {
|
||||
$existing_form->handleRequest($request);
|
||||
if ($existing_form->isSubmitted() && $existing_form->isValid()) {
|
||||
$handle_existing($existing_form, $form_definition);
|
||||
}
|
||||
}
|
||||
|
||||
return [$add_form, $existing_form];
|
||||
}
|
||||
}
|
@ -34,9 +34,11 @@ use App\Entity\ActorTag;
|
||||
use App\Entity\Language;
|
||||
use App\Entity\Note;
|
||||
use App\Entity\NoteTag;
|
||||
use App\Util\Common;
|
||||
use App\Util\Exception\ClientException;
|
||||
use App\Util\Formatting;
|
||||
use App\Util\HTML;
|
||||
use Component\Tag\Controller as C;
|
||||
use Doctrine\Common\Collections\ExpressionBuilder;
|
||||
use Doctrine\ORM\Query\Expr;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
@ -122,7 +124,12 @@ class Tag extends Component
|
||||
|
||||
public static function ensureValid(string $tag)
|
||||
{
|
||||
return self::ensureLength(str_replace('#', '', $tag));
|
||||
$tag = self::ensureLength(Formatting::removePrefix($tag, '#'));
|
||||
if (preg_match(self::TAG_REGEX, '#' . $tag)) {
|
||||
return $tag;
|
||||
} else {
|
||||
throw new ClientException(_m('Invalid tag given: {tag}', ['{tag}' => $tag]));
|
||||
}
|
||||
}
|
||||
|
||||
public static function ensureLength(string $tag): string
|
||||
@ -192,4 +199,17 @@ class Tag extends Component
|
||||
$extra_args['tag_use_canonical'] = $data['tag_use_canonical'];
|
||||
return Event::next;
|
||||
}
|
||||
|
||||
public function onPopulateSettingsTabs(Request $request, string $section, array &$tabs)
|
||||
{
|
||||
if ($section === 'profile' && $request->get('_route') === 'settings') {
|
||||
$tabs[] = [
|
||||
'title' => 'Self tags',
|
||||
'desc' => 'Add or remove tags on yourself',
|
||||
'id' => 'settings-self-tags',
|
||||
'controller' => C\Tag::settingsSelfTags($request, Common::actor(), 'settings-self-tags-details'),
|
||||
];
|
||||
}
|
||||
return Event::next;
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,4 @@
|
||||
{% if existing_self_tags_form is defined and existing_self_tags_form is not null %}
|
||||
{{ form(existing_self_tags_form) }}
|
||||
{% endif %}
|
||||
{{ form(add_self_tags_form) }}
|
@ -36,7 +36,6 @@ use App\Util\Exception\NotFoundException;
|
||||
use App\Util\Formatting;
|
||||
use App\Util\Nickname;
|
||||
use Component\Avatar\Avatar;
|
||||
use Component\Tag\Tag as TagComponent;
|
||||
use DateTimeInterface;
|
||||
use Functional as F;
|
||||
|
||||
@ -251,8 +250,10 @@ class Actor extends Entity
|
||||
public const BUSINESS = 4;
|
||||
public const BOT = 5;
|
||||
|
||||
public static function cacheKeys(int $actor_id, mixed $other = null): array
|
||||
public static function cacheKeys(int|self $actor_id, mixed $other = null): array
|
||||
{
|
||||
$actor_id = \is_int($actor_id) ? $actor_id : $actor_id->getId();
|
||||
|
||||
return [
|
||||
'id' => "actor-id-{$actor_id}",
|
||||
'nickname' => "actor-nickname-id-{$actor_id}",
|
||||
@ -339,7 +340,7 @@ class Actor extends Entity
|
||||
/**
|
||||
* Tags attributed to self, shortcut function for increased legibility
|
||||
*
|
||||
* @return array<int, array> [ActorCircle[], ActorTag[]] resulting lists
|
||||
* @return ActorTag[] resulting lists
|
||||
*/
|
||||
public function getSelfTags(bool $_test_force_recompute = false): array
|
||||
{
|
||||
@ -356,7 +357,7 @@ class Actor extends Entity
|
||||
* @param null|int $offset Offset from latest
|
||||
* @param null|int $limit Max number to get
|
||||
*
|
||||
* @return array<int, array> [ActorCircle[], ActorTag[]] resulting lists
|
||||
* @return ActorTag[] resulting lists
|
||||
*/
|
||||
public function getOtherTags(self|int|null $context = null, ?int $offset = null, ?int $limit = null, bool $_test_force_recompute = false): array
|
||||
{
|
||||
@ -365,11 +366,8 @@ class Actor extends Entity
|
||||
self::cacheKeys($this->getId())['tags'],
|
||||
fn () => DB::dql(
|
||||
<<< 'EOQ'
|
||||
SELECT circle, tag
|
||||
SELECT tag
|
||||
FROM actor_tag tag
|
||||
JOIN actor_circle circle
|
||||
WITH tag.tagger = circle.tagger
|
||||
AND tag.tag = circle.tag
|
||||
WHERE tag.tagged = :id
|
||||
ORDER BY tag.modified DESC, tag.tagged DESC
|
||||
EOQ,
|
||||
@ -383,58 +381,18 @@ class Actor extends Entity
|
||||
self::cacheKeys($this->getId(), $context_id)['tags'],
|
||||
fn () => DB::dql(
|
||||
<<< 'EOQ'
|
||||
SELECT circle, tag
|
||||
SELECT tag
|
||||
FROM actor_tag tag
|
||||
JOIN actor_circle circle
|
||||
WITH tag.tagger = circle.tagger
|
||||
AND tag.tag = circle.tag
|
||||
WHERE
|
||||
tag.tagged = :id
|
||||
AND (circle.private != true
|
||||
OR (circle.tagger = :scoped
|
||||
AND circle.private = true
|
||||
)
|
||||
)
|
||||
WHERE tag.tagged = :tagged_id AND tag.tagger = :tagger_id
|
||||
ORDER BY tag.modified DESC, tag.tagged DESC
|
||||
EOQ,
|
||||
['id' => $this->getId(), 'scoped' => $context_id],
|
||||
['tagged_id' => $this->getId(), 'tagger_id' => $context_id],
|
||||
options: ['offset' => $offset, 'limit' => $limit],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $tags array of strings to become self tags
|
||||
* @param null|array $existing array of existing self tags (ActorTag[])
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setSelfTags(array $tags, ?array $existing = null): self
|
||||
{
|
||||
$tags = F\filter($tags, fn ($tag) => Nickname::isCanonical($tag)); // TODO: Have an actual #Tag test
|
||||
$tags = array_unique($tags);
|
||||
if (\is_null($existing)) {
|
||||
[$_, $existing] = $this->getSelfTags();
|
||||
}
|
||||
$existing_actor_tags = F\map($existing, fn ($actor_tag) => $actor_tag->getTag());
|
||||
$tags_to_add = array_diff($tags, $existing_actor_tags);
|
||||
$tags_to_remove = array_diff($existing_actor_tags, $tags);
|
||||
$actor_tags_to_remove = F\filter($existing, fn ($actor_tag) => \in_array($actor_tag->getTag(), $tags_to_remove));
|
||||
foreach ($tags_to_add as $tag) {
|
||||
$canonical_tag = TagComponent::canonicalTag($tag, $this->getTopLanguage()->getLocale());
|
||||
DB::persist(ActorCircle::create(['tagger' => $this->getId(), 'tag' => $tag, 'private' => false]));
|
||||
DB::persist(ActorTag::create(['tagger' => $this->id, 'tagged' => $this->id, 'tag' => $tag, 'canonical' => $canonical_tag, 'use_canonical' => false])); // TODO make use canonical configurable
|
||||
}
|
||||
foreach ($actor_tags_to_remove as $actor_tag) {
|
||||
DB::removeBy('actor_tag', ['tagger' => $this->getId(), 'tagged' => $this->getId(), 'tag' => $actor_tag->getTag(), 'use_canonical' => $actor_tag->getUseCanonical()]);
|
||||
DB::removeBy('actor_circle', ['tagger' => $this->getId(), 'tag' => $actor_tag->getTag()]); // TODO only remove if unused
|
||||
}
|
||||
Cache::delete(self::cacheKeys($this->getId())['tags']);
|
||||
Cache::delete(self::cacheKeys($this->getId(), $this->getId())['tags']);
|
||||
return $this;
|
||||
}
|
||||
|
||||
private function getSubCount(string $which, string $column): int
|
||||
{
|
||||
return Cache::get(
|
||||
@ -602,6 +560,8 @@ class Actor extends Entity
|
||||
public function canAdmin(self $other): bool
|
||||
{
|
||||
switch ($other->getType()) {
|
||||
case self::PERSON:
|
||||
return $this->getId() === $other->getId();
|
||||
case self::GROUP:
|
||||
return Cache::get(
|
||||
self::cacheKeys($this->getId(), $other->getId())['can-admin'],
|
||||
|
@ -127,17 +127,9 @@ class ActorTag extends Entity
|
||||
// @codeCoverageIgnoreEnd
|
||||
// }}} Autocode
|
||||
|
||||
public static function cacheKey(int|Actor $actor_id)
|
||||
{
|
||||
if (!\is_int($actor_id)) {
|
||||
$actor_id = $actor_id->getId();
|
||||
}
|
||||
return "actor-tags-{$actor_id}";
|
||||
}
|
||||
|
||||
public static function getByActorId(int $actor_id): array
|
||||
{
|
||||
return Cache::getList(self::cacheKey($actor_id), fn () => DB::dql('select at from actor_tag at join actor a with a.id = at.tagger where a.id = :id', ['id' => $actor_id]));
|
||||
return Cache::getList(Actor::cacheKeys($actor_id)['tags'], fn () => DB::dql('select at from actor_tag at join actor a with a.id = at.tagger where a.id = :id', ['id' => $actor_id]));
|
||||
}
|
||||
|
||||
public function getUrl(?Actor $actor = null): string
|
||||
@ -163,9 +155,9 @@ class ActorTag extends Entity
|
||||
],
|
||||
'primary key' => ['tagger', 'tagged', 'tag', 'use_canonical'],
|
||||
'indexes' => [
|
||||
'actor_tag_modified_idx' => ['modified'],
|
||||
'actor_tag_modified_idx' => ['modified'],
|
||||
'actor_tag_tagger_tag_idx' => ['tagger', 'tag'], // For Circles
|
||||
'actor_tag_tagged_idx' => ['tagged'],
|
||||
'actor_tag_tagged_idx' => ['tagged'],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
{% set actor_nickname = actor.getNickname() %}
|
||||
{% set actor_avatar = actor.getAvatarUrl() %}
|
||||
{% set actor_avatar_dimensions = actor.getAvatarDimensions() %}
|
||||
{% set actor_tags = actor.getSelfTags()[1] %} {# Take only the actor_tags, not the circles #}
|
||||
{% set actor_tags = actor.getSelfTags() %}
|
||||
{% set actor_bio = actor.getBio() %}
|
||||
{% set actor_uri = actor.getUri() %}
|
||||
|
||||
|
@ -15,7 +15,7 @@
|
||||
<h2>Settings</h2>
|
||||
<ul>
|
||||
<li>
|
||||
{% set profile_tabs = [{'title': 'Personal Info', 'desc': 'Nickname, Homepage, Bio, Self Tags and more.', 'id': 'settings-personal-info', 'form': personal_info_form}] %}
|
||||
{% set profile_tabs = [{'title': 'Personal Info', 'desc': 'Nickname, Homepage, Bio and more.', 'id': 'settings-personal-info', 'form': personal_info_form}] %}
|
||||
{% set profile_tabs = profile_tabs|merge(handle_event('PopulateSettingsTabs', app.request, 'profile')) %}
|
||||
{{ macros.settings_details_container('Profile', 'Personal Information, Avatar and Profile', 'settings-profile-details', profile_tabs, _context) }}
|
||||
</li>
|
||||
|
Loading…
Reference in New Issue
Block a user