* Add SiteConfig.feed_pinned_article and validation * Display pinned article at the top of feed * Add (basic) functionality to pin/unpin post * Admins can pin other users posts as well * Hide the button if looking at the non pinned post * Add pinned/unpinned snackbar message * Rename SiteConfig usage to Settings::General * Add pinned article to the Admin articles index * Show the pin post button when there's no pinned article * Move pinning to a separate controller * Fix SiteConfig reference * Hide PinController actions to unauthorized users * PinnedArticlesController#show action and refactor some of the code * Add Modal interaction * Fix modal-pinned checkbox interaction * Fixed pin/unpin post * Add ArticleDecorator#pinned? specs * Add PinnedArticlePolicy and PinnedArticlesController specs * Add ability to actually pin an article from the admin after submit * Add partial Cypress pin/unpin spec * Fix pinned article and add basic Cypress interaction tests * Add Crayons styling to modal * Only render the pinned article on the default Feed page * Use persisted? * Add some comments * Update app/javascript/articles/Article.jsx Co-authored-by: Suzanne Aitchison <suzanne@forem.com> * Update app/javascript/packs/homePageFeed.jsx Co-authored-by: Suzanne Aitchison <suzanne@forem.com> * Fix Cypress tests * Update app/javascript/admin/controllers/article_controller.js Co-authored-by: Ridhwana <Ridhwana.Khan16@gmail.com> * Fix pinning in article show page * Used PinnedArticle domain model * Fix spec * Update cypress/integration/adminFlows/articles/pinArticle.spec.js Co-authored-by: Suzanne Aitchison <suzanne@forem.com> * Update cypress/integration/adminFlows/articles/pinArticle.spec.js Co-authored-by: Suzanne Aitchison <suzanne@forem.com> * Update cypress/integration/adminFlows/articles/pinArticle.spec.js Co-authored-by: Suzanne Aitchison <suzanne@forem.com> * Update cypress/integration/adminFlows/articles/pinArticle.spec.js Co-authored-by: Suzanne Aitchison <suzanne@forem.com> * Update cypress/integration/adminFlows/articles/pinArticle.spec.js Co-authored-by: Suzanne Aitchison <suzanne@forem.com> * Update cypress/integration/articleFlows/pinArticle.spec.js Co-authored-by: Suzanne Aitchison <suzanne@forem.com> * Update cypress/integration/articleFlows/pinArticle.spec.js Co-authored-by: Suzanne Aitchison <suzanne@forem.com> * Update app/views/admin/articles/index.html.erb Co-authored-by: Suzanne Aitchison <suzanne@forem.com> * Fix merge woes * Add missing article pin post flows * Add missing admin article flows * Add Unpin to Admin as well * Add Audit::Log entries for pin/unpin actions * Update app/controllers/stories/feeds_controller.rb Co-authored-by: Michael Kohl <citizen428@dev.to> * Do not rate limit in E2E tests * Use .find instead of .filter * Rename ArticleIdValidator to ExistingArticleIdValidator * Treat draft and deleted articles the same * Make sure posts can be pinned after the pinned article is unpublished or deleted * Use .get directly * Fix spec and fix PinnedArticlesController#show * Strengthen pinArticle Cypress tests * Add Cypress test heading guard * Add another Cypress test heading guard * Remove duplicate validator * Try using the Tools: header instead of the article title Co-authored-by: Suzanne Aitchison <suzanne@forem.com> Co-authored-by: Ridhwana <Ridhwana.Khan16@gmail.com> Co-authored-by: Michael Kohl <citizen428@dev.to>
163 lines
5.2 KiB
JavaScript
163 lines
5.2 KiB
JavaScript
import { h, render } from 'preact';
|
|
import { Snackbar, addSnackbarItem } from '../Snackbar';
|
|
import { addFullScreenModeControl } from '../utilities/codeFullscreenModeSwitcher';
|
|
import { initializeDropdown } from '@utilities/dropdownUtils';
|
|
|
|
/* global Runtime */
|
|
|
|
const fullscreenActionElements = document.getElementsByClassName(
|
|
'js-fullscreen-code-action',
|
|
);
|
|
|
|
if (fullscreenActionElements) {
|
|
addFullScreenModeControl(fullscreenActionElements);
|
|
}
|
|
|
|
// The Snackbar for the article page
|
|
const snackZone = document.getElementById('snack-zone');
|
|
if (snackZone) {
|
|
render(<Snackbar lifespan="3" />, snackZone);
|
|
}
|
|
|
|
// eslint-disable-next-line no-restricted-globals
|
|
top.addSnackbarItem = addSnackbarItem;
|
|
|
|
// Dropdown accessibility
|
|
function hideCopyLinkAnnouncerIfVisible() {
|
|
document.getElementById('article-copy-link-announcer').hidden = true;
|
|
}
|
|
|
|
// Initialize the share options
|
|
const shareDropdownButton = document.getElementById('article-show-more-button');
|
|
|
|
if (shareDropdownButton.dataset.initialized !== 'true') {
|
|
if (Runtime.isNativeAndroid('shareText')) {
|
|
// Android native apps have enhanced sharing capabilities for Articles and don't use our standard dropdown
|
|
shareDropdownButton.addEventListener('click', () =>
|
|
AndroidBridge.shareText(location.href),
|
|
);
|
|
} else {
|
|
const { closeDropdown } = initializeDropdown({
|
|
triggerElementId: 'article-show-more-button',
|
|
dropdownContentId: 'article-show-more-dropdown',
|
|
onClose: hideCopyLinkAnnouncerIfVisible,
|
|
});
|
|
|
|
// We want to close the dropdown on link select (since they open in a new tab)
|
|
document
|
|
.querySelectorAll('#article-show-more-dropdown [href]')
|
|
.forEach((link) => link.addEventListener('click', closeDropdown));
|
|
}
|
|
shareDropdownButton.dataset.initialized = 'true';
|
|
}
|
|
|
|
// Initialize the copy to clipboard functionality
|
|
function showAnnouncer() {
|
|
const { activeElement } = document;
|
|
const input =
|
|
activeElement.localName === 'clipboard-copy'
|
|
? activeElement.querySelector('input')
|
|
: document.getElementById('article-copy-link-input');
|
|
input.focus();
|
|
input.setSelectionRange(0, input.value.length);
|
|
|
|
document.getElementById('article-copy-link-announcer').hidden = false;
|
|
}
|
|
|
|
function copyArticleLink() {
|
|
const inputValue = document.getElementById('article-copy-link-input').value;
|
|
Runtime.copyToClipboard(inputValue).then(() => {
|
|
showAnnouncer();
|
|
});
|
|
}
|
|
document
|
|
.querySelector('clipboard-copy')
|
|
?.addEventListener('click', copyArticleLink);
|
|
|
|
// Comment Subscription
|
|
const userDataIntervalID = setInterval(async () => {
|
|
const { user = null, userStatus } = document.body.dataset;
|
|
|
|
clearInterval(userDataIntervalID);
|
|
const root = document.getElementById('comment-subscription');
|
|
const isLoggedIn = userStatus === 'logged-in';
|
|
|
|
try {
|
|
const {
|
|
getCommentSubscriptionStatus,
|
|
setCommentSubscriptionStatus,
|
|
CommentSubscription,
|
|
} = await import('../CommentSubscription');
|
|
|
|
const { articleId } = document.getElementById('article-body').dataset;
|
|
|
|
let subscriptionType = 'not_subscribed';
|
|
|
|
if (isLoggedIn && user !== null) {
|
|
({ config: subscriptionType } = await getCommentSubscriptionStatus(
|
|
articleId,
|
|
));
|
|
}
|
|
|
|
const subscriptionRequestHandler = async (type) => {
|
|
const message = await setCommentSubscriptionStatus(articleId, type);
|
|
|
|
addSnackbarItem({ message, addCloseButton: true });
|
|
};
|
|
|
|
render(
|
|
<CommentSubscription
|
|
subscriptionType={subscriptionType}
|
|
positionType="static"
|
|
onSubscribe={subscriptionRequestHandler}
|
|
onUnsubscribe={subscriptionRequestHandler}
|
|
isLoggedIn={isLoggedIn}
|
|
/>,
|
|
root,
|
|
);
|
|
} catch (e) {
|
|
document.getElementById('comment-subscription').innerHTML =
|
|
'<p className="color-accent-danger">Unable to load Comment Subscription component.<br />Try refreshing the page.</p>';
|
|
}
|
|
});
|
|
|
|
// Pin/Unpin article
|
|
// these element are added by initializeBaseUserData.js:addRelevantButtonsToArticle
|
|
const toggleArticlePin = async (button) => {
|
|
const isPinButton = button.id === 'js-pin-article';
|
|
const { articleId, path } = button.dataset;
|
|
const method = isPinButton ? 'PUT' : 'DELETE';
|
|
const body = method === 'PUT' ? JSON.stringify({ id: articleId }) : null;
|
|
|
|
const response = await fetch(path, {
|
|
method,
|
|
body,
|
|
headers: {
|
|
Accept: 'application/json',
|
|
'X-CSRF-Token': window.csrfToken,
|
|
'Content-Type': 'application/json',
|
|
},
|
|
credentials: 'same-origin',
|
|
});
|
|
|
|
// response could potentially fail if the article is draft but we don't show
|
|
// the buttons in those cases, so I think there's no need to handle that scenario client side
|
|
if (response.ok) {
|
|
// replace id and label
|
|
button.id = isPinButton ? 'js-unpin-article' : 'js-pin-article';
|
|
button.innerHTML = `${isPinButton ? 'Unpin' : 'Pin'} Post`;
|
|
|
|
const message = isPinButton
|
|
? 'The post has been succesfully pinned'
|
|
: 'The post has been succesfully unpinned';
|
|
addSnackbarItem({ message });
|
|
}
|
|
};
|
|
|
|
const actionsContainer = document.getElementById('action-space');
|
|
const pinTargets = ['js-pin-article', 'js-unpin-article'];
|
|
actionsContainer.addEventListener('click', async (event) => {
|
|
if (pinTargets.includes(event.target.id)) {
|
|
toggleArticlePin(event.target);
|
|
}
|
|
});
|