狀態:設計定稿待 user review(Phase 2 產出) 提出:2026-07-21 來源 Notion case:
3a4346da-4cd0-80fd「雲端整合的Tab要有跟雲端空間整合設定做連接的才看得到」(功能=專案規劃) arc 關聯:relates to FR-016(Google Drive 整合)、FR-042(框架儲存脫鉤 / storage scope)
專案規劃頁的「雲端整合」Tab(TabView tab index 5),只有在該租戶(tenant)的雲端空間整合狀態為 CONNECTED 時才顯示;未連接就直接隱藏該 tab(不渲染)。
| # | 決策 | 選定 |
|---|---|---|
| 1 | Gate 門檻 | 只認 CONNECTED(EXPIRED / REVOKED / DISCONNECTED 皆隱藏) |
| 2 | 未連接時 tab | 直接隱藏(不渲染,非 disabled) |
| 3 | FE 信號來源 | 新增輕量 status endpoint(user 選)—— 但見 §2.2 ⚠️ 已有現成非 admin status endpoint,實作前請 user 二選一 |
compliance-manager-fe/src/views/project/ProjectPlanningView.vue:2110
<TabPanel :header="t('lang.project_planning.tab_cloud_integrations') ?? '雲端整合'">
<ProjectCloudIntegrationsPanel :project-id="projectId" :can-edit="isProjectManager" />
</TabPanel>0 控制項實作 / 1 SSP / 2 專案基本資訊 / 3 參與人員 / 4 程序書管理 / 5 雲端整合(:283 註解)。雲端整合是最後一個 tab(index 5)。app/cloud_integration/service/google_drive_integration_service.py::get_status(tenant_id) → GoogleDriveIntegrationStatusDTO,status ∈ {CONNECTED / EXPIRED / REVOKED / DISCONNECTED}(tenant 層級,DISCONNECTED = 從無整合)。api/cloud_integration/routes/google_drive_integration_route.py:22:
class GoogleDriveIntegrationRoute(MethodResource):
# GET /integrations/google-drive — 取當前 tenant Drive 狀態,jwt_required()(非 admin gated)
def get(self):
dto = service.get_status(user.tenant_id)
return return_response(True, GoogleDriveStatusResponse().dump(asdict(dto)))
# DELETE 才是 super_admin gatedGET 已對所有登入者開放、回 status。故 FR-052 有兩條路(CLAUDE.md「先找既有能力、禁止重複造輪子」,本設計建議走 A):
| 選項 | 做法 | BE 改動 | 取捨 |
|---|---|---|---|
| A(建議)reuse 既有 | FE 直接打 GET /integrations/google-drive,判 status === 'CONNECTED' |
零 | 回傳多帶 email/folder(已對全 JWT 開放,非新暴露);最省 |
| B(user 原選)新輕量端點 | 新增 GET /integrations/google-drive/connected 回 {connected: bool} |
一支 route + service 一行 | 語意最窄、不外洩 email/folder;但多一支端點與既有重疊 |
user Phase 0 選了 B(新輕量端點)——但當時我尚未 grep 出既有 GET 已非 admin gated。實作前一句話讓 user 二選一(見 implementation-plan §Step 0)。以下設計兩案並存,預設 A。
情境一:tenant 已 CONNECTED
1. Manager 進專案規劃頁 → onMounted 取 drive 狀態 → connected=true。
2. TabView 顯示 6 個 tab,最後一個「雲端整合」可見、可點。→ 行為同今。
情境二:tenant 未連接(DISCONNECTED / EXPIRED / REVOKED)
1. Manager 進專案規劃頁 → 取狀態 → connected=false。
2. TabView 只顯示前 5 個 tab(控制項/SSP/基本資訊/參與人員/程序書),
「雲端整合」tab 根本不出現。
3. Manager 若要用雲端整合 → 需先由 super_admin 於系統層「雲端空間整合設定」接好 Drive,
之後重進本頁才看得到(本案不做頁內引導文案——直接隱藏,決策 2)。
邊界:狀態查詢失敗(API error)
- connected 預設 false → tab 隱藏(fail-closed:拿不到狀態時不顯示,比誤顯示一個連不上的 tab 好)。
ProjectPlanningView.vue:
<!-- 只有 driveConnected 才渲染整個 TabPanel -->
<TabPanel v-if="driveConnected" :header="t('lang.project_planning.tab_cloud_integrations') ?? '雲端整合'">
<ProjectCloudIntegrationsPanel :project-id="projectId" :can-edit="isProjectManager" />
</TabPanel>const driveConnected = ref(false)
async function loadDriveStatus() {
try {
// 選項 A:既有 endpoint
const s = await CloudIntegrationService.getDriveStatus() // GET /integrations/google-drive
driveConnected.value = s?.status === 'CONNECTED'
// 選項 B:新端點 → driveConnected.value = s?.connected === true
} catch (e) {
driveConnected.value = false // fail-closed
}
}
onMounted(loadDriveStatus)v-if 動態 tab 雷區(FR-047 frontend-overview §3.6):active-index 依「渲染出的 TabPanel 順序」計 index。雲端整合是最後一個 tab,用 v-if 移除末尾 tab 不影響前 5 個 index(若移的是中間 tab 才會 off-by-one)→ 本案安全。loadDriveStatus resolve 前 activeTab 若 >4 且 !connected → clamp 到 0)。plan 列為 FE 驗證項。若 user 確認走 B:
| 層 | 檔案 | 動作 |
|---|---|---|
| Route | api/cloud_integration/routes/google_drive_integration_route.py |
新增 GoogleDriveConnectedRoute,GET /integrations/google-drive/connected,jwt_required() |
| App | google_drive_integration_service.py |
加 is_connected(tenant_id) -> bool(get_status(tenant_id).status == "CONNECTED",reuse) |
| 註冊 | api/cloud_integration/__init__.py |
add_resource(..., "/integrations/google-drive/connected") |
CloudIntegrationService.getDriveStatus()(若 FE 尚無)。jwt_required(),回自己 tenant 狀態(既有 get_status 已 tenant-scoped,RLS 不涉)。CloudIntegrationService 是否已有取 tenant drive status 的方法(grep FE)——有就直接用,避免 FE 也重造。