fe_comment: Feedback Overview content element in the page JSON
- New content element "Feedback Overview" (CType fecomment_overview):
the current FE user's feedback state under content.feedbackOverview,
so the overview page renders like any other page instead of a
front-end special case
- Rendered as USER_INT inside the cached page JSON (EXT:headless replaces
the field per request): data is per user, visitors without a session
get {"allowed": false} - verified that nothing leaks from the cache
- One implementation for both consumers: the new FeedbackState service
builds the state payload; the API middleware delegates to it
- State payload (API and element) gained canSubmit and actions (all
write endpoints with method and URL), so the front end needs no
hard-coded endpoints for the submit button below the ticket list
- New site set evomedien/fe-comment carries the TypoScript, added to the
site configuration; icon fecomment-overview
This commit is contained in:
1 parent
887caed2aa
commit
eb0d0d9ebc
9 files changed
+301
-110
No files matched your search
@@ -24,6 +24,7 @@ dependencies:
|
|||||||
- apache-solr-for-typo3/solr-stylesheets
|
- apache-solr-for-typo3/solr-stylesheets
|
||||||
- apache-solr-for-typo3/solr-bootstrap-css
|
- apache-solr-for-typo3/solr-bootstrap-css
|
||||||
- evomedien/megamenu
|
- evomedien/megamenu
|
||||||
|
- evomedien/fe-comment
|
||||||
- vitec/quotation
|
- vitec/quotation
|
||||||
- vitec/featured-content
|
- vitec/featured-content
|
||||||
frontendBase: ''
|
frontendBase: ''
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
|||||||
namespace Evomedien\FeComment\Middleware;
|
namespace Evomedien\FeComment\Middleware;
|
||||||
|
|
||||||
use Evomedien\FeComment\Service\ExtensionSettings;
|
use Evomedien\FeComment\Service\ExtensionSettings;
|
||||||
|
use Evomedien\FeComment\Service\FeedbackState;
|
||||||
use Psr\Http\Message\ResponseInterface;
|
use Psr\Http\Message\ResponseInterface;
|
||||||
use Psr\Http\Message\ServerRequestInterface;
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
use Psr\Http\Server\MiddlewareInterface;
|
use Psr\Http\Server\MiddlewareInterface;
|
||||||
@@ -91,23 +92,13 @@ final class FeedbackApiMiddleware implements MiddlewareInterface
|
|||||||
/** @return array{uid:int,username:string,name:string,email:string}|null */
|
/** @return array{uid:int,username:string,name:string,email:string}|null */
|
||||||
private function allowedUser(ServerRequestInterface $request): ?array
|
private function allowedUser(ServerRequestInterface $request): ?array
|
||||||
{
|
{
|
||||||
$frontendUser = $request->getAttribute('frontend.user');
|
return $this->stateService()->allowedUser($request);
|
||||||
if (!$frontendUser instanceof FrontendUserAuthentication || empty($frontendUser->user['uid'])) {
|
}
|
||||||
return null;
|
|
||||||
}
|
/** Shared with the "Feedback Overview" content element - one implementation. */
|
||||||
$requiredGroup = $this->settings->feUserGroup();
|
private function stateService(): FeedbackState
|
||||||
if ($requiredGroup > 0) {
|
{
|
||||||
$groups = GeneralUtility::intExplode(',', (string)($frontendUser->user['usergroup'] ?? ''), true);
|
return GeneralUtility::makeInstance(FeedbackState::class, $this->settings);
|
||||||
if (!in_array($requiredGroup, $groups, true)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return [
|
|
||||||
'uid' => (int)$frontendUser->user['uid'],
|
|
||||||
'username' => (string)($frontendUser->user['username'] ?? ''),
|
|
||||||
'name' => trim((string)($frontendUser->user['name'] ?? '')) ?: (string)($frontendUser->user['username'] ?? ''),
|
|
||||||
'email' => (string)($frontendUser->user['email'] ?? ''),
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- routes -------------------------------------------------------------
|
// ---- routes -------------------------------------------------------------
|
||||||
@@ -115,31 +106,7 @@ final class FeedbackApiMiddleware implements MiddlewareInterface
|
|||||||
/** @param array{uid:int,username:string,name:string,email:string} $user */
|
/** @param array{uid:int,username:string,name:string,email:string} $user */
|
||||||
private function state(array $user): ResponseInterface
|
private function state(array $user): ResponseInterface
|
||||||
{
|
{
|
||||||
$summary = $this->openSummary($user['uid'], false);
|
return new JsonResponse($this->stateService()->payload($user));
|
||||||
return new JsonResponse([
|
|
||||||
'allowed' => true,
|
|
||||||
'user' => ['name' => $user['name'], 'email' => $user['email']],
|
|
||||||
'recipients' => $this->recipientOptions(),
|
|
||||||
'overviewUrl' => $this->overviewUrl(),
|
|
||||||
'summary' => $summary === null ? null : $this->summaryPayload($summary),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Visitor-facing URL of the feedback overview page (extension setting
|
|
||||||
* overviewPid), resolved via the page slug. Empty string when unset.
|
|
||||||
*/
|
|
||||||
private function overviewUrl(): string
|
|
||||||
{
|
|
||||||
$pid = $this->settings->overviewPid();
|
|
||||||
if ($pid <= 0) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
|
|
||||||
$slug = $queryBuilder->select('slug')->from('pages')
|
|
||||||
->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($pid, \Doctrine\DBAL\ParameterType::INTEGER)))
|
|
||||||
->executeQuery()->fetchOne();
|
|
||||||
return is_string($slug) ? $slug : '';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @param array{uid:int,username:string,name:string,email:string} $user */
|
/** @param array{uid:int,username:string,name:string,email:string} $user */
|
||||||
@@ -375,69 +342,16 @@ final class FeedbackApiMiddleware implements MiddlewareInterface
|
|||||||
/** @return array<string,mixed>|null */
|
/** @return array<string,mixed>|null */
|
||||||
private function openSummary(int $userUid, bool $createIfMissing): ?array
|
private function openSummary(int $userUid, bool $createIfMissing): ?array
|
||||||
{
|
{
|
||||||
$connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable(self::T_SUMMARY);
|
return $this->stateService()->openSummary($userUid, $createIfMissing);
|
||||||
$summary = $connection->fetchAssociative(
|
|
||||||
'SELECT * FROM ' . self::T_SUMMARY . " WHERE deleted = 0 AND status = 'open' AND fe_user = ? ORDER BY uid DESC",
|
|
||||||
[$userUid]
|
|
||||||
);
|
|
||||||
if ($summary) {
|
|
||||||
return $summary;
|
|
||||||
}
|
|
||||||
if (!$createIfMissing) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
$connection->insert(self::T_SUMMARY, [
|
|
||||||
'pid' => $this->settings->storagePid(),
|
|
||||||
'tstamp' => time(),
|
|
||||||
'crdate' => time(),
|
|
||||||
'fe_user' => $userUid,
|
|
||||||
'status' => 'open',
|
|
||||||
]);
|
|
||||||
return $this->openSummary($userUid, false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array<string,mixed> $summary
|
* @param array<string,mixed> $summary
|
||||||
* @return array{uid:int,generalNotes:string,recipients:int[],items:array<int,array{uid:int,url:string,note:string,attachments:array<int,array{refUid:int,name:string,url:string}>}>}
|
* @return array<string,mixed>
|
||||||
*/
|
*/
|
||||||
private function summaryPayload(array $summary): array
|
private function summaryPayload(array $summary): array
|
||||||
{
|
{
|
||||||
$connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable(self::T_FEEDBACK);
|
return $this->stateService()->summaryPayload($summary);
|
||||||
$items = [];
|
|
||||||
foreach ($connection->fetchAllAssociative(
|
|
||||||
'SELECT uid, url, note FROM ' . self::T_FEEDBACK . ' WHERE deleted = 0 AND summary = ? ORDER BY sorting',
|
|
||||||
[(int)$summary['uid']]
|
|
||||||
) as $row) {
|
|
||||||
$items[] = [
|
|
||||||
'uid' => (int)$row['uid'],
|
|
||||||
'url' => (string)$row['url'],
|
|
||||||
'note' => (string)$row['note'],
|
|
||||||
'attachments' => $this->attachments((int)$row['uid']),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
return [
|
|
||||||
'uid' => (int)$summary['uid'],
|
|
||||||
'generalNotes' => (string)($summary['general_notes'] ?? ''),
|
|
||||||
'recipients' => GeneralUtility::intExplode(',', (string)($summary['recipients'] ?? ''), true),
|
|
||||||
'items' => $items,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @return array<int,array{refUid:int,name:string,url:string}> */
|
|
||||||
private function attachments(int $feedbackUid): array
|
|
||||||
{
|
|
||||||
$connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable('sys_file_reference');
|
|
||||||
$rows = $connection->fetchAllAssociative(
|
|
||||||
'SELECT r.uid AS ref_uid, f.name, f.identifier FROM sys_file_reference r
|
|
||||||
JOIN sys_file f ON f.uid = r.uid_local
|
|
||||||
WHERE r.deleted = 0 AND r.tablenames = ? AND r.fieldname = ? AND r.uid_foreign = ? ORDER BY r.uid',
|
|
||||||
[self::T_FEEDBACK, 'attachments', $feedbackUid]
|
|
||||||
);
|
|
||||||
return array_map(static fn(array $r): array => [
|
|
||||||
'refUid' => (int)$r['ref_uid'],
|
|
||||||
'name' => (string)$r['name'],
|
|
||||||
'url' => '/fileadmin' . $r['identifier'],
|
|
||||||
], $rows);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @return array<string,mixed> the feedback row - throws when not the user's */
|
/** @return array<string,mixed> the feedback row - throws when not the user's */
|
||||||
@@ -471,16 +385,6 @@ final class FeedbackApiMiddleware implements MiddlewareInterface
|
|||||||
return $this->settings->recipients();
|
return $this->settings->recipients();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @return array<int,array{id:int,label:string}> */
|
|
||||||
private function recipientOptions(): array
|
|
||||||
{
|
|
||||||
$options = [];
|
|
||||||
foreach ($this->recipientMap() as $id => $recipient) {
|
|
||||||
$options[] = ['id' => $id, 'label' => $recipient['label']];
|
|
||||||
}
|
|
||||||
return $options;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @return array<string,mixed> */
|
/** @return array<string,mixed> */
|
||||||
private function jsonBody(ServerRequestInterface $request): array
|
private function jsonBody(ServerRequestInterface $request): array
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,197 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Evomedien\FeComment\Service;
|
||||||
|
|
||||||
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
|
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||||
|
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||||
|
use TYPO3\CMS\Frontend\Authentication\FrontendUserAuthentication;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The fe_comment state of the current frontend user - ONE implementation
|
||||||
|
* for both consumers (2026-10-09): GET /api/fe-comment/state and the
|
||||||
|
* "Feedback Overview" content element in the page JSON. Who is allowed,
|
||||||
|
* the open summary with its items and attachments, recipients, overview URL.
|
||||||
|
*/
|
||||||
|
final class FeedbackState
|
||||||
|
{
|
||||||
|
private const T_SUMMARY = 'tx_fecomment_summary';
|
||||||
|
private const T_FEEDBACK = 'tx_fecomment_feedback';
|
||||||
|
|
||||||
|
private readonly ExtensionSettings $settings;
|
||||||
|
|
||||||
|
public function __construct(?ExtensionSettings $settings = null)
|
||||||
|
{
|
||||||
|
$this->settings = $settings ?? GeneralUtility::makeInstance(ExtensionSettings::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The logged-in frontend user when the extension is enabled and the user
|
||||||
|
* belongs to the configured group; null otherwise.
|
||||||
|
*
|
||||||
|
* @return array{uid:int,username:string,name:string,email:string}|null
|
||||||
|
*/
|
||||||
|
public function allowedUser(ServerRequestInterface $request): ?array
|
||||||
|
{
|
||||||
|
if (!$this->settings->enabled()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
$frontendUser = $request->getAttribute('frontend.user');
|
||||||
|
if (!$frontendUser instanceof FrontendUserAuthentication || empty($frontendUser->user['uid'])) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
$requiredGroup = $this->settings->feUserGroup();
|
||||||
|
if ($requiredGroup > 0) {
|
||||||
|
$groups = GeneralUtility::intExplode(',', (string)($frontendUser->user['usergroup'] ?? ''), true);
|
||||||
|
if (!in_array($requiredGroup, $groups, true)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
'uid' => (int)$frontendUser->user['uid'],
|
||||||
|
'username' => (string)($frontendUser->user['username'] ?? ''),
|
||||||
|
'name' => trim((string)($frontendUser->user['name'] ?? '')) ?: (string)($frontendUser->user['username'] ?? ''),
|
||||||
|
'email' => (string)($frontendUser->user['email'] ?? ''),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The state payload: `{allowed: false}` for everyone not allowed, the full
|
||||||
|
* shape otherwise.
|
||||||
|
*
|
||||||
|
* @param array{uid:int,username:string,name:string,email:string}|null $user
|
||||||
|
* @return array<string,mixed>
|
||||||
|
*/
|
||||||
|
public function payload(?array $user): array
|
||||||
|
{
|
||||||
|
if ($user === null) {
|
||||||
|
return ['allowed' => false];
|
||||||
|
}
|
||||||
|
$summary = $this->openSummary($user['uid'], false);
|
||||||
|
$summaryPayload = $summary === null ? null : $this->summaryPayload($summary);
|
||||||
|
$hasContent = $summaryPayload !== null
|
||||||
|
&& ($summaryPayload['items'] !== [] || trim(strip_tags($summaryPayload['generalNotes'])) !== '');
|
||||||
|
return [
|
||||||
|
'allowed' => true,
|
||||||
|
'user' => ['name' => $user['name'], 'email' => $user['email']],
|
||||||
|
'recipients' => $this->recipientOptions(),
|
||||||
|
'overviewUrl' => $this->overviewUrl(),
|
||||||
|
'summary' => $summaryPayload,
|
||||||
|
// the submit button may be active (the endpoint rejects an empty summary)
|
||||||
|
'canSubmit' => $hasContent,
|
||||||
|
'actions' => self::ACTIONS,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The write endpoints, delivered with the state so a front end needs no
|
||||||
|
* hard-coded URLs ({uid}/{refUid} are placeholders). All POST, JSON
|
||||||
|
* bodies except `upload` (multipart: file + feedback).
|
||||||
|
*/
|
||||||
|
private const ACTIONS = [
|
||||||
|
'submit' => ['method' => 'POST', 'url' => '/api/fe-comment/submit'],
|
||||||
|
'updateSummary' => ['method' => 'POST', 'url' => '/api/fe-comment/summary', 'body' => ['generalNotes' => 'html', 'recipients' => 'int[]']],
|
||||||
|
'createItem' => ['method' => 'POST', 'url' => '/api/fe-comment/feedback', 'body' => ['url' => 'string', 'note' => 'html']],
|
||||||
|
'updateItem' => ['method' => 'POST', 'url' => '/api/fe-comment/feedback/{uid}', 'body' => ['url' => 'string', 'note' => 'html']],
|
||||||
|
'deleteItem' => ['method' => 'POST', 'url' => '/api/fe-comment/feedback/{uid}/delete'],
|
||||||
|
'upload' => ['method' => 'POST', 'url' => '/api/fe-comment/upload', 'body' => ['file' => 'multipart', 'feedback' => 'int']],
|
||||||
|
'deleteAttachment' => ['method' => 'POST', 'url' => '/api/fe-comment/attachment/{refUid}/delete'],
|
||||||
|
];
|
||||||
|
|
||||||
|
/** @return array<string,mixed>|null the user's open summary row */
|
||||||
|
public function openSummary(int $userUid, bool $createIfMissing): ?array
|
||||||
|
{
|
||||||
|
$connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable(self::T_SUMMARY);
|
||||||
|
$summary = $connection->fetchAssociative(
|
||||||
|
'SELECT * FROM ' . self::T_SUMMARY . " WHERE deleted = 0 AND status = 'open' AND fe_user = ? ORDER BY uid DESC",
|
||||||
|
[$userUid]
|
||||||
|
);
|
||||||
|
if ($summary) {
|
||||||
|
return $summary;
|
||||||
|
}
|
||||||
|
if (!$createIfMissing) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
$connection->insert(self::T_SUMMARY, [
|
||||||
|
'pid' => $this->settings->storagePid(),
|
||||||
|
'tstamp' => time(),
|
||||||
|
'crdate' => time(),
|
||||||
|
'fe_user' => $userUid,
|
||||||
|
'status' => 'open',
|
||||||
|
]);
|
||||||
|
return $this->openSummary($userUid, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string,mixed> $summary
|
||||||
|
* @return array{uid:int,generalNotes:string,recipients:int[],items:array<int,array{uid:int,url:string,note:string,attachments:array<int,array{refUid:int,name:string,url:string}>}>}
|
||||||
|
*/
|
||||||
|
public function summaryPayload(array $summary): array
|
||||||
|
{
|
||||||
|
$connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable(self::T_FEEDBACK);
|
||||||
|
$items = [];
|
||||||
|
foreach ($connection->fetchAllAssociative(
|
||||||
|
'SELECT uid, url, note FROM ' . self::T_FEEDBACK . ' WHERE deleted = 0 AND summary = ? ORDER BY sorting',
|
||||||
|
[(int)$summary['uid']]
|
||||||
|
) as $row) {
|
||||||
|
$items[] = [
|
||||||
|
'uid' => (int)$row['uid'],
|
||||||
|
'url' => (string)$row['url'],
|
||||||
|
'note' => (string)$row['note'],
|
||||||
|
'attachments' => $this->attachments((int)$row['uid']),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
'uid' => (int)$summary['uid'],
|
||||||
|
'generalNotes' => (string)($summary['general_notes'] ?? ''),
|
||||||
|
'recipients' => GeneralUtility::intExplode(',', (string)($summary['recipients'] ?? ''), true),
|
||||||
|
'items' => $items,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array<int,array{refUid:int,name:string,url:string}> */
|
||||||
|
private function attachments(int $feedbackUid): array
|
||||||
|
{
|
||||||
|
$connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable('sys_file_reference');
|
||||||
|
$rows = $connection->fetchAllAssociative(
|
||||||
|
'SELECT r.uid AS ref_uid, f.name, f.identifier FROM sys_file_reference r
|
||||||
|
JOIN sys_file f ON f.uid = r.uid_local
|
||||||
|
WHERE r.deleted = 0 AND r.tablenames = ? AND r.fieldname = ? AND r.uid_foreign = ? ORDER BY r.uid',
|
||||||
|
[self::T_FEEDBACK, 'attachments', $feedbackUid]
|
||||||
|
);
|
||||||
|
return array_map(static fn(array $r): array => [
|
||||||
|
'refUid' => (int)$r['ref_uid'],
|
||||||
|
'name' => (string)$r['name'],
|
||||||
|
'url' => '/fileadmin' . $r['identifier'],
|
||||||
|
], $rows);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array<int,array{id:int,label:string}> */
|
||||||
|
private function recipientOptions(): array
|
||||||
|
{
|
||||||
|
$options = [];
|
||||||
|
foreach ($this->settings->recipients() as $id => $recipient) {
|
||||||
|
$options[] = ['id' => $id, 'label' => $recipient['label']];
|
||||||
|
}
|
||||||
|
return $options;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Visitor-facing URL of the feedback overview page (extension setting
|
||||||
|
* overviewPid), resolved via the page slug. Empty string when unset.
|
||||||
|
*/
|
||||||
|
private function overviewUrl(): string
|
||||||
|
{
|
||||||
|
$pid = $this->settings->overviewPid();
|
||||||
|
if ($pid <= 0) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
|
||||||
|
$slug = $queryBuilder->select('slug')->from('pages')
|
||||||
|
->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($pid, \Doctrine\DBAL\ParameterType::INTEGER)))
|
||||||
|
->executeQuery()->fetchOne();
|
||||||
|
return is_string($slug) ? $slug : '';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Evomedien\FeComment\UserFunc;
|
||||||
|
|
||||||
|
use Evomedien\FeComment\Service\FeedbackState;
|
||||||
|
use TYPO3\CMS\Core\Attribute\AsAllowedCallable;
|
||||||
|
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* "Feedback Overview" content element (CType fecomment_overview,
|
||||||
|
* 2026-10-09): the current FE user's feedback state in the page JSON, under
|
||||||
|
* content.feedbackOverview - exactly the payload of GET
|
||||||
|
* /api/fe-comment/state (shared FeedbackState service), so a front end can
|
||||||
|
* render the overview page like any other page.
|
||||||
|
*
|
||||||
|
* Wired as USER_INT: the data is per user. EXT:headless keeps the page
|
||||||
|
* cached and replaces only this field per request. Visitors who are not
|
||||||
|
* allowed get {"allowed": false} (front end: redirect to the login page).
|
||||||
|
*/
|
||||||
|
final class OverviewJsonRenderer
|
||||||
|
{
|
||||||
|
#[AsAllowedCallable]
|
||||||
|
public function render(string $content, array $conf): string
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$request = $GLOBALS['TYPO3_REQUEST'] ?? null;
|
||||||
|
$state = GeneralUtility::makeInstance(FeedbackState::class);
|
||||||
|
$payload = $request !== null ? $state->payload($state->allowedUser($request)) : ['allowed' => false];
|
||||||
|
} catch (\Throwable) {
|
||||||
|
$payload = ['allowed' => false];
|
||||||
|
}
|
||||||
|
return (string)json_encode($payload, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
'fecomment-overview' => [
|
||||||
|
'provider' => \TYPO3\CMS\Core\Imaging\IconProvider\SvgIconProvider::class,
|
||||||
|
'source' => 'EXT:fe_comment/Resources/Public/Icons/Extension.svg',
|
||||||
|
],
|
||||||
|
];
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
name: evomedien/fe-comment
|
||||||
|
label: 'fe_comment: Feedback Overview (headless JSON)'
|
||||||
|
# The overview element renders through EXT:headless (lib.contentElementWithHeader).
|
||||||
|
dependencies:
|
||||||
|
- friendsoftypo3/headless
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
# "Feedback Overview" content element as headless JSON (2026-10-09):
|
||||||
|
# content.feedbackOverview = the payload of GET /api/fe-comment/state.
|
||||||
|
# USER_INT on purpose - the data is per FE user; EXT:headless caches the page
|
||||||
|
# and replaces only this field on every request.
|
||||||
|
tt_content.fecomment_overview < lib.contentElementWithHeader
|
||||||
|
tt_content.fecomment_overview {
|
||||||
|
fields {
|
||||||
|
content {
|
||||||
|
fields {
|
||||||
|
feedbackOverview = USER_INT
|
||||||
|
feedbackOverview.userFunc = Evomedien\FeComment\UserFunc\OverviewJsonRenderer->render
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
defined('TYPO3') or die();
|
||||||
|
|
||||||
|
// "Feedback Overview": the current FE user's feedback state as JSON
|
||||||
|
// (content.feedbackOverview, rendered by OverviewJsonRenderer). Place it on
|
||||||
|
// the overview page (extension setting overviewPid). No plugin options.
|
||||||
|
\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addPlugin(
|
||||||
|
[
|
||||||
|
'label' => 'Feedback Overview',
|
||||||
|
'description' => 'The logged-in user\'s open feedback summary (items, attachments, notes, recipients) as JSON for the front end.',
|
||||||
|
'value' => 'fecomment_overview',
|
||||||
|
'icon' => 'fecomment-overview',
|
||||||
|
'group' => 'plugins',
|
||||||
|
],
|
||||||
|
'CType',
|
||||||
|
'fe_comment'
|
||||||
|
);
|
||||||
|
|
||||||
|
$GLOBALS['TCA']['tt_content']['types']['fecomment_overview']['showitem'] = '
|
||||||
|
--div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:general,
|
||||||
|
--palette--;;general,
|
||||||
|
--palette--;;headers,
|
||||||
|
--div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:access,
|
||||||
|
--palette--;;hidden';
|
||||||
@@ -23,8 +23,8 @@
|
|||||||
<meta name="twitter:description" content="Enterprise IPTV, video streaming, and digital signage solutions." />
|
<meta name="twitter:description" content="Enterprise IPTV, video streaming, and digital signage solutions." />
|
||||||
<link rel="icon" type="image/svg+xml" href="/fileadmin/icons/favicon.ico" />
|
<link rel="icon" type="image/svg+xml" href="/fileadmin/icons/favicon.ico" />
|
||||||
<title>VITEC</title>
|
<title>VITEC</title>
|
||||||
<script type="module" crossorigin src="/_frontend/assets/index-Dvbm25nF.js"></script>
|
<script type="module" crossorigin src="/_frontend/assets/index-B1plOsdz.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/_frontend/assets/index-PE6EdhLv.css">
|
<link rel="stylesheet" crossorigin href="/_frontend/assets/index-BADFa6PF.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="page"></div>␍
|
<div id="page"></div>␍
|
||||||
|
|||||||
Reference in new issue
Block a user