From f059832671a54dd794e5569544294200dafcb5e2 Mon Sep 17 00:00:00 2001 From: Marcel Werk Date: Fri, 3 Jan 2025 16:14:18 +0100 Subject: [PATCH] Popover for CMS articles --- com.woltlab.wcf/templates/article.tpl | 4 +- com.woltlab.wcf/templates/articlePopover.tpl | 31 +++++++++++ com.woltlab.wcf/templates/boxArticleList.tpl | 2 +- .../Core/Api/Articles/GetArticlePopover.ts | 28 ++++++++++ ts/WoltLabSuite/Core/BootstrapFrontend.ts | 16 ++++++ .../Core/Api/Articles/GetArticlePopover.js | 24 +++++++++ .../js/WoltLabSuite/Core/BootstrapFrontend.js | 30 ++++++++--- .../files/lib/bootstrap/com.woltlab.wcf.php | 1 + .../files/lib/data/article/Article.class.php | 9 +++- .../core/articles/GetArticlePopover.class.php | 52 +++++++++++++++++++ 10 files changed, 185 insertions(+), 12 deletions(-) create mode 100644 com.woltlab.wcf/templates/articlePopover.tpl create mode 100644 ts/WoltLabSuite/Core/Api/Articles/GetArticlePopover.ts create mode 100644 wcfsetup/install/files/js/WoltLabSuite/Core/Api/Articles/GetArticlePopover.js create mode 100644 wcfsetup/install/files/lib/system/endpoint/controller/core/articles/GetArticlePopover.class.php diff --git a/com.woltlab.wcf/templates/article.tpl b/com.woltlab.wcf/templates/article.tpl index eed09e2e4e..6373f9deb1 100644 --- a/com.woltlab.wcf/templates/article.tpl +++ b/com.woltlab.wcf/templates/article.tpl @@ -272,7 +272,7 @@ {lang}wcf.article.previousArticle{/lang} - @@ -291,7 +291,7 @@ {lang}wcf.article.nextArticle{/lang} - diff --git a/com.woltlab.wcf/templates/articlePopover.tpl b/com.woltlab.wcf/templates/articlePopover.tpl new file mode 100644 index 0000000000..864f2bce47 --- /dev/null +++ b/com.woltlab.wcf/templates/articlePopover.tpl @@ -0,0 +1,31 @@ +
+ {if $article->getTeaserImage()} +
+ +
+ {/if} + +
+ {event name='beforeHeader'} + +
+ {user object=$article->getUserProfile() type='avatar48' ariaHidden='true' tabindex='-1'} +
+ +
+ {time time=$article->time} +
+ + {event name='afterHeader'} +
+ + {event name='beforeText'} + +
+ {unsafe:$article->getFormattedTeaser()} +
+ + {event name='afterText'} +
diff --git a/com.woltlab.wcf/templates/boxArticleList.tpl b/com.woltlab.wcf/templates/boxArticleList.tpl index 0dcd50e506..e82b06fe6e 100644 --- a/com.woltlab.wcf/templates/boxArticleList.tpl +++ b/com.woltlab.wcf/templates/boxArticleList.tpl @@ -5,7 +5,7 @@
-

{$boxArticle->getTitle()}

+

{anchor object=$boxArticle class='articleLink' title=$boxArticle->getTitle()}

{if $boxSortField == 'time'} diff --git a/ts/WoltLabSuite/Core/Api/Articles/GetArticlePopover.ts b/ts/WoltLabSuite/Core/Api/Articles/GetArticlePopover.ts new file mode 100644 index 0000000000..f261bf532a --- /dev/null +++ b/ts/WoltLabSuite/Core/Api/Articles/GetArticlePopover.ts @@ -0,0 +1,28 @@ +/** + * Gets the html code for the rendering of an article popover. + * + * @author Marcel Werk + * @copyright 2001-2025 WoltLab GmbH + * @license GNU Lesser General Public License + * @since 6.2 + */ + +import { prepareRequest } from "WoltLabSuite/Core/Ajax/Backend"; +import { ApiResult, apiResultFromError, apiResultFromValue } from "WoltLabSuite/Core/Api/Result"; + +type Response = { + template: string; +}; + +export async function getArticlePopover(articleId: number): Promise> { + const url = new URL(`${window.WSC_RPC_API_URL}core/articles/${articleId}/popover`); + + let response: Response; + try { + response = (await prepareRequest(url).get().fetchAsJson()) as Response; + } catch (e) { + return apiResultFromError(e); + } + + return apiResultFromValue(response.template); +} diff --git a/ts/WoltLabSuite/Core/BootstrapFrontend.ts b/ts/WoltLabSuite/Core/BootstrapFrontend.ts index bf3f2b9c14..efc594736f 100644 --- a/ts/WoltLabSuite/Core/BootstrapFrontend.ts +++ b/ts/WoltLabSuite/Core/BootstrapFrontend.ts @@ -19,6 +19,7 @@ import UiPageMenuMainFrontend from "./Ui/Page/Menu/Main/Frontend"; import { whenFirstSeen } from "./LazyLoader"; import { prepareRequest } from "./Ajax/Backend"; import { setup as serviceWorkerSetup } from "./Notification/ServiceWorker"; +import { getArticlePopover } from "./Api/Articles/GetArticlePopover"; interface BootstrapOptions { backgroundQueue: { @@ -57,6 +58,20 @@ function setupUserPopover(endpoint: string): void { }); } +function setupArticlePopover(): void { + whenFirstSeen(".articleLink", () => { + void import("WoltLabSuite/Core/Component/Popover").then(({ setupFor }) => { + setupFor({ + endpoint: async (objectId: number) => { + return (await getArticlePopover(objectId)).unwrap(); + }, + identifier: "com.woltlab.wcf.article", + selector: ".articleLink", + }); + }); + }); +} + declare const COMPILER_TARGET_DEFAULT: boolean; /** @@ -80,6 +95,7 @@ export function setup(options: BootstrapOptions): void { } setupUserPopover(options.endpointUserPopover); + setupArticlePopover(); if (options.executeCronjobs !== undefined) { void prepareRequest(options.executeCronjobs) diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/Api/Articles/GetArticlePopover.js b/wcfsetup/install/files/js/WoltLabSuite/Core/Api/Articles/GetArticlePopover.js new file mode 100644 index 0000000000..e359df9466 --- /dev/null +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/Api/Articles/GetArticlePopover.js @@ -0,0 +1,24 @@ +/** + * Gets the html code for the rendering of an article popover. + * + * @author Marcel Werk + * @copyright 2001-2025 WoltLab GmbH + * @license GNU Lesser General Public License + * @since 6.2 + */ +define(["require", "exports", "WoltLabSuite/Core/Ajax/Backend", "WoltLabSuite/Core/Api/Result"], function (require, exports, Backend_1, Result_1) { + "use strict"; + Object.defineProperty(exports, "__esModule", { value: true }); + exports.getArticlePopover = getArticlePopover; + async function getArticlePopover(articleId) { + const url = new URL(`${window.WSC_RPC_API_URL}core/articles/${articleId}/popover`); + let response; + try { + response = (await (0, Backend_1.prepareRequest)(url).get().fetchAsJson()); + } + catch (e) { + return (0, Result_1.apiResultFromError)(e); + } + return (0, Result_1.apiResultFromValue)(response.template); + } +}); diff --git a/wcfsetup/install/files/js/WoltLabSuite/Core/BootstrapFrontend.js b/wcfsetup/install/files/js/WoltLabSuite/Core/BootstrapFrontend.js index 4ef44badd6..5188e74aa2 100644 --- a/wcfsetup/install/files/js/WoltLabSuite/Core/BootstrapFrontend.js +++ b/wcfsetup/install/files/js/WoltLabSuite/Core/BootstrapFrontend.js @@ -5,7 +5,7 @@ * @copyright 2001-2019 WoltLab GmbH * @license GNU Lesser General Public License */ -define(["require", "exports", "tslib", "./BackgroundQueue", "./Bootstrap", "./Ui/User/Ignore", "./Ui/Page/Header/Menu", "./Ui/Message/UserConsent", "./Ui/Message/Share/Dialog", "./Ui/Message/Share/Providers", "./Ui/Feed/Dialog", "./User", "./Ui/Page/Menu/Main/Frontend", "./LazyLoader", "./Ajax/Backend", "./Notification/ServiceWorker"], function (require, exports, tslib_1, BackgroundQueue, Bootstrap, UiUserIgnore, UiPageHeaderMenu, UiMessageUserConsent, UiMessageShareDialog, Providers_1, UiFeedDialog, User_1, Frontend_1, LazyLoader_1, Backend_1, ServiceWorker_1) { +define(["require", "exports", "tslib", "./BackgroundQueue", "./Bootstrap", "./Ui/User/Ignore", "./Ui/Page/Header/Menu", "./Ui/Message/UserConsent", "./Ui/Message/Share/Dialog", "./Ui/Message/Share/Providers", "./Ui/Feed/Dialog", "./User", "./Ui/Page/Menu/Main/Frontend", "./LazyLoader", "./Ajax/Backend", "./Notification/ServiceWorker", "./Api/Articles/GetArticlePopover"], function (require, exports, tslib_1, BackgroundQueue, Bootstrap, UiUserIgnore, UiPageHeaderMenu, UiMessageUserConsent, UiMessageShareDialog, Providers_1, UiFeedDialog, User_1, Frontend_1, LazyLoader_1, Backend_1, ServiceWorker_1, GetArticlePopover_1) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.setup = setup; @@ -35,6 +35,19 @@ define(["require", "exports", "tslib", "./BackgroundQueue", "./Bootstrap", "./Ui }); }); } + function setupArticlePopover() { + (0, LazyLoader_1.whenFirstSeen)(".articleLink", () => { + void new Promise((resolve_2, reject_2) => { require(["WoltLabSuite/Core/Component/Popover"], resolve_2, reject_2); }).then(tslib_1.__importStar).then(({ setupFor }) => { + setupFor({ + endpoint: async (objectId) => { + return (await (0, GetArticlePopover_1.getArticlePopover)(objectId)).unwrap(); + }, + identifier: "com.woltlab.wcf.article", + selector: ".articleLink", + }); + }); + }); + } /** * Bootstraps general modules and frontend exclusive ones. */ @@ -48,11 +61,12 @@ define(["require", "exports", "tslib", "./BackgroundQueue", "./Bootstrap", "./Ui }); UiPageHeaderMenu.init(); if (options.styleChanger) { - void new Promise((resolve_2, reject_2) => { require(["./Controller/Style/Changer"], resolve_2, reject_2); }).then(tslib_1.__importStar).then((ControllerStyleChanger) => { + void new Promise((resolve_3, reject_3) => { require(["./Controller/Style/Changer"], resolve_3, reject_3); }).then(tslib_1.__importStar).then((ControllerStyleChanger) => { ControllerStyleChanger.setup(); }); } setupUserPopover(options.endpointUserPopover); + setupArticlePopover(); if (options.executeCronjobs !== undefined) { void (0, Backend_1.prepareRequest)(options.executeCronjobs) .get() @@ -82,22 +96,22 @@ define(["require", "exports", "tslib", "./BackgroundQueue", "./Bootstrap", "./Ui } } (0, LazyLoader_1.whenFirstSeen)("woltlab-core-reaction-summary", () => { - void new Promise((resolve_3, reject_3) => { require(["./Ui/Reaction/SummaryDetails"], resolve_3, reject_3); }).then(tslib_1.__importStar).then(({ setup }) => setup()); + void new Promise((resolve_4, reject_4) => { require(["./Ui/Reaction/SummaryDetails"], resolve_4, reject_4); }).then(tslib_1.__importStar).then(({ setup }) => setup()); }); (0, LazyLoader_1.whenFirstSeen)("woltlab-core-comment", () => { - void new Promise((resolve_4, reject_4) => { require(["./Component/Comment/woltlab-core-comment"], resolve_4, reject_4); }).then(tslib_1.__importStar); + void new Promise((resolve_5, reject_5) => { require(["./Component/Comment/woltlab-core-comment"], resolve_5, reject_5); }).then(tslib_1.__importStar); }); (0, LazyLoader_1.whenFirstSeen)("woltlab-core-comment-response", () => { - void new Promise((resolve_5, reject_5) => { require(["./Component/Comment/Response/woltlab-core-comment-response"], resolve_5, reject_5); }).then(tslib_1.__importStar); + void new Promise((resolve_6, reject_6) => { require(["./Component/Comment/Response/woltlab-core-comment-response"], resolve_6, reject_6); }).then(tslib_1.__importStar); }); (0, LazyLoader_1.whenFirstSeen)("woltlab-core-emoji-picker", () => { - void new Promise((resolve_6, reject_6) => { require(["./Component/EmojiPicker/woltlab-core-emoji-picker"], resolve_6, reject_6); }).then(tslib_1.__importStar); + void new Promise((resolve_7, reject_7) => { require(["./Component/EmojiPicker/woltlab-core-emoji-picker"], resolve_7, reject_7); }).then(tslib_1.__importStar); }); (0, LazyLoader_1.whenFirstSeen)("[data-follow-user]", () => { - void new Promise((resolve_7, reject_7) => { require(["./Component/User/Follow"], resolve_7, reject_7); }).then(tslib_1.__importStar).then(({ setup }) => setup()); + void new Promise((resolve_8, reject_8) => { require(["./Component/User/Follow"], resolve_8, reject_8); }).then(tslib_1.__importStar).then(({ setup }) => setup()); }); (0, LazyLoader_1.whenFirstSeen)("[data-ignore-user]", () => { - void new Promise((resolve_8, reject_8) => { require(["./Component/User/Ignore"], resolve_8, reject_8); }).then(tslib_1.__importStar).then(({ setup }) => setup()); + void new Promise((resolve_9, reject_9) => { require(["./Component/User/Ignore"], resolve_9, reject_9); }).then(tslib_1.__importStar).then(({ setup }) => setup()); }); } }); diff --git a/wcfsetup/install/files/lib/bootstrap/com.woltlab.wcf.php b/wcfsetup/install/files/lib/bootstrap/com.woltlab.wcf.php index 6bdd453fef..47f82dfd40 100644 --- a/wcfsetup/install/files/lib/bootstrap/com.woltlab.wcf.php +++ b/wcfsetup/install/files/lib/bootstrap/com.woltlab.wcf.php @@ -116,6 +116,7 @@ return static function (): void { $eventHandler->register( \wcf\event\endpoint\ControllerCollecting::class, static function (\wcf\event\endpoint\ControllerCollecting $event) { + $event->register(new \wcf\system\endpoint\controller\core\articles\GetArticlePopover); $event->register(new \wcf\system\endpoint\controller\core\files\DeleteFile); $event->register(new \wcf\system\endpoint\controller\core\files\GenerateThumbnails); $event->register(new \wcf\system\endpoint\controller\core\files\PrepareUpload); diff --git a/wcfsetup/install/files/lib/data/article/Article.class.php b/wcfsetup/install/files/lib/data/article/Article.class.php index 93de809aac..5df3fb1dae 100644 --- a/wcfsetup/install/files/lib/data/article/Article.class.php +++ b/wcfsetup/install/files/lib/data/article/Article.class.php @@ -7,6 +7,7 @@ use wcf\data\article\content\ArticleContent; use wcf\data\attachment\GroupedAttachmentList; use wcf\data\DatabaseObject; use wcf\data\ILinkableObject; +use wcf\data\IPopoverObject; use wcf\data\IUserContent; use wcf\data\object\type\ObjectTypeCache; use wcf\data\user\UserProfile; @@ -38,7 +39,7 @@ use wcf\system\WCF; * @property-read int $isDeleted is 1 if the article is in trash bin, otherwise 0 * @property-read int $hasLabels is `1` if labels are assigned to the article */ -class Article extends DatabaseObject implements ILinkableObject, IUserContent +class Article extends DatabaseObject implements ILinkableObject, IPopoverObject, IUserContent { /** * indicates that article is unpublished @@ -443,4 +444,10 @@ class Article extends DatabaseObject implements ILinkableObject, IUserContent return null; } + + #[\Override] + public function getPopoverLinkClass() + { + return 'articleLink'; + } } diff --git a/wcfsetup/install/files/lib/system/endpoint/controller/core/articles/GetArticlePopover.class.php b/wcfsetup/install/files/lib/system/endpoint/controller/core/articles/GetArticlePopover.class.php new file mode 100644 index 0000000000..05c0e7e0b5 --- /dev/null +++ b/wcfsetup/install/files/lib/system/endpoint/controller/core/articles/GetArticlePopover.class.php @@ -0,0 +1,52 @@ + + * @since 6.2 + */ +#[GetRequest('/core/articles/{id:\d+}/popover')] +final class GetArticlePopover implements IController +{ + #[\Override] + public function __invoke(ServerRequestInterface $request, array $variables): ResponseInterface + { + $article = Helper::fetchObjectFromRequestParameter($variables['id'], Article::class); + + $this->assertArticleIsAccessible($article); + + return new JsonResponse([ + 'template' => $this->renderPopover($article), + ]); + } + + private function assertArticleIsAccessible(Article $article): void + { + if (!$article->canRead()) { + throw new PermissionDeniedException(); + } + } + + private function renderPopover(Article $article): string + { + return WCF::getTPL()->fetch('articlePopover', 'wcf', [ + 'article' => ViewableArticle::getArticle($article->articleID), + ], true); + } +} -- 2.20.1