Tag moderation UI changes (#19906)
* Basic add remove functionality with few tests * Updated cypress test and added more unit tests * Rename display ads vars in the views (#19877) * Renamed display_ads vars to billboards * Rename local vars in the feed view * Renamed display-ad in the comment Co-authored-by: Rajat Talesra <rajattalesra4914@gmail.com> * Renamed variables in the views --------- Co-authored-by: Rajat Talesra <rajattalesra4914@gmail.com> * Update js-routes to version 2.2.7 (#19883) Co-authored-by: depfu[bot] <23717796+depfu[bot]@users.noreply.github.com> * add field for billboard target geolocations, with unit and e2e tests (#19855) Co-authored-by: depfu[bot] <23717796+depfu[bot]@users.noreply.github.com> Co-authored-by: Duke Greene <dukegreene@gmail.com> Co-authored-by: PJ <pj@forem.com> Co-authored-by: Anna Buianova <lightallloy@gmail.com> Co-authored-by: Mac Siri <mac@forem.com> Co-authored-by: zhangted <tedcbook@gmail.com> Co-authored-by: Josh Klar <jklar@forem.com> * Added autocomplete value off (#19882) * Bust cache of organization page when new member is added (#19861) * Remove unused freezeScrolling (#19808) * Remove unused freezeScrolling * chore: try see if this helps to skip the flakey test that wasn't being skipped * chore: try see if this helps to skip the flakey test that wasn't being skipped * oops: revert the gemfile changes --------- Co-authored-by: Mai Irie <mai@forem.com> Co-authored-by: Mac Siri <mac@forem.com> Co-authored-by: Ridhwana <ridhwana.khan16@gmail.com> * ci: Allow workflow_dispatch runs to log in to GHCR. (#19885) * ci: Fix base image builder... the right way... (#19889) * Update postcss to version 8.4.27 (#19888) Co-authored-by: depfu[bot] <23717796+depfu[bot]@users.noreply.github.com> * Replace uses of toBeDefined with toExist (#19862) * Replaced toBeDefined with toExist * Revert db changes * Rename display ads to billboards: css and js code (#19887) * Renamed js vars/css names * Renamed display_ads to billboards in js functions, variables, components * [ruby] Update sidekiq-unique-jobs 7.1.29 → 7.1.30 (patch) (#19902) Co-authored-by: depfu[bot] <23717796+depfu[bot]@users.noreply.github.com> * Update oj to version 3.15.1 (#19901) Co-authored-by: depfu[bot] <23717796+depfu[bot]@users.noreply.github.com> * Updated failing cypress test * Removed commented code * Attempt at fixing tests * Fixed failing tests * Added more tests * Basic color and design changes part -1 * Disable/Enable buttons correctly * Various design updates in Tag adjustement mod UI * I18N and documentation * Updated mod panel * Fixed mod related cypress tests * Minor design fixes * Dark mode designs * Test update * Mod UI with some bugs * Updated/simplified code fully-working * Updated tests * Added more tests * Accessibility improvements * hr line code fix * Close mod icon bug fix * Nit fixes * Design fixes as suggested by Anuj. * Design fixes * Dark mode file usage * Design fixes * Nit fixes --------- Co-authored-by: Anna Buianova <lightallloy@gmail.com> Co-authored-by: depfu[bot] <23717796+depfu[bot]@users.noreply.github.com> Co-authored-by: Duke Greene <dukegreene@gmail.com> Co-authored-by: PJ <pj@forem.com> Co-authored-by: Mac Siri <mac@forem.com> Co-authored-by: zhangted <tedcbook@gmail.com> Co-authored-by: Josh Klar <jklar@forem.com> Co-authored-by: Joshua Wehner <joshua@forem.com> Co-authored-by: Mai Irie <mai@forem.com> Co-authored-by: Ridhwana <ridhwana.khan16@gmail.com>
This commit is contained in:
parent
85b0a18880
commit
992ced1908
14 changed files with 777 additions and 222 deletions
|
|
@ -225,6 +225,7 @@ Lint/MissingSuper:
|
|||
Enabled: true
|
||||
Exclude:
|
||||
- 'app/components/**/*.rb'
|
||||
- 'app/policies/**/*.rb'
|
||||
|
||||
Lint/MixedRegexpCaptureTypes:
|
||||
Description: 'Do not mix named captures and numbered captures in a Regexp literal.'
|
||||
|
|
|
|||
|
|
@ -366,6 +366,30 @@
|
|||
--banner-color: var(--white);
|
||||
--banner-accent: var(--accent-brand);
|
||||
|
||||
/***********************************************
|
||||
** Tag Moderation ******************************
|
||||
***********************************************/
|
||||
|
||||
/* Tag item */
|
||||
--tag-item-bg-hover: rgb(var(--indigo-50));
|
||||
|
||||
/* Subtract */
|
||||
--subtract-color: rgb(var(--red-600));
|
||||
--subtract-color-hover: rgb(var(--white));
|
||||
--subtract-bg: rgba(var(--red-600), 0.2);
|
||||
--subtract-bg-hover: rgba(var(--red-600));
|
||||
|
||||
/* Plus */
|
||||
--plus-color: rgb(var(--green-800));
|
||||
--plus-color-hover: rgb(var(--white));
|
||||
--plus-bg: rgb(var(--green-100));
|
||||
--plus-bg-hover: rgb(var(--green-600));
|
||||
|
||||
/* Tag name */
|
||||
--tag-hash: rgb(var(--grey-500));
|
||||
--tag-name: rgb(var(--grey-800));
|
||||
--add-tag-hash: rgb(var(--grey-700));
|
||||
|
||||
/***********************************************
|
||||
** Utilities ***********************************
|
||||
***********************************************/
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
:root {
|
||||
color-scheme: dark;
|
||||
|
||||
|
||||
--theme-secondary-color: #cedae2;
|
||||
--theme-container-background: #141f2d;
|
||||
--theme-container-accent-background: #202c3d;
|
||||
|
|
@ -353,6 +353,30 @@
|
|||
--selected-date-color: var(--body-color);
|
||||
--selected-range-color: var(--body-color-inverted);
|
||||
|
||||
/***********************************************
|
||||
** Tag Moderation ******************************
|
||||
***********************************************/
|
||||
|
||||
/* Tag item */
|
||||
--tag-item-bg-hover: rgba(var(--indigo-900), 0.5);
|
||||
|
||||
/* Subtract */
|
||||
--subtract-color: rgb(var(--red-200));
|
||||
--subtract-color-hover: rgb(var(--white));
|
||||
--subtract-bg: rgba(var(--red-600), 0.4);
|
||||
--subtract-bg-hover: rgb(var(--red-600));
|
||||
|
||||
/* Plus */
|
||||
--plus-color: rgb(var(--green-200));
|
||||
--plus-color-hover: rgb(var(--white));
|
||||
--plus-bg: rgba(var(--green-600), 0.4);
|
||||
--plus-bg-hover: rgb(var(--green-600));
|
||||
|
||||
/* Tag name */
|
||||
--tag-hash: rgb(var(--grey-500));
|
||||
--tag-name: rgb(var(--grey-50));
|
||||
--add-tag-hash: rgb(var(--grey-200));
|
||||
|
||||
/***********************************************
|
||||
** Utilities ***********************************
|
||||
***********************************************/
|
||||
|
|
|
|||
|
|
@ -31,9 +31,18 @@
|
|||
}
|
||||
|
||||
.mod-container {
|
||||
hr {
|
||||
width: 100% !important;
|
||||
max-width: none !important;
|
||||
height: 1px;
|
||||
margin: 8px 0px 8px 0px !important;
|
||||
padding: 0px;
|
||||
background-color: var(--grey-200);
|
||||
}
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: var(--base-0);
|
||||
background-color: var(--base-inverted);
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
margin-bottom: 0;
|
||||
|
|
@ -201,6 +210,20 @@
|
|||
}
|
||||
}
|
||||
|
||||
.max-tags-text {
|
||||
display: flex;
|
||||
padding: var(--su-2) var(--su-3);
|
||||
align-items: center;
|
||||
gap: var(--su-2);
|
||||
|
||||
span {
|
||||
color: var(--color-secondary);
|
||||
font-size: var(--fs-s);
|
||||
line-height: var(--lh-base);
|
||||
letter-spacing: -0.28px;
|
||||
}
|
||||
}
|
||||
|
||||
.additional-subtext-section {
|
||||
font-weight: var(--fw-medium);
|
||||
font-size: var(--fs-xs);
|
||||
|
|
@ -375,49 +398,133 @@
|
|||
.add-tag-container {
|
||||
padding: var(--su-2) var(--su-1);
|
||||
}
|
||||
.add-tag-header {
|
||||
color: var(--tag-name);
|
||||
font-size: var(--fs-base);
|
||||
font-weight: var(--fw-bold);
|
||||
line-height: var(--lh-base);
|
||||
}
|
||||
.tag-input-container {
|
||||
display: flex;
|
||||
margin-top: 12px;
|
||||
padding: 8px 8px 8px 12px;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
align-self: stretch;
|
||||
border-radius: 6px;
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
border-color: var(--grey-500);
|
||||
|
||||
span {
|
||||
font-weight: var(--fw-medium);
|
||||
font-size: var(--fs-xl);
|
||||
color: var(--add-tag-hash);
|
||||
}
|
||||
input {
|
||||
border: none;
|
||||
outline: none;
|
||||
background-color: transparent;
|
||||
color: var(--grey-500);
|
||||
font-size: var(--fs-base);
|
||||
line-height: var(--ls-base);
|
||||
}
|
||||
.input:active,
|
||||
.input:focus {
|
||||
border: none;
|
||||
outline: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
button.adjustable-tag {
|
||||
background: inherit;
|
||||
background-color: inherit;
|
||||
border: none;
|
||||
padding: var(--su-2) 2px;
|
||||
padding: var(--su-2) var(--su-3);
|
||||
font-family: var(--ff-monospace);
|
||||
font-size: var(--fs-base);
|
||||
text-align: left;
|
||||
color: var(--base-80);
|
||||
font-family: var(--ff-monospace);
|
||||
.num-sign {
|
||||
color: var(--base-40);
|
||||
padding-left: 2px;
|
||||
line-height: var(--lh-base);
|
||||
border-radius: var(--radius);
|
||||
letter-spacing: -0.32px;
|
||||
color: var(--tag-name);
|
||||
|
||||
.js-focus-visible &.focus-visible:focus,
|
||||
&:is(label):focus-within {
|
||||
z-index: var(--z-elevate);
|
||||
}
|
||||
.adjustment-icon {
|
||||
|
||||
.js-focus-visible &.focus-visible:focus,
|
||||
&:is(label):focus-within {
|
||||
box-shadow: var(--focus-ring);
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
.num-sign {
|
||||
color: var(--tag-hash);
|
||||
padding-left: 2px;
|
||||
padding-right: 2px;
|
||||
}
|
||||
.remove-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
float: right;
|
||||
margin-right: 2px;
|
||||
&.subtract {
|
||||
background: var(--accent-danger);
|
||||
}
|
||||
&.plus {
|
||||
background: var(--accent-success-darker);
|
||||
}
|
||||
svg > path {
|
||||
fill: var(--base-inverted);
|
||||
}
|
||||
background-color: var(--subtract-bg);
|
||||
color: var(--subtract-color);
|
||||
}
|
||||
&:hover {
|
||||
background: var(--base-10);
|
||||
border-radius: 8px;
|
||||
background-color: var(--tag-item-bg-hover);
|
||||
border-radius: 6px;
|
||||
.remove-icon {
|
||||
opacity: 1;
|
||||
background-color: var(--subtract-bg-hover);
|
||||
color: var(--subtract-color-hover);
|
||||
}
|
||||
}
|
||||
&.active {
|
||||
background: var(--base-10);
|
||||
border-radius: 8px;
|
||||
}
|
||||
// &.selected {}
|
||||
}
|
||||
.adjustment-reason-container {
|
||||
button.add-tag {
|
||||
background-color: inherit;
|
||||
border: none;
|
||||
padding: var(--su-2) var(--su-3);
|
||||
font-size: var(--fs-base);
|
||||
text-align: left;
|
||||
color: var(--base-80);
|
||||
border-radius: var(--radius);
|
||||
font-family: var(--ff-monospace);
|
||||
.add-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
float: right;
|
||||
margin-right: 2px;
|
||||
background: var(--plus-bg);
|
||||
color: var(--plus-color);
|
||||
}
|
||||
&:hover {
|
||||
background-color: var(--tag-item-bg-hover);
|
||||
border-radius: 6px;
|
||||
.add-icon {
|
||||
background-color: var(--plus-bg-hover);
|
||||
color: var(--plus-color-hover);
|
||||
}
|
||||
}
|
||||
|
||||
.js-focus-visible &.focus-visible:focus,
|
||||
&:is(label):focus-within {
|
||||
z-index: var(--z-elevate);
|
||||
}
|
||||
|
||||
.js-focus-visible &.focus-visible:focus,
|
||||
&:is(label):focus-within {
|
||||
box-shadow: var(--focus-ring);
|
||||
outline: 0;
|
||||
}
|
||||
}
|
||||
.reason-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: var(--su-1);
|
||||
padding: var(--su-1) var(--su-3) var(--su-2) var(--su-3);
|
||||
textarea {
|
||||
margin-bottom: var(--su-2);
|
||||
margin-bottom: var(--su-3);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,19 @@
|
|||
/* eslint-disable no-restricted-globals */
|
||||
import fetch from 'jest-fetch-mock';
|
||||
import { fireEvent } from '@testing-library/preact';
|
||||
import {
|
||||
addCloseListener,
|
||||
initializeHeight,
|
||||
addReactionButtonListeners,
|
||||
addAdjustTagListeners,
|
||||
handleAddTagButtonListeners,
|
||||
handleAddModTagButtonsListeners,
|
||||
handleRemoveTagButtonsListeners,
|
||||
} from '../actionsPanel';
|
||||
|
||||
global.fetch = fetch;
|
||||
global.top.addSnackbarItem = jest.fn();
|
||||
global.alert = jest.fn();
|
||||
|
||||
describe('addCloseListener()', () => {
|
||||
test('toggles the mod actions panel and its button on click', () => {
|
||||
document.body.innerHTML = `
|
||||
|
|
@ -152,72 +159,288 @@ describe('addReactionButtonListeners()', () => {
|
|||
});
|
||||
|
||||
describe('addAdjustTagListeners()', () => {
|
||||
describe('when the user is tag moderator of #discuss', () => {
|
||||
describe('when an article has no tags', () => {
|
||||
beforeEach(() => {
|
||||
const tagName = 'discuss';
|
||||
document.body.innerHTML = `
|
||||
<a href="/t/${tagName}" class="tag">${tagName}</a>
|
||||
<button class="adjustable-tag" data-adjustment-type="subtract" data-tag-name="${tagName}">
|
||||
#${tagName}
|
||||
</button>
|
||||
<form id="adjustment-reason-container" class="adjustment-reason-container">
|
||||
<textarea class="crayons-textfield" placeholder="Reason for tag adjustment (required)" id="tag-adjustment-reason" required></textarea>
|
||||
<button class="crayons-btn" id="tag-adjust-submit" type="submit">Submit</button>
|
||||
</form>
|
||||
`;
|
||||
fetch.resetMocks();
|
||||
|
||||
addAdjustTagListeners();
|
||||
document.body.innerHTML = `
|
||||
<button id="add-tag-button" class="add-tag" type="button">
|
||||
Add new tag
|
||||
</button>
|
||||
<div id="add-tag-container" class="hidden">
|
||||
<div id="add-reason-container" class="reason-container">
|
||||
<input id="admin-add-tag" class="crayons-textfield" type="text" autocomplete="off" placeholder="Add a tag" data-article-id="32" data-adjustment-type="plus">
|
||||
<textarea class="crayons-textfield" placeholder="Reason to add tag (optional)" id="tag-add-reason"></textarea>
|
||||
<div class="flex gap-3">
|
||||
<button class="w-100 c-btn c-btn--primary" disabled="disabled" id="tag-add-submit">Submit</button>
|
||||
<button class="w-100 c-btn" id="cancel-add-tag-button" type="button">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
handleAddTagButtonListeners();
|
||||
});
|
||||
describe('when an article is tagged with #discuss', () => {
|
||||
it('toggles the tag button and the form', () => {
|
||||
const tagBtn = document.getElementsByClassName('adjustable-tag')[0];
|
||||
tagBtn.click();
|
||||
expect(tagBtn.classList).toContain('active');
|
||||
|
||||
function tagResponse() {
|
||||
return JSON.stringify({
|
||||
status: 'Success',
|
||||
result: 'addition',
|
||||
colors: {
|
||||
bg: '#8c5595',
|
||||
text: '#39ad55',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
it('shows the add tag button', () => {
|
||||
expect(document.getElementById('add-tag-button').classList).not.toContain(
|
||||
'hidden',
|
||||
);
|
||||
|
||||
expect(document.getElementById('add-tag-container').classList).toContain(
|
||||
'hidden',
|
||||
);
|
||||
});
|
||||
|
||||
it('click on add tag button shows the container and hides the button', () => {
|
||||
document.getElementById('add-tag-button').click();
|
||||
expect(document.getElementById('add-tag-button').classList).toContain(
|
||||
'hidden',
|
||||
);
|
||||
|
||||
expect(
|
||||
document.getElementById('add-tag-container').classList,
|
||||
).not.toContain('hidden');
|
||||
});
|
||||
|
||||
it('by default the add tag submit button is disabled and gets enabled when entered tag', () => {
|
||||
document.getElementById('add-tag-button').click();
|
||||
expect(
|
||||
document.getElementById('tag-add-submit').hasAttribute('disabled'),
|
||||
).toBeTruthy();
|
||||
|
||||
fireEvent.input(document.getElementById('admin-add-tag'), {
|
||||
target: { value: 'New Tag' },
|
||||
});
|
||||
|
||||
expect(
|
||||
document.getElementById('tag-add-submit').hasAttribute('disabled'),
|
||||
).toBeFalsy();
|
||||
});
|
||||
|
||||
it('click on cancel button hides the container and shows add-tag button', () => {
|
||||
document.getElementById('add-tag-button').click();
|
||||
document.getElementById('cancel-add-tag-button').click();
|
||||
expect(document.getElementById('add-tag-button').classList).not.toContain(
|
||||
'hidden',
|
||||
);
|
||||
|
||||
expect(document.getElementById('add-tag-container').classList).toContain(
|
||||
'hidden',
|
||||
);
|
||||
});
|
||||
|
||||
it('click on add-tag button hides the container and shows add-tag button', async () => {
|
||||
fetch.mockResponseOnce(tagResponse());
|
||||
|
||||
const addTagButton = document.getElementById('add-tag-button');
|
||||
|
||||
addTagButton.click();
|
||||
|
||||
fireEvent.input(document.getElementById('admin-add-tag'), {
|
||||
target: { value: 'New Tag' },
|
||||
});
|
||||
expect(
|
||||
document.getElementById('tag-add-submit').hasAttribute('disabled'),
|
||||
).toBeFalsy();
|
||||
|
||||
document.getElementById('tag-add-reason').value = 'Adding a new tag';
|
||||
document.getElementById('tag-add-submit').click();
|
||||
|
||||
expect(fetch).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('shows the adjustment container when admin input is focused', () => {
|
||||
document.getElementById('add-tag-button').click();
|
||||
document.getElementById('admin-add-tag').value = 'pizza';
|
||||
document.getElementById('tag-add-reason').value = 'Adding a new tag';
|
||||
document.getElementById('tag-add-submit').click();
|
||||
|
||||
expect(
|
||||
document.getElementById('add-reason-container').classList,
|
||||
).not.toContain('hidden');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the user is an admin and the article has room for tags', () => {
|
||||
describe('tag adjustment interactions', () => {
|
||||
beforeEach(() => {
|
||||
const tagName = 'discuss';
|
||||
document.body.innerHTML = `
|
||||
<div class="add-tag-container">
|
||||
<input id="admin-add-tag" class="crayons-textfield" type="text" placeholder="Add a tag" data-article-id="1" data-adjustment-type="plus">
|
||||
describe('article has 1 tag', () => {
|
||||
const discussTag = 'discuss';
|
||||
|
||||
function removeTagResponse() {
|
||||
return JSON.stringify({
|
||||
status: 'Success',
|
||||
result: 'removal',
|
||||
colors: {
|
||||
bg: '#8c5595',
|
||||
text: '#39ad55',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
fetch.resetMocks();
|
||||
|
||||
document.body.innerHTML = `
|
||||
<button id="remove-tag-button-${discussTag}" class="adjustable-tag" type="button" data-adjustment-type="subtract" data-tag-name="${discussTag}" data-article-id="32">
|
||||
<span class="num-sign">#</span>${discussTag}
|
||||
<div id="remove-tag-icon-${discussTag}" class="circle centered-icon adjustment-icon subtract color-base-inverted hidden">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" class="crayons-icon" role="img"><title id="random">Remove tag</title>
|
||||
<path d="M19 11H5v2h14v-2Z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
<div id="remove-tag-container-${discussTag}" class="hidden">
|
||||
<div id="adjustment-reason-container-${discussTag}" class="reason-container">
|
||||
<textarea class="crayons-textfield" placeholder="Reason to remove tag (optional)" id="tag-removal-reason-${discussTag}"></textarea>
|
||||
<div class="flex gap-3">
|
||||
<button class="crayons-btn" id="remove-tag-submit-${discussTag}">Submit</button>
|
||||
<button class="c-btn" id="cancel-remove-tag-button-${discussTag}" type="button">Cancel</button>
|
||||
</div>
|
||||
<a href="/t/${tagName}" class="tag">${tagName}</a>
|
||||
<button class="adjustable-tag" data-adjustment-type="subtract" data-tag-name="${tagName}">
|
||||
#${tagName}
|
||||
</button>
|
||||
<button class="adjustable-tag" data-adjustment-type="subtract" data-tag-name="ruby">
|
||||
#ruby
|
||||
</button>
|
||||
<form id="adjustment-reason-container" class="adjustment-reason-container">
|
||||
<textarea class="crayons-textfield" placeholder="Reason for tag adjustment" id="tag-adjustment-reason" required></textarea>
|
||||
<button class="crayons-btn" id="tag-adjust-submit" type="submit">Submit</button>
|
||||
</form>
|
||||
`;
|
||||
addAdjustTagListeners();
|
||||
});
|
||||
it('shows the adjustment container when admin input is focused', () => {
|
||||
document.getElementById('admin-add-tag').focus();
|
||||
expect(
|
||||
document.getElementById('adjustment-reason-container').classList,
|
||||
).not.toContain('hidden');
|
||||
});
|
||||
it('triggers a confirmation if the admin add tag input was filled in', () => {
|
||||
window.confirm = jest.fn();
|
||||
document.getElementById('admin-add-tag').value = 'pizza';
|
||||
document.querySelector('.adjustable-tag[data-tag-name="ruby"]').click();
|
||||
expect(window.confirm).toHaveBeenCalled();
|
||||
});
|
||||
it('does not the hide reason container when going from one tag to another tag', () => {
|
||||
document.getElementsByClassName('adjustable-tag')[0].click();
|
||||
document.querySelector('.adjustable-tag[data-tag-name="ruby"]').click();
|
||||
expect(
|
||||
document.getElementById('adjustment-reason-container').classList,
|
||||
).not.toContain('hidden');
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
handleRemoveTagButtonsListeners();
|
||||
});
|
||||
|
||||
it('remove tag container is hidden', () => {
|
||||
expect(
|
||||
document.getElementById(`remove-tag-container-${discussTag}`).classList,
|
||||
).toContain('hidden');
|
||||
});
|
||||
|
||||
it('hides remove icon on button click', () => {
|
||||
document.getElementById(`remove-tag-button-${discussTag}`).click();
|
||||
|
||||
expect(
|
||||
document.getElementById(`remove-tag-icon-${discussTag}`).style.display,
|
||||
).toEqual('none');
|
||||
});
|
||||
|
||||
it('shows tag container on button click', () => {
|
||||
document.getElementById(`remove-tag-button-${discussTag}`).click();
|
||||
|
||||
expect(
|
||||
document.getElementById(`remove-tag-container-${discussTag}`).classList,
|
||||
).not.toContain('hidden');
|
||||
});
|
||||
|
||||
it('click on cancel button hides the container', () => {
|
||||
document.getElementById(`remove-tag-button-${discussTag}`).click();
|
||||
document.getElementById(`cancel-remove-tag-button-${discussTag}`).click();
|
||||
|
||||
expect(
|
||||
document.getElementById(`remove-tag-container-${discussTag}`).classList,
|
||||
).toContain('hidden');
|
||||
});
|
||||
|
||||
it('click on remove button should successfully remove the item', async () => {
|
||||
fetch.mockResponseOnce(removeTagResponse());
|
||||
|
||||
document.getElementById(`remove-tag-button-${discussTag}`).click();
|
||||
document.getElementById(`tag-removal-reason-${discussTag}`).value =
|
||||
'Removing a tag';
|
||||
document.getElementById(`remove-tag-submit-${discussTag}`).click();
|
||||
|
||||
expect(fetch).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('tag moderator role', () => {
|
||||
const discussTag = 'discuss';
|
||||
|
||||
function addModTagResponse() {
|
||||
return JSON.stringify({
|
||||
status: 'Success',
|
||||
result: 'addition',
|
||||
colors: {
|
||||
bg: '#8c5595',
|
||||
text: '#39ad55',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
fetch.resetMocks();
|
||||
|
||||
document.body.innerHTML = `
|
||||
<button
|
||||
id="add-tag-button-${discussTag}"
|
||||
class="adjustable-tag add-tag" type="button"
|
||||
data-adjustment-type="plus"
|
||||
data-tag-name="${discussTag}"
|
||||
data-article-id="<%= @moderatable.id %>">
|
||||
<span class="num-sign">#</span>${discussTag}
|
||||
<div id="add-tag-icon-${discussTag}" class="circle centered-icon add-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" class="crayons-icon" role="img"><title id="random">Remove tag</title>
|
||||
<path d="M19 11H5v2h14v-2Z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
<div id="add-tag-container-${discussTag}" class="hidden">
|
||||
<div id="add-reason-container-${discussTag}" class="reason-container">
|
||||
<textarea id="tag-add-reason-${discussTag}" class="crayons-textfield mt-3" placeholder="<%= t("views.moderations.actions.adjust.reason.add_tag") %>"></textarea>
|
||||
<div class="flex gap-3">
|
||||
<button class="w-100 c-btn c-btn--primary" id="tag-add-submit-${discussTag}"><%= t("views.moderations.actions.adjust.add") %></button>
|
||||
<button class="w-100 c-btn" id="cancel-add-tag-button-${discussTag}" type="button"><%= t("views.moderations.actions.adjust.cancel") %></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
handleAddModTagButtonsListeners();
|
||||
});
|
||||
|
||||
it('add moderator tag container is hidden', () => {
|
||||
expect(
|
||||
document.getElementById(`add-tag-container-${discussTag}`).classList,
|
||||
).toContain('hidden');
|
||||
});
|
||||
|
||||
it('hides add icon on button click', () => {
|
||||
document.getElementById(`add-tag-button-${discussTag}`).click();
|
||||
|
||||
expect(
|
||||
document.getElementById(`add-tag-icon-${discussTag}`).style.display,
|
||||
).toEqual('none');
|
||||
});
|
||||
|
||||
it('shows tag container on button click', () => {
|
||||
document.getElementById(`add-tag-button-${discussTag}`).click();
|
||||
|
||||
expect(
|
||||
document.getElementById(`add-tag-container-${discussTag}`).classList,
|
||||
).not.toContain('hidden');
|
||||
});
|
||||
|
||||
it('click on cancel button hides the container', () => {
|
||||
document.getElementById(`add-tag-button-${discussTag}`).click();
|
||||
document.getElementById(`cancel-add-tag-button-${discussTag}`).click();
|
||||
|
||||
expect(
|
||||
document.getElementById(`add-tag-container-${discussTag}`).classList,
|
||||
).toContain('hidden');
|
||||
});
|
||||
|
||||
it('click on add button should successfully add the item', async () => {
|
||||
fetch.mockResponseOnce(addModTagResponse());
|
||||
|
||||
document.getElementById(`add-tag-button-${discussTag}`).click();
|
||||
document.getElementById(`tag-add-reason-${discussTag}`).value =
|
||||
'Add a tag';
|
||||
document.getElementById(`tag-add-submit-${discussTag}`).click();
|
||||
|
||||
expect(fetch).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -6,6 +6,11 @@ import { request } from '@utilities/http';
|
|||
|
||||
export function addCloseListener() {
|
||||
const button = document.getElementsByClassName('close-actions-panel')[0];
|
||||
const parentPath = window.parent.location.pathname;
|
||||
if (!parentPath.startsWith('/mod')) {
|
||||
button.classList.remove('hidden');
|
||||
}
|
||||
|
||||
button.addEventListener('click', () => {
|
||||
// getting the article show page document because this is called within an iframe
|
||||
// eslint-disable-next-line no-restricted-globals
|
||||
|
|
@ -182,10 +187,6 @@ const adminFeatureArticle = async (id, featured) => {
|
|||
}
|
||||
};
|
||||
|
||||
function clearAdjustmentReason() {
|
||||
document.getElementById('tag-adjustment-reason').value = '';
|
||||
}
|
||||
|
||||
function renderTagOnArticle(tagName, colors) {
|
||||
const articleTagsContainer =
|
||||
getArticleContainer().getElementsByClassName('spec__tags')[0];
|
||||
|
|
@ -208,24 +209,31 @@ function getArticleContainer() {
|
|||
: window.parent.document.getElementById('main-content');
|
||||
}
|
||||
|
||||
async function adjustTag(el) {
|
||||
const reasonForAdjustment = document.getElementById(
|
||||
'tag-adjustment-reason',
|
||||
).value;
|
||||
/**
|
||||
* This function sends an asynchronous request to the server to add or remove
|
||||
* a specific tag from an article.
|
||||
*/
|
||||
async function adjustTag(el, reasonElement) {
|
||||
const tagName = el.dataset.tagName || el.value;
|
||||
const body = {
|
||||
tag_adjustment: {
|
||||
// TODO: change to tag ID
|
||||
tag_name: el.dataset.tagName || el.value,
|
||||
tag_name: tagName,
|
||||
article_id: el.dataset.articleId,
|
||||
adjustment_type:
|
||||
el.dataset.adjustmentType === 'subtract' ? 'removal' : 'addition',
|
||||
reason_for_adjustment: reasonForAdjustment,
|
||||
reason_for_adjustment: reasonElement.value,
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await request('/tag_adjustments', {
|
||||
const response = await fetch('/tag_adjustments', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'X-CSRF-Token': window.csrfToken,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
|
|
@ -235,15 +243,12 @@ async function adjustTag(el) {
|
|||
let adjustedTagName;
|
||||
if (el.tagName === 'BUTTON') {
|
||||
adjustedTagName = el.dataset.tagName;
|
||||
el.remove();
|
||||
} else {
|
||||
adjustedTagName = el.value;
|
||||
// eslint-disable-next-line no-param-reassign, require-atomic-updates
|
||||
el.value = '';
|
||||
}
|
||||
|
||||
clearAdjustmentReason();
|
||||
|
||||
if (outcome.result === 'addition') {
|
||||
renderTagOnArticle(adjustedTagName, outcome.colors);
|
||||
} else {
|
||||
|
|
@ -259,6 +264,7 @@ async function adjustTag(el) {
|
|||
}.`,
|
||||
addCloseButton: true,
|
||||
});
|
||||
window.location.reload();
|
||||
} else {
|
||||
// eslint-disable-next-line no-restricted-globals
|
||||
top.addSnackbarItem({
|
||||
|
|
@ -272,81 +278,174 @@ async function adjustTag(el) {
|
|||
}
|
||||
}
|
||||
|
||||
export function handleAdjustTagBtn(btn) {
|
||||
const currentActiveTags = document.querySelectorAll(
|
||||
'button.adjustable-tag.active',
|
||||
export function handleAddModTagButton(btn) {
|
||||
const { tagName } = btn.dataset;
|
||||
const addButton = document.getElementById(`add-tag-button-${tagName}`);
|
||||
const addIcon = document.getElementById(`add-tag-icon-${tagName}`);
|
||||
const addTagContainer = document.getElementById(
|
||||
`add-tag-container-${tagName}`,
|
||||
);
|
||||
const adminTagInput = document.getElementById('admin-add-tag');
|
||||
/* eslint-disable no-restricted-globals */
|
||||
/* eslint-disable no-alert */
|
||||
if (
|
||||
adminTagInput &&
|
||||
adminTagInput.value !== '' &&
|
||||
confirm(
|
||||
'This will clear your current "Add a tag" input. Do you want to continue?',
|
||||
)
|
||||
) {
|
||||
/* eslint-enable no-restricted-globals */
|
||||
/* eslint-enable no-alert */
|
||||
adminTagInput.value = '';
|
||||
} else if (currentActiveTags.length > 0) {
|
||||
currentActiveTags.forEach((tag) => {
|
||||
if (tag !== btn) {
|
||||
tag.classList.remove('active');
|
||||
}
|
||||
btn.classList.toggle('active');
|
||||
});
|
||||
|
||||
const containerIsVisible = addTagContainer.classList.contains('hidden');
|
||||
if (containerIsVisible) {
|
||||
addIcon.style.display = 'none';
|
||||
addTagContainer.classList.remove('hidden');
|
||||
addButton.classList.add('fw-bold');
|
||||
addButton.classList.remove('fw-normal');
|
||||
} else {
|
||||
btn.classList.toggle('active');
|
||||
addIcon.style.display = 'flex';
|
||||
addTagContainer.classList.add('hidden');
|
||||
addButton.classList.remove('fw-bold');
|
||||
addButton.classList.add('fw-normal');
|
||||
}
|
||||
|
||||
const cancelAddModTagButton = document.getElementById(
|
||||
`cancel-add-tag-button-${tagName}`,
|
||||
);
|
||||
cancelAddModTagButton.addEventListener('click', () => {
|
||||
handleAddModTagButton(btn);
|
||||
});
|
||||
|
||||
const addTagButton = document.getElementById(`tag-add-submit-${tagName}`);
|
||||
if (addTagButton) {
|
||||
addTagButton.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
const dataSource = document.getElementById(`add-tag-button-${tagName}`);
|
||||
const reasonFoRemoval = document.getElementById(
|
||||
`tag-add-reason-${tagName}`,
|
||||
);
|
||||
adjustTag(dataSource, reasonFoRemoval);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function handleAdminInput() {
|
||||
const addTagInput = document.getElementById('admin-add-tag');
|
||||
/**
|
||||
* Handles various listeners required to handle remove tag functionality.
|
||||
*/
|
||||
export function handleRemoveTagButton(btn) {
|
||||
const { tagName } = btn.dataset;
|
||||
|
||||
if (addTagInput) {
|
||||
addTagInput.addEventListener('focus', () => {
|
||||
const activeTagBtns = Array.from(
|
||||
document.querySelectorAll('button.adjustable-tag.active'),
|
||||
const removeButton = document.getElementById(`remove-tag-button-${tagName}`);
|
||||
const removeIcon = document.getElementById(`remove-tag-icon-${tagName}`);
|
||||
const removeTagContainer = document.getElementById(
|
||||
`remove-tag-container-${tagName}`,
|
||||
);
|
||||
|
||||
if (!(removeButton && removeIcon && removeTagContainer)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const containerIsVisible = removeTagContainer?.classList.contains('hidden');
|
||||
if (containerIsVisible) {
|
||||
removeIcon.style.display = 'none';
|
||||
removeTagContainer.classList.remove('hidden');
|
||||
removeButton.classList.add('fw-bold');
|
||||
removeButton.classList.remove('fw-normal');
|
||||
} else {
|
||||
removeIcon.style.display = 'flex';
|
||||
removeTagContainer.classList.add('hidden');
|
||||
removeButton.classList.remove('fw-bold');
|
||||
removeButton.classList.add('fw-normal');
|
||||
}
|
||||
|
||||
const cancelRemoveTagButton = document.getElementById(
|
||||
`cancel-remove-tag-button-${tagName}`,
|
||||
);
|
||||
cancelRemoveTagButton.addEventListener('click', () => {
|
||||
handleRemoveTagButton(btn);
|
||||
});
|
||||
|
||||
const removeTagButton = document.getElementById(
|
||||
`remove-tag-submit-${tagName}`,
|
||||
);
|
||||
if (removeTagButton) {
|
||||
removeTagButton.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const dataSource = document.getElementById(
|
||||
`remove-tag-button-${tagName}`,
|
||||
);
|
||||
activeTagBtns.forEach((btn) => {
|
||||
btn.classList.remove('active');
|
||||
});
|
||||
const reasonFoRemoval = document.getElementById(
|
||||
`tag-removal-reason-${tagName}`,
|
||||
);
|
||||
adjustTag(dataSource, reasonFoRemoval);
|
||||
});
|
||||
addTagInput.addEventListener('focusout', () => {
|
||||
if (addTagInput.value === '') {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles various listeners required to handle add tag functionality.
|
||||
*/
|
||||
export function handleAddTagButtonListeners() {
|
||||
const inputTag = document.getElementById('admin-add-tag');
|
||||
const submitButton = document.getElementById('tag-add-submit');
|
||||
|
||||
if (inputTag) {
|
||||
inputTag.addEventListener('input', () => {
|
||||
if (inputTag.value.trim().length > 0) {
|
||||
submitButton.removeAttribute('disabled');
|
||||
} else {
|
||||
submitButton.setAttribute('disabled', 'disabled');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const addTagButton = document.getElementById('add-tag-button');
|
||||
|
||||
if (addTagButton) {
|
||||
addTagButton.addEventListener('click', () => {
|
||||
const addTagContainer = document.getElementById('add-tag-container');
|
||||
addTagContainer.classList.remove('hidden');
|
||||
addTagButton.classList.add('hidden');
|
||||
});
|
||||
|
||||
const cancelAddTagButton = document.getElementById('cancel-add-tag-button');
|
||||
|
||||
if (cancelAddTagButton) {
|
||||
cancelAddTagButton.addEventListener('click', () => {
|
||||
const addTagContainer = document.getElementById('add-tag-container');
|
||||
addTagContainer.classList.add('hidden');
|
||||
addTagButton.classList.remove('hidden');
|
||||
});
|
||||
}
|
||||
|
||||
const addTagSubmitButton = document.getElementById('tag-add-submit');
|
||||
if (addTagSubmitButton) {
|
||||
addTagSubmitButton.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const dataSource = document.getElementById('admin-add-tag');
|
||||
const reasonFoAddition = document.getElementById('tag-add-reason');
|
||||
adjustTag(dataSource, reasonFoAddition);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function addAdjustTagListeners() {
|
||||
Array.from(document.getElementsByClassName('adjustable-tag')).forEach(
|
||||
export function handleAddModTagButtonsListeners() {
|
||||
Array.from(document.getElementsByClassName('adjustable-tag add-tag')).forEach(
|
||||
(btn) => {
|
||||
btn.addEventListener('click', () => {
|
||||
handleAdjustTagBtn(btn);
|
||||
handleAddModTagButton(btn);
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const form = document.getElementById('tag-adjust-submit')?.form;
|
||||
if (form) {
|
||||
form.addEventListener('submit', (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const dataSource =
|
||||
document.querySelector('button.adjustable-tag.active') ??
|
||||
document.getElementById('admin-add-tag');
|
||||
|
||||
adjustTag(dataSource);
|
||||
});
|
||||
|
||||
handleAdminInput();
|
||||
}
|
||||
export function handleRemoveTagButtonsListeners() {
|
||||
Array.from(document.getElementsByClassName('adjustable-tag')).forEach(
|
||||
(btn) => {
|
||||
btn.addEventListener('click', () => {
|
||||
handleRemoveTagButton(btn);
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export function addModActionsListeners() {
|
||||
addAdjustTagListeners();
|
||||
handleAddTagButtonListeners();
|
||||
handleAddModTagButtonsListeners();
|
||||
handleRemoveTagButtonsListeners();
|
||||
Array.from(document.getElementsByClassName('other-things-btn')).forEach(
|
||||
(btn) => {
|
||||
btn.addEventListener('click', () => {
|
||||
|
|
|
|||
|
|
@ -170,21 +170,7 @@ class ArticlePolicy < ApplicationPolicy
|
|||
|
||||
def tag_moderator_eligible?
|
||||
tag_ids_moderated_by_user = Tag.with_role(:tag_moderator, @user).ids
|
||||
return false unless tag_ids_moderated_by_user.size.positive?
|
||||
|
||||
adjustments = TagAdjustment.where(article_id: @record.id)
|
||||
has_room_for_tags = @record.tag_list.size < MAX_TAG_LIST_SIZE
|
||||
# ensures that mods cannot adjust an already-adjusted tag
|
||||
# "zero?" because intersection has just one integer (0 or 1)
|
||||
has_no_relevant_adjustments = adjustments.pluck(:tag_id).intersection(tag_ids_moderated_by_user).empty?
|
||||
|
||||
# tag_mod can add their moderated tags
|
||||
return true if has_room_for_tags && has_no_relevant_adjustments
|
||||
|
||||
authorized_to_adjust = @record.tags.ids.intersection(tag_ids_moderated_by_user).size.positive?
|
||||
|
||||
# tag_mod can remove their moderated tags
|
||||
!has_room_for_tags && has_no_relevant_adjustments && authorized_to_adjust
|
||||
tag_ids_moderated_by_user.size.positive?
|
||||
end
|
||||
|
||||
def destroy?
|
||||
|
|
|
|||
|
|
@ -93,7 +93,7 @@
|
|||
|
||||
<%# Checking if the user should see the "Adjust tags" dropdown %>
|
||||
<% if policy(@moderatable).allow_tag_adjustment? %>
|
||||
<button aria-haspopup="true" aria-expanded="false" aria-controls="adjust-tags-options" class="other-things-btn adjust-tags" type="button" data-other-things-type="adjust-tags" aria-label="<%= t("views.moderations.actions.adjust.aria_label") %>">
|
||||
<button id="adjust-tags-button" aria-haspopup="true" aria-expanded="false" aria-controls="adjust-tags-options" class="other-things-btn adjust-tags" type="button" data-other-things-type="adjust-tags" aria-label="<%= t("views.moderations.actions.adjust.aria_label") %>">
|
||||
<div class="label-wrapper">
|
||||
<div class="icon circle centered-icon">
|
||||
<%= crayons_icon_tag("mod-filled", title: t("views.moderations.actions.adjust.icon")) %>
|
||||
|
|
@ -109,37 +109,100 @@
|
|||
</button>
|
||||
|
||||
<div id="adjust-tags-options" class="adjust-tags-options dropdown-options hidden">
|
||||
<% if policy(@moderatable).can_adjust_any_tag? %>
|
||||
<% if @moderatable.tag_list.size < Article::MAX_TAG_LIST_SIZE %>
|
||||
<div class="add-tag-container">
|
||||
<input id="admin-add-tag" class="crayons-textfield" type="text" autocomplete="off" placeholder="<%= t("views.moderations.actions.adjust.add_placeholder") %>" data-article-id="<%= @moderatable.id %>" data-adjustment-type="plus">
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<% @moderatable.tags.each do |tag| %>
|
||||
<button class="adjustable-tag" type="button" data-adjustment-type="subtract" data-tag-name="<%= tag.name %>" data-article-id="<%= @moderatable.id %>">
|
||||
<span class="num-sign">#</span><%= tag.name %>
|
||||
<div class="circle centered-icon adjustment-icon subtract color-base-inverted">
|
||||
<% @moderatable.tags.each do |tag| %>
|
||||
<% moderatable_tag = @tag_moderator_tags.pluck(:name).include?(tag.name) || policy(@moderatable).can_adjust_any_tag? %>
|
||||
<button
|
||||
id="remove-tag-button-<%= tag.name %>"
|
||||
class="adjustable-tag" type="button"
|
||||
data-adjustment-type="subtract"
|
||||
data-tag-name="<%= tag.name %>"
|
||||
data-article-id="<%= @moderatable.id %>"
|
||||
title="<%= moderatable_tag ? "" : "Not allowed to adjust this tag" %>">
|
||||
<span class="num-sign">#</span><%= tag.name %>
|
||||
<% if moderatable_tag %>
|
||||
<div id="remove-tag-icon-<%= tag.name %>" class="circle centered-icon remove-icon hidden">
|
||||
<%= crayons_icon_tag(:minus, title: t("views.moderations.actions.adjust.remove")) %>
|
||||
</div>
|
||||
</button>
|
||||
<% end %>
|
||||
<% elsif policy(@moderatable).tag_moderator_eligible? %>
|
||||
<% @tag_moderator_tags.each do |tag| %>
|
||||
<% subtract_or_plus = @moderatable.tag_list.include?(tag.name) ? "subtract" : "plus" %>
|
||||
<% subtract_or_plus_title = t("views.moderations.actions.adjust.#{subtract_or_plus == 'subtract' ? 'remove' : 'add'}") %>
|
||||
<button class="adjustable-tag" type="button" data-adjustment-type="<%= subtract_or_plus %>" data-tag-name="<%= tag.name %>" data-article-id="<%= @moderatable.id %>">
|
||||
<span class="num-sign">#</span><%= tag.name %>
|
||||
<div class="circle centered-icon adjustment-icon <%= subtract_or_plus %>">
|
||||
<%= crayons_icon_tag(subtract_or_plus, title: subtract_or_plus_title) %>
|
||||
<% end %>
|
||||
</button>
|
||||
<% if moderatable_tag %>
|
||||
<div id="remove-tag-container-<%= tag.name %>" class="hidden">
|
||||
<div id="adjustment-reason-container-<%= tag.name %>" class="reason-container">
|
||||
<textarea class="crayons-textfield" placeholder="<%= t("views.moderations.actions.adjust.reason.remove_tag") %>" id="tag-removal-reason-<%= tag.name %>"></textarea>
|
||||
<div class="flex gap-3 align-items-stretch">
|
||||
<button class="w-100 c-btn c-btn--primary c-btn--destructive" id="remove-tag-submit-<%= tag.name %>"><%= t("views.moderations.actions.adjust.remove") %></button>
|
||||
<button class="w-100 c-btn" id="cancel-remove-tag-button-<%= tag.name %>" type="button"><%= t("views.moderations.actions.adjust.cancel") %></button>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<form id="adjustment-reason-container" class="adjustment-reason-container">
|
||||
<textarea class="crayons-textfield" placeholder="<%= t("views.moderations.actions.adjust.reason") %>" id="tag-adjustment-reason"></textarea>
|
||||
<button class="crayons-btn" id="tag-adjust-submit" type="submit"><%= t("views.moderations.actions.adjust.submit") %></button>
|
||||
</form>
|
||||
<% if @moderatable.tag_list.size < Article::MAX_TAG_LIST_SIZE %>
|
||||
<% if policy(@moderatable).can_adjust_any_tag? %>
|
||||
<hr />
|
||||
<button id="add-tag-button" data-testid="add-tag-button" class="add-tag" type="button">
|
||||
<%= t("views.moderations.actions.adjust.add_tag.button_text") %>
|
||||
<div class="circle centered-icon add-icon">
|
||||
<%= crayons_icon_tag(:plus, title: t("views.moderations.actions.adjust.add_tag.icon")) %>
|
||||
</div>
|
||||
</button>
|
||||
<div id="add-tag-container" class="hidden">
|
||||
<div id="add-reason-container" class="reason-container">
|
||||
<div class="add-tag-header"><%= t("views.moderations.actions.adjust.add_tag.header") %></div>
|
||||
<input id="admin-add-tag" class="crayons-textfield mt-3" type="text" autocomplete="off" placeholder="<%= t("views.moderations.actions.adjust.add_placeholder") %>" data-article-id="<%= @moderatable.id %>" data-adjustment-type="plus">
|
||||
<textarea id="tag-add-reason" class="crayons-textfield mt-3" placeholder="<%= t("views.moderations.actions.adjust.reason.add_tag") %>"></textarea>
|
||||
<div class="flex gap-3">
|
||||
<button class="w-100 c-btn c-btn--primary" disabled="disabled" id="tag-add-submit"><%= t("views.moderations.actions.adjust.add") %></button>
|
||||
<button class="w-100 c-btn" id="cancel-add-tag-button" type="button"><%= t("views.moderations.actions.adjust.cancel") %></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% else %>
|
||||
<% count = 0 %>
|
||||
<% @tag_moderator_tags.each do |tag| %>
|
||||
<% if !@moderatable.tags.pluck(:name).include?(tag.name) %>
|
||||
<% count += 1 %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<% if count > 0 %>
|
||||
<hr />
|
||||
<% end %>
|
||||
<% @tag_moderator_tags.each do |tag| %>
|
||||
<% if !@moderatable.tags.pluck(:name).include?(tag.name) %>
|
||||
<button
|
||||
id="add-tag-button-<%= tag.name %>"
|
||||
class="adjustable-tag add-tag" type="button"
|
||||
data-adjustment-type="plus"
|
||||
data-tag-name="<%= tag.name %>"
|
||||
data-article-id="<%= @moderatable.id %>">
|
||||
<span class="num-sign">#</span><%= tag.name %>
|
||||
<div id="add-tag-icon-<%= tag.name %>" class="circle centered-icon add-icon">
|
||||
<%= crayons_icon_tag(:plus, title: t("views.moderations.actions.adjust.add")) %>
|
||||
</div>
|
||||
</button>
|
||||
<div id="add-tag-container-<%= tag.name %>" class="hidden">
|
||||
<div id="add-reason-container-<%= tag.name %>" class="reason-container">
|
||||
<textarea id="tag-add-reason-<%= tag.name %>" class="crayons-textfield mt-3" placeholder="<%= t("views.moderations.actions.adjust.reason.add_tag") %>"></textarea>
|
||||
<div class="flex gap-3">
|
||||
<button class="w-100 c-btn c-btn--primary" id="tag-add-submit-<%= tag.name %>"><%= t("views.moderations.actions.adjust.add") %></button>
|
||||
<button class="w-100 c-btn" id="cancel-add-tag-button-<%= tag.name %>" type="button"><%= t("views.moderations.actions.adjust.cancel") %></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<hr />
|
||||
<% else %>
|
||||
<hr />
|
||||
<span class="max-tags-text">
|
||||
<%= crayons_icon_tag(:info, title: t("views.moderations.actions.adjust.maximum_tags.icon"), width: 18, height: 18) %>
|
||||
<span>
|
||||
<%= t("views.moderations.actions.adjust.maximum_tags.text") %>
|
||||
</span>
|
||||
</span>
|
||||
<% end %>
|
||||
<a href="<%= URL.url %>/tag-moderation#how-to-remove-or-add-tags" target="_blank" rel="noopener">
|
||||
<span class="additional-subtext-section py-3">
|
||||
<%= t("views.moderations.actions.how") %>
|
||||
|
|
|
|||
|
|
@ -19,9 +19,19 @@ en:
|
|||
icon: Adjust tags
|
||||
add: Add tag
|
||||
add_placeholder: Add a tag
|
||||
reason: Reason for tag adjustment (optional)
|
||||
reason:
|
||||
remove_tag: Reason to remove tag (optional)
|
||||
add_tag: Reason to add tag (optional)
|
||||
remove: Remove tag
|
||||
submit: Submit
|
||||
cancel: Cancel
|
||||
maximum_tags:
|
||||
icon: Info
|
||||
text: A post can only have four tags.
|
||||
add_tag:
|
||||
header: Add new tag
|
||||
button_text: Add new tag
|
||||
icon: Add tag
|
||||
approve:
|
||||
add: Mark as approved
|
||||
remove: Remove approval
|
||||
|
|
|
|||
|
|
@ -19,9 +19,19 @@ fr:
|
|||
icon: Ajuster les étiquettes
|
||||
add: Ajouter une étiquette
|
||||
add_placeholder: Ajouter une étiquette
|
||||
reason: Raison de l'ajustement de l'étiquette (optionnel)
|
||||
reason:
|
||||
remove_tag: Raison de supprimer la balise
|
||||
add_tag: Raison d'ajouter la balise (optionnel)
|
||||
remove: Retirer l'étiquette
|
||||
submit: Soumettre
|
||||
cancel: Annuler
|
||||
maximum_tags:
|
||||
icon: Info
|
||||
text: Une publication ne peut avoir que quatre balises.
|
||||
add_tag:
|
||||
header: Ajouter une nouvelle balise
|
||||
button_text: Ajouter une nouvelle balise
|
||||
icon: Ajouter une étiquette
|
||||
approve:
|
||||
add: Marquer comme approuvé
|
||||
remove: Retirer l'approbation
|
||||
|
|
|
|||
|
|
@ -41,8 +41,12 @@ describe('Moderation Tools for Posts', () => {
|
|||
.pipe(click)
|
||||
.should('have.attr', 'aria-expanded', 'true');
|
||||
|
||||
cy.findByRole('button', { name: '#tag1 Remove tag' }).click();
|
||||
cy.findByRole('button', { name: 'Submit' }).click();
|
||||
cy.findByText('tag1').click();
|
||||
cy.findByPlaceholderText('Reason to remove tag (optional)').type(
|
||||
'testing',
|
||||
);
|
||||
|
||||
cy.findByRole('button', { name: 'Remove tag' }).click();
|
||||
});
|
||||
|
||||
cy.findByTestId('snackbar').should('not.exist');
|
||||
|
|
|
|||
|
|
@ -17,11 +17,12 @@ describe('Adjust post tags', () => {
|
|||
|
||||
cy.getIframeBody('.actions-panel-iframe').within(() => {
|
||||
cy.findByRole('button', { name: 'Open adjust tags section' }).click();
|
||||
cy.findByTestId('add-tag-button').click();
|
||||
cy.findByPlaceholderText('Add a tag').type('tag2');
|
||||
cy.findByPlaceholderText('Reason for tag adjustment (optional)').type(
|
||||
cy.findByPlaceholderText('Reason to add tag (optional)').type(
|
||||
'testing',
|
||||
);
|
||||
cy.findByRole('button', { name: 'Submit' }).click();
|
||||
cy.findByRole('button', { name: 'Add tag' }).click();
|
||||
});
|
||||
|
||||
cy.getIframeBody('.article-iframe').within(() => {
|
||||
|
|
@ -38,12 +39,12 @@ describe('Adjust post tags', () => {
|
|||
|
||||
cy.getIframeBody('.actions-panel-iframe').within(() => {
|
||||
cy.findByRole('button', { name: 'Open adjust tags section' }).click();
|
||||
cy.findByPlaceholderText('Reason for tag adjustment (optional)').type(
|
||||
cy.findByText('tag1').click();
|
||||
cy.findByPlaceholderText('Reason to remove tag (optional)').type(
|
||||
'testing',
|
||||
);
|
||||
cy.findByRole('button', { name: '#tag1 Remove tag' }).click();
|
||||
|
||||
cy.findByRole('button', { name: 'Submit' }).click();
|
||||
cy.findByRole('button', { name: 'Remove tag' }).click();
|
||||
});
|
||||
|
||||
cy.getIframeBody('.article-iframe').within(() => {
|
||||
|
|
@ -71,11 +72,12 @@ describe('Adjust post tags', () => {
|
|||
|
||||
cy.getIframeBody('.actions-panel-iframe').within(() => {
|
||||
cy.findByRole('button', { name: 'Open adjust tags section' }).click();
|
||||
cy.findByTestId('add-tag-button').click();
|
||||
cy.findByPlaceholderText('Add a tag').type('tag2');
|
||||
cy.findByPlaceholderText('Reason for tag adjustment (optional)').type(
|
||||
cy.findByPlaceholderText('Reason to add tag (optional)').type(
|
||||
'testing',
|
||||
);
|
||||
cy.findByRole('button', { name: 'Submit' }).click();
|
||||
cy.findByRole('button', { name: 'Add tag' }).click();
|
||||
});
|
||||
|
||||
cy.getIframeBody('.article-iframe').within(() => {
|
||||
|
|
@ -92,12 +94,13 @@ describe('Adjust post tags', () => {
|
|||
|
||||
cy.getIframeBody('.actions-panel-iframe').within(() => {
|
||||
cy.findByRole('button', { name: 'Open adjust tags section' }).click();
|
||||
cy.findByPlaceholderText('Reason for tag adjustment (optional)').type(
|
||||
|
||||
cy.findByText('tag1').click();
|
||||
cy.findByPlaceholderText('Reason to remove tag (optional)').type(
|
||||
'testing',
|
||||
);
|
||||
cy.findByRole('button', { name: '#tag1 Remove tag' }).click();
|
||||
|
||||
cy.findByRole('button', { name: 'Submit' }).click();
|
||||
cy.findByRole('button', { name: 'Remove tag' }).click();
|
||||
});
|
||||
|
||||
cy.getIframeBody('.article-iframe').within(() => {
|
||||
|
|
@ -135,11 +138,12 @@ describe('Adjust post tags', () => {
|
|||
.pipe(click)
|
||||
.should('have.attr', 'aria-expanded', 'true');
|
||||
|
||||
cy.findByTestId('add-tag-button').click();
|
||||
cy.findByPlaceholderText('Add a tag').type('tag2');
|
||||
cy.findByPlaceholderText('Reason for tag adjustment (optional)').type(
|
||||
cy.findByPlaceholderText('Reason to add tag (optional)').type(
|
||||
'testing',
|
||||
);
|
||||
cy.findByRole('button', { name: 'Submit' }).click();
|
||||
cy.findByRole('button', { name: 'Add tag' }).click();
|
||||
});
|
||||
|
||||
cy.get('@main').within(() => {
|
||||
|
|
@ -162,12 +166,12 @@ describe('Adjust post tags', () => {
|
|||
.pipe(click)
|
||||
.should('have.attr', 'aria-expanded', 'true');
|
||||
|
||||
cy.findByPlaceholderText('Reason for tag adjustment (optional)').type(
|
||||
cy.findByText('tag1').click();
|
||||
cy.findByPlaceholderText('Reason to remove tag (optional)').type(
|
||||
'testing',
|
||||
);
|
||||
cy.findByRole('button', { name: '#tag1 Remove tag' }).click();
|
||||
|
||||
cy.findByRole('button', { name: 'Submit' }).click();
|
||||
cy.findByRole('button', { name: 'Remove tag' }).click();
|
||||
});
|
||||
|
||||
cy.get('@main').within(() => {
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ RSpec.describe ArticlePolicy do
|
|||
it "omits suspended and regular users" do
|
||||
results = described_class.scope_users_authorized_to_action(users_scope: User, action: :create?).to_a
|
||||
|
||||
expect(results).to match_array([super_admin])
|
||||
expect(results).to contain_exactly(super_admin)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -46,7 +46,7 @@ RSpec.describe ArticlePolicy do
|
|||
|
||||
it "omits only suspended users" do
|
||||
results = described_class.scope_users_authorized_to_action(users_scope: User, action: :create?).to_a
|
||||
expect(results).to match_array([author, super_admin])
|
||||
expect(results).to contain_exactly(author, super_admin)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -174,7 +174,7 @@ RSpec.describe ArticlePolicy do
|
|||
context "when article excludes tagmod_tag, has no room for more tags, and no relevant adjustments" do
|
||||
let(:resource) { create(:article, tags: "tagtwo, tagthree, tagfour, tagfive") }
|
||||
|
||||
it { is_expected.not_to be_tag_moderator_eligible }
|
||||
it { is_expected.to be_tag_moderator_eligible }
|
||||
end
|
||||
|
||||
context "when tag moderator's tag has been adjusted" do
|
||||
|
|
@ -189,7 +189,7 @@ RSpec.describe ArticlePolicy do
|
|||
tag_name: tagmod_tag,
|
||||
adjustment_type: "removal",
|
||||
)
|
||||
expect(policy).not_to be_tag_moderator_eligible
|
||||
expect(policy).to be_tag_moderator_eligible
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -94,7 +94,7 @@ RSpec.describe "Moderations" do
|
|||
sign_in tag_mod
|
||||
|
||||
get "#{article1.path}/actions_panel"
|
||||
expect(response.body).to include "circle centered-icon adjustment-icon subtract"
|
||||
expect(response.body).to include "circle centered-icon remove-icon"
|
||||
end
|
||||
|
||||
it "shows the option to add the tag when the article has the tag" do
|
||||
|
|
@ -102,7 +102,7 @@ RSpec.describe "Moderations" do
|
|||
sign_in tag_mod
|
||||
|
||||
get "#{article2.path}/actions_panel"
|
||||
expect(response.body).to include "circle centered-icon adjustment-icon plus"
|
||||
expect(response.body).to include "circle centered-icon add-icon"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -133,7 +133,7 @@ RSpec.describe "Moderations" do
|
|||
|
||||
it "shows the admin tag options", :aggregate_failures do
|
||||
expect(response.body).to include "admin-add-tag"
|
||||
expect(response.body).to include "circle centered-icon adjustment-icon subtract"
|
||||
expect(response.body).to include "circle centered-icon remove-icon"
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue