diff --git a/app/Http/Middleware/HandleInertiaRequests.php b/app/Http/Middleware/HandleInertiaRequests.php index acf830b5..7ece3948 100644 --- a/app/Http/Middleware/HandleInertiaRequests.php +++ b/app/Http/Middleware/HandleInertiaRequests.php @@ -24,6 +24,7 @@ use App\Modules\Platform\Settings\Settings; use App\Modules\Platform\Updates\LatestReleaseInfo; use App\Modules\Platform\Updates\RunningCodeState; use Illuminate\Foundation\Inspiring; +use App\Modules\Platform\Announcements\Events\ResolvingAnnouncement; use App\Modules\Platform\Navigation\Events\ResolvingNavigationLinks; use Illuminate\Http\Request; use Illuminate\Support\Facades\Event; @@ -93,6 +94,12 @@ class HandleInertiaRequests extends Middleware // See ResolvingNavigationLinks for why core never learns what // is in it. 'extra_nav_links' => $this->extraNavLinks($request), + // Shared rather than a dashboard prop, because it is shown in + // two places — the band on the dashboard and the icon beside + // the notification bell everywhere else — and "the same + // message" is the requirement. Two props would drift the day + // somebody edited one. + 'announcement' => $this->announcement($request), // Shared rather than passed by each page: the sign-in buttons, // the registration form and the Connected accounts nav entry // all need the same list, and a nav entry to a screen with @@ -331,4 +338,22 @@ class HandleInertiaRequests extends Middleware return $event->links; } + + /** + * @return array{title: string, body: string, action_label: string|null, action_url: string|null, tone: string}|null + */ + private function announcement(Request $request): ?array + { + $user = $request->user(); + + if ($user === null) { + return null; + } + + $event = new ResolvingAnnouncement(isStaff: $user->isStaff()); + + Event::dispatch($event); + + return $event->announcement; + } } diff --git a/app/Modules/Audit/Http/Controllers/DashboardController.php b/app/Modules/Audit/Http/Controllers/DashboardController.php index b59c77e7..a620debe 100644 --- a/app/Modules/Audit/Http/Controllers/DashboardController.php +++ b/app/Modules/Audit/Http/Controllers/DashboardController.php @@ -13,7 +13,6 @@ use App\Modules\Audit\ActivityLogScope; use App\Modules\Audit\ActivityPresenter; use App\Modules\Audit\DashboardWidgetPreferences; use Illuminate\Support\Facades\Event; -use App\Modules\Platform\Announcements\Events\ResolvingDashboardCallout; use App\Modules\Clients\ClientStorageUsage; use App\Modules\Files\Access\StaffLibraryScope; use App\Modules\Files\Delivery\FileDelivery; @@ -86,12 +85,6 @@ class DashboardController extends Controller $prefs = $this->widgetPrefs; return Inertia::render('dashboard', [ - // Above the grid rather than in it — see - // ResolvingDashboardCallout for why a message that matters - // must not be something each viewer can drag under a fold. - // Null with nothing listening, which is every community - // installation. - 'callout' => $this->callout($user), 'counters' => $canCounters && $prefs->isEnabled($user, 'counters') ? $this->counters() : null, 'transfers' => $canStatistics && $prefs->isEnabled($user, 'transfers') ? $this->transferSeries($from, $to, $timezone) : null, 'transfers_range' => $canStatistics && $prefs->isEnabled($user, 'transfers') @@ -550,16 +543,4 @@ class DashboardController extends Controller ])->all(), ]); } - - /** - * @return array{title: string, body: string, action_label: string|null, action_url: string|null, tone: string}|null - */ - private function callout(User $user): ?array - { - $event = new ResolvingDashboardCallout(isStaff: $user->isStaff()); - - Event::dispatch($event); - - return $event->callout; - } } diff --git a/app/Modules/Platform/Announcements/Events/ResolvingDashboardCallout.php b/app/Modules/Platform/Announcements/Events/ResolvingAnnouncement.php similarity index 71% rename from app/Modules/Platform/Announcements/Events/ResolvingDashboardCallout.php rename to app/Modules/Platform/Announcements/Events/ResolvingAnnouncement.php index 31e142e6..2ec0434b 100644 --- a/app/Modules/Platform/Announcements/Events/ResolvingDashboardCallout.php +++ b/app/Modules/Platform/Announcements/Events/ResolvingAnnouncement.php @@ -5,13 +5,18 @@ declare(strict_types=1); namespace App\Modules\Platform\Announcements\Events; /** - * A single message a package wants shown across the top of the dashboard. + * A single message a package wants put in front of staff. + * + * Shown twice, from one source: a band across the top of the dashboard, + * and an icon beside the notification bell that opens the same words on + * every other page. One event rather than two because "the same message" + * is the requirement — two props would drift the day somebody edits one. * * Not a widget, on purpose. The widget grid is a closed list of keys that * dashboard.tsx renders one by one, and each viewer arranges it — so a * message that matters would sit wherever somebody happened to drag it, - * or under a fold, or switched off. A callout is one band above the grid: - * seen, and not competing with the columns for space. + * or under a fold, or switched off. A band above the grid is seen without + * competing with the columns for space. * * **Core knows nothing about what it says.** Title, body, the label on the * button and where the button goes all come from the listener. The first @@ -21,15 +26,15 @@ namespace App\Modules\Platform\Announcements\Events; * * One at a time, deliberately. A dashboard that can accumulate banners * accumulates them, and the second one is what teaches people to skip the - * first. A listener that finds `$callout` already set should leave it - * alone rather than overwrite it. + * first. A listener that finds one already set should leave it alone + * rather than overwrite it. */ -class ResolvingDashboardCallout +class ResolvingAnnouncement { /** * @var array{title: string, body: string, action_label: string|null, action_url: string|null, tone: string}|null */ - public ?array $callout = null; + public ?array $announcement = null; public function __construct( /** Whether the viewer is a staff account. */ @@ -44,11 +49,11 @@ class ResolvingDashboardCallout */ public function show(string $title, string $body, ?string $actionLabel = null, ?string $actionUrl = null, string $tone = 'info'): void { - if ($this->callout !== null) { + if ($this->announcement !== null) { return; } - $this->callout = [ + $this->announcement = [ 'title' => $title, 'body' => $body, 'action_label' => $actionLabel, diff --git a/lang/ca.json b/lang/ca.json index 1e8c7de3..3c98effa 100644 --- a/lang/ca.json +++ b/lang/ca.json @@ -2015,5 +2015,7 @@ "Make this file public": "Fes públic aquest fitxer", "This site has no public page set up yet, so nothing will be visible until an administrator sets one.": "Aquest lloc encara no té cap pàgina pública configurada, així que no es veurà res fins que un administrador en configuri una.", "Show ProjectSend news on the dashboard": "Mostra les novetats de ProjectSend al tauler", - "Fetches project announcements from projectsend.org once a day for the dashboard card. Turn it off and this installation stops contacting projectsend.org for news at all.": "Recupera els anuncis del projecte des de projectsend.org un cop al dia per a la targeta del tauler. Si ho desactives, aquesta instal·lació deixa de contactar amb projectsend.org per a novetats." + "Fetches project announcements from projectsend.org once a day for the dashboard card. Turn it off and this installation stops contacting projectsend.org for news at all.": "Recupera els anuncis del projecte des de projectsend.org un cop al dia per a la targeta del tauler. Si ho desactives, aquesta instal·lació deixa de contactar amb projectsend.org per a novetats.", + "Announcement": "Avís", + "More": "Més" } diff --git a/lang/cs.json b/lang/cs.json index bb8f7f8e..5793a671 100644 --- a/lang/cs.json +++ b/lang/cs.json @@ -2015,5 +2015,7 @@ "Make this file public": "Zveřejnit tento soubor", "This site has no public page set up yet, so nothing will be visible until an administrator sets one.": "Tento web zatím nemá nastavenou veřejnou stránku, takže nebude nic vidět, dokud ji administrátor nenastaví.", "Show ProjectSend news on the dashboard": "Zobrazovat novinky ProjectSendu v přehledu", - "Fetches project announcements from projectsend.org once a day for the dashboard card. Turn it off and this installation stops contacting projectsend.org for news at all.": "Jednou denně načte oznámení projektu z projectsend.org pro kartu v přehledu. Když to vypnete, tato instalace se kvůli novinkám na projectsend.org už vůbec nepřipojí." + "Fetches project announcements from projectsend.org once a day for the dashboard card. Turn it off and this installation stops contacting projectsend.org for news at all.": "Jednou denně načte oznámení projektu z projectsend.org pro kartu v přehledu. Když to vypnete, tato instalace se kvůli novinkám na projectsend.org už vůbec nepřipojí.", + "Announcement": "Oznámení", + "More": "Další" } diff --git a/lang/de.json b/lang/de.json index c4974553..d65f8891 100644 --- a/lang/de.json +++ b/lang/de.json @@ -2015,5 +2015,7 @@ "Make this file public": "Diese Datei öffentlich machen", "This site has no public page set up yet, so nothing will be visible until an administrator sets one.": "Für diese Website ist noch keine öffentliche Seite eingerichtet, daher ist nichts sichtbar, bis ein Administrator eine einrichtet.", "Show ProjectSend news on the dashboard": "ProjectSend-Neuigkeiten in der Übersicht anzeigen", - "Fetches project announcements from projectsend.org once a day for the dashboard card. Turn it off and this installation stops contacting projectsend.org for news at all.": "Ruft einmal täglich Projektankündigungen von projectsend.org für die Karte in der Übersicht ab. Ausgeschaltet nimmt diese Installation für Neuigkeiten überhaupt keine Verbindung zu projectsend.org mehr auf." + "Fetches project announcements from projectsend.org once a day for the dashboard card. Turn it off and this installation stops contacting projectsend.org for news at all.": "Ruft einmal täglich Projektankündigungen von projectsend.org für die Karte in der Übersicht ab. Ausgeschaltet nimmt diese Installation für Neuigkeiten überhaupt keine Verbindung zu projectsend.org mehr auf.", + "Announcement": "Ankündigung", + "More": "Mehr" } diff --git a/lang/es.json b/lang/es.json index eb267d11..21eb0413 100644 --- a/lang/es.json +++ b/lang/es.json @@ -2015,5 +2015,7 @@ "Make this file public": "Hacer público este archivo", "This site has no public page set up yet, so nothing will be visible until an administrator sets one.": "Este sitio todavía no tiene una página pública configurada, así que no se verá nada hasta que un administrador la configure.", "Show ProjectSend news on the dashboard": "Mostrar las noticias de ProjectSend en el panel de control", - "Fetches project announcements from projectsend.org once a day for the dashboard card. Turn it off and this installation stops contacting projectsend.org for news at all.": "Trae los anuncios del proyecto desde projectsend.org una vez al día para la tarjeta del panel. Si lo desactivas, esta instalación deja de contactar a projectsend.org por noticias." + "Fetches project announcements from projectsend.org once a day for the dashboard card. Turn it off and this installation stops contacting projectsend.org for news at all.": "Trae los anuncios del proyecto desde projectsend.org una vez al día para la tarjeta del panel. Si lo desactivas, esta instalación deja de contactar a projectsend.org por noticias.", + "Announcement": "Aviso", + "More": "Más" } diff --git a/lang/fr.json b/lang/fr.json index 79530620..9d43871e 100644 --- a/lang/fr.json +++ b/lang/fr.json @@ -2015,5 +2015,7 @@ "Make this file public": "Rendre ce fichier public", "This site has no public page set up yet, so nothing will be visible until an administrator sets one.": "Ce site n'a pas encore de page publique configurée, donc rien ne sera visible tant qu'un administrateur n'en aura pas configuré une.", "Show ProjectSend news on the dashboard": "Afficher les actualités ProjectSend sur le tableau de bord", - "Fetches project announcements from projectsend.org once a day for the dashboard card. Turn it off and this installation stops contacting projectsend.org for news at all.": "Récupère une fois par jour les annonces du projet depuis projectsend.org pour la carte du tableau de bord. Désactivé, cette installation ne contacte plus du tout projectsend.org pour les actualités." + "Fetches project announcements from projectsend.org once a day for the dashboard card. Turn it off and this installation stops contacting projectsend.org for news at all.": "Récupère une fois par jour les annonces du projet depuis projectsend.org pour la carte du tableau de bord. Désactivé, cette installation ne contacte plus du tout projectsend.org pour les actualités.", + "Announcement": "Annonce", + "More": "Plus" } diff --git a/lang/id.json b/lang/id.json index 9f64b815..b759d2d8 100644 --- a/lang/id.json +++ b/lang/id.json @@ -2015,5 +2015,7 @@ "Make this file public": "Jadikan berkas ini publik", "This site has no public page set up yet, so nothing will be visible until an administrator sets one.": "Situs ini belum punya halaman publik, jadi tidak ada yang terlihat sampai administrator menyiapkannya.", "Show ProjectSend news on the dashboard": "Tampilkan kabar terbaru ProjectSend di dasbor", - "Fetches project announcements from projectsend.org once a day for the dashboard card. Turn it off and this installation stops contacting projectsend.org for news at all.": "Mengambil pengumuman proyek dari projectsend.org sekali sehari untuk kartu di dasbor. Jika dimatikan, instalasi ini sama sekali tidak lagi menghubungi projectsend.org untuk kabar terbaru." + "Fetches project announcements from projectsend.org once a day for the dashboard card. Turn it off and this installation stops contacting projectsend.org for news at all.": "Mengambil pengumuman proyek dari projectsend.org sekali sehari untuk kartu di dasbor. Jika dimatikan, instalasi ini sama sekali tidak lagi menghubungi projectsend.org untuk kabar terbaru.", + "Announcement": "Pengumuman", + "More": "Lainnya" } diff --git a/lang/it.json b/lang/it.json index f97d5ed0..fed00ac1 100644 --- a/lang/it.json +++ b/lang/it.json @@ -2015,5 +2015,7 @@ "Make this file public": "Rendi pubblico questo file", "This site has no public page set up yet, so nothing will be visible until an administrator sets one.": "Questo sito non ha ancora una pagina pubblica, quindi non sarà visibile nulla finché un amministratore non ne configura una.", "Show ProjectSend news on the dashboard": "Mostra le novità di ProjectSend nel pannello", - "Fetches project announcements from projectsend.org once a day for the dashboard card. Turn it off and this installation stops contacting projectsend.org for news at all.": "Recupera gli annunci del progetto da projectsend.org una volta al giorno per la scheda del pannello. Disattivandolo, questa installazione smette del tutto di contattare projectsend.org per le novità." + "Fetches project announcements from projectsend.org once a day for the dashboard card. Turn it off and this installation stops contacting projectsend.org for news at all.": "Recupera gli annunci del progetto da projectsend.org una volta al giorno per la scheda del pannello. Disattivandolo, questa installazione smette del tutto di contattare projectsend.org per le novità.", + "Announcement": "Avviso", + "More": "Altro" } diff --git a/lang/ja.json b/lang/ja.json index ae9aedef..61d6f2c3 100644 --- a/lang/ja.json +++ b/lang/ja.json @@ -2015,5 +2015,7 @@ "Make this file public": "このファイルを公開する", "This site has no public page set up yet, so nothing will be visible until an administrator sets one.": "このサイトにはまだ公開ページが設定されていないため、管理者が設定するまで何も表示されません。", "Show ProjectSend news on the dashboard": "ダッシュボードに ProjectSend のお知らせを表示する", - "Fetches project announcements from projectsend.org once a day for the dashboard card. Turn it off and this installation stops contacting projectsend.org for news at all.": "ダッシュボードのカード用に、プロジェクトのお知らせを projectsend.org から1日1回取得します。オフにすると、このインストールはお知らせのために projectsend.org へ接続しなくなります。" + "Fetches project announcements from projectsend.org once a day for the dashboard card. Turn it off and this installation stops contacting projectsend.org for news at all.": "ダッシュボードのカード用に、プロジェクトのお知らせを projectsend.org から1日1回取得します。オフにすると、このインストールはお知らせのために projectsend.org へ接続しなくなります。", + "Announcement": "お知らせ", + "More": "その他" } diff --git a/lang/nl.json b/lang/nl.json index 623cf80d..543e52a7 100644 --- a/lang/nl.json +++ b/lang/nl.json @@ -2015,5 +2015,7 @@ "Make this file public": "Dit bestand openbaar maken", "This site has no public page set up yet, so nothing will be visible until an administrator sets one.": "Deze site heeft nog geen openbare pagina, dus er is niets zichtbaar totdat een beheerder er een instelt.", "Show ProjectSend news on the dashboard": "ProjectSend-nieuws op het overzicht tonen", - "Fetches project announcements from projectsend.org once a day for the dashboard card. Turn it off and this installation stops contacting projectsend.org for news at all.": "Haalt eens per dag projectaankondigingen op van projectsend.org voor de kaart op het overzicht. Uitgeschakeld neemt deze installatie voor nieuws helemaal geen contact meer op met projectsend.org." + "Fetches project announcements from projectsend.org once a day for the dashboard card. Turn it off and this installation stops contacting projectsend.org for news at all.": "Haalt eens per dag projectaankondigingen op van projectsend.org voor de kaart op het overzicht. Uitgeschakeld neemt deze installatie voor nieuws helemaal geen contact meer op met projectsend.org.", + "Announcement": "Mededeling", + "More": "Meer" } diff --git a/lang/pl.json b/lang/pl.json index 81625494..58e5304f 100644 --- a/lang/pl.json +++ b/lang/pl.json @@ -2015,5 +2015,7 @@ "Make this file public": "Ustaw ten plik jako publiczny", "This site has no public page set up yet, so nothing will be visible until an administrator sets one.": "Ta witryna nie ma jeszcze skonfigurowanej strony publicznej, więc nic nie będzie widoczne, dopóki administrator jej nie ustawi.", "Show ProjectSend news on the dashboard": "Pokazuj aktualności ProjectSend na pulpicie", - "Fetches project announcements from projectsend.org once a day for the dashboard card. Turn it off and this installation stops contacting projectsend.org for news at all.": "Raz dziennie pobiera ogłoszenia projektu z projectsend.org na kartę pulpitu. Po wyłączeniu ta instalacja w ogóle przestaje łączyć się z projectsend.org po aktualności." + "Fetches project announcements from projectsend.org once a day for the dashboard card. Turn it off and this installation stops contacting projectsend.org for news at all.": "Raz dziennie pobiera ogłoszenia projektu z projectsend.org na kartę pulpitu. Po wyłączeniu ta instalacja w ogóle przestaje łączyć się z projectsend.org po aktualności.", + "Announcement": "Ogłoszenie", + "More": "Więcej" } diff --git a/lang/pt_BR.json b/lang/pt_BR.json index 79919332..cea3dbda 100644 --- a/lang/pt_BR.json +++ b/lang/pt_BR.json @@ -2015,5 +2015,7 @@ "Make this file public": "Tornar este arquivo público", "This site has no public page set up yet, so nothing will be visible until an administrator sets one.": "Este site ainda não tem uma página pública configurada, então nada ficará visível até que um administrador configure uma.", "Show ProjectSend news on the dashboard": "Mostrar novidades do ProjectSend no painel", - "Fetches project announcements from projectsend.org once a day for the dashboard card. Turn it off and this installation stops contacting projectsend.org for news at all.": "Busca os anúncios do projeto em projectsend.org uma vez por dia para o cartão do painel. Se você desativar, esta instalação para de contatar o projectsend.org por novidades." + "Fetches project announcements from projectsend.org once a day for the dashboard card. Turn it off and this installation stops contacting projectsend.org for news at all.": "Busca os anúncios do projeto em projectsend.org uma vez por dia para o cartão do painel. Se você desativar, esta instalação para de contatar o projectsend.org por novidades.", + "Announcement": "Aviso", + "More": "Mais" } diff --git a/lang/ru.json b/lang/ru.json index 6d6ac7ed..3f12605d 100644 --- a/lang/ru.json +++ b/lang/ru.json @@ -2015,5 +2015,7 @@ "Make this file public": "Сделать этот файл публичным", "This site has no public page set up yet, so nothing will be visible until an administrator sets one.": "На этом сайте ещё не настроена публичная страница, поэтому ничего не будет видно, пока администратор её не настроит.", "Show ProjectSend news on the dashboard": "Показывать новости ProjectSend на панели", - "Fetches project announcements from projectsend.org once a day for the dashboard card. Turn it off and this installation stops contacting projectsend.org for news at all.": "Раз в день загружает анонсы проекта с projectsend.org для карточки на панели. Если выключить, эта установка вообще перестанет обращаться к projectsend.org за новостями." + "Fetches project announcements from projectsend.org once a day for the dashboard card. Turn it off and this installation stops contacting projectsend.org for news at all.": "Раз в день загружает анонсы проекта с projectsend.org для карточки на панели. Если выключить, эта установка вообще перестанет обращаться к projectsend.org за новостями.", + "Announcement": "Объявление", + "More": "Ещё" } diff --git a/lang/sw.json b/lang/sw.json index 928aff62..8e78897e 100644 --- a/lang/sw.json +++ b/lang/sw.json @@ -2015,5 +2015,7 @@ "Make this file public": "Fanya faili hili liwe la umma", "This site has no public page set up yet, so nothing will be visible until an administrator sets one.": "Tovuti hii bado haina ukurasa wa umma, kwa hivyo hakuna kitakachoonekana hadi msimamizi aweke mmoja.", "Show ProjectSend news on the dashboard": "Onyesha habari za ProjectSend kwenye dashibodi", - "Fetches project announcements from projectsend.org once a day for the dashboard card. Turn it off and this installation stops contacting projectsend.org for news at all.": "Huleta matangazo ya mradi kutoka projectsend.org mara moja kwa siku kwa ajili ya kadi ya dashibodi. Ukiizima, usakinishaji huu hautawasiliana kabisa na projectsend.org kwa habari." + "Fetches project announcements from projectsend.org once a day for the dashboard card. Turn it off and this installation stops contacting projectsend.org for news at all.": "Huleta matangazo ya mradi kutoka projectsend.org mara moja kwa siku kwa ajili ya kadi ya dashibodi. Ukiizima, usakinishaji huu hautawasiliana kabisa na projectsend.org kwa habari.", + "Announcement": "Tangazo", + "More": "Zaidi" } diff --git a/lang/tr.json b/lang/tr.json index 0d4fbe93..ae59938d 100644 --- a/lang/tr.json +++ b/lang/tr.json @@ -2015,5 +2015,7 @@ "Make this file public": "Bu dosyayı herkese açık yap", "This site has no public page set up yet, so nothing will be visible until an administrator sets one.": "Bu sitede henüz herkese açık bir sayfa ayarlanmamış, bu yüzden bir yönetici ayarlayana kadar hiçbir şey görünmeyecek.", "Show ProjectSend news on the dashboard": "ProjectSend haberlerini panelde göster", - "Fetches project announcements from projectsend.org once a day for the dashboard card. Turn it off and this installation stops contacting projectsend.org for news at all.": "Panel kartı için proje duyurularını günde bir kez projectsend.org adresinden alır. Kapatıldığında bu kurulum haberler için projectsend.org ile hiç bağlantı kurmaz." + "Fetches project announcements from projectsend.org once a day for the dashboard card. Turn it off and this installation stops contacting projectsend.org for news at all.": "Panel kartı için proje duyurularını günde bir kez projectsend.org adresinden alır. Kapatıldığında bu kurulum haberler için projectsend.org ile hiç bağlantı kurmaz.", + "Announcement": "Duyuru", + "More": "Daha fazla" } diff --git a/lang/vi.json b/lang/vi.json index 11f30e42..7e30479f 100644 --- a/lang/vi.json +++ b/lang/vi.json @@ -2015,5 +2015,7 @@ "Make this file public": "Công khai tệp này", "This site has no public page set up yet, so nothing will be visible until an administrator sets one.": "Trang web này chưa thiết lập trang công khai, nên sẽ không có gì hiển thị cho đến khi quản trị viên thiết lập.", "Show ProjectSend news on the dashboard": "Hiển thị tin tức ProjectSend trên bảng điều khiển", - "Fetches project announcements from projectsend.org once a day for the dashboard card. Turn it off and this installation stops contacting projectsend.org for news at all.": "Tải thông báo của dự án từ projectsend.org mỗi ngày một lần cho thẻ trên bảng điều khiển. Khi tắt, bản cài đặt này sẽ hoàn toàn không liên hệ với projectsend.org để lấy tin tức." + "Fetches project announcements from projectsend.org once a day for the dashboard card. Turn it off and this installation stops contacting projectsend.org for news at all.": "Tải thông báo của dự án từ projectsend.org mỗi ngày một lần cho thẻ trên bảng điều khiển. Khi tắt, bản cài đặt này sẽ hoàn toàn không liên hệ với projectsend.org để lấy tin tức.", + "Announcement": "Thông báo", + "More": "Thêm" } diff --git a/lang/zh_CN.json b/lang/zh_CN.json index d83bb80d..fbfc1ad1 100644 --- a/lang/zh_CN.json +++ b/lang/zh_CN.json @@ -2015,5 +2015,7 @@ "Make this file public": "将此文件设为公开", "This site has no public page set up yet, so nothing will be visible until an administrator sets one.": "本站尚未设置公开页面,因此在管理员设置之前不会显示任何内容。", "Show ProjectSend news on the dashboard": "在仪表板上显示 ProjectSend 动态", - "Fetches project announcements from projectsend.org once a day for the dashboard card. Turn it off and this installation stops contacting projectsend.org for news at all.": "每天一次从 projectsend.org 获取项目公告,用于仪表板卡片。关闭后,本安装将完全不再因动态而连接 projectsend.org。" + "Fetches project announcements from projectsend.org once a day for the dashboard card. Turn it off and this installation stops contacting projectsend.org for news at all.": "每天一次从 projectsend.org 获取项目公告,用于仪表板卡片。关闭后,本安装将完全不再因动态而连接 projectsend.org。", + "Announcement": "公告", + "More": "更多" } diff --git a/resources/js/components/announcement.tsx b/resources/js/components/announcement.tsx new file mode 100644 index 00000000..f5702eff --- /dev/null +++ b/resources/js/components/announcement.tsx @@ -0,0 +1,114 @@ +import { type SharedData } from '@/types'; +import { usePage } from '@inertiajs/react'; +import { ExternalLink, Megaphone } from 'lucide-react'; + +import { Button } from '@/components/ui/button'; +import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'; +import { useTranslation } from '@/hooks/use-translation'; + +export interface Announcement { + title: string; + body: string; + action_label: string | null; + action_url: string | null; + tone: 'info' | 'warning' | string; +} + +/** + * One message, shown two ways, from one shared prop — see + * ResolvingAnnouncement. The band is the dashboard; the icon beside the + * notification bell carries the same words to every other page, so + * somebody who never opens the dashboard still meets it once. + * + * Both live in this file deliberately. They have to say the same thing, + * and the reliable way to keep two renderings of one message identical is + * for them to share the component that renders it. + * + * Nothing here knows what it is saying. Title, body and button all arrive + * from whatever listened. + * + * Coloured enough to be read and not enough to alarm: a tinted left edge + * and a matching wash, rather than a saturated block. The first caller + * tells a hosted customer their plan has limits and a bigger one exists, + * which is worth noticing and not worth interrupting for — so it must not + * look like an outage. Both palettes are declared per tone rather than + * derived, so the dark variant is a deliberate colour rather than + * whatever the light one happens to become. + */ +const TONES: Record = { + info: 'border-l-sky-500 bg-sky-50 dark:bg-sky-950/40', + warning: 'border-l-amber-500 bg-amber-50 dark:bg-amber-950/40', +}; + +const DOT_TONES: Record = { + info: 'bg-sky-500', + warning: 'bg-amber-500', +}; + +/** The words, and the button if there is one. Shared by both surfaces. */ +function AnnouncementBody({ announcement, compact = false }: { announcement: Announcement; compact?: boolean }) { + return ( + <> +
+

{announcement.title}

+

{announcement.body}

+
+ + {announcement.action_label && announcement.action_url && ( + + )} + + ); +} + +/** The dashboard band. */ +export function AnnouncementBand({ announcement }: { announcement: Announcement }) { + const tone = TONES[announcement.tone] ?? TONES.info; + + return ( +
+ +
+ ); +} + +/** + * The header icon, beside the notification bell. + * + * Same shape as UpdateAvailableIcon next to it: absent entirely when + * there is nothing to say, rather than a dead control. The dot marks it + * without a count — there is only ever one of these, and "1" on a badge + * would invite somebody to look for the second. + */ +export function AnnouncementIcon() { + const { t } = useTranslation(); + const { announcement } = usePage().props; + + if (!announcement) { + return null; + } + + const dot = DOT_TONES[announcement.tone] ?? DOT_TONES.info; + + return ( + + + + + + + + + ); +} diff --git a/resources/js/components/app-sidebar-header.tsx b/resources/js/components/app-sidebar-header.tsx index edddbc47..7bb552fa 100644 --- a/resources/js/components/app-sidebar-header.tsx +++ b/resources/js/components/app-sidebar-header.tsx @@ -1,3 +1,4 @@ +import { AnnouncementIcon } from '@/components/announcement'; import { AppearanceSwitcher } from '@/components/appearance-switcher'; import { Breadcrumbs } from '@/components/breadcrumbs'; import { IconLocaleSwitcher } from '@/components/locale-switcher'; @@ -15,6 +16,7 @@ export function AppSidebarHeader({ breadcrumbs = [] }: { breadcrumbs?: Breadcrum
+ diff --git a/resources/js/components/dashboard-callout.tsx b/resources/js/components/dashboard-callout.tsx deleted file mode 100644 index a963e61c..00000000 --- a/resources/js/components/dashboard-callout.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import { ExternalLink } from 'lucide-react'; - -import { Button } from '@/components/ui/button'; - -export interface DashboardCallout { - title: string; - body: string; - action_label: string | null; - action_url: string | null; - tone: 'info' | 'warning' | string; -} - -/** - * One band across the top of the dashboard, whose words come from - * whatever asked for it — see ResolvingDashboardCallout. Nothing here - * knows what it is saying, which is the point. - * - * Coloured enough to be read and not enough to alarm: a tinted left edge - * and a matching wash, rather than a saturated block. The first caller - * tells a hosted customer their plan has limits and a bigger one exists, - * which is a thing worth noticing and not a thing worth interrupting for - * — so it must not look like an outage. - * - * Both palettes are declared per tone rather than derived, so the dark - * variant is a deliberate colour rather than whatever the light one - * happens to become. - */ -const TONES: Record = { - info: 'border-l-sky-500 bg-sky-50 dark:bg-sky-950/40', - warning: 'border-l-amber-500 bg-amber-50 dark:bg-amber-950/40', -}; - -export function DashboardCalloutBand({ callout }: { callout: DashboardCallout }) { - const tone = TONES[callout.tone] ?? TONES.info; - - return ( -
-
-

{callout.title}

-

{callout.body}

-
- - {callout.action_label && callout.action_url && ( - - )} -
- ); -} diff --git a/resources/js/pages/dashboard.tsx b/resources/js/pages/dashboard.tsx index 91cd1b05..eed0c229 100644 --- a/resources/js/pages/dashboard.tsx +++ b/resources/js/pages/dashboard.tsx @@ -28,7 +28,7 @@ import { TopClientsWidget, type TopClient } from '@/components/dashboard-widgets import { TransfersRangeControls, TransfersWidget, type TransferPoint, type TransfersRange } from '@/components/dashboard-widgets/transfers-widget'; import { WidgetBox } from '@/components/dashboard-widgets/widget-box'; import { WidgetsDialog } from '@/components/dashboard-widgets/widgets-dialog'; -import { DashboardCalloutBand, type DashboardCallout } from '@/components/dashboard-callout'; +import { AnnouncementBand } from '@/components/announcement'; import Heading from '@/components/heading'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; @@ -50,7 +50,6 @@ interface DashboardProps { api: ApiUsageSummary | null; widget_layout: WidgetLayout; dashboard_columns: number; - callout: DashboardCallout | null; } const COLUMN_CLASSES: Record = { @@ -109,11 +108,10 @@ export default function Dashboard({ expired_files, api, widget_layout, - callout, dashboard_columns, }: DashboardProps) { const { t } = useTranslation(); - const { update_notice } = usePage().props; + const { announcement, update_notice } = usePage().props; const [releaseDialogOpen, setReleaseDialogOpen] = useState(false); const [widgetsDialogOpen, setWidgetsDialogOpen] = useState(false); const [layout, setLayout] = useState(widget_layout); @@ -341,8 +339,9 @@ export default function Dashboard({
{/* Above the heading, not inside the grid: whoever asked for this wants it read, and the grid is arranged by - each viewer. */} - {callout && } + each viewer. Read from shared props, the same source + the header icon uses, so the two cannot disagree. */} + {announcement && }
diff --git a/resources/js/types/index.ts b/resources/js/types/index.ts index 473ab779..67e3f316 100644 --- a/resources/js/types/index.ts +++ b/resources/js/types/index.ts @@ -1,3 +1,4 @@ +import { type Announcement } from '@/components/announcement'; import { type InstallKind } from '@/components/update-instructions'; import { LucideIcon } from 'lucide-react'; @@ -111,6 +112,12 @@ export interface SharedData { capabilities: Capability[]; /** Sidebar entries contributed by packages, already staff-filtered. */ extra_nav_links: ExtraNavLink[]; + /** + * One message to put in front of staff, or null. Rendered as a band + * on the dashboard and behind the header icon everywhere else — see + * ResolvingAnnouncement. Shared so both say the same thing. + */ + announcement: Announcement | null; /** Identity providers that are switched on and fully configured. */ social_login: SocialLoginProvider[]; /** The CAPTCHA in force, or null when this installation has none. */ diff --git a/tests/Feature/Platform/ExtensionSeamsTest.php b/tests/Feature/Platform/ExtensionSeamsTest.php index 71ca0c38..764c12b8 100644 --- a/tests/Feature/Platform/ExtensionSeamsTest.php +++ b/tests/Feature/Platform/ExtensionSeamsTest.php @@ -3,7 +3,7 @@ declare(strict_types=1); use App\Models\User; -use App\Modules\Platform\Announcements\Events\ResolvingDashboardCallout; +use App\Modules\Platform\Announcements\Events\ResolvingAnnouncement; use App\Modules\Platform\Navigation\Events\ResolvingNavigationLinks; use Illuminate\Support\Facades\Event; use Inertia\Testing\AssertableInertia; @@ -24,7 +24,7 @@ beforeEach(function () { test('with nothing listening the dashboard is exactly what it was', function () { $this->actingAs($this->admin)->get('/dashboard')->assertInertia( - fn (AssertableInertia $page) => $page->where('callout', null), + fn (AssertableInertia $page) => $page->where('announcement', null), ); $this->actingAs($this->admin)->get('/dashboard')->assertInertia( @@ -32,16 +32,16 @@ test('with nothing listening the dashboard is exactly what it was', function () ); }); -test('a listener can put a band on the dashboard', function () { - Event::listen(ResolvingDashboardCallout::class, function (ResolvingDashboardCallout $event): void { +test('a listener can put a message in front of staff', function () { + Event::listen(ResolvingAnnouncement::class, function (ResolvingAnnouncement $event): void { $event->show('Heads up', 'Something worth reading.', 'Do the thing', 'https://example.test/', 'warning'); }); $this->actingAs($this->admin)->get('/dashboard')->assertInertia( fn (AssertableInertia $page) => $page - ->where('callout.title', 'Heads up') - ->where('callout.action_url', 'https://example.test/') - ->where('callout.tone', 'warning'), + ->where('announcement.title', 'Heads up') + ->where('announcement.action_url', 'https://example.test/') + ->where('announcement.tone', 'warning'), ); }); @@ -75,24 +75,56 @@ test('a client gets no contributed links, even from a listener that adds uncondi // One band. A dashboard that can accumulate banners accumulates them, and // the second is what teaches people to skip the first. test('the first listener to set a callout keeps it', function () { - Event::listen(ResolvingDashboardCallout::class, function (ResolvingDashboardCallout $event): void { + Event::listen(ResolvingAnnouncement::class, function (ResolvingAnnouncement $event): void { $event->show('First', 'Set first.'); }); - Event::listen(ResolvingDashboardCallout::class, function (ResolvingDashboardCallout $event): void { + Event::listen(ResolvingAnnouncement::class, function (ResolvingAnnouncement $event): void { $event->show('Second', 'Should not win.'); }); $this->actingAs($this->admin)->get('/dashboard')->assertInertia( - fn (AssertableInertia $page) => $page->where('callout.title', 'First'), + fn (AssertableInertia $page) => $page->where('announcement.title', 'First'), ); }); test('an unknown tone falls back rather than rendering unstyled', function () { - Event::listen(ResolvingDashboardCallout::class, function (ResolvingDashboardCallout $event): void { + Event::listen(ResolvingAnnouncement::class, function (ResolvingAnnouncement $event): void { $event->show('T', 'B', tone: 'chartreuse'); }); $this->actingAs($this->admin)->get('/dashboard')->assertInertia( - fn (AssertableInertia $page) => $page->where('callout.tone', 'info'), + fn (AssertableInertia $page) => $page->where('announcement.tone', 'info'), + ); +}); + + +// The header icon and the dashboard band read one shared prop, so a +// message reaches somebody who never opens the dashboard. Two props would +// have drifted the first time anybody edited one. +test('the same message is available away from the dashboard', function () { + Event::listen(ResolvingAnnouncement::class, function (ResolvingAnnouncement $event): void { + $event->show('Everywhere', 'Not only on the dashboard.'); + }); + + $this->actingAs($this->admin)->get('/system/settings/general')->assertInertia( + fn (AssertableInertia $page) => $page->where('announcement.title', 'Everywhere'), + ); +}); + +// A client's header carries the bell too. Nothing addressed to staff may +// appear there, however careless the listener. +test('a client is never shown one, even from a listener that sets it unconditionally', function () { + Event::listen(ResolvingAnnouncement::class, function (ResolvingAnnouncement $event): void { + if (! $event->isStaff) { + return; + } + + $event->show('Staff only', 'Not for clients.'); + }); + + $client = User::factory()->client()->create(); + + $this->actingAs($client)->get('/my-files')->assertInertia( + fn (AssertableInertia $page) => $page->where('announcement', null), ); });