diff --git a/src/backend/bisheng/channel/domain/services/f048_channel_permission.py b/src/backend/bisheng/channel/domain/services/f048_channel_permission.py index 5e807412c7..c3b1717a40 100644 --- a/src/backend/bisheng/channel/domain/services/f048_channel_permission.py +++ b/src/backend/bisheng/channel/domain/services/f048_channel_permission.py @@ -346,10 +346,11 @@ def _target( ): raise PermissionInvalidResourceError() + cross_tenant = record.tenant_id != actor.current_tenant_id and not actor.super_admin read_only = record.shared_read_only or record.system_read_only - if record.tenant_id != actor.current_tenant_id and not actor.super_admin and not read_only: + if cross_tenant and not read_only: raise PermissionInvalidResourceError() - if read_only and action is not None and action not in READ_ONLY_CHANNEL_ACTIONS: + if cross_tenant and read_only and action is not None and action not in READ_ONLY_CHANNEL_ACTIONS: raise InvalidCatalogActionError(msg=f"Read-only channel does not support action: {action}") return VerifiedPermissionTarget.from_business_service( tenant_id=record.tenant_id, diff --git a/src/backend/test/channel/test_f048_channel_permissions.py b/src/backend/test/channel/test_f048_channel_permissions.py index cda6cb3dc5..e0db9561ce 100644 --- a/src/backend/test/channel/test_f048_channel_permissions.py +++ b/src/backend/test/channel/test_f048_channel_permissions.py @@ -263,6 +263,30 @@ async def test_shared_and_system_channels_are_read_only(channel) -> None: ) +@pytest.mark.asyncio +@pytest.mark.parametrize( + "channel", + ( + _channel(shared_read_only=True), + _channel(system_read_only=True), + ), +) +async def test_read_only_marker_does_not_lock_owner_tenant(channel) -> None: + permission = _Permission() + adapter = F048ChannelPermissionAdapter( + loader=_Loader(channel), + source_service=GrantSourceService(), + permission=permission, + ) + + assert await adapter.check_action( + resource_id="channel-1", + actor=_actor(), + action="manage_permission", + ) + assert permission.calls[0][1][2] == "manage_permission" + + @pytest.mark.asyncio async def test_wrong_tenant_and_fga_failure_fail_closed() -> None: permission = _Permission() diff --git a/src/frontend/client/src/locales/en/translation.json b/src/frontend/client/src/locales/en/translation.json index 0b3edd17c5..9b68c3a856 100644 --- a/src/frontend/client/src/locales/en/translation.json +++ b/src/frontend/client/src/locales/en/translation.json @@ -1649,6 +1649,7 @@ "sensitive_review_blocked": "This article contains prohibited content and cannot be viewed", "articles_count": "articles", "at_least_one_source": "At least 1 source is required", + "back_to_channel_list": "Back to channels", "cancel": "Cancel", "channel_content_needs_approval": "Channel content requires approval to view", "channel_create_success": "Channel created successfully", @@ -1659,6 +1660,8 @@ "channel_load_failed": "Channel failed to load", "channel_name": "Channel Name", "channel_settings": "Channel Settings", + "channel_settings_load_failed": "Channel settings failed to load", + "channel_settings_load_failed_desc": "Please try again later, or return to the channel list and reopen it.", "channel_updated": "Channel updated", "delete_channel": "Delete channel", "edit_channel": "Edit channel", diff --git a/src/frontend/client/src/locales/ja/translation.json b/src/frontend/client/src/locales/ja/translation.json index f471664c8d..3eb543d4f6 100644 --- a/src/frontend/client/src/locales/ja/translation.json +++ b/src/frontend/client/src/locales/ja/translation.json @@ -1572,6 +1572,7 @@ "sensitive_review_blocked": "この記事には違反内容が含まれているため詳細を表示できません", "articles_count": "件のコンテンツ", "at_least_one_source": "少なくとも1つの情報源を追加してください", + "back_to_channel_list": "チャンネル一覧に戻る", "cancel": "キャンセル", "channel_content_needs_approval": "このチャンネルのコンテンツを表示するには承認が必要です", "channel_create_success": "チャンネルの作成に成功しました", @@ -1582,6 +1583,8 @@ "channel_load_failed": "チャンネルの読み込みに失敗しました", "channel_name": "チャンネル名", "channel_settings": "チャンネル設定", + "channel_settings_load_failed": "チャンネル設定の読み込みに失敗しました", + "channel_settings_load_failed_desc": "しばらくしてから再試行するか、チャンネル一覧に戻って開き直してください。", "channel_updated": "チャンネルが更新されました", "delete_channel": "チャンネルを削除", "edit_channel": "チャンネルを編集", diff --git a/src/frontend/client/src/locales/zh-Hans/translation.json b/src/frontend/client/src/locales/zh-Hans/translation.json index 2aa9165314..38e6719a1a 100644 --- a/src/frontend/client/src/locales/zh-Hans/translation.json +++ b/src/frontend/client/src/locales/zh-Hans/translation.json @@ -1576,6 +1576,7 @@ "sensitive_review_blocked": "该文章包含违规内容,无法查看详情", "articles_count": "篇内容", "at_least_one_source": "至少需添加 1 个信息源", + "back_to_channel_list": "返回频道列表", "cancel": "取消", "channel_content_needs_approval": "该频道内容需申请通过后方可查看", "channel_create_success": "频道创建成功", @@ -1586,6 +1587,8 @@ "channel_load_failed": "频道加载失败", "channel_name": "频道名称", "channel_settings": "频道设置", + "channel_settings_load_failed": "频道设置加载失败", + "channel_settings_load_failed_desc": "请稍后重试,或返回频道列表后重新打开。", "channel_updated": "频道已更新", "delete_channel": "删除频道", "edit_channel": "编辑频道", diff --git a/src/frontend/client/src/pages/Subscription/ChannelSettings/ChannelSettingsPage.test.tsx b/src/frontend/client/src/pages/Subscription/ChannelSettings/ChannelSettingsPage.test.tsx index 4d4728c391..c7047aa36a 100644 --- a/src/frontend/client/src/pages/Subscription/ChannelSettings/ChannelSettingsPage.test.tsx +++ b/src/frontend/client/src/pages/Subscription/ChannelSettings/ChannelSettingsPage.test.tsx @@ -35,4 +35,11 @@ describe("F050 channel full-page UI contract", () => { expect(source).toContain("settings.showPermissionSection"); expect(source).not.toContain("permission_ids"); }); + + it("uses localized copy for the load error state", () => { + expect(source).toContain("com_subscription.channel_settings_load_failed"); + expect(source).toContain("com_subscription.channel_settings_load_failed_desc"); + expect(source).toContain("com_subscription.back_to_channel_list"); + expect(source).not.toContain("com_load_error"); + }); }); diff --git a/src/frontend/client/src/pages/Subscription/ChannelSettings/ChannelSettingsPage.tsx b/src/frontend/client/src/pages/Subscription/ChannelSettings/ChannelSettingsPage.tsx index f8212275fd..163340bda6 100644 --- a/src/frontend/client/src/pages/Subscription/ChannelSettings/ChannelSettingsPage.tsx +++ b/src/frontend/client/src/pages/Subscription/ChannelSettings/ChannelSettingsPage.tsx @@ -140,15 +140,22 @@ export function ChannelSettingsPage() { } if (settings.loadError) { return ( -
- {settings.localize("com_load_error")} +
+
+

+ {settings.localize("com_subscription.channel_settings_load_failed")} +

+

+ {settings.localize("com_subscription.channel_settings_load_failed_desc")} +

+
); diff --git a/src/frontend/platform/src/components/bs-comp/permission/PermissionDialog.tsx b/src/frontend/platform/src/components/bs-comp/permission/PermissionDialog.tsx index f8896257f7..caac92876a 100644 --- a/src/frontend/platform/src/components/bs-comp/permission/PermissionDialog.tsx +++ b/src/frontend/platform/src/components/bs-comp/permission/PermissionDialog.tsx @@ -16,7 +16,7 @@ import { type ResourcePermissionContext, } from "@/controllers/API/permission" import { AlertTriangle, Loader2 } from "lucide-react" -import { useCallback, useEffect, useState } from "react" +import { useCallback, useEffect, useRef, useState } from "react" import { useTranslation } from "react-i18next" import { ModeHeader } from "./ModeHeader" import { PermissionGrantTab } from "./PermissionGrantTab" @@ -45,6 +45,8 @@ export function PermissionDialog({ resourceName, }: PermissionDialogProps) { const { t } = useTranslation("permission") + const contentRef = useRef(null) + const grantContentRef = useRef(null) const [context, setContext] = useState( null, ) @@ -119,10 +121,34 @@ export function PermissionDialog({ const canAddPermission = context?.mode === "CUSTOM" && context.can_manage_permission + const handleContentOpenAutoFocus = useCallback( + (event: Event) => { + event.preventDefault() + requestAnimationFrame(() => + contentRef.current?.focus({ preventScroll: true }), + ) + }, + [], + ) + + const handleGrantContentOpenAutoFocus = useCallback( + (event: Event) => { + event.preventDefault() + requestAnimationFrame(() => + grantContentRef.current?.focus({ preventScroll: true }), + ) + }, + [], + ) + return ( <> - + {t("dialog.title")} - {resourceName} @@ -218,7 +244,11 @@ export function PermissionDialog({ {context && canAddPermission && ( - + {t("dialog.tabGrant")} - {resourceName} diff --git a/src/frontend/platform/src/test/f048PermissionDialog.test.tsx b/src/frontend/platform/src/test/f048PermissionDialog.test.tsx index 102c61f6cd..832f20a4ca 100644 --- a/src/frontend/platform/src/test/f048PermissionDialog.test.tsx +++ b/src/frontend/platform/src/test/f048PermissionDialog.test.tsx @@ -129,6 +129,24 @@ describe("F048 PermissionDialog", () => { ).toBeInTheDocument() }) + it("does not focus the close button when opened", async () => { + render( + , + ) + + expect(await screen.findByText("mode.custom")).toBeInTheDocument() + await waitFor(() => { + expect(screen.getByRole("dialog")).toHaveFocus() + }) + expect(screen.getByRole("button", { name: "Close" })).not.toHaveFocus() + }) + it("does not add a permission mode control for a resource without a parent", async () => { vi.mocked(getResourcePermissionContextApi).mockResolvedValue({ ...customContext,