Bulk action: add or delete tag to multiple bookmarks (#1898)
This commit is contained in:
parent
cd618bd8be
commit
00cce1f8c7
6 changed files with 543 additions and 0 deletions
|
@ -517,6 +517,16 @@ class Bookmark
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a tag in tags list.
|
||||
*
|
||||
* @param string $tag
|
||||
*/
|
||||
public function addTag(string $tag): self
|
||||
{
|
||||
return $this->setTags(array_merge($this->getTags(), [$tag]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a tag from tags list.
|
||||
*
|
||||
|
|
|
@ -203,4 +203,85 @@ class ShaareManageController extends ShaarliAdminController
|
|||
'/shaare/' . $hash . '?key=' . $bookmark->getAdditionalContentEntry('private_key')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /admin/shaare/update-tags
|
||||
*
|
||||
* Bulk add or delete a tags on one or multiple bookmarks.
|
||||
*/
|
||||
public function addOrDeleteTags(Request $request, Response $response): Response
|
||||
{
|
||||
$this->checkToken($request);
|
||||
|
||||
$ids = trim(escape($request->getParam('id') ?? ''));
|
||||
if (empty($ids) || strpos($ids, ' ') !== false) {
|
||||
// multiple, space-separated ids provided
|
||||
$ids = array_values(array_filter(preg_split('/\s+/', $ids), 'ctype_digit'));
|
||||
} else {
|
||||
// only a single id provided
|
||||
$ids = [$ids];
|
||||
}
|
||||
|
||||
// assert at least one id is given
|
||||
if (0 === count($ids)) {
|
||||
$this->saveErrorMessage(t('Invalid bookmark ID provided.'));
|
||||
|
||||
return $this->redirectFromReferer($request, $response, ['/updateTag'], []);
|
||||
}
|
||||
|
||||
// assert that the action is valid
|
||||
$action = $request->getParam('action');
|
||||
if (!in_array($action, ['add', 'delete'], true)) {
|
||||
$this->saveErrorMessage(t('Invalid action provided.'));
|
||||
|
||||
return $this->redirectFromReferer($request, $response, ['/updateTag'], []);
|
||||
}
|
||||
|
||||
// assert that the tag name is valid
|
||||
$tagString = trim($request->getParam('tag'));
|
||||
if (empty($tagString)) {
|
||||
$this->saveErrorMessage(t('Invalid tag name provided.'));
|
||||
|
||||
return $this->redirectFromReferer($request, $response, ['/updateTag'], []);
|
||||
}
|
||||
|
||||
$tags = tags_str2array($tagString, $this->container->conf->get('general.tags_separator', ' '));
|
||||
$formatter = $this->container->formatterFactory->getFormatter('raw');
|
||||
$count = 0;
|
||||
|
||||
foreach ($ids as $id) {
|
||||
try {
|
||||
$bookmark = $this->container->bookmarkService->get((int) $id);
|
||||
} catch (BookmarkNotFoundException $e) {
|
||||
$this->saveErrorMessage(sprintf(
|
||||
t('Bookmark with identifier %s could not be found.'),
|
||||
$id
|
||||
));
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($tags as $tag) {
|
||||
if ($action === 'add') {
|
||||
$bookmark->addTag($tag);
|
||||
} else {
|
||||
$bookmark->deleteTag($tag);
|
||||
}
|
||||
}
|
||||
|
||||
// To preserve backward compatibility with 3rd parties, plugins still use arrays
|
||||
$data = $formatter->format($bookmark);
|
||||
$this->executePageHooks('save_link', $data);
|
||||
$bookmark->fromArray($data, $this->container->conf->get('general.tags_separator', ' '));
|
||||
|
||||
$this->container->bookmarkService->set($bookmark, false);
|
||||
++$count;
|
||||
}
|
||||
|
||||
if ($count > 0) {
|
||||
$this->container->bookmarkService->save();
|
||||
}
|
||||
|
||||
return $this->redirectFromReferer($request, $response, ['/updateTag'], []);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -383,6 +383,10 @@ function init(description) {
|
|||
});
|
||||
|
||||
sub.classList.toggle('open');
|
||||
const autofocus = sub.querySelector('.autofocus');
|
||||
if (autofocus) {
|
||||
autofocus.focus();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
@ -507,6 +511,37 @@ function init(description) {
|
|||
});
|
||||
}
|
||||
|
||||
['add', 'delete'].forEach((action) => {
|
||||
const subHeader = document.getElementById(`bulk-tag-action-${action}`);
|
||||
|
||||
if (subHeader) {
|
||||
subHeader.querySelectorAll('a.button').forEach((link) => {
|
||||
if (!link.classList.contains('action')) {
|
||||
return;
|
||||
}
|
||||
|
||||
subHeader.querySelector('input[name="tag"]').addEventListener('keypress', (event) => {
|
||||
if (event.keyCode === 13) { // enter
|
||||
link.click();
|
||||
}
|
||||
});
|
||||
|
||||
link.addEventListener('click', (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
const ids = [];
|
||||
const linkCheckedCheckboxes = document.querySelectorAll('.link-checkbox:checked');
|
||||
[...linkCheckedCheckboxes].forEach((checkbox) => {
|
||||
ids.push(checkbox.value);
|
||||
});
|
||||
|
||||
subHeader.querySelector('input[name="id"]').value = ids.join(' ');
|
||||
subHeader.querySelector('form').submit();
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Select all button
|
||||
*/
|
||||
|
|
|
@ -151,6 +151,7 @@ $app->group('/admin', function () {
|
|||
$this->post('/shaare', '\Shaarli\Front\Controller\Admin\ShaarePublishController:save');
|
||||
$this->get('/shaare/delete', '\Shaarli\Front\Controller\Admin\ShaareManageController:deleteBookmark');
|
||||
$this->get('/shaare/visibility', '\Shaarli\Front\Controller\Admin\ShaareManageController:changeVisibility');
|
||||
$this->post('/shaare/update-tags', '\Shaarli\Front\Controller\Admin\ShaareManageController:addOrDeleteSingleTag');
|
||||
$this->get('/shaare/{id:[0-9]+}/pin', '\Shaarli\Front\Controller\Admin\ShaareManageController:pinBookmark');
|
||||
$this->patch(
|
||||
'/shaare/{id:[0-9]+}/update-thumbnail',
|
||||
|
|
|
@ -0,0 +1,380 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shaarli\Front\Controller\Admin\ShaareManageControllerTest;
|
||||
|
||||
use Shaarli\Bookmark\Bookmark;
|
||||
use Shaarli\Bookmark\Exception\BookmarkNotFoundException;
|
||||
use Shaarli\Formatter\BookmarkFormatter;
|
||||
use Shaarli\Formatter\BookmarkRawFormatter;
|
||||
use Shaarli\Formatter\FormatterFactory;
|
||||
use Shaarli\Front\Controller\Admin\FrontAdminControllerMockHelper;
|
||||
use Shaarli\Front\Controller\Admin\ShaareManageController;
|
||||
use Shaarli\Http\HttpAccess;
|
||||
use Shaarli\Security\SessionManager;
|
||||
use Shaarli\TestCase;
|
||||
use Slim\Http\Request;
|
||||
use Slim\Http\Response;
|
||||
|
||||
class AddOrDeleteTagTest extends TestCase
|
||||
{
|
||||
use FrontAdminControllerMockHelper;
|
||||
|
||||
/** @var ShaareManageController */
|
||||
protected $controller;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
$this->createContainer();
|
||||
|
||||
$this->container->httpAccess = $this->createMock(HttpAccess::class);
|
||||
$this->controller = new ShaareManageController($this->container);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add 1 tag to 1 bookmark
|
||||
*/
|
||||
public function testAddOneTagOnOneBookmark(): void
|
||||
{
|
||||
$parameters = ['id' => '123', 'tag' => 'newtag', 'action' => 'add'];
|
||||
|
||||
$request = $this->createMock(Request::class);
|
||||
$request
|
||||
->method('getParam')
|
||||
->willReturnCallback(function (string $key) use ($parameters): ?string {
|
||||
return $parameters[$key] ?? null;
|
||||
})
|
||||
;
|
||||
$response = new Response();
|
||||
$bookmark = (new Bookmark())
|
||||
->setId(123)->setUrl('http://domain.tld')->setTitle('Title 123')
|
||||
->setTagsString('first second');
|
||||
|
||||
static::assertSame(['first', 'second'], $bookmark->getTags());
|
||||
|
||||
$this->container->bookmarkService->expects(static::once())->method('get')->with(123)->willReturn($bookmark);
|
||||
$this->container->bookmarkService->expects(static::once())->method('set')->with($bookmark, false);
|
||||
$this->container->bookmarkService->expects(static::once())->method('save');
|
||||
$this->container->formatterFactory = $this->createMock(FormatterFactory::class);
|
||||
$this->container->formatterFactory
|
||||
->expects(static::once())
|
||||
->method('getFormatter')
|
||||
->with('raw')
|
||||
->willReturnCallback(function () use ($bookmark): BookmarkFormatter {
|
||||
return new BookmarkRawFormatter($this->container->conf, true);
|
||||
})
|
||||
;
|
||||
|
||||
// Make sure that PluginManager hook is triggered
|
||||
$this->container->pluginManager
|
||||
->expects(static::once())
|
||||
->method('executeHooks')
|
||||
->with('save_link')
|
||||
;
|
||||
|
||||
$result = $this->controller->addOrDeleteTags($request, $response);
|
||||
|
||||
static::assertSame(['first', 'second', 'newtag'], $bookmark->getTags());
|
||||
|
||||
static::assertSame(302, $result->getStatusCode());
|
||||
static::assertSame(['/subfolder/'], $result->getHeader('location'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Add 2 tags to 2 bookmarks
|
||||
*/
|
||||
public function testAddTwoTagsOnTwoBookmarks(): void
|
||||
{
|
||||
$parameters = ['id' => '123 456', 'tag' => 'newtag@othertag', 'action' => 'add'];
|
||||
|
||||
$request = $this->createMock(Request::class);
|
||||
$request
|
||||
->method('getParam')
|
||||
->willReturnCallback(function (string $key) use ($parameters): ?string {
|
||||
return $parameters[$key] ?? null;
|
||||
})
|
||||
;
|
||||
$response = new Response();
|
||||
$bookmark1 = (new Bookmark())
|
||||
->setId(123)->setUrl('http://domain.tld')->setTitle('Title 123')
|
||||
->setTagsString('first second');
|
||||
$bookmark2 = (new Bookmark())
|
||||
->setId(456)->setUrl('http://domain.tld')->setTitle('Title 123');
|
||||
|
||||
static::assertSame(['first', 'second'], $bookmark1->getTags());
|
||||
static::assertSame([], $bookmark2->getTags());
|
||||
|
||||
$this->container->bookmarkService->expects(static::exactly(2))->method('get')
|
||||
->withConsecutive([123], [456])
|
||||
->willReturnOnConsecutiveCalls($bookmark1, $bookmark2);
|
||||
$this->container->bookmarkService->expects(static::exactly(2))->method('set')
|
||||
->withConsecutive([$bookmark1, false], [$bookmark2, false]);
|
||||
$this->container->bookmarkService->expects(static::once())->method('save');
|
||||
$this->container->formatterFactory = $this->createMock(FormatterFactory::class);
|
||||
$this->container->formatterFactory
|
||||
->expects(static::once())
|
||||
->method('getFormatter')
|
||||
->with('raw')
|
||||
->willReturnCallback(function (): BookmarkFormatter {
|
||||
return new BookmarkRawFormatter($this->container->conf, true);
|
||||
})
|
||||
;
|
||||
|
||||
// Make sure that PluginManager hook is triggered
|
||||
$this->container->pluginManager
|
||||
->expects(static::exactly(2))
|
||||
->method('executeHooks')
|
||||
->with('save_link')
|
||||
;
|
||||
|
||||
$result = $this->controller->addOrDeleteTags($request, $response);
|
||||
|
||||
static::assertSame(['first', 'second', 'newtag', 'othertag'], $bookmark1->getTags());
|
||||
static::assertSame(['newtag', 'othertag'], $bookmark2->getTags());
|
||||
|
||||
static::assertSame(302, $result->getStatusCode());
|
||||
static::assertSame(['/subfolder/'], $result->getHeader('location'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete 1 tag to 1 bookmark
|
||||
*/
|
||||
public function testDeleteOneTagOnOneBookmark(): void
|
||||
{
|
||||
$parameters = ['id' => '123', 'tag' => 'second', 'action' => 'delete'];
|
||||
|
||||
$request = $this->createMock(Request::class);
|
||||
$request
|
||||
->method('getParam')
|
||||
->willReturnCallback(function (string $key) use ($parameters): ?string {
|
||||
return $parameters[$key] ?? null;
|
||||
})
|
||||
;
|
||||
$response = new Response();
|
||||
$bookmark = (new Bookmark())
|
||||
->setId(123)->setUrl('http://domain.tld')->setTitle('Title 123')
|
||||
->setTagsString('first second third');
|
||||
|
||||
static::assertSame(['first', 'second', 'third'], $bookmark->getTags());
|
||||
|
||||
$this->container->bookmarkService->expects(static::once())->method('get')->with(123)->willReturn($bookmark);
|
||||
$this->container->bookmarkService->expects(static::once())->method('set')->with($bookmark, false);
|
||||
$this->container->bookmarkService->expects(static::once())->method('save');
|
||||
$this->container->formatterFactory = $this->createMock(FormatterFactory::class);
|
||||
$this->container->formatterFactory
|
||||
->expects(static::once())
|
||||
->method('getFormatter')
|
||||
->with('raw')
|
||||
->willReturnCallback(function () use ($bookmark): BookmarkFormatter {
|
||||
return new BookmarkRawFormatter($this->container->conf, true);
|
||||
})
|
||||
;
|
||||
|
||||
// Make sure that PluginManager hook is triggered
|
||||
$this->container->pluginManager
|
||||
->expects(static::once())
|
||||
->method('executeHooks')
|
||||
->with('save_link')
|
||||
;
|
||||
|
||||
$result = $this->controller->addOrDeleteTags($request, $response);
|
||||
|
||||
static::assertSame(['first', 'third'], $bookmark->getTags());
|
||||
|
||||
static::assertSame(302, $result->getStatusCode());
|
||||
static::assertSame(['/subfolder/'], $result->getHeader('location'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete 2 tags to 2 bookmarks
|
||||
*/
|
||||
public function testDeleteTwoTagOnTwoBookmarks(): void
|
||||
{
|
||||
$parameters = ['id' => '123 456', 'tag' => 'second@first', 'action' => 'delete'];
|
||||
|
||||
$request = $this->createMock(Request::class);
|
||||
$request
|
||||
->method('getParam')
|
||||
->willReturnCallback(function (string $key) use ($parameters): ?string {
|
||||
return $parameters[$key] ?? null;
|
||||
})
|
||||
;
|
||||
$response = new Response();
|
||||
$bookmark1 = (new Bookmark())
|
||||
->setId(123)->setUrl('http://domain.tld')->setTitle('Title 123')
|
||||
->setTagsString('first second third other');
|
||||
$bookmark2 = (new Bookmark())
|
||||
->setId(456)->setUrl('http://domain.tld')->setTitle('Title 123')
|
||||
->setTagsString('first second');
|
||||
|
||||
static::assertSame(['first', 'second', 'third', 'other'], $bookmark1->getTags());
|
||||
static::assertSame(['first', 'second'], $bookmark2->getTags());
|
||||
|
||||
$this->container->bookmarkService->expects(static::exactly(2))->method('get')
|
||||
->withConsecutive([123], [456])
|
||||
->willReturnOnConsecutiveCalls($bookmark1, $bookmark2);
|
||||
$this->container->bookmarkService->expects(static::exactly(2))->method('set')
|
||||
->withConsecutive([$bookmark1, false], [$bookmark2, false]);
|
||||
$this->container->bookmarkService->expects(static::once())->method('save');
|
||||
$this->container->formatterFactory = $this->createMock(FormatterFactory::class);
|
||||
$this->container->formatterFactory
|
||||
->expects(static::once())
|
||||
->method('getFormatter')
|
||||
->with('raw')
|
||||
->willReturnCallback(function (): BookmarkFormatter {
|
||||
return new BookmarkRawFormatter($this->container->conf, true);
|
||||
})
|
||||
;
|
||||
|
||||
// Make sure that PluginManager hook is triggered
|
||||
$this->container->pluginManager
|
||||
->expects(static::exactly(2))
|
||||
->method('executeHooks')
|
||||
->with('save_link')
|
||||
;
|
||||
|
||||
$result = $this->controller->addOrDeleteTags($request, $response);
|
||||
|
||||
static::assertSame(['third', 'other'], $bookmark1->getTags());
|
||||
static::assertSame([], $bookmark2->getTags());
|
||||
|
||||
static::assertSame(302, $result->getStatusCode());
|
||||
static::assertSame(['/subfolder/'], $result->getHeader('location'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test add a tag without passing an ID.
|
||||
*/
|
||||
public function testAddTagWithoutId(): void
|
||||
{
|
||||
$parameters = ['tag' => 'newtag', 'action' => 'add'];
|
||||
$request = $this->createMock(Request::class);
|
||||
$request
|
||||
->method('getParam')
|
||||
->willReturnCallback(function (string $key) use ($parameters): ?string {
|
||||
return $parameters[$key] ?? null;
|
||||
})
|
||||
;
|
||||
$response = new Response();
|
||||
|
||||
$this->container->sessionManager
|
||||
->expects(static::once())
|
||||
->method('setSessionParameter')
|
||||
->with(SessionManager::KEY_ERROR_MESSAGES, ['Invalid bookmark ID provided.'])
|
||||
;
|
||||
|
||||
$result = $this->controller->addOrDeleteTags($request, $response);
|
||||
|
||||
static::assertSame(302, $result->getStatusCode());
|
||||
static::assertSame(['/subfolder/'], $result->getHeader('location'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test add a tag without passing an ID.
|
||||
*/
|
||||
public function testDeleteTagWithoutId(): void
|
||||
{
|
||||
$parameters = ['tag' => 'newtag', 'action' => 'delete'];
|
||||
$request = $this->createMock(Request::class);
|
||||
$request
|
||||
->method('getParam')
|
||||
->willReturnCallback(function (string $key) use ($parameters): ?string {
|
||||
return $parameters[$key] ?? null;
|
||||
})
|
||||
;
|
||||
$response = new Response();
|
||||
|
||||
$this->container->sessionManager
|
||||
->expects(static::once())
|
||||
->method('setSessionParameter')
|
||||
->with(SessionManager::KEY_ERROR_MESSAGES, ['Invalid bookmark ID provided.'])
|
||||
;
|
||||
|
||||
$result = $this->controller->addOrDeleteTags($request, $response);
|
||||
|
||||
static::assertSame(302, $result->getStatusCode());
|
||||
static::assertSame(['/subfolder/'], $result->getHeader('location'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test add a tag without passing an action.
|
||||
*/
|
||||
public function testAddTagWithoutAction(): void
|
||||
{
|
||||
$parameters = ['id' => '123', 'tag' => 'newtag'];
|
||||
$request = $this->createMock(Request::class);
|
||||
$request
|
||||
->method('getParam')
|
||||
->willReturnCallback(function (string $key) use ($parameters): ?string {
|
||||
return $parameters[$key] ?? null;
|
||||
})
|
||||
;
|
||||
$response = new Response();
|
||||
|
||||
$this->container->sessionManager
|
||||
->expects(static::once())
|
||||
->method('setSessionParameter')
|
||||
->with(SessionManager::KEY_ERROR_MESSAGES, ['Invalid action provided.'])
|
||||
;
|
||||
|
||||
$result = $this->controller->addOrDeleteTags($request, $response);
|
||||
|
||||
static::assertSame(302, $result->getStatusCode());
|
||||
static::assertSame(['/subfolder/'], $result->getHeader('location'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test add a tag without passing a tag string value.
|
||||
*/
|
||||
public function testAddTagWithoutValue(): void
|
||||
{
|
||||
$parameters = ['id' => '123', 'tag' => '', 'action' => 'add'];
|
||||
$request = $this->createMock(Request::class);
|
||||
$request
|
||||
->method('getParam')
|
||||
->willReturnCallback(function (string $key) use ($parameters): ?string {
|
||||
return $parameters[$key] ?? null;
|
||||
})
|
||||
;
|
||||
$response = new Response();
|
||||
|
||||
$this->container->sessionManager
|
||||
->expects(static::once())
|
||||
->method('setSessionParameter')
|
||||
->with(SessionManager::KEY_ERROR_MESSAGES, ['Invalid tag name provided.'])
|
||||
;
|
||||
|
||||
$result = $this->controller->addOrDeleteTags($request, $response);
|
||||
|
||||
static::assertSame(302, $result->getStatusCode());
|
||||
static::assertSame(['/subfolder/'], $result->getHeader('location'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test delete a tag without passing a tag string value.
|
||||
*/
|
||||
public function testDeleteTagWithoutValue(): void
|
||||
{
|
||||
$parameters = ['id' => '123', 'tag' => '', 'action' => 'delete'];
|
||||
$request = $this->createMock(Request::class);
|
||||
$request
|
||||
->method('getParam')
|
||||
->willReturnCallback(function (string $key) use ($parameters): ?string {
|
||||
return $parameters[$key] ?? null;
|
||||
})
|
||||
;
|
||||
$response = new Response();
|
||||
|
||||
$this->container->sessionManager
|
||||
->expects(static::once())
|
||||
->method('setSessionParameter')
|
||||
->with(SessionManager::KEY_ERROR_MESSAGES, ['Invalid tag name provided.'])
|
||||
;
|
||||
|
||||
$result = $this->controller->addOrDeleteTags($request, $response);
|
||||
|
||||
static::assertSame(302, $result->getStatusCode());
|
||||
static::assertSame(['/subfolder/'], $result->getHeader('location'));
|
||||
}
|
||||
}
|
|
@ -131,10 +131,46 @@
|
|||
<a href="" class="actions-change-visibility button" data-visibility="private">
|
||||
<i class="fa fa-user-secret" aria-hidden="true"></i>
|
||||
{'Set private'|t}
|
||||
</a>
|
||||
<a href="" class="subheader-opener button" data-open-id="bulk-tag-action-add">
|
||||
<i class="fa fa-tag" aria-hidden="true"></i>
|
||||
{'Add tag'|t}
|
||||
</a>
|
||||
<a href="" class="subheader-opener button" data-open-id="bulk-tag-action-delete">
|
||||
<i class="fa fa-window-close" aria-hidden="true"></i>
|
||||
{'Delete tag'|t}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{$addDelete=['add', 'delete']}
|
||||
{loop="$addDelete"}
|
||||
<div id="bulk-tag-action-{$value}" class="subheader-form">
|
||||
<form class="pure-g" action="{$base_path}/admin/shaare/update-tags" method="post">
|
||||
<div class="pure-u-1">
|
||||
<span>
|
||||
<input
|
||||
type="text" name="tag" class="autofocus"
|
||||
aria-label="{$value === 'add' ? t('Tag to add') : t('Tag to delete')}"
|
||||
placeholder="{$value === 'add' ? t('Tag to add') : t('Tag to delete')}"
|
||||
autocomplete="off" data-multiple data-autofirst data-minChars="1"
|
||||
data-list="{loop="$tags"}{$key}, {/loop}"
|
||||
>
|
||||
<input type="hidden" name="action" value="{$value}" />
|
||||
<input type="hidden" name="id" value="" />
|
||||
<input type="hidden" name="token" value="{$token}" />
|
||||
</span>
|
||||
<a href="" class="button action">
|
||||
<i class="fa fa-tag" aria-hidden="true"></i>
|
||||
{$value === 'add' ? t('Add tag') : t('Delete tag')}
|
||||
</a>
|
||||
<a href="" class="subheader-opener button cancel" data-open-id="actions">{'Cancel'|t}</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{/loop}
|
||||
|
||||
{if="!$is_logged_in"}
|
||||
<form method="post" name="loginform">
|
||||
<div class="subheader-form header-login-form" id="header-login-form">
|
||||
|
|
Loading…
Reference in a new issue