Add more visibility to admins (part-2) (#19375)

* Basic thumbsup, thumbsdown and vomit

* Refined UI of flags and vomits

* If check

* Optimised code and added a method in article.rb

* Removed public keyword

* Nit fixes

* Section for flags and quality details

* Basic vomit item

* Quality item UI

* Separated code into partials

* UI changes and empty state added

* With correct implementation

* Fixed showing reaction items in all cases

* Dropdown visible

* Enabled click listerners and call functions for dropdown-button clicks

* Empty state UI fixed

* Tabs and divider UI

* Mark as valid/invalid conditions

* Nit fixes

* Logical fixes

* Added comments

* Partial item status update fix

* Reload page feature fixed

* Removed snackbar

* I18n strings

* Bug fix

* I18n strings

* Nit fixes

* Added date

* Added view details button

* Added view details test

* Sample test

* Test fixes

* Added tests for new article

* Flagged item tests added

* Added more tests

* Nit fix

* Suggested design changes

* Nit design fixes

* Tests with cy.intercept

* Nit fixes

* Nit fixes

* Hero Billboard Placement (#19467)

* init

* add option to swagger docs

* in_house validation

* filtered ad quesry changes

* styling and prioritizing home hero banner over campaign banner

* rubocop

* typo

* broken spec

* more spec fixes

* found the spec break culprit

* derp

* Update app/views/shared/_display_ad.html.erb

Co-authored-by: Ridhwana <Ridhwana.Khan16@gmail.com>

* Update app/controllers/stories_controller.rb

Co-authored-by: Ridhwana <Ridhwana.Khan16@gmail.com>

* remove immediate return

* styling feedback and moving banner to articles index

* remove nil

* updated styling

* add signed in bool

* add feature flag

* remove feature flag data scripts

---------

Co-authored-by: Ridhwana <Ridhwana.Khan16@gmail.com>

* Fix typo / erroneous word in Admin Section (#19488)

* Typo fixes

* Update admin_menu.rb

* Minor background and shadow change (#19501)

* Refresh audience segment for 'active' user (#19470)

* Refresh an individual user's segmentation

* Refresh segment if creating a published article

* Quick refactor

* Refresh segment if update publishing

* Quick refactor

* Refresh segment if update user settings

* Refresh segments after user update + role change

* Using latest_article_updated_at

* Sidekiq uses JSON for arguments, needs basic types

* Fix test issues with last_updated_at and sidekiq params

* Tests update continues

* Consolidate dependencies

* Method names acknowledging manual exclusive

* Fix test name copypasta

* Rubocop

* Try to clarify Creator#series

* Try to avoid naming predicate

* Remove erroneously included helper

* Improve onboarding “suggested tags” page design (#19475)

* Onboarding tags

* Logical design issue fixes

* Posts count added

* Checkbox design fixes

* Optimised css

* Nit fixes

* Test case added

* Reduced data

* Nit fix

* Added more tests

* Nit fix

* Tests updatedt

* Update @honeybadger-io/js to version 5.4.1 (#19479)

Co-authored-by: depfu[bot] <23717796+depfu[bot]@users.noreply.github.com>

* Update sidekiq-cron to version 1.10.1 (#19473)

Co-authored-by: depfu[bot] <23717796+depfu[bot]@users.noreply.github.com>

* Update nokogiri to version 1.15.0 (#19495)

Co-authored-by: depfu[bot] <23717796+depfu[bot]@users.noreply.github.com>

* use tabindex instead of disabled to get proper keyboard nav on /onboarding tags (#19513)

* Add billboard to safe param list in Fastly vcl (#19515)

* Removed static article id from setup

* All tests passing

* Nit fixes

* Nit fixes

* Fixed tests with Joshua's help

* Added trusted user related tests

* Fixed few tests

* Added intercept and wait

* Removed tab and enter key

* Updated comment

* Updated tests

---------

Co-authored-by: Lawrence <lawrence@forem.com>
Co-authored-by: Ridhwana <Ridhwana.Khan16@gmail.com>
Co-authored-by: Peter Frank <peter.kim.frank@gmail.com>
Co-authored-by: Joshua Wehner <joshua@forem.com>
Co-authored-by: depfu[bot] <23717796+depfu[bot]@users.noreply.github.com>
Co-authored-by: Ben Halpern <bendhalpern@gmail.com>
This commit is contained in:
Rajat Talesra 2023-05-26 19:15:12 +05:30 committed by GitHub
parent a6a3417e5e
commit bbb9fd2e1e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 1093 additions and 198 deletions

View file

@ -0,0 +1,17 @@
<svg width="57" height="56" viewBox="0 0 57 56" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_1485_4171)">
<path d="M56.5 28C56.5 43.4638 43.9638 56 28.5 56C13.0378 56 0.5 43.4638 0.5 28C0.5 12.5378 13.0378 0 28.5 0C43.9638 0 56.5 12.5378 56.5 28Z" fill="#A5B4FC"/>
<path d="M19.4379 28.647C21.7119 28.647 23.5554 26.0659 23.5554 22.8821C23.5554 19.6982 21.7119 17.1172 19.4379 17.1172C17.1638 17.1172 15.3203 19.6982 15.3203 22.8821C15.3203 26.0659 17.1638 28.647 19.4379 28.647Z" fill="#312E81"/>
<path d="M39.2053 32.9409C45.1177 32.9409 49.9107 28.148 49.9107 22.2356C49.9107 16.3232 45.1177 11.5303 39.2053 11.5303C33.2929 11.5303 28.5 16.3232 28.5 22.2356C28.5 28.148 33.2929 32.9409 39.2053 32.9409Z" fill="#E0E7FF"/>
<path d="M23.5623 15.472C23.0614 15.472 22.5667 15.2449 22.2431 14.814C20.7016 12.7591 19.0885 11.6577 17.4474 11.5411C15.2851 11.3933 13.5227 13.0142 13.1945 13.3424C12.5505 13.9864 11.5083 13.9864 10.8658 13.3424C10.2218 12.6984 10.2218 11.6562 10.8658 11.0137C11.5767 10.3029 14.1776 8.00219 17.6807 8.25575C20.3174 8.44241 22.7378 9.98397 24.8783 12.8369C25.4243 13.5649 25.2765 14.5977 24.55 15.1422C24.2514 15.3646 23.9045 15.472 23.5623 15.472Z" fill="#312E81"/>
<path d="M51.5692 55.9999C50.6592 55.9999 49.9219 55.2626 49.9219 54.3526V23.0594C49.9219 22.1494 50.6592 21.4121 51.5692 21.4121C52.4792 21.4121 53.2165 22.1494 53.2165 23.0594V54.3541C53.215 55.2626 52.4792 55.9999 51.5692 55.9999Z" fill="#312E81"/>
<path d="M51.5663 19.7653C51.4823 19.7653 51.4029 19.7839 51.3205 19.7901C50.1818 14.1466 45.1869 9.88281 39.212 9.88281C32.4003 9.88281 26.8594 15.4237 26.8594 22.2355C26.8594 29.0473 32.4003 34.5881 39.212 34.5881C44.6067 34.5881 49.1925 31.1068 50.8725 26.2768C51.0965 26.325 51.3267 26.353 51.5647 26.353C53.3847 26.353 54.8594 24.8784 54.8594 23.0584C54.8594 21.2399 53.3847 19.7653 51.5663 19.7653ZM39.212 31.295C34.2172 31.295 30.154 27.2319 30.154 22.2355C30.154 17.239 34.2187 13.1759 39.212 13.1759C44.2069 13.1759 48.2716 17.239 48.2716 22.2355C48.2716 27.2319 44.2069 31.295 39.212 31.295Z" fill="#312E81"/>
<path d="M33.4287 44.4702C33.1813 44.4702 32.9293 44.4142 32.6929 44.296C29.8634 42.8804 27.1131 42.8804 24.2836 44.296C23.4685 44.702 22.4791 44.3737 22.0731 43.5586C21.6656 42.7451 21.9954 41.7557 22.8105 41.3482C26.5765 39.466 30.3969 39.466 34.166 41.3482C34.9796 41.7557 35.3093 42.7451 34.9033 43.5586C34.6156 44.1373 34.0338 44.4702 33.4287 44.4702Z" fill="#312E81"/>
<path d="M44.1266 14.639C42.708 13.7165 41.0186 13.1768 39.2002 13.1768C34.1975 13.1768 30.1406 17.2321 30.1406 22.2363C30.1406 24.0548 30.682 25.7441 31.6028 27.1628L44.1266 14.639ZM35.3346 30.4185C36.5091 30.9739 37.8157 31.2943 39.2002 31.2943C44.2028 31.2943 48.2597 27.239 48.2597 22.2348C48.2597 20.8503 47.9393 19.5421 47.384 18.3692L35.3346 30.4185Z" fill="#EEF2FF"/>
</g>
<defs>
<clipPath id="clip0_1485_4171">
<rect width="56" height="56" fill="white" transform="translate(0.5)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

View file

@ -0,0 +1,18 @@
<svg width="71" height="60" viewBox="0 0 71 60" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_1488_7058)">
<path d="M34.51 15.9786C34.3943 15.5468 34.1827 15.1691 33.9114 14.847C31.7648 11.4412 26.4472 13.1569 18.7314 15.0301C17.4414 15.3436 17.7691 13.6334 17.3287 10.19C17.0424 7.95049 14.7597 5.00539 12.4174 5.63299C8.55547 6.6678 13.0869 8.71865 11.6908 17.167C10.9448 21.681 6.26233 20.5191 7.34569 24.5622L9.81171 33.7656C10.2334 35.3394 10.7933 36.8108 13.0735 36.4667C15.2838 36.1331 15.211 37.7915 18.3062 36.9621L33.7988 32.8109C35.3769 32.3881 36.3163 30.7597 35.8934 29.1816C35.7181 28.5273 35.3292 27.9929 34.8299 27.6095C35.5743 26.8846 35.9273 25.798 35.6397 24.7247C35.4649 24.0722 35.0768 23.5375 34.5786 23.1548C35.3258 22.43 35.6803 21.3421 35.3922 20.2671C35.1831 19.4866 34.6776 18.8638 34.0297 18.4825C34.5391 17.7837 34.7506 16.8767 34.51 15.9786Z" fill="#A5B4FC"/>
<path d="M25.0192 21.587L32.4066 19.6076C33.4111 19.3384 34.2098 18.5483 34.4916 17.5461C34.6073 17.1332 34.3662 16.7037 33.9522 16.5873C33.539 16.4707 33.1099 16.7136 32.9933 17.1267C32.8606 17.6031 32.4796 17.9776 32.0031 18.1053L24.4389 20.1321C23.6893 20.3329 22.9164 19.8867 22.7155 19.1371C22.5147 18.3875 22.9609 17.6146 23.7105 17.4138L28.765 16.0594C29.1797 15.9483 29.4261 15.5215 29.315 15.1068C29.2039 14.6921 28.7771 14.4457 28.3624 14.5568L23.307 15.9114C21.7289 16.3343 20.7893 17.9618 21.2121 19.5399C21.4479 20.42 22.0644 21.0895 22.832 21.447C22.4431 22.1079 22.3076 22.9173 22.521 23.7141C22.7576 24.5967 23.3773 25.2681 24.1481 25.6238C23.7624 26.283 23.6296 27.0888 23.8426 27.8839C24.1003 28.8455 24.8076 29.5652 25.6766 29.8891C25.3622 30.5154 25.257 31.2494 25.4521 31.9775C25.8749 33.5556 27.5025 34.4952 29.0806 34.0724L33.7891 32.8107C34.7937 32.5416 35.5935 31.7521 35.8753 30.7499C35.9919 30.3368 35.7507 29.9072 35.3375 29.7906C34.924 29.676 34.494 29.9154 34.3783 30.3283C34.2438 30.8052 33.863 31.1805 33.3865 31.3082L28.678 32.5698C27.9284 32.7707 27.1555 32.3244 26.9546 31.5749C26.7538 30.8253 27.2 30.0523 27.9496 29.8515L33.5356 28.3547C34.5402 28.0856 35.3406 27.295 35.6215 26.2931C35.7379 25.8791 35.4967 25.4495 35.0838 25.3338C34.6663 25.2146 34.2394 25.4588 34.1245 25.8715C33.9884 26.3561 33.6173 26.7224 33.133 26.8522L27.0688 28.4771C26.3192 28.6779 25.546 28.2308 25.3452 27.4813C25.1443 26.7317 25.5906 25.9588 26.3402 25.7579L33.2871 23.8965C34.2916 23.6273 35.0914 22.8379 35.3732 21.8357C35.4898 21.4225 35.2486 20.993 34.8355 20.8764C34.4208 20.7611 33.9919 21.0012 33.8762 21.4141C33.7399 21.8979 33.3687 22.2642 32.8845 22.3939L25.7478 24.3062C24.9982 24.507 24.2253 24.0608 24.0245 23.3112C23.8236 22.5617 24.2688 21.7881 25.0192 21.587Z" fill="#312E81"/>
</g>
<g clip-path="url(#clip1_1488_7058)">
<path d="M65.5801 44.0239C65.4644 44.4557 65.2528 44.8334 64.9815 45.1555C62.8349 48.5613 57.5173 46.8457 49.8015 44.9724C48.5116 44.6589 48.8392 46.3691 48.3988 49.8125C48.1125 52.052 45.8298 54.9971 43.4875 54.3695C39.6256 53.3347 44.157 51.2839 42.7609 42.8355C42.0149 38.3215 37.3324 39.4834 38.4158 35.4403L40.8816 26.2378C41.3033 24.664 41.8632 23.1926 44.1434 23.5367C46.3539 23.8695 46.2811 22.211 49.3764 23.0404L64.8689 27.1916C66.447 27.6144 67.3864 29.2428 66.9635 30.8209C66.7882 31.4752 66.3993 32.0096 65.9 32.393C66.6444 33.1179 66.9974 34.2045 66.7098 35.2778C66.535 35.9303 66.1469 36.465 65.6487 36.8478C66.3959 37.5725 66.7504 38.6604 66.4623 39.7354C66.2532 40.5159 65.7477 41.1387 65.0998 41.52C65.6092 42.2188 65.8208 43.1258 65.5801 44.0239Z" fill="#A5B4FC"/>
<path d="M56.0844 38.415L63.4718 40.3945C64.4763 40.6636 65.275 41.4537 65.5568 42.4559C65.6725 42.8688 65.4314 43.2984 65.0174 43.4148C64.6042 43.5314 64.1751 43.2885 64.0585 42.8753C63.9258 42.3989 63.5448 42.0245 63.0683 41.8968L55.504 39.87C54.7545 39.6691 53.9815 40.1154 53.7807 40.8649C53.5799 41.6145 54.0261 42.3875 54.7757 42.5883L59.8302 43.9427C60.2449 44.0538 60.4913 44.4805 60.3802 44.8952C60.2691 45.3099 59.8423 45.5563 59.4276 45.4452L54.3722 44.0906C52.7941 43.6678 51.8544 42.0402 52.2773 40.4621C52.5131 39.582 53.1296 38.9126 53.8972 38.5551C53.5083 37.8941 53.3727 37.0848 53.5862 36.288C53.8227 35.4054 54.4425 34.734 55.2133 34.3783C54.8276 33.7191 54.6948 32.9133 54.9078 32.1182C55.1655 31.1566 55.8728 30.4369 56.7417 30.113C56.4274 29.4867 56.3222 28.7527 56.5173 28.0246C56.9401 26.4465 58.5677 25.5068 60.1458 25.9297L64.8543 27.1913C65.8589 27.4605 66.6587 28.2499 66.9405 29.2521C67.0571 29.6653 66.8159 30.0948 66.4027 30.2114C65.9892 30.3261 65.5592 30.0866 65.4435 29.6737C65.309 29.1969 64.9282 28.8216 64.4517 28.6939L59.7432 27.4322C58.9936 27.2314 58.2207 27.6776 58.0198 28.4272C57.819 29.1768 58.2652 29.9497 59.0148 30.1505L64.6008 31.6473C65.6054 31.9165 66.4058 32.707 66.6867 33.709C66.8031 34.123 66.5619 34.5525 66.149 34.6683C65.7315 34.7874 65.3046 34.5433 65.1897 34.1306C65.0536 33.6459 64.6825 33.2796 64.1982 33.1499L58.134 31.525C57.3844 31.3241 56.6112 31.7712 56.4104 32.5208C56.2095 33.2704 56.6558 34.0433 57.4053 34.2441L64.3523 36.1056C65.3568 36.3747 66.1566 37.1642 66.4384 38.1664C66.555 38.5795 66.3138 39.0091 65.9007 39.1257C65.486 39.241 65.0571 39.0009 64.9414 38.588C64.8051 38.1042 64.4339 37.7379 63.9496 37.6081L56.813 35.6958C56.0634 35.495 55.2905 35.9413 55.0896 36.6908C54.8888 37.4404 55.334 38.214 56.0844 38.415Z" fill="#312E81"/>
</g>
<defs>
<clipPath id="clip0_1488_7058">
<rect width="31.9998" height="32" fill="white" transform="translate(0.367188 8.63867) rotate(-15)"/>
</clipPath>
<clipPath id="clip1_1488_7058">
<rect width="31.9998" height="32" fill="white" transform="translate(39.7188 20.4531) rotate(15)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 5.5 KiB

View file

@ -15,7 +15,13 @@ hr {
margin-top: 1rem;
margin-bottom: 1rem;
border: 0;
border-top: 1px solid rgba(0,0,0,.1);
border-top: 1px solid rgba(0, 0, 0, 0.1);
}
.hr-no-margins {
margin-top: 0px;
margin-bottom: 0px;
border-top: 1px solid rgba(var(--grey-100), 1);
}
a:hover {
@ -133,8 +139,8 @@ which may be more easily removed once we no longer wish admin layouts to have a
justify-content: space-between;
padding: 0.75rem 1.25rem;
margin-bottom: 0;
background-color: rgba(0,0,0,.03);
border-bottom: 1px solid rgba(0,0,0,.125);
background-color: rgba(0, 0, 0, 0.03);
border-bottom: 1px solid rgba(0, 0, 0, 0.125);
}
.card-header__actions {

View file

@ -33,7 +33,7 @@ module Admin
end
def show
@article = Article.find(params[:id])
@article = Article.includes(reactions: :user).find(params[:id])
end
def update
@ -59,7 +59,7 @@ module Admin
redirect_to admin_article_path(article.id)
end
format.js do
render partial: "admin/articles/individual_article", locals: { article: article }, content_type: "text/html"
render partial: "admin/articles/article_item", locals: { article: article }, content_type: "text/html"
end
end
end
@ -75,7 +75,7 @@ module Admin
redirect_to admin_article_path(article.id)
end
format.js do
render partial: "admin/articles/individual_article", locals: { article: article }, content_type: "text/html"
render partial: "admin/articles/article_item", locals: { article: article }, content_type: "text/html"
end
end
end

View file

@ -7,7 +7,7 @@ export default class ReactionController extends Controller {
url: String,
};
updateReaction(status) {
updateReaction(status, removeElement = true) {
const id = this.idValue;
fetch(this.urlValue, {
@ -15,7 +15,7 @@ export default class ReactionController extends Controller {
headers: {
Accept: 'application/json',
'X-CSRF-Token': document.querySelector("meta[name='csrf-token']")
.content,
?.content,
'Content-Type': 'application/json',
},
body: JSON.stringify({
@ -28,8 +28,16 @@ export default class ReactionController extends Controller {
.json()
.then((json) => {
if (json.outcome === 'Success') {
this.element.remove();
document.getElementById(`js__reaction__div__hr__${id}`).remove();
if (removeElement === true) {
this.element.remove();
document.getElementById(`js__reaction__div__hr__${id}`).remove();
} else {
// TODO (#19531): Code Optimisation- avoid reloading entire page for this minor item change.
// Once the status of item gets updated in admin/content_manager/articles/<article-id>, we
// reload the entire page here. Ideally we should only re-render the item which was updated
// but given the case that this feature is used by internal-team, for now its fine.
location.reload();
}
} else {
window.alert(json.error);
}
@ -40,12 +48,14 @@ export default class ReactionController extends Controller {
);
}
updateReactionInvalid() {
this.updateReaction(this.invalidStatus);
updateReactionInvalid(event) {
const { removeElement } = event.target.dataset;
this.updateReaction(this.invalidStatus, removeElement);
}
updateReactionConfirmed() {
this.updateReaction(this.confirmedStatus);
updateReactionConfirmed(event) {
const { removeElement } = event.target.dataset;
this.updateReaction(this.confirmedStatus, removeElement);
}
reactableUserCheck() {

View file

@ -0,0 +1,70 @@
import { openDropdown, closeDropdown } from '@utilities/dropdownUtils';
// We present up to 50 users in the UI at once, and for performance reasons we don't want to add individual click listeners to each dropdown menu or inner menu item
// Instead we listen for click events anywhere in the table, and identify required actions based on data attributes of the target
document
.getElementById('reaction-content')
?.addEventListener('click', ({ target }) => {
const {
dataset: { markValid, markInvalid, toggleDropdown },
} = target;
if (markValid) {
closeCurrentlyOpenDropdown();
return;
}
if (markInvalid) {
closeCurrentlyOpenDropdown();
return;
}
if (toggleDropdown) {
handleDropdownToggle({
triggerElementId: target.getAttribute('id'),
dropdownContentId: toggleDropdown,
});
return;
}
});
// We keep track of the currently opened dropdown to make sure we only ever have one open at a time
let currentlyOpenDropdownId;
const handleDropdownToggle = ({ triggerElementId, dropdownContentId }) => {
const triggerButton = document.getElementById(triggerElementId);
const isCurrentlyOpen =
triggerButton.getAttribute('aria-expanded') === 'true';
if (isCurrentlyOpen) {
closeDropdown({ triggerElementId, dropdownContentId });
triggerButton.focus();
currentlyOpenDropdownId = null;
} else {
closeCurrentlyOpenDropdown();
openDropdown({ triggerElementId, dropdownContentId });
currentlyOpenDropdownId = dropdownContentId;
}
};
/**
* Make sure any currently opened dropdown is closed
*/
const closeCurrentlyOpenDropdown = (focusTrigger = false) => {
if (!currentlyOpenDropdownId) {
return;
}
const triggerButton = document.querySelector(
`[aria-controls='${currentlyOpenDropdownId}']`,
);
closeDropdown({
dropdownContentId: currentlyOpenDropdownId,
triggerElementId: triggerButton?.getAttribute('id'),
});
if (focusTrigger) {
triggerButton.focus();
}
};

View file

@ -0,0 +1,203 @@
<% decorated_article = article.decorate %>
<article class="js-individual-article crayons-card p-6 flex flex-col gap-4"
data-controller="article"
data-article-id-value="<%= article.id %>"
data-article-pin-path-value="<%= stories_feed_pinned_article_path %>"
data-article-bg-highlighted-class="bg-highlighted"
data-article-border-highlighted-class="border-highlighted"
data-action="ajax:success@document->article#ajaxSuccess">
<header>
<% if decorated_article.pinned? || article.featured || article.approved || (!article.published? && article.published_from_feed?) || article.video || article.user&.warned? %>
<div class="flex gap-1 mb-1">
<% if !article.published? && article.published_from_feed? %>
<span class="c-indicator c-indicator--danger"><%= t("views.moderations.article.unplublished") %></span>
<% end %>
<% if decorated_article.pinned? %>
<span class="c-indicator c-indicator--warning" data-testid="pinned-indicator"><%= t("views.moderations.article.pinned") %></span>
<% end %>
<% if article.featured %>
<span class="c-indicator c-indicator--success"><%= t("views.moderations.article.featured") %></span>
<% end %>
<% if article.approved %>
<span class="c-indicator c-indicator--info"><%= t("views.moderations.article.approved") %></span>
<% end %>
<% if article.video %>
<span class="c-indicator"><%= t("views.moderations.article.contains_video") %></span>
<% end %>
<% cache "admin-user-info-#{article.user_id}-#{article.user&.updated_at}", expires_in: 4.hours do %>
<% if article.user&.warned? %>
<span class="c-indicator c-indicator--danger"><%= t("views.moderations.article.user_warned") %></span>
<% end %>
<% end %>
</div>
<% end %>
<% if article.main_image.present? %>
<a href="<%= article.path %>" target="_blank" rel="noopener" class="c-link c-link--branded mb-4">
<img src="<%= cloud_cover_url(article.main_image) %>" style="background: <%= article.main_image_background_hex_color %>; max-height:150px; max-width: 300px;" alt="" loading="lazy" class="block radius-default h-auto w-100" />
</a>
<% end %>
<% privileged_article_reactions = article.reactions.privileged_category.select { |reaction| reaction.reactable_type == "Article" } %>
<% vomit_article_reactions = privileged_article_reactions.select { |reaction| reaction.category == "vomit" } %>
<% countable_vomits = 0 %>
<% if vomit_article_reactions.present? %>
<% vomit_article_reactions.each do |vomit_reaction| %>
<% if vomit_reaction.status != 'invalid' %>
<% countable_vomits += 1 %>
<% end %>
<% end %>
<% end %>
<div class="flex justify-between gap-4 items-center">
<h2 class="crayons-subtitle-1">
<a href="<%= article.path %>" target="_blank" rel="noopener" class="c-link c-link--branded">
<%= article.title %>
</a>
</h2>
<div class="flex items-center gap-1">
<span class="crayons-card crayons-card--secondary px-3 py-1 flex gap-2 items-center" title="<%= t("views.moderations.actions.thumb_up") %>">
<%= crayons_icon_tag("twemoji/thumb-up", native: true, width: 16, height: 16) %>
<span class="fs-s fw-medium lh-base"><%= article.privileged_reaction_counts["thumbsup"] || "0" %></span>
</span>
<span class="crayons-card crayons-card--secondary px-3 py-1 flex gap-2 items-center" title="<%= t("views.moderations.actions.thumb_down") %>">
<%= crayons_icon_tag("twemoji/thumb-down", native: true, width: 16, height: 16) %>
<span class="fs-s fw-medium lh-base"><%= article.privileged_reaction_counts["thumbsdown"] || "0" %></span>
</span>
<span class="crayons-card crayons-card--secondary px-3 py-1 flex gap-2 items-center" title="<%= t("views.moderations.actions.vomit") %>">
<%= crayons_icon_tag("twemoji/suspicious", native: true, width: 16, height: 16) %>
<span class="fs-s fw-medium lh-base"><%= countable_vomits %></span>
</span>
<span class="crayons-card crayons-card--secondary px-3 py-1 ml-3 flex gap-2 items-center" title="<%= t("views.moderations.actions.score") %>">
<%= crayons_icon_tag("analytics", native: true, width: 16, height: 16) %>
<span class="fs-s fw-medium lh-base"><%= article.score %></span>
</span>
</div>
</div>
<div class="-ml-1 fs-s flex items-center gap-4">
<div class="flex gap-px">
<% decorated_article.cached_tag_list_array.each do |tag| %>
<a class="crayons-tag crayons-tag--monochrome" href='<%= tag_path(tag) %>'><span class="crayons-tag__prefix">#</span><%= tag %></a>
<% end %>
</div>
<span>❤️&nbsp;<%= article.public_reactions_count %> <%= t("views.moderations.article.likes") %></span>
<span>💬&nbsp;<%= article.comments_count %> <%= t("views.moderations.article.comments") %></span>
</div>
<div class="flex justify-between gap-4 items-center">
<div>
<a href="<%= admin_user_path(article.user_id) %>" class="c-link">@<%= article.user&.username %></a>
<% if article.published_from_feed? && !article.published? %>
RSS Import <%= article.created_at.strftime("%b %d, %Y") %>
Originally Published <%= article.published_at&.strftime("%b %d, %Y") %>
<% elsif article.crossposted_at? %>
Crossposted <%= article.crossposted_at.strftime("%b %d, %Y") %> & Published
<%= article.published_at&.strftime("%b %d, %Y") %>
<% else %>
<%= article.published_at&.strftime("%b %d, %Y") %>
<% end %>
</div>
<div class="flex gap-1">
<a href="/admin/content_manager/articles/<%= article.id %>" class="c-link c-link--block"><%= t("views.moderations.article.view_details") %></a>
<% if decorated_article.pinned? %>
<form method="post" action="<%= unpin_admin_article_path(decorated_article.id) %>" class="inline"
data-action="submit->article#unpinArticle">
<input type="hidden" name="_method" value="delete" />
<input type="hidden" name="authenticity_token" value="<%= form_authenticity_token %>" />
<button type="submit" class="c-btn">
<%= t("views.moderations.article.unpin_post") %>
</button>
</form>
<% else %>
<form method="post" action="<%= pin_admin_article_path(decorated_article.id) %>" class="inline"
data-action="submit->article#pinArticle">
<input type="hidden" name="authenticity_token" value="<%= form_authenticity_token %>" />
<button type="submit" class="c-btn">
<%= t("views.moderations.article.pin_post") %>
</button>
</form>
<% end %>
<a class="c-link c-link--block" href="<%= admin_user_path(article.user_id) %>" target="_blank" rel="noopener"><%= t("views.moderations.article.user") %></a>
<a href="<%= article.path %>/edit" target="_blank" rel="noopener" class="c-link c-link--block"><%= t("views.moderations.article.edit") %></a>
</div>
</div>
</header>
<% cache "admin-user-notes-#{article.user_id}-#{article.user&.updated_at}", expires_in: 4.hours do %>
<% if article.user&.notes&.any? %>
<div class="crayons-card crayons-card--secondary p-4 pt-3">
<div class="flex gap-2 items-center">
<h3 class="crayons-subtitle-2"><%= article.user&.notes&.size %> <%= t("views.moderations.article.notes.user_notes") %></h3>
<a href="<%= admin_user_path(article.user_id) %>" class="c-link c-link--block c-link--branded"><%= t("views.moderations.article.notes.view_all") %></a>
</div>
<% article.user&.notes&.last(3)&.each do |note| %>
<p>
<%= note.content %>
<span class="color-secondary fs-s">
<%= note.author_id ? User.find(note.author_id).username : "No author" %>
<time datetime="<%= note.created_at.strftime("%Y-%m-%dT%H:%M:%S%z") %>" title="<%= note.created_at.strftime("%Y-%m-%d, %H:%M") %>">
<%= note.created_at.strftime("%b %e '%y") %>
</time>
</span>
</p>
<% end %>
</div>
<% end %>
<% end %>
<%= form_with url: admin_article_path(article.id), model: article, local: true, class: "crayons-card crayons-card--secondary p-4 flex flex-col gap-3" do |f| %>
<input name="utf8" type="hidden" value="✓">
<input type="hidden" name="authenticity_token" value="<%= form_authenticity_token %>" />
<input type="hidden" name="_method" value="patch" />
<div class="flex gap-4">
<div class="crayons-field flex-1">
<label for="author_id_<%= article.id %>" class="crayons-field__label"><%= t("views.moderations.article.author_id") %></label>
<input id="author_id_<%= article.id %>" class="crayons-textfield" size="6" name="article[user_id]"
value="<%= article.user_id %>">
</div>
<div class="crayons-field flex-1">
<label for="co_author_ids_list_<%= article.id %>" class="crayons-field__label"><%= t("views.moderations.article.co_author_ids") %></label>
<input id="co_author_ids_list_<%= article.id %>" class="crayons-textfield" size="6" name="article[co_author_ids_list]" placeholder="Comma separated"
value="<%= article.co_author_ids&.join(", ") %>">
</div>
</div>
<% if article.published? %>
<div class="flex">
<div class="crayons-field">
<label for="published_at_<%= article.id %>" class="crayons-field__label"><%= t("views.moderations.article.published_at") %></label>
<div class="flex gap-1 items-center flex-wrap">
<%= f.datetime_select :published_at, { required: true, include_blank: true, include_seconds: true }, class: "crayons-select w-auto" %>
<span class="whitespace-nowrap color-secondary">UTC</span>
</div>
</div>
</div>
<% end %>
<div class="flex items-center gap-4">
<div class="crayons-field crayons-field--checkbox">
<%= f.check_box :featured, id: "featured-#{article.id}", class: "crayons-checkbox" %>
<label for="featured-<%= article.id %>" class="crayons-field__label">
<%= t("views.moderations.article.featured") %>
</label>
</div>
<div class="crayons-field crayons-field--checkbox">
<%= f.check_box :approved, id: "approved-#{article.id}", class: "crayons-checkbox" %>
<label for="approved-<%= article.id %>" class="crayons-field__label">
<%= t("views.moderations.article.approved") %>
</label>
</div>
</div>
<div>
<button class="c-btn c-btn--secondary"><%= t("views.moderations.article.save") %></button>
</div>
<% end %>
</article>

View file

@ -0,0 +1,49 @@
<div class="flex items-center gap-0 m:gap-4"
data-controller="reaction"
data-reaction-id-value="<%= vomit_reaction.id %>"
data-reaction-url-value="<%= admin_reaction_path(vomit_reaction.id) %>">
<div class="flex items-center w-100">
<div class="flex items-center p-0 w-100 m:w-40">
<div class="crayons-card crayons-card--secondary p-3 mr-3 m:mr-7" title="Vomit">
<%= crayons_icon_tag("twemoji/suspicious", native: true, width: 24, height: 24) %>
</div>
<div class="p-0">
<p class="crayons-subtitle-3"><%= vomit_reaction.user.name %></p>
<time datetime="<%= vomit_reaction.updated_at&.strftime("%Y-%m-%dT%H:%M:%S%z") %>" class="color-secondary fs-s shrink-0" title="<%= l(vomit_reaction.updated_at, format: :admin_user) if vomit_reaction.created_at %>">
<%= l(vomit_reaction.updated_at, format: :long) %>
</time>
</div>
</div>
<%# Note: There are discrepancies in status naming between the frontend (FE) and backend (BE). %>
<%# In the FE, 'Open/Unresolved' corresponds to 'valid' in the BE. This status appears when a trusted user flags an article, with a score of -50. %>
<%# In the FE, 'Valid' corresponds to 'confirmed' in the BE. This status appears when an admin user flags an article or marks a trusted user-flagged article as valid, with a score of -100. %>
<%# In both the FE and BE, 'Invalid' remains the same. This status appears when an admin user marks a flag or vomit reaction as invalid, with a score of 0. %>
<div class="flex w-100 m:w-60 gap-0 m:gap-6 ml-2">
<% if vomit_reaction.status == 'valid' %>
<span class="c-indicator c-indicator--relaxed c-indicator--warning" title="<%= t("views.moderations.flags.open.title") %>">
<%= t("views.moderations.flags.open.value") %>
</span>
<% elsif vomit_reaction.status == 'invalid' %>
<span class="c-indicator c-indicator--relaxed" title="<%= t("views.moderations.flags.invalid.title") %>">
<%= t("views.moderations.flags.invalid.value") %>
</span>
<% elsif vomit_reaction.status == 'confirmed' %>
<span class="c-indicator c-indicator--relaxed c-indicator--danger" title="<%= t("views.moderations.flags.valid.title") %>">
<%= t("views.moderations.flags.valid.value") %>
</span>
<% else %>
<%# This case should never arise. %>
<span class="c-indicator c-indicator--relaxed c-indicator--info" title="<%= t("views.moderations.flags.unidentified.title") %>">
<%= t("views.moderations.flags.unidentified.value") %>
</span>
<% end %>
<span data-testid="flag-score" class="crayons-card crayons-card--secondary px-3 py-1 ml-3 flex gap-2 items-center" title="Score affected by a particular flag">
<%= crayons_icon_tag("analytics", native: true, width: 16, height: 16) %>
<span class="fs-s fw-medium lh-base"><%= vomit_reaction.points %></span>
</span>
</div>
</div>
<%= render "admin/articles/priviledged_reaction_actions_dropdown", vomit_reaction: vomit_reaction %>
</div>

View file

@ -1,191 +1,252 @@
<% decorated_article = article.decorate %>
<%= javascript_packs_with_chunks_tag "admin/articles/vomitReactionItem", defer: true %>
<div
data-controller="article"
data-article-id-value="<%= article.id %>"
data-article-pin-path-value="<%= stories_feed_pinned_article_path %>"
data-article-bg-highlighted-class="bg-highlighted"
data-article-border-highlighted-class="border-highlighted"
data-action="ajax:success@document->article#ajaxSuccess">
<article class="js-individual-article crayons-card p-6 flex flex-col gap-4"
data-controller="article"
data-article-id-value="<%= article.id %>"
data-article-pin-path-value="<%= stories_feed_pinned_article_path %>"
data-article-bg-highlighted-class="bg-highlighted"
data-article-border-highlighted-class="border-highlighted"
data-action="ajax:success@document->article#ajaxSuccess">
<header>
<% if decorated_article.pinned? || article.featured || article.approved || (!article.published? && article.published_from_feed?) || article.video || article.user&.warned? %>
<div class="flex gap-1 mb-1">
<% if !article.published? && article.published_from_feed? %>
<span class="c-indicator c-indicator--danger">Unpublished</span>
<% end %>
<% if decorated_article.pinned? %>
<span class="c-indicator c-indicator--warning" data-testid="pinned-indicator">Pinned</span>
<% end %>
<% if article.featured %>
<span class="c-indicator c-indicator--success">Featured</span>
<% end %>
<% if article.approved %>
<span class="c-indicator c-indicator--info">Approved</span>
<% end %>
<% if article.video %>
<span class="c-indicator">Contains video</span>
<% end %>
<% cache "admin-user-info-#{article.user_id}-#{article.user&.updated_at}", expires_in: 4.hours do %>
<% if article.user&.warned? %>
<span class="c-indicator c-indicator--danger">User warned</span>
<% end %>
<% end %>
</div>
<% end %>
<% if article.main_image.present? %>
<a href="<%= article.path %>" target="_blank" rel="noopener" class="c-link c-link--branded mb-4">
<img src="<%= cloud_cover_url(article.main_image) %>" style="background: <%= article.main_image_background_hex_color %>; max-height:150px; max-width: 300px;" alt="" loading="lazy" class="block radius-default h-auto w-100" />
</a>
<% end %>
<div class="flex justify-between gap-4 items-center">
<h2 class="crayons-subtitle-1">
<a href="<%= article.path %>" target="_blank" rel="noopener" class="c-link c-link--branded">
<%= article.title %>
</a>
</h2>
<div class="flex items-center gap-1">
<span class="crayons-card crayons-card--secondary px-3 py-1 flex gap-2 items-center">
<%= crayons_icon_tag("twemoji/thumb-up", native: true, title: t("views.moderations.actions.thumb_up"), width: 16, height: 16) %>
<span class="fs-s fw-medium lh-base"><%= article.privileged_reaction_counts['thumbsup'] || '0' %></span>
</span>
<span class="crayons-card crayons-card--secondary px-3 py-1 flex gap-2 items-center">
<%= crayons_icon_tag("twemoji/thumb-down", native: true, title: t("views.moderations.actions.thumb_down"), width: 16, height: 16) %>
<span class="fs-s fw-medium lh-base"><%= article.privileged_reaction_counts['thumbsdown'] || '0' %></span>
</span>
<span class="crayons-card crayons-card--secondary px-3 py-1 flex gap-2 items-center">
<%= crayons_icon_tag("twemoji/suspicious", native: true, title: "Vomit", width: 16, height: 16) %>
<span class="fs-s fw-medium lh-base"><%= article.privileged_reaction_counts['vomit'] || '0' %></span>
</span>
<span class="crayons-card crayons-card--secondary px-3 py-1 ml-3 flex gap-2 items-center">
<%= crayons_icon_tag("analytics", native: true, title: "Score", width: 16, height: 16) %>
<span class="fs-s fw-medium lh-base"><%= article.score %></span>
</span>
</div>
</div>
<div class="-ml-1 fs-s flex items-center gap-4">
<div class="flex gap-px">
<% decorated_article.cached_tag_list_array.each do |tag| %>
<a class="crayons-tag crayons-tag--monochrome" href='<%= tag_path(tag) %>'><span class="crayons-tag__prefix">#</span><%= tag %></a>
<% end %>
</div>
<span>❤️&nbsp;<%= article.public_reactions_count %> likes</span>
<span>💬&nbsp;<%= article.comments_count %> comments</span>
</div>
<div class="flex justify-between gap-4 items-center">
<div>
<a href="<%= admin_user_path(article.user_id) %>" class="c-link">@<%= article.user&.username %></a>
<% if article.published_from_feed? && !article.published? %>
RSS Import <%= article.created_at.strftime("%b %d, %Y") %>
Originally Published <%= article.published_at&.strftime("%b %d, %Y") %>
<% elsif article.crossposted_at? %>
Crossposted <%= article.crossposted_at.strftime("%b %d, %Y") %> & Published
<%= article.published_at&.strftime("%b %d, %Y") %>
<% else %>
<%= article.published_at&.strftime("%b %d, %Y") %>
<% end %>
</div>
<div class="flex gap-1">
<% if decorated_article.pinned? %>
<form method="post" action="<%= unpin_admin_article_path(decorated_article.id) %>" class="inline"
data-action="submit->article#unpinArticle">
<input type="hidden" name="_method" value="delete" />
<input type="hidden" name="authenticity_token" value="<%= form_authenticity_token %>" />
<button type="submit" class="c-btn">
Unpin post
</button>
</form>
<% else %>
<form method="post" action="<%= pin_admin_article_path(decorated_article.id) %>" class="inline"
data-action="submit->article#pinArticle">
<input type="hidden" name="authenticity_token" value="<%= form_authenticity_token %>" />
<button type="submit" class="c-btn">
Pin post
</button>
</form>
<% end %>
<a class="c-link c-link--block" href="<%= admin_user_path(article.user_id) %>" target="_blank" rel="noopener">User</a>
<a href="<%= article.path %>/edit" target="_blank" rel="noopener" class="c-link c-link--block">Edit</a>
</div>
</div>
</header>
<% cache "admin-user-notes-#{article.user_id}-#{article.user&.updated_at}", expires_in: 4.hours do %>
<% if article.user&.notes&.any? %>
<div class="crayons-card crayons-card--secondary p-4 pt-3">
<div class="flex gap-2 items-center">
<h3 class="crayons-subtitle-2"><%= article.user&.notes&.size %> user notes</h3>
<a href="<%= admin_user_path(article.user_id) %>" class="c-link c-link--block c-link--branded">View all</a>
</div>
<% article.user&.notes&.last(3)&.each do |note| %>
<p>
<%= note.content %>
<span class="color-secondary fs-s">
<%= note.author_id ? User.find(note.author_id).username : "No author" %>
<time datetime="<%= note.created_at.strftime("%Y-%m-%dT%H:%M:%S%z") %>" title="<%= note.created_at.strftime("%Y-%m-%d, %H:%M") %>">
<%= note.created_at.strftime("%b %e '%y") %>
</time>
</span>
</p>
<% end %>
</div>
<% privileged_article_reactions = article.reactions.privileged_category.select { |reaction| reaction.reactable_type == "Article" } %>
<% vomit_article_reactions = privileged_article_reactions.select { |reaction| reaction.category == "vomit" }.reverse %>
<% quality_article_reactions = (privileged_article_reactions - vomit_article_reactions).reverse %>
<% countable_vomits = 0 %>
<% if vomit_article_reactions.present? %>
<% vomit_article_reactions.each do |vomit_reaction| %>
<% if vomit_reaction.status != 'invalid' %>
<% countable_vomits += 1 %>
<% end %>
<% end %>
<% end %>
<%= form_with url: admin_article_path(article.id), model: article, local: true, class: "crayons-card crayons-card--secondary p-4 flex flex-col gap-3" do |f| %>
<input name="utf8" type="hidden" value="✓">
<input type="hidden" name="authenticity_token" value="<%= form_authenticity_token %>" />
<input type="hidden" name="_method" value="patch" />
<div class="flex gap-4">
<div class="crayons-field flex-1">
<label for="author_id_<%= article.id %>" class="crayons-field__label">Author ID</label>
<input id="author_id_<%= article.id %>" class="crayons-textfield" size="6" name="article[user_id]"
value="<%= article.user_id %>">
<article class="js-individual-article crayons-card p-6 flex flex-col gap-4">
<header>
<% if decorated_article.pinned? || article.featured || article.approved || (!article.published? && article.published_from_feed?) || article.video || article.user&.warned? %>
<div class="flex gap-1 mb-1">
<% if !article.published? && article.published_from_feed? %>
<span class="c-indicator c-indicator--danger"><%= t("views.moderations.article.unplublished") %></span>
<% end %>
<% if decorated_article.pinned? %>
<span class="c-indicator c-indicator--warning" data-testid="pinned-indicator"><%= t("views.moderations.article.pinned") %></span>
<% end %>
<% if article.featured %>
<span class="c-indicator c-indicator--success"><%= t("views.moderations.article.featured") %></span>
<% end %>
<% if article.approved %>
<span class="c-indicator c-indicator--info"><%= t("views.moderations.article.approved") %></span>
<% end %>
<% if article.video %>
<span class="c-indicator"><%= t("views.moderations.article.contains_video") %></span>
<% end %>
<% cache "admin-user-info-#{article.user_id}-#{article.user&.updated_at}", expires_in: 4.hours do %>
<% if article.user&.warned? %>
<span class="c-indicator c-indicator--danger"><%= t("views.moderations.article.user_warned") %></span>
<% end %>
<% end %>
</div>
<% end %>
<% if article.main_image.present? %>
<a href="<%= article.path %>" target="_blank" rel="noopener" class="c-link c-link--branded mb-4">
<img src="<%= cloud_cover_url(article.main_image) %>" style="background: <%= article.main_image_background_hex_color %>; max-height:150px; max-width: 300px;" alt="" loading="lazy" class="block radius-default h-auto w-100" />
</a>
<% end %>
<div class="flex justify-between gap-4 items-center">
<h2 class="crayons-subtitle-1">
<a href="<%= article.path %>" target="_blank" rel="noopener" class="c-link c-link--branded">
<%= article.title %>
</a>
</h2>
<div class="flex items-center gap-1">
<span class="crayons-card crayons-card--secondary px-3 py-1 flex gap-2 items-center" title="<%= t("views.moderations.actions.thumb_up") %>">
<%= crayons_icon_tag("twemoji/thumb-up", native: true, width: 16, height: 16) %>
<span class="fs-s fw-medium lh-base"><%= article.privileged_reaction_counts["thumbsup"] || "0" %></span>
</span>
<span class="crayons-card crayons-card--secondary px-3 py-1 flex gap-2 items-center" title="<%= t("views.moderations.actions.thumb_down") %>">
<%= crayons_icon_tag("twemoji/thumb-down", native: true, width: 16, height: 16) %>
<span class="fs-s fw-medium lh-base"><%= article.privileged_reaction_counts["thumbsdown"] || "0" %></span>
</span>
<span class="crayons-card crayons-card--secondary px-3 py-1 flex gap-2 items-center" title="<%= t("views.moderations.actions.vomit") %>">
<%= crayons_icon_tag("twemoji/suspicious", native: true, width: 16, height: 16) %>
<span class="fs-s fw-medium lh-base"><%= countable_vomits %></span>
</span>
<span class="crayons-card crayons-card--secondary px-3 py-1 ml-3 flex gap-2 items-center" title="<%= t("views.moderations.actions.score") %>">
<%= crayons_icon_tag("analytics", native: true, width: 16, height: 16) %>
<span class="fs-s fw-medium lh-base"><%= article.score %></span>
</span>
</div>
</div>
<div class="crayons-field flex-1">
<label for="co_author_ids_list_<%= article.id %>" class="crayons-field__label">Co-Author IDs</label>
<input id="co_author_ids_list_<%= article.id %>" class="crayons-textfield" size="6" name="article[co_author_ids_list]" placeholder="Comma separated"
value="<%= article.co_author_ids&.join(", ") %>">
<div class="-ml-1 fs-s flex items-center gap-4">
<div class="flex gap-px">
<% decorated_article.cached_tag_list_array.each do |tag| %>
<a class="crayons-tag crayons-tag--monochrome" href='<%= tag_path(tag) %>'><span class="crayons-tag__prefix">#</span><%= tag %></a>
<% end %>
</div>
<span>❤️&nbsp;<%= article.public_reactions_count %> <%= t("views.moderations.article.likes") %></span>
<span>💬&nbsp;<%= article.comments_count %> <%= t("views.moderations.article.comments") %></span>
</div>
</div>
<% if article.published? %>
<div class="flex">
<div class="crayons-field">
<label for="published_at_<%= article.id %>" class="crayons-field__label">Published at</label>
<div class="flex gap-1 items-center flex-wrap">
<%= f.datetime_select :published_at, { required: true, include_blank: true, include_seconds: true }, class: "crayons-select w-auto" %>
<span class="whitespace-nowrap color-secondary">UTC</span>
<div class="flex justify-between gap-4 items-center">
<div>
<a href="<%= admin_user_path(article.user_id) %>" class="c-link">@<%= article.user&.username %></a>
<% if article.published_from_feed? && !article.published? %>
RSS Import <%= article.created_at.strftime("%b %d, %Y") %>
Originally Published <%= article.published_at&.strftime("%b %d, %Y") %>
<% elsif article.crossposted_at? %>
Crossposted <%= article.crossposted_at.strftime("%b %d, %Y") %> & Published
<%= article.published_at&.strftime("%b %d, %Y") %>
<% else %>
<%= article.published_at&.strftime("%b %d, %Y") %>
<% end %>
</div>
<div class="flex gap-1">
<% if decorated_article.pinned? %>
<form method="post" action="<%= unpin_admin_article_path(decorated_article.id) %>" class="inline"
data-action="submit->article#unpinArticle">
<input type="hidden" name="_method" value="delete" />
<input type="hidden" name="authenticity_token" value="<%= form_authenticity_token %>" />
<button type="submit" class="c-btn">
<%= t("views.moderations.article.unpin_post") %>
</button>
</form>
<% else %>
<form method="post" action="<%= pin_admin_article_path(decorated_article.id) %>" class="inline"
data-action="submit->article#pinArticle">
<input type="hidden" name="authenticity_token" value="<%= form_authenticity_token %>" />
<button type="submit" class="c-btn">
<%= t("views.moderations.article.pin_post") %>
</button>
</form>
<% end %>
<a class="c-link c-link--block" href="<%= admin_user_path(article.user_id) %>" target="_blank" rel="noopener">User</a>
<a href="<%= article.path %>/edit" target="_blank" rel="noopener" class="c-link c-link--block">Edit</a>
</div>
</div>
</header>
<% cache "admin-user-notes-#{article.user_id}-#{article.user&.updated_at}", expires_in: 4.hours do %>
<% if article.user&.notes&.any? %>
<div class="crayons-card crayons-card--secondary p-4 pt-3">
<div class="flex gap-2 items-center">
<h3 class="crayons-subtitle-2"><%= article.user&.notes&.size %> <%= t("views.moderations.article.notes.user_notes") %></h3>
<a href="<%= admin_user_path(article.user_id) %>" class="c-link c-link--block c-link--branded"><%= t("views.moderations.article.notes.view_all") %></a>
</div>
<% article.user&.notes&.last(3)&.each do |note| %>
<p>
<%= note.content %>
<span class="color-secondary fs-s">
<%= note.author_id ? User.find(note.author_id).username : "No author" %>
<time datetime="<%= note.created_at.strftime("%Y-%m-%dT%H:%M:%S%z") %>" title="<%= note.created_at.strftime("%Y-%m-%d, %H:%M") %>">
<%= note.created_at.strftime("%b %e '%y") %>
</time>
</span>
</p>
<% end %>
</div>
<% end %>
<% end %>
<%= form_with url: admin_article_path(article.id), model: article, local: true, class: "crayons-card crayons-card--secondary p-4 flex flex-col gap-3" do |f| %>
<input name="utf8" type="hidden" value="✓">
<input type="hidden" name="authenticity_token" value="<%= form_authenticity_token %>" />
<input type="hidden" name="_method" value="patch" />
<div class="flex gap-4">
<div class="crayons-field flex-1">
<label for="author_id_<%= article.id %>" class="crayons-field__label"><%= t("views.moderations.article.author_id") %></label>
<input id="author_id_<%= article.id %>" class="crayons-textfield" size="6" name="article[user_id]"
value="<%= article.user_id %>">
</div>
<div class="crayons-field flex-1">
<label for="co_author_ids_list_<%= article.id %>" class="crayons-field__label"><%= t("views.moderations.article.co_author_ids") %></label>
<input id="co_author_ids_list_<%= article.id %>" class="crayons-textfield" size="6" name="article[co_author_ids_list]" placeholder="Comma separated"
value="<%= article.co_author_ids&.join(", ") %>">
</div>
</div>
<% if article.published? %>
<div class="flex">
<div class="crayons-field">
<label for="published_at_<%= article.id %>" class="crayons-field__label"><%= t("views.moderations.article.published_at") %></label>
<div class="flex gap-1 items-center flex-wrap">
<%= f.datetime_select :published_at, { required: true, include_blank: true, include_seconds: true }, class: "crayons-select w-auto" %>
<span class="whitespace-nowrap color-secondary">UTC</span>
</div>
</div>
</div>
<% end %>
<div class="flex items-center gap-4">
<div class="crayons-field crayons-field--checkbox">
<%= f.check_box :featured, id: "featured-#{article.id}", class: "crayons-checkbox" %>
<label for="featured-<%= article.id %>" class="crayons-field__label">
<%= t("views.moderations.article.featured") %>
</label>
</div>
<div class="crayons-field crayons-field--checkbox">
<%= f.check_box :approved, id: "approved-#{article.id}", class: "crayons-checkbox" %>
<label for="approved-<%= article.id %>" class="crayons-field__label">
<%= t("views.moderations.article.approved") %>
</label>
</div>
</div>
<div>
<button class="c-btn c-btn--secondary"><%= t("views.moderations.article.save") %></button>
</div>
<% end %>
<div class="flex items-center gap-4">
<div class="crayons-field crayons-field--checkbox">
<%= f.check_box :featured, id: "featured-#{article.id}", class: "crayons-checkbox" %>
<label for="featured-<%= article.id %>" class="crayons-field__label">
Featured
</label>
</div>
</article>
<div class="crayons-field crayons-field--checkbox">
<%= f.check_box :approved, id: "approved-#{article.id}", class: "crayons-checkbox" %>
<label for="approved-<%= article.id %>" class="crayons-field__label">
Approved
</label>
</div>
</div>
<article class="js-individual-article crayons-card py-6 flex flex-col mt-4">
<h2 class="crayons-subtitle-2 mx-6"><%= t("views.moderations.priviliged_actions.title") %></h2>
<p class="crayons-subtitle-3 fw-normal color-secondary mt-1 mx-6"><%= t("views.moderations.priviliged_actions.description") %></p>
<nav class="mt-4 pt-1 pb-2 px-3 member-data-heading" aria-label="Member details">
<ul class="crayons-navigation crayons-navigation--horizontal">
<li><%= link_to "Flags", admin_article_path(tab: :flags), class: "crayons-navigation__item #{'crayons-navigation__item crayons-navigation__item--current' if params[:tab] == 'flags' || params[:tab].blank?}", aria: @current_tab == "flags" ? { current: "" } : {} %></li></li>
<li><%= link_to "Quality reactions", admin_article_path(tab: :quality_reactions), class: "crayons-navigation__item #{'crayons-navigation__item crayons-navigation__item--current' if params[:tab] == 'quality_reactions'}",
aria: @current_tab == "quality_reactions" ? { current: "" } : {} %></li></li>
</ul>
</nav>
<div id="reaction-content" class="flex flex-col gap-3 px-6 mt-6" style="overflow: scroll; height: 406px;">
<% if params[:tab].blank? || params[:tab] == "flags" %>
<% if vomit_article_reactions.present? %>
<% vomit_article_reactions.each do |vomit_reaction| %>
<%= render "admin/articles/flag_action_item", vomit_reaction: vomit_reaction %>
<hr id="js__reaction__div__hr__<%= vomit_reaction.id %>" class="w-100 hr-no-margins">
<% end %>
<% else %>
<div class="flex flex-col justify-center items-center gap-4 h-100">
<div class="flex p-4 gap-2 radius-default" style="background: #EEF2FF;">
<%= crayons_icon_tag("flags", native: true, width: 56, height: 56) %>
</div>
<p class="crayons-subtitle-3 fw-normal color-secondary"><%= t("views.moderations.priviliged_actions.no_flags") %></p>
</div>
<% end %>
<% end %>
<% if params[:tab] == "quality_reactions" %>
<% if quality_article_reactions.present? %>
<% quality_article_reactions.each do |quality_reaction| %>
<%= render "admin/articles/quality_action_item", quality_reaction: quality_reaction %>
<hr id="js__reaction__div__hr__<%= quality_reaction.id %>" class="w-100 hr-no-margins">
<% end %>
<% else %>
<div class="flex flex-col justify-center items-center gap-4 h-100">
<div class="flex p-4 gap-2 radius-default" style="background: #EEF2FF;">
<%= crayons_icon_tag("quality-reactions", native: true, width: 56, height: 56) %>
</div>
<p class="crayons-subtitle-3 fw-normal color-secondary"><%= t("views.moderations.priviliged_actions.no_quality_reactions") %></p>
</div>
<% end %>
<% end %>
<div>
<button class="c-btn c-btn--secondary">Save</button>
</div>
<% end %>
</article>
</article>
</div>

View file

@ -0,0 +1,42 @@
<div class="relative">
<button type="button" id="<%= vomit_reaction.id %>-action-dropdown-btn" data-toggle-dropdown="<%= vomit_reaction.id %>-action-dropdown" class="c-btn c-btn--icon-alone ml-auto"
aria-expanded="false" aria-controls="<%= vomit_reaction.id %>-action-dropdown" aria-haspopup="true" aria-label="Actions: <%= vomit_reaction.id %>">
<%= crayons_icon_tag("overflow-horizontal", aria_hidden: true) %>
</button>
<div id="<%= vomit_reaction.id %>-action-dropdown" class="crayons-dropdown right-0">
<ul class="list-none">
<% if vomit_reaction.status != 'confirmed' %>
<li>
<button
data-testid="mark-as-valid"
type="button"
data-mark-valid="<%= vomit_reaction.id %>"
data-status="confirmed"
data-reaction-target="confirmed"
data-action="reaction#updateReactionConfirmed"
data-remove-element="false"
class="c-btn align-left w-100 c-btn--icon-left">
<%= crayons_icon_tag("checkmark", aria_hidden: true, class: "c-btn__icon") %>
<%= t("views.moderations.flags.actions.mark_as_valid") %>
</button>
</li>
<% end %>
<% if vomit_reaction.status != 'invalid' %>
<li>
<button
data-testid="mark-as-invalid"
type="button"
data-mark-invalid="<%= vomit_reaction.id %>"
data-altstatus="invalid"
data-reaction-target="invalid"
data-action="reaction#updateReactionInvalid"
data-remove-element="false"
class="c-btn align-left w-100 c-btn--icon-left">
<%= crayons_icon_tag("x", aria_hidden: true, class: "c-btn__icon") %>
<%= t("views.moderations.flags.actions.mark_as_invalid") %>
</button>
</li>
<% end %>
</ul>
</div>
</div>

View file

@ -0,0 +1,18 @@
<div class="flex justify-between items-center">
<div class="flex items-center">
<div class="crayons-card crayons-card--secondary p-3 mr-7">
<%= crayons_icon_tag("twemoji/#{quality_reaction.category == 'thumbsup' ? 'thumb-up' : 'thumb-down'}", native: true, title: t("views.moderations.actions.#{quality_reaction.category == 'thumbsup' ? 'thumb_up' : 'thumb_down'}"), width: 24, height: 24) %>
</div>
<div class="p-0">
<p class="crayons-subtitle-3"><%= quality_reaction.user.name %></p>
<time datetime="<%= quality_reaction.updated_at&.strftime("%Y-%m-%dT%H:%M:%S%z") %>" class="color-secondary fs-s shrink-0" title="<%= l(quality_reaction.updated_at, format: :admin_user) if quality_reaction.created_at %>">
<%= l(quality_reaction.updated_at, format: :long) %>
</time>
</div>
</div>
<span class="crayons-card crayons-card--secondary px-3 py-1 ml-3 flex gap-2 items-center" title="Score affected by particular quality reaction">
<%= crayons_icon_tag("analytics", native: true, width: 16, height: 16) %>
<span class="fs-s fw-medium lh-base"><%= quality_reaction.points %></span>
</span>
</div>

View file

@ -20,7 +20,7 @@
<%= paginate @articles %>
<div
<div id="member-index-content"
class="flex flex-col gap-4"
data-controller="article-pinned-modal"
data-article-pinned-modal-root-selector-value="#article-pin-modal-root"
@ -34,18 +34,18 @@
<% if @pinned_article.present? %>
<div class="crayons-card crayons-card--elevated p-2">
<h2 class="crayons-subtitle-1 flex gap-2 items-center mb-2 p-2"><%= crayons_icon_tag("pin.svg") %> Pinned Article</h2>
<%= render partial: "individual_article", locals: { article: @pinned_article } %>
<%= render partial: "article_item", locals: { article: @pinned_article } %>
</div>
<% end %>
<% if @featured_articles.present? %>
<div class="crayons-card p-3">
<h2 class="crayons-subtitle-1 flex gap-2 items-center mb-2">Manually Featured Articles</h2>
<%= render partial: "individual_article", collection: @featured_articles, as: :article %>
<%= render partial: "article_item", collection: @featured_articles, as: :article %>
</div>
<% end %>
<%= render partial: "individual_article", collection: @articles, as: :article %>
<%= render partial: "article_item", collection: @articles, as: :article %>
<%= paginate @articles %>
<%= render partial: "pinned_article_modal" %>

View file

@ -149,6 +149,8 @@ Rails.application.configure do
Bullet.add_safelist(type: :n_plus_one_query, class_name: "User", association: :profile)
Bullet.add_safelist(type: :unused_eager_loading, class_name: "FeedbackMessage", association: :reporter)
Bullet.add_safelist(type: :unused_eager_loading, class_name: "Article", association: :reactions)
Bullet.add_safelist(type: :unused_eager_loading, class_name: "Reaction", association: :user)
# Check if there are any data update scripts to run during startup
if %w[Console Server DBConsole].any? { |const| Rails.const_defined?(const) } && DataUpdateScript.scripts_to_run?

View file

@ -75,6 +75,7 @@ en:
Use <b>Flag to Admins</b> for code of conduct violations (harassment, being a jerk, spam, etc.).
other: Other things you can do
add_reaction: Add a reaction
score: Score
suspend:
suspend_title: Suspend %{username}
suspend_user: Suspend %{username}
@ -117,6 +118,7 @@ en:
unpublish_post:
modal_text1: Once unpublished, this post will become invisible to the public and only accessible to %{username}.
modal_text2: They can still re-publish the post if they are not suspended.
vomit: Valid and open flags
vote_down: Low Quality
flag_to_admins: Flag to Admins
vote_up: High Quality
@ -124,6 +126,27 @@ en:
one: '%{count} post from the past day is featured.'
other: '%{count} posts from the past day are featured.'
all: All topics
article:
approved: Approved
author_id: Author ID
comments: comments
contains_video: Contains video
co_author_ids: Co-Author IDs
edit: Edit
featured: Featured
likes: likes
notes:
user_notes: user notes
view_all: View all
pinned: Pinned
pin_post: Pin post
published_at: Published at
save: Save
unpin_post: Unpin post
unplublished: Unplublished
user: User
user_warned: User warned
view_details: View details
aside:
all: All topics
code_of_conduct: Code of Conduct
@ -140,6 +163,22 @@ en:
trusted: Trusted User Guide
author: Author
date: Date
flags:
actions:
mark_as_invalid: Mark as Invalid
mark_as_valid: Mark as Valid
invalid:
title: Flag marked as invalid by admin
value: Invalid
open:
title: Open Flag
value: Open
unidentified:
title: Unidentified
value: Unidentified
valid:
title: Flag marked as valid by admin
value: Valid
notice:
subtitle: '%{community} Mods'
desc1_html: We periodically award some %{community} members with heightened privileges to help moderate the community.
@ -150,3 +189,8 @@ en:
tag: Tag Moderation Guide
trusted: Trusted User Guide
post: Post
priviliged_actions:
description: All the moderator actions affect the score. The overall score is a combination of moderator actions and public reactions.
no_flags: Article has no flags.
no_quality_reactions: Article has no quality reactions by trusted users.
title: Moderator actions

View file

@ -74,6 +74,7 @@ fr:
<br />
Utilisez <b>Signaler aux admins</b> pour les violations du code de conduite (harcèlement, être un abruti, spam, etc.).
other: Autres choses que vous pouvez faire
score: Score
suspend:
suspend_title: Suspendre %{username}
suspend_user: Suspendre %{username}
@ -116,6 +117,7 @@ fr:
unpublish_post:
modal_text1: Une fois dépublié, cet article sera invisible pour le public et accessible uniquement à %{username}.
modal_text2: Ils peuvent toujours republier l'article tant qu'ils ne sont pas suspendus.
vomit: Drapeaux valides et ouverts
vote_down: Faible qualité
flag_to_admins: Signaler aux admins
vote_up: Haute qualité
@ -123,6 +125,27 @@ fr:
one: '%{count} des articles de la journée précédente sont mis en avant.'
other: '%{count} des articles de la journée précédente sont mis en avant.'
all: Tous les sujets
article:
approved: Approuvée
author_id: Identifiant de l'auteur
comments: commentaires
contains_video: Contient une vidéo
co_author_ids: ID de co-auteur
edit: Modifier
featured: Mis en exergue
likes: aime
notes:
user_notes: notes d'utilisateur
view_all: Voir tout
pinned: Épinglé
pin_post: Épingler la publication
published_at: Publié à
save: Sauvegarder
unpin_post: Détacher la publication
unplublished: Inédit
user: Utilisateur
user_warned: Utilisateur averti
view_details: Voir les détails
aside:
all: Tous les sujets
code_of_conduct: Code de conduite
@ -139,6 +162,22 @@ fr:
trusted: Guide de l'utilisateur de confiance
author: Auteur
date: Date
flags:
actions:
mark_as_invalid: Marquer comme non valide
mark_as_valid: Marquer comme valide
invalid:
title: Drapeau marqué comme invalide par l'administrateur
value: Invalide
open:
title: Drapeau ouvert
value: Ouvrir
unidentified:
title: Non identifié
value: Non identifié
valid:
title: Drapeau marqué comme valide par l'administrateur
value: Valide
notice:
subtitle: '%{community} Mods'
desc1_html: Nous accordons périodiquement à certains membres de la %{community} des privilèges accrus afin de contribuer à la modération de la communauté.
@ -149,3 +188,8 @@ fr:
tag: Guide de modération des étiquettes
trusted: Guide de l'utilisateur de confiance
post: Article
priviliged_actions:
description: Toutes les actions du modérateur affectent le score. Le score global est une combinaison des actions des modérateurs et des réactions du public.
no_flags: L'article n'a pas de drapeaux.
no_quality_reactions: L'article n'a pas de réactions de qualité par des utilisateurs de confiance
title: Actions privilégiées

View file

@ -0,0 +1,268 @@
describe('New article has empty flags, quality reactions and score', () => {
let articleId;
beforeEach(() => {
cy.testSetup();
cy.fixture('users/adminUser.json').as('user');
cy.get('@user').then((user) => {
cy.loginUser(user).then(() => {
cy.createArticle({
title: 'Test article',
tags: ['beginner', 'ruby', 'go'],
content: `This is another test article's contents.`,
published: true,
}).then((response) => {
articleId = response.body.id;
cy.visit(`/admin/content_manager/articles/${articleId}`);
});
});
});
});
it('should update url on flag tab click', () => {
cy.findByRole('link', { name: 'Flags' }).click();
cy.url().should(
'contains',
`/admin/content_manager/articles/${articleId}?tab=flags`,
);
});
it('should update url on quality reactions tab click', () => {
cy.findByRole('link', { name: 'Quality reactions' }).click();
cy.url().should(
'contains',
`/admin/content_manager/articles/${articleId}?tab=quality_reactions`,
);
});
it('should not contain any flags and quality reactions', () => {
cy.findByText('Article has no flags.').should('exist');
cy.findByRole('link', { name: 'Quality reactions' }).click();
cy.findByText('Article has no quality reactions by trusted users.').should(
'exist',
);
});
it('should display the correct values for privileged reactions and score', () => {
// Thumbs-up count
cy.get('.flex .crayons-card:nth-child(1) .fs-s').should('contain', '0');
// Thumbs-down count
cy.get('.flex .crayons-card:nth-child(2) .fs-s').should('contain', '0');
// Vomit/flag count
cy.get('.flex .crayons-card:nth-child(3) .fs-s').should('contain', '0');
//Score
cy.get('.flex .crayons-card:nth-child(4) .fs-s').should('contain', '0');
});
});
describe('Article flagged by an admin user', () => {
beforeEach(() => {
cy.testSetup();
cy.fixture('users/adminUser.json').as('user');
cy.get('@user').then((user) => {
cy.loginUser(user).then(() => {
cy.visit('/admin/content_manager/articles');
cy.contains('.crayons-subtitle-1 a', 'Punctuation user article')
.parents('.js-individual-article')
.find('a[href*="/admin/content_manager/articles/"]')
.click();
});
});
});
it('should display the correct default data for flag reaction', () => {
// Vomit/flag count
cy.get('.flex .crayons-card:nth-child(3) .fs-s').should('contain', '1');
// Reaction username
cy.get('.flex .crayons-subtitle-3').should('contain', 'Admin McAdmin');
// Shows "Valid" status
cy.get('.flex .c-indicator').should('be.visible').as('flagStatus');
cy.get('@flagStatus')
.should('have.class', 'c-indicator--danger')
.and('contain', 'Valid');
// Reeaction score
cy.get('.flex .crayons-card:nth-child(2) .fs-s').should('contain', '-100');
});
it('should open the dropdown on button click and close the dropdown on same button click', () => {
// Opens the dropdown
cy.get('.c-btn--icon-alone').click();
cy.get('.crayons-dropdown').should('be.visible');
// Closes the dropdown on same button click
cy.get('.c-btn--icon-alone').click();
cy.get('.crayons-dropdown').should('not.visible');
});
it('should update status to Invalid on click of "Mark as Invalid"', () => {
cy.intercept('PATCH', '/admin/reactions/**', (req) => {
req.reply((res) => {
const { id } = req.body;
const response = {
statusCode: 200,
body: {
outcome: 'Success',
},
};
if (req.body.removeElement === true) {
cy.get(`#${id}`).then(($element) => {
$element.remove();
});
cy.get(`#js__reaction__div__hr__${id}`).then(($element) => {
$element.remove();
});
}
res.send(response);
});
}).as('request');
cy.get('.c-btn--icon-alone:first').click();
cy.get('.crayons-dropdown')
.find('ul.list-none')
.should('contain', 'Mark as Invalid')
.click();
cy.wait('@request');
cy.get('.flex .c-indicator').should('be.visible').as('flagStatus');
cy.get('@flagStatus').should('contain', 'Invalid');
});
});
describe('Article flagged by a trusted user', () => {
beforeEach(() => {
cy.testSetup();
cy.fixture('users/adminUser.json').as('user');
cy.get('@user').then((user) => {
cy.loginUser(user).then(() => {
cy.visit('/admin/content_manager/articles');
cy.contains('.crayons-subtitle-1 a', 'Notification article')
.parents('.js-individual-article')
.find('a[href*="/admin/content_manager/articles/"]')
.click();
});
});
});
it('should display the open flag status and correct score', () => {
// Open flag status
cy.get('.flex .c-indicator').should('be.visible').as('flagStatus');
cy.get('@flagStatus')
.should('have.class', 'c-indicator--warning')
.and('contain', 'Open');
// Score
cy.get('.flex .crayons-card:nth-child(2) .fs-s').should('contain', '-50');
});
it('should display both "Mark as Valid" and "Mark as Invalid" options in the dropdown', () => {
cy.get('.c-btn--icon-alone').click();
cy.get('.crayons-dropdown')
.find('ul.list-none')
.should('contain', 'Mark as Valid')
.and('contain', 'Mark as Invalid');
});
it('should close dropdown on click of "Mark as Valid"', () => {
cy.get('.c-btn--icon-alone:first').click();
cy.get('.crayons-dropdown')
.find('ul.list-none')
.should('contain', 'Mark as Valid')
.click();
cy.get('.crayons-dropdown').should('not.be.visible');
});
it('should update status to Invalid on click of "Mark as Invalid"', () => {
cy.intercept('PATCH', '/admin/reactions/**', (req) => {
req.reply((res) => {
const { id } = req.body;
const response = {
statusCode: 200,
body: {
outcome: 'Success',
},
};
if (req.body.removeElement === true) {
cy.get(`#${id}`).then(($element) => {
$element.remove();
});
cy.get(`#js__reaction__div__hr__${id}`).then(($element) => {
$element.remove();
});
}
res.send(response);
});
}).as('request');
cy.get('.c-btn--icon-alone:first').click();
cy.get('.crayons-dropdown')
.find('ul.list-none')
.should('contain', 'Mark as Invalid')
.click();
cy.wait('@request');
cy.get('.flex .c-indicator').should('be.visible').as('flagStatus');
cy.get('@flagStatus')
.should('have.class', 'c-indicator--relaxed')
.and('contain', 'Invalid');
});
it('should update status to Valid on click of "Mark as Valid"', () => {
cy.intercept('PATCH', '/admin/reactions/**', (req) => {
req.reply((res) => {
const { id } = req.body;
const response = {
statusCode: 200,
body: {
outcome: 'Success',
},
};
if (req.body.removeElement === true) {
cy.get(`#${id}`).then(($element) => {
$element.remove();
});
cy.get(`#js__reaction__div__hr__${id}`).then(($element) => {
$element.remove();
});
}
res.send(response);
});
}).as('request');
cy.get('.c-btn--icon-alone:first').click();
cy.get('.crayons-dropdown')
.find('ul.list-none')
.findByTestId('mark-as-valid')
.should('contain', 'Mark as Valid')
.click();
cy.wait('@request');
cy.get('.flex .c-indicator').should('be.visible').as('flagStatus');
cy.get('@flagStatus').should('contain', 'Valid');
});
});

View file

@ -0,0 +1,40 @@
describe('View details link on article list page admin area', () => {
beforeEach(() => {
cy.testSetup();
cy.fixture('users/adminUser.json').as('user');
cy.get('@user').then((user) => {
cy.loginAndVisit(user, '/').then(() => {
cy.createArticle({
title: 'Test Article',
tags: ['beginner', 'ruby', 'go'],
content: `This is a test article's contents.`,
published: true,
}).then(() => {
cy.visit('/admin/content_manager/articles');
});
});
});
});
it('should contain view details button with correct link', () => {
cy.findAllByRole('link', { name: 'View details' }).each(($link) => {
const href = $link.attr('href');
const regex = /\/(\d+)/; // Regular expression to match any number in the URL
expect(href).to.match(regex);
});
});
it('should not details view details button on individual article page', () => {
cy.createArticle({
title: 'A new article',
tags: ['beginner', 'ruby', 'go'],
content: `This is another test article's contents.`,
published: true,
}).then((response) => {
cy.visit(`/admin/content_manager/articles/${response.body.id}`);
});
cy.findAllByRole('link', { name: 'View details' }).should('not.exist');
});
});

View file

@ -128,18 +128,19 @@ seeder.create_if_doesnt_exist(User, "email", "punctuated-name-user@forem.local")
#{Faker::Markdown.random}
#{Faker::Hipster.paragraph(sentence_count: 2)}
MARKDOWN
Article.create!(
article = Article.create!(
body_markdown: markdown,
featured: true,
show_comments: true,
user_id: user.id,
slug: "apostrophe-user-slug",
)
seeder.create_if_none(Reaction) do
admin_user.reactions.create!(category: :vomit, reactable: article, status: :confirmed)
end
end
end
##############################################################################
seeder.create_if_doesnt_exist(User, "email", "user-with-many-orgs@forem.local") do
User.create!(
name: "Many orgs user",
@ -440,6 +441,8 @@ seeder.create_if_doesnt_exist(User, "email", "notifications-user@forem.local") d
commentable_type: "Article"
}
trusted_user.reactions.create!(category: :vomit, reactable: article)
parent_comment = Comment.create!(parent_comment_attributes)
Notification.send_new_comment_notifications_without_delay(parent_comment)