mirror of
https://github.com/philomena-dev/philomena.git
synced 2024-11-23 12:08:00 +01:00
Merge pull request #369 from philomena-dev/quick-tag-ts
Convert quicktag to TypeScript
This commit is contained in:
commit
41c10d59cb
3 changed files with 283 additions and 117 deletions
159
assets/js/__tests__/quick-tag.spec.ts
Normal file
159
assets/js/__tests__/quick-tag.spec.ts
Normal file
|
@ -0,0 +1,159 @@
|
|||
import { $, $$ } from '../utils/dom';
|
||||
import { assertNotNull } from '../utils/assert';
|
||||
import { setupQuickTag } from '../quick-tag';
|
||||
import { fetchMock } from '../../test/fetch-mock.ts';
|
||||
import { waitFor } from '@testing-library/dom';
|
||||
|
||||
const quickTagData = `<div>
|
||||
<a class="js-quick-tag">Tag</a>
|
||||
<a class="js-quick-tag--abort hidden"><span>Abort tagging</span></a>
|
||||
<a class="js-quick-tag--submit hidden"><span>Submit</span></a>
|
||||
<a class="js-quick-tag--all hidden"><span>Toggle all</span></a>
|
||||
<div id="imagelist-container">
|
||||
<div class="media-box" data-image-id="0">
|
||||
<div class="media-box__header" data-image-id="0"></div>
|
||||
</div>
|
||||
<div class="media-box" data-image-id="1">
|
||||
<div class="media-box__header" data-image-id="1"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
describe('Batch tagging', () => {
|
||||
let tagButton: HTMLAnchorElement;
|
||||
let abortButton: HTMLAnchorElement;
|
||||
let submitButton: HTMLAnchorElement;
|
||||
let toggleAllButton: HTMLAnchorElement;
|
||||
let mediaBoxes: HTMLDivElement[];
|
||||
|
||||
beforeEach(() => {
|
||||
localStorage.clear();
|
||||
document.body.innerHTML = quickTagData;
|
||||
|
||||
tagButton = assertNotNull($<HTMLAnchorElement>('.js-quick-tag'));
|
||||
abortButton = assertNotNull($<HTMLAnchorElement>('.js-quick-tag--abort'));
|
||||
submitButton = assertNotNull($<HTMLAnchorElement>('.js-quick-tag--submit'));
|
||||
toggleAllButton = assertNotNull($<HTMLAnchorElement>('.js-quick-tag--all'));
|
||||
mediaBoxes = $$<HTMLDivElement>('.media-box');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('should prompt the user on click', () => {
|
||||
const spy = vi.spyOn(window, 'prompt').mockImplementation(() => 'a');
|
||||
tagButton.click();
|
||||
|
||||
expect(spy).toHaveBeenCalledOnce();
|
||||
expect(tagButton.classList).toContain('hidden');
|
||||
expect(abortButton.classList).not.toContain('hidden');
|
||||
expect(submitButton.classList).not.toContain('hidden');
|
||||
expect(toggleAllButton.classList).not.toContain('hidden');
|
||||
});
|
||||
|
||||
it('should not modify media boxes before entry', () => {
|
||||
mediaBoxes[0].click();
|
||||
expect(mediaBoxes[0].firstElementChild).not.toHaveClass('media-box__header--selected');
|
||||
});
|
||||
|
||||
it('should restore the list of tagged images on reload', () => {
|
||||
// TODO: this is less than ideal, because it depends on the internal
|
||||
// implementation of the quick-tag file. But we can't reload the page
|
||||
// with jsdom.
|
||||
localStorage.setItem('quickTagQueue', JSON.stringify(['0', '1']));
|
||||
localStorage.setItem('quickTagName', JSON.stringify('a'));
|
||||
|
||||
setupQuickTag();
|
||||
expect(mediaBoxes[0].firstElementChild).toHaveClass('media-box__header--selected');
|
||||
expect(mediaBoxes[1].firstElementChild).toHaveClass('media-box__header--selected');
|
||||
});
|
||||
|
||||
describe('after entry', () => {
|
||||
beforeEach(() => {
|
||||
vi.spyOn(window, 'prompt').mockImplementation(() => 'a');
|
||||
tagButton.click();
|
||||
});
|
||||
|
||||
it('should abort the tagging process if accepted', () => {
|
||||
const spy = vi.spyOn(window, 'confirm').mockImplementation(() => true);
|
||||
abortButton.click();
|
||||
|
||||
expect(spy).toHaveBeenCalledOnce();
|
||||
expect(tagButton.classList).not.toContain('hidden');
|
||||
expect(abortButton.classList).toContain('hidden');
|
||||
expect(submitButton.classList).toContain('hidden');
|
||||
expect(toggleAllButton.classList).toContain('hidden');
|
||||
});
|
||||
|
||||
it('should not abort the tagging process if rejected', () => {
|
||||
const spy = vi.spyOn(window, 'confirm').mockImplementation(() => false);
|
||||
abortButton.click();
|
||||
|
||||
expect(spy).toHaveBeenCalledOnce();
|
||||
expect(tagButton.classList).toContain('hidden');
|
||||
expect(abortButton.classList).not.toContain('hidden');
|
||||
expect(submitButton.classList).not.toContain('hidden');
|
||||
expect(toggleAllButton.classList).not.toContain('hidden');
|
||||
});
|
||||
|
||||
it('should toggle media box state on click', () => {
|
||||
mediaBoxes[0].click();
|
||||
expect(mediaBoxes[0].firstElementChild).toHaveClass('media-box__header--selected');
|
||||
expect(mediaBoxes[1].firstElementChild).not.toHaveClass('media-box__header--selected');
|
||||
});
|
||||
|
||||
it('should toggle all media box states', () => {
|
||||
mediaBoxes[0].click();
|
||||
toggleAllButton.click();
|
||||
expect(mediaBoxes[0].firstElementChild).not.toHaveClass('media-box__header--selected');
|
||||
expect(mediaBoxes[1].firstElementChild).toHaveClass('media-box__header--selected');
|
||||
});
|
||||
});
|
||||
|
||||
describe('for submission', () => {
|
||||
beforeAll(() => {
|
||||
fetchMock.enableMocks();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
fetchMock.disableMocks();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
vi.spyOn(window, 'prompt').mockImplementation(() => 'a');
|
||||
tagButton.click();
|
||||
|
||||
fetchMock.resetMocks();
|
||||
mediaBoxes[0].click();
|
||||
mediaBoxes[1].click();
|
||||
});
|
||||
|
||||
it('should return to normal state on successful submission', () => {
|
||||
fetchMock.mockResponse('{"failed":[]}');
|
||||
submitButton.click();
|
||||
|
||||
expect(fetch).toHaveBeenCalledOnce();
|
||||
|
||||
return waitFor(() => {
|
||||
expect(mediaBoxes[0].firstElementChild).not.toHaveClass('media-box__header--selected');
|
||||
expect(mediaBoxes[1].firstElementChild).not.toHaveClass('media-box__header--selected');
|
||||
});
|
||||
});
|
||||
|
||||
it('should show error on failed submission', () => {
|
||||
fetchMock.mockResponse('{"failed":[0,1]}');
|
||||
submitButton.click();
|
||||
|
||||
const spy = vi.spyOn(window, 'alert').mockImplementation(() => {});
|
||||
|
||||
expect(fetch).toHaveBeenCalledOnce();
|
||||
|
||||
return waitFor(() => {
|
||||
expect(spy).toHaveBeenCalledOnce();
|
||||
expect(mediaBoxes[0].firstElementChild).not.toHaveClass('media-box__header--selected');
|
||||
expect(mediaBoxes[1].firstElementChild).not.toHaveClass('media-box__header--selected');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,117 +0,0 @@
|
|||
/**
|
||||
* Quick Tag
|
||||
*/
|
||||
|
||||
import store from './utils/store';
|
||||
import { $, $$, toggleEl, onLeftClick } from './utils/dom';
|
||||
import { fetchJson, handleError } from './utils/requests';
|
||||
|
||||
const imageQueueStorage = 'quickTagQueue';
|
||||
const currentTagStorage = 'quickTagName';
|
||||
|
||||
function currentQueue() {
|
||||
return store.get(imageQueueStorage) || [];
|
||||
}
|
||||
|
||||
function currentTags() {
|
||||
return store.get(currentTagStorage) || '';
|
||||
}
|
||||
|
||||
function getTagButton() {
|
||||
return $('.js-quick-tag');
|
||||
}
|
||||
|
||||
function setTagButton(text) {
|
||||
$('.js-quick-tag--submit span').textContent = text;
|
||||
}
|
||||
|
||||
function toggleActiveState() {
|
||||
toggleEl($('.js-quick-tag'), $('.js-quick-tag--abort'), $('.js-quick-tag--all'), $('.js-quick-tag--submit'));
|
||||
|
||||
setTagButton(`Submit (${currentTags()})`);
|
||||
|
||||
$$('.media-box__header').forEach(el => el.classList.toggle('media-box__header--unselected'));
|
||||
$$('.media-box__header').forEach(el => el.classList.remove('media-box__header--selected'));
|
||||
currentQueue().forEach(id =>
|
||||
$$(`.media-box__header[data-image-id="${id}"]`).forEach(el => el.classList.add('media-box__header--selected')),
|
||||
);
|
||||
}
|
||||
|
||||
function activate() {
|
||||
store.set(currentTagStorage, window.prompt('A comma-delimited list of tags you want to add:'));
|
||||
|
||||
if (currentTags()) toggleActiveState();
|
||||
}
|
||||
|
||||
function reset() {
|
||||
store.remove(currentTagStorage);
|
||||
store.remove(imageQueueStorage);
|
||||
|
||||
toggleActiveState();
|
||||
}
|
||||
|
||||
function promptReset() {
|
||||
if (window.confirm('Are you sure you want to abort batch tagging?')) {
|
||||
reset();
|
||||
}
|
||||
}
|
||||
|
||||
function submit() {
|
||||
setTagButton(`Wait... (${currentTags()})`);
|
||||
|
||||
fetchJson('PUT', '/admin/batch/tags', {
|
||||
tags: currentTags(),
|
||||
image_ids: currentQueue(),
|
||||
})
|
||||
.then(handleError)
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.failed.length) window.alert(`Failed to add tags to the images with these IDs: ${data.failed}`);
|
||||
|
||||
reset();
|
||||
});
|
||||
}
|
||||
|
||||
function modifyImageQueue(mediaBox) {
|
||||
if (currentTags()) {
|
||||
const imageId = mediaBox.dataset.imageId;
|
||||
const queue = currentQueue();
|
||||
const isSelected = queue.includes(imageId);
|
||||
|
||||
isSelected ? queue.splice(queue.indexOf(imageId), 1) : queue.push(imageId);
|
||||
|
||||
$$(`.media-box__header[data-image-id="${imageId}"]`).forEach(el =>
|
||||
el.classList.toggle('media-box__header--selected'),
|
||||
);
|
||||
|
||||
store.set(imageQueueStorage, queue);
|
||||
}
|
||||
}
|
||||
|
||||
function toggleAllImages() {
|
||||
$$('#imagelist-container .media-box').forEach(modifyImageQueue);
|
||||
}
|
||||
|
||||
function clickHandler(event) {
|
||||
const targets = {
|
||||
'.js-quick-tag': activate,
|
||||
'.js-quick-tag--abort': promptReset,
|
||||
'.js-quick-tag--submit': submit,
|
||||
'.js-quick-tag--all': toggleAllImages,
|
||||
'.media-box': modifyImageQueue,
|
||||
};
|
||||
|
||||
for (const target in targets) {
|
||||
if (event.target && event.target.closest(target)) {
|
||||
targets[target](event.target.closest(target));
|
||||
currentTags() && event.preventDefault();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setupQuickTag() {
|
||||
if (getTagButton() && currentTags()) toggleActiveState();
|
||||
if (getTagButton()) onLeftClick(clickHandler);
|
||||
}
|
||||
|
||||
export { setupQuickTag };
|
124
assets/js/quick-tag.ts
Normal file
124
assets/js/quick-tag.ts
Normal file
|
@ -0,0 +1,124 @@
|
|||
/**
|
||||
* Quick Tag
|
||||
*/
|
||||
|
||||
import store from './utils/store';
|
||||
import { assertNotNull, assertNotUndefined } from './utils/assert';
|
||||
import { $, $$, toggleEl } from './utils/dom';
|
||||
import { fetchJson, handleError } from './utils/requests';
|
||||
import { delegate, leftClick } from './utils/events';
|
||||
|
||||
const imageQueueStorage = 'quickTagQueue';
|
||||
const currentTagStorage = 'quickTagName';
|
||||
|
||||
function currentQueue(): string[] {
|
||||
return store.get<string[]>(imageQueueStorage) || [];
|
||||
}
|
||||
|
||||
function currentTags(): string {
|
||||
return store.get<string>(currentTagStorage) || '';
|
||||
}
|
||||
|
||||
function setTagButton(text: string) {
|
||||
assertNotNull($('.js-quick-tag--submit span')).textContent = text;
|
||||
}
|
||||
|
||||
function toggleActiveState() {
|
||||
toggleEl($$<HTMLElement>('.js-quick-tag,.js-quick-tag--abort,.js-quick-tag--all,.js-quick-tag--submit'));
|
||||
|
||||
setTagButton(`Submit (${currentTags()})`);
|
||||
|
||||
$$('.media-box__header').forEach(el => el.classList.toggle('media-box__header--unselected'));
|
||||
$$('.media-box__header').forEach(el => el.classList.remove('media-box__header--selected'));
|
||||
|
||||
currentQueue().forEach(id =>
|
||||
$$(`.media-box__header[data-image-id="${id}"]`).forEach(el => el.classList.add('media-box__header--selected')),
|
||||
);
|
||||
}
|
||||
|
||||
function activate(event: Event) {
|
||||
event.preventDefault();
|
||||
|
||||
store.set(currentTagStorage, window.prompt('A comma-delimited list of tags you want to add:'));
|
||||
|
||||
if (currentTags()) {
|
||||
toggleActiveState();
|
||||
}
|
||||
}
|
||||
|
||||
function reset() {
|
||||
store.remove(currentTagStorage);
|
||||
store.remove(imageQueueStorage);
|
||||
|
||||
toggleActiveState();
|
||||
}
|
||||
|
||||
function promptReset(event: Event) {
|
||||
event.preventDefault();
|
||||
|
||||
if (window.confirm('Are you sure you want to abort batch tagging?')) {
|
||||
reset();
|
||||
}
|
||||
}
|
||||
|
||||
function submit(event: Event) {
|
||||
event.preventDefault();
|
||||
|
||||
setTagButton(`Wait... (${currentTags()})`);
|
||||
|
||||
fetchJson('PUT', '/admin/batch/tags', {
|
||||
tags: currentTags(),
|
||||
image_ids: currentQueue(),
|
||||
})
|
||||
.then(handleError)
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.failed.length) window.alert(`Failed to add tags to the images with these IDs: ${data.failed}`);
|
||||
|
||||
reset();
|
||||
});
|
||||
}
|
||||
|
||||
function modifyImageQueue(event: Event, mediaBox: HTMLDivElement) {
|
||||
if (!currentTags()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const imageId = assertNotUndefined(mediaBox.dataset.imageId);
|
||||
const queue = currentQueue();
|
||||
const isSelected = queue.includes(imageId);
|
||||
|
||||
if (isSelected) {
|
||||
queue.splice(queue.indexOf(imageId), 1);
|
||||
} else {
|
||||
queue.push(imageId);
|
||||
}
|
||||
|
||||
for (const boxHeader of $$(`.media-box__header[data-image-id="${imageId}"]`)) {
|
||||
boxHeader.classList.toggle('media-box__header--selected');
|
||||
}
|
||||
|
||||
store.set(imageQueueStorage, queue);
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
function toggleAllImages(event: Event, _target: Element) {
|
||||
for (const mediaBox of $$<HTMLDivElement>('#imagelist-container .media-box')) {
|
||||
modifyImageQueue(event, mediaBox);
|
||||
}
|
||||
}
|
||||
|
||||
delegate(document, 'click', {
|
||||
'.js-quick-tag': leftClick(activate),
|
||||
'.js-quick-tag--abort': leftClick(promptReset),
|
||||
'.js-quick-tag--submit': leftClick(submit),
|
||||
'.js-quick-tag--all': leftClick(toggleAllImages),
|
||||
'.media-box': leftClick(modifyImageQueue),
|
||||
});
|
||||
|
||||
export function setupQuickTag() {
|
||||
const tagButton = $<HTMLAnchorElement>('.js-quick-tag');
|
||||
if (tagButton && currentTags()) {
|
||||
toggleActiveState();
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue