Browse Source

feat(news): add admin UI toggle for news delivery

Add a NewsDeliverySetting component on `/admin/app` so an admin can
toggle `news:isDeliveryEnabled` without touching env vars or restarting
the pod. The component reads/writes via SWR against the new admin
endpoints; updates revalidate the SWR cache so the UI reflects the new
state immediately. Adds the section header and toggle labels to all
five locales (ja_JP, en_US, zh_CN, ko_KR, fr_FR).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Ryotaro Nagahara 1 week ago
parent
commit
0c13345e0b

+ 7 - 0
apps/app/public/static/locales/en_US/admin.json

@@ -332,6 +332,13 @@
     "migration_succeeded": "Your upgrade has been successfully completed! Exit maintenance mode and GROWI can be used.",
     "migration_failed": "Upgrade failed. Please refer to the GROWI docs for information on what to do in the event of failure."
   },
+  "news_delivery": {
+    "section_title": "News delivery",
+    "label": "News delivery",
+    "enable": "Enable news delivery",
+    "description": "Controls whether the cron job pulls the news feed and updates the local cache. Existing cached items remain visible while delivery is disabled.",
+    "update_succeeded": "News delivery setting updated"
+  },
   "maintenance_mode": {
     "maintenance_mode": "Maintenance Mode",
     "under_maintenance_mode": "Under Maintenance Mode",

+ 7 - 0
apps/app/public/static/locales/fr_FR/admin.json

@@ -332,6 +332,13 @@
     "migration_succeeded": "Conversion réussie! Le mode maintenance peut être désactivée et GROWI utilisé.",
     "migration_failed": "Conversion échouée. Lire la documentation GROWI pour des informations supplémentaires."
   },
+  "news_delivery": {
+    "section_title": "Diffusion des actualités",
+    "label": "Diffusion des actualités",
+    "enable": "Activer la diffusion des actualités",
+    "description": "Contrôle si la tâche cron récupère le flux d'actualités et met à jour le cache local. Les actualités déjà en cache restent visibles lorsque la diffusion est désactivée.",
+    "update_succeeded": "Paramètre de diffusion des actualités mis à jour"
+  },
   "maintenance_mode": {
     "maintenance_mode": "Mode maintenance",
     "under_maintenance_mode": "Mode maintenance activé",

+ 7 - 0
apps/app/public/static/locales/ja_JP/admin.json

@@ -341,6 +341,13 @@
     "migration_succeeded": "アップグレードが正常に完了しました!メンテナンスモードを終了して、GROWI を使用することができます。",
     "migration_failed": "アップグレードが失敗しました。失敗した場合の対処法は GROWI docs を参照してください。"
   },
+  "news_delivery": {
+    "section_title": "ニュース配信",
+    "label": "ニュース配信",
+    "enable": "ニュース配信を有効にする",
+    "description": "外部フィードからニュースを取得してローカルにキャッシュする cron ジョブの動作を制御します。無効にしてもキャッシュ済みのニュースは引き続き表示されます。",
+    "update_succeeded": "ニュース配信設定を更新しました"
+  },
   "maintenance_mode": {
     "maintenance_mode": "メンテナンスモード",
     "under_maintenance_mode": "メンテナンスモード中",

+ 7 - 0
apps/app/public/static/locales/ko_KR/admin.json

@@ -332,6 +332,13 @@
     "migration_succeeded": "업그레이드가 성공적으로 완료되었습니다! 유지 보수 모드를 종료하면 GROWI를 사용할 수 있습니다.",
     "migration_failed": "업그레이드 실패. 실패 시 수행할 작업에 대한 정보는 GROWI 문서를 참조하십시오."
   },
+  "news_delivery": {
+    "section_title": "뉴스 배포",
+    "label": "뉴스 배포",
+    "enable": "뉴스 배포 활성화",
+    "description": "외부 피드에서 뉴스를 가져와 로컬 캐시를 업데이트하는 cron 작업의 동작을 제어합니다. 비활성화해도 캐시된 뉴스는 계속 표시됩니다.",
+    "update_succeeded": "뉴스 배포 설정이 업데이트되었습니다"
+  },
   "maintenance_mode": {
     "maintenance_mode": "유지 보수 모드",
     "under_maintenance_mode": "유지 보수 모드 중",

+ 7 - 0
apps/app/public/static/locales/zh_CN/admin.json

@@ -341,6 +341,13 @@
     "migration_succeeded": "您的升级已经成功完成! 退出维护模式,可以使用GROWI。",
     "migration_failed": "升级失败。请参考GROWI的文档,了解在失败情况下该如何处理。"
   },
+  "news_delivery": {
+    "section_title": "新闻推送",
+    "label": "新闻推送",
+    "enable": "启用新闻推送",
+    "description": "控制 cron 任务是否拉取新闻订阅源并更新本地缓存。停用期间,已缓存的新闻仍可显示。",
+    "update_succeeded": "新闻推送设置已更新"
+  },
   "maintenance_mode": {
     "maintenance_mode": "维护模式",
     "under_maintenance_mode": "在维护模式下",

+ 12 - 0
apps/app/src/client/components/Admin/App/AppSettingsPageContents.tsx

@@ -3,6 +3,7 @@ import { useTranslation } from 'next-i18next';
 
 import AdminAppContainer from '~/client/services/AdminAppContainer';
 import { toastError } from '~/client/util/toastr';
+import { NewsDeliverySetting } from '~/features/news/client/components/admin/NewsDeliverySetting';
 import { useIsMaintenanceMode } from '~/states/global';
 import { useSWRxAppSettings } from '~/stores/admin/app-settings';
 import { toArrayIfNot } from '~/utils/array-utils';
@@ -133,6 +134,17 @@ const AppSettingsPageContents = (props: Props) => {
         </div>
       </div>
 
+      <div className="row mt-5">
+        <div className="col-lg-12">
+          <h2 className="admin-setting-header" id="news-delivery">
+            {t('admin:news_delivery.section_title', {
+              defaultValue: 'News delivery',
+            })}
+          </h2>
+          <NewsDeliverySetting />
+        </div>
+      </div>
+
       <div className="row">
         <div className="col-lg-12">
           <h2 className="admin-setting-header" id="maintenance-mode">

+ 77 - 0
apps/app/src/features/news/client/components/admin/NewsDeliverySetting.tsx

@@ -0,0 +1,77 @@
+import type { FC } from 'react';
+import { useCallback } from 'react';
+import { useTranslation } from 'next-i18next';
+
+import { toastError, toastSuccess } from '~/client/util/toastr';
+import loggerFactory from '~/utils/logger';
+
+import {
+  useSWRxNewsDeliverySetting,
+  useUpdateNewsDeliverySetting,
+} from '../../services/news-delivery-setting';
+
+const logger = loggerFactory('growi:feature:news:admin:NewsDeliverySetting');
+
+export const NewsDeliverySetting: FC = () => {
+  const { t } = useTranslation('admin');
+
+  const { data, isLoading } = useSWRxNewsDeliverySetting();
+  const updateDeliverySetting = useUpdateNewsDeliverySetting();
+
+  const isEnabled = data?.isDeliveryEnabled ?? true;
+
+  const onChange = useCallback(
+    async (event: React.ChangeEvent<HTMLInputElement>) => {
+      const next = event.target.checked;
+      try {
+        await updateDeliverySetting(next);
+        toastSuccess(
+          t('admin:news_delivery.update_succeeded', {
+            defaultValue: 'News delivery setting updated',
+          }),
+        );
+      } catch (err) {
+        toastError(err);
+        logger.error(err);
+      }
+    },
+    [updateDeliverySetting, t],
+  );
+
+  return (
+    <div className="row mb-3">
+      <label
+        className="text-start text-md-end col-md-3 col-form-label"
+        htmlFor="checkbox-news-is-delivery-enabled"
+      >
+        {t('admin:news_delivery.label', { defaultValue: 'News delivery' })}
+      </label>
+      <div className="col-md-6 py-2">
+        <div className="form-check form-switch">
+          <input
+            type="checkbox"
+            id="checkbox-news-is-delivery-enabled"
+            className="form-check-input"
+            checked={isEnabled}
+            disabled={isLoading}
+            onChange={onChange}
+          />
+          <label
+            className="form-label form-check-label"
+            htmlFor="checkbox-news-is-delivery-enabled"
+          >
+            {t('admin:news_delivery.enable', {
+              defaultValue: 'Enable news delivery',
+            })}
+          </label>
+        </div>
+        <p className="form-text text-muted">
+          {t('admin:news_delivery.description', {
+            defaultValue:
+              'Controls whether the cron job pulls the news feed and updates the local cache. Existing cached items remain visible while delivery is disabled.',
+          })}
+        </p>
+      </div>
+    </div>
+  );
+};

+ 42 - 0
apps/app/src/features/news/client/services/news-delivery-setting.ts

@@ -0,0 +1,42 @@
+import { useCallback } from 'react';
+import useSWR, { type SWRResponse } from 'swr';
+
+import { apiv3Get, apiv3Post } from '~/client/util/apiv3-client';
+
+const ENDPOINT = '/news/admin/delivery-setting';
+
+type DeliverySettingResponse = {
+  isDeliveryEnabled: boolean;
+};
+
+/**
+ * Fetch the current value of `news:isDeliveryEnabled` (admin only).
+ */
+export const useSWRxNewsDeliverySetting = (): SWRResponse<
+  DeliverySettingResponse,
+  Error
+> => {
+  return useSWR(
+    ENDPOINT,
+    async (endpoint) =>
+      (await apiv3Get<DeliverySettingResponse>(endpoint)).data,
+  );
+};
+
+/**
+ * Returns a callback that updates the news delivery flag on the server and
+ * revalidates the SWR cache so the UI reflects the new value.
+ */
+export const useUpdateNewsDeliverySetting = (): ((
+  flag: boolean,
+) => Promise<void>) => {
+  const { mutate } = useSWRxNewsDeliverySetting();
+
+  return useCallback(
+    async (flag: boolean) => {
+      await apiv3Post(ENDPOINT, { flag });
+      await mutate({ isDeliveryEnabled: flag }, { revalidate: false });
+    },
+    [mutate],
+  );
+};