Q164: every settings section says whether it affects this company, every company, or this computer
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
9826b163a9
commit
93c5eb2d71
8 files changed
+39
-5
No files matched your search
@@ -54,6 +54,9 @@ export const zh: Record<string, string> = {
|
|||||||
"Cancel run": "取消运行",
|
"Cancel run": "取消运行",
|
||||||
"Show older runs": "显示更早的运行",
|
"Show older runs": "显示更早的运行",
|
||||||
"Target date": "目标日期",
|
"Target date": "目标日期",
|
||||||
|
"Applies to every company in this app.": "适用于本应用中的所有公司。",
|
||||||
|
"Applies to {company} only; the folders are on this computer.": "仅适用于 {company};文件夹位于这台电脑上。",
|
||||||
|
"Applies to {company} only.": "仅适用于 {company}。",
|
||||||
"{n} others": "其余 {n} 人",
|
"{n} others": "其余 {n} 人",
|
||||||
"Work accepted (7 days)": "工作验收(近 7 天)",
|
"Work accepted (7 days)": "工作验收(近 7 天)",
|
||||||
"{accepted} done · {returned} sent back · sessions: {ok} ended, {failed} failed": "{accepted} 项完成 · {returned} 项退回 · 会话:{ok} 次结束,{failed} 次失败",
|
"{accepted} done · {returned} sent back · sessions: {ok} ended, {failed} failed": "{accepted} 项完成 · {returned} 项退回 · 会话:{ok} 次结束,{failed} 次失败",
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ export function SettingsPage({ snapshot, actions, focus }: PageProps) {
|
|||||||
{tab === "providers" && (
|
{tab === "providers" && (
|
||||||
<ProvidersPanel
|
<ProvidersPanel
|
||||||
orgId={orgId}
|
orgId={orgId}
|
||||||
|
companyName={organization.name}
|
||||||
employeeId={employee.id}
|
employeeId={employee.id}
|
||||||
providers={providers}
|
providers={providers}
|
||||||
authProfiles={providerAuthProfiles}
|
authProfiles={providerAuthProfiles}
|
||||||
@@ -59,7 +60,7 @@ export function SettingsPage({ snapshot, actions, focus }: PageProps) {
|
|||||||
actions={actions}
|
actions={actions}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{tab === "integrations" && <IntegrationKeysPanel keys={snapshot.integrationKeys} actions={actions} />}
|
{tab === "integrations" && <IntegrationKeysPanel keys={snapshot.integrationKeys} companyName={organization.name} actions={actions} />}
|
||||||
{tab === "advanced" && (
|
{tab === "advanced" && (
|
||||||
<AdvancedPanel
|
<AdvancedPanel
|
||||||
orgId={orgId}
|
orgId={orgId}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { ScopeNote } from "./ScopeNote";
|
||||||
import type { UsageStatsRecord } from "../../../../shared/usage-stats";
|
import type { UsageStatsRecord } from "../../../../shared/usage-stats";
|
||||||
import type { ProviderRateLimitStateRecord } from "../../../../shared/provider-runtime-domain";
|
import type { ProviderRateLimitStateRecord } from "../../../../shared/provider-runtime-domain";
|
||||||
import type {
|
import type {
|
||||||
@@ -45,6 +46,7 @@ export function AdvancedPanel({
|
|||||||
return (
|
return (
|
||||||
<div className="page">
|
<div className="page">
|
||||||
<h1 className="page-h1">{t("Advanced")}</h1>
|
<h1 className="page-h1">{t("Advanced")}</h1>
|
||||||
|
<ScopeNote scope="company" company={organization.name} />
|
||||||
|
|
||||||
{connected.length > 0 && (
|
{connected.length > 0 && (
|
||||||
<div style={{ marginTop: 16 }}>
|
<div style={{ marginTop: 16 }}>
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { ScopeNote } from "./ScopeNote";
|
||||||
import { useState, type FormEvent } from "react";
|
import { useState, type FormEvent } from "react";
|
||||||
import type { IntegrationKeyName, IntegrationKeyStatus } from "../../../../shared/commands";
|
import type { IntegrationKeyName, IntegrationKeyStatus } from "../../../../shared/commands";
|
||||||
import { Button, Field, Pill } from "../../shared/ui";
|
import { Button, Field, Pill } from "../../shared/ui";
|
||||||
@@ -22,11 +23,12 @@ const KEY_COPY: Record<IntegrationKeyName, { label: string; hint: string; placeh
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export function IntegrationKeysPanel({ keys, actions }: { keys: IntegrationKeyStatus[]; actions: SnapshotActions }) {
|
export function IntegrationKeysPanel({ keys, companyName, actions }: { keys: IntegrationKeyStatus[]; companyName: string; actions: SnapshotActions }) {
|
||||||
return (
|
return (
|
||||||
<div className="page">
|
<div className="page">
|
||||||
<h1 className="page-h1">{t("Integrations")}</h1>
|
<h1 className="page-h1">{t("Integrations")}</h1>
|
||||||
<p style={{ margin: "8px 0 24px", color: "var(--text-2)" }}>
|
<ScopeNote scope="app" company={companyName} />
|
||||||
|
<p style={{ margin: "0 0 24px", color: "var(--text-2)" }}>
|
||||||
{t("Keys are stored on this device, encrypted by the operating system, and shared by every company.")}
|
{t("Keys are stored on this device, encrypted by the operating system, and shared by every company.")}
|
||||||
</p>
|
</p>
|
||||||
{keys.map((status) => <KeyRow key={status.name} status={status} actions={actions} />)}
|
{keys.map((status) => <KeyRow key={status.name} status={status} actions={actions} />)}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { ScopeNote } from "./ScopeNote";
|
||||||
import { useState, type FormEvent } from "react";
|
import { useState, type FormEvent } from "react";
|
||||||
import { Button, Pill } from "../../shared/ui";
|
import { Button, Pill } from "../../shared/ui";
|
||||||
import { Dialog } from "../../shared/Dialog";
|
import { Dialog } from "../../shared/Dialog";
|
||||||
@@ -16,6 +17,7 @@ import { composeSecret } from "./provider-secret";
|
|||||||
import type { SnapshotActions } from "../../app/useSnapshot";
|
import type { SnapshotActions } from "../../app/useSnapshot";
|
||||||
|
|
||||||
interface ProvidersPanelProps {
|
interface ProvidersPanelProps {
|
||||||
|
companyName: string;
|
||||||
orgId: string;
|
orgId: string;
|
||||||
employeeId: string;
|
employeeId: string;
|
||||||
providers: ProviderRecord[];
|
providers: ProviderRecord[];
|
||||||
@@ -30,7 +32,7 @@ const PRICE_GRID = "minmax(150px, 1.4fr) 110px 110px 110px 92px";
|
|||||||
const POLICY_GRID = "minmax(150px, 1.2fr) minmax(118px, 0.8fr) minmax(140px, 1fr) 112px 124px 124px 92px";
|
const POLICY_GRID = "minmax(150px, 1.2fr) minmax(118px, 0.8fr) minmax(140px, 1fr) 112px 124px 124px 92px";
|
||||||
type WorkspaceScope = "organization" | "workspace";
|
type WorkspaceScope = "organization" | "workspace";
|
||||||
|
|
||||||
export function ProvidersPanel({ orgId, employeeId, providers, authProfiles, catalog, agents, providerRateLimits, actions }: ProvidersPanelProps) {
|
export function ProvidersPanel({ orgId, companyName, employeeId, providers, authProfiles, catalog, agents, providerRateLimits, actions }: ProvidersPanelProps) {
|
||||||
const [active, setActive] = useState<ProviderRecord | null>(null);
|
const [active, setActive] = useState<ProviderRecord | null>(null);
|
||||||
const connected = providers.filter((provider) => provider.enabled);
|
const connected = providers.filter((provider) => provider.enabled);
|
||||||
const available = providers.filter((provider) => !provider.enabled);
|
const available = providers.filter((provider) => !provider.enabled);
|
||||||
@@ -58,6 +60,7 @@ export function ProvidersPanel({ orgId, employeeId, providers, authProfiles, cat
|
|||||||
return (
|
return (
|
||||||
<div className="page">
|
<div className="page">
|
||||||
<h1 className="page-h1">{t("Providers")}</h1>
|
<h1 className="page-h1">{t("Providers")}</h1>
|
||||||
|
<ScopeNote scope="company" company={companyName} />
|
||||||
|
|
||||||
{connected.length > 0 && (
|
{connected.length > 0 && (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { t } from "../../i18n/i18n";
|
||||||
|
|
||||||
|
/** Where a setting takes effect: only the current company, every company in this app, or the company with folders on this computer. */
|
||||||
|
export type SettingScope = "company" | "app" | "company_folders";
|
||||||
|
|
||||||
|
/** Says, under a settings section, what a change there affects. */
|
||||||
|
export function ScopeNote({ scope, company }: { scope: SettingScope; company: string }) {
|
||||||
|
const text = scope === "app"
|
||||||
|
? t("Applies to every company in this app.")
|
||||||
|
: scope === "company_folders"
|
||||||
|
? t("Applies to {company} only; the folders are on this computer.", { company })
|
||||||
|
: t("Applies to {company} only.", { company });
|
||||||
|
return <div role="note" className="scope-note" style={{ fontSize: 12, color: "var(--text-3)", margin: "4px 0 10px" }}>{text}</div>;
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { ScopeNote } from "./ScopeNote";
|
||||||
import { useState, type FormEvent } from "react";
|
import { useState, type FormEvent } from "react";
|
||||||
import type {
|
import type {
|
||||||
OrganizationRecord,
|
OrganizationRecord,
|
||||||
@@ -62,6 +63,7 @@ export function WorkspacePanel({
|
|||||||
return (
|
return (
|
||||||
<div className="page">
|
<div className="page">
|
||||||
<h1 className="page-h1">{t("General")}</h1>
|
<h1 className="page-h1">{t("General")}</h1>
|
||||||
|
<ScopeNote scope="company" company={organization.name} />
|
||||||
|
|
||||||
<div className="row" style={{ gap: 8, margin: "16px 0 24px" }}>
|
<div className="row" style={{ gap: 8, margin: "16px 0 24px" }}>
|
||||||
<Pill tone="violet" dot>{organization.executionMode === "ai_ceo" ? t("AI CEO") : t("AI Co-Founder")}</Pill>
|
<Pill tone="violet" dot>{organization.executionMode === "ai_ceo" ? t("AI CEO") : t("AI Co-Founder")}</Pill>
|
||||||
@@ -76,7 +78,8 @@ export function WorkspacePanel({
|
|||||||
<KeyValue label={t("Description")} value={organization.description || "—"} />
|
<KeyValue label={t("Description")} value={organization.description || "—"} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="section-label" style={{ marginBottom: 8 }}>{t("Appearance")}</div>
|
<div className="section-label">{t("Appearance")}</div>
|
||||||
|
<ScopeNote scope="app" company={organization.name} />
|
||||||
<div style={{ display: "grid", gridTemplateColumns: "repeat(2, minmax(0, 240px))", gap: 12, marginBottom: 28 }}>
|
<div style={{ display: "grid", gridTemplateColumns: "repeat(2, minmax(0, 240px))", gap: 12, marginBottom: 28 }}>
|
||||||
<Field label={t("Theme")}>
|
<Field label={t("Theme")}>
|
||||||
<select
|
<select
|
||||||
@@ -113,6 +116,7 @@ export function WorkspacePanel({
|
|||||||
<Button tone="ghost" onClick={() => setCreateOpen(true)}><Icon name="plus" size={16} /> {t("Add")}</Button>
|
<Button tone="ghost" onClick={() => setCreateOpen(true)}><Icon name="plus" size={16} /> {t("Add")}</Button>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<ScopeNote scope="company_folders" company={organization.name} />
|
||||||
{workspaces.map((workspace) => (
|
{workspaces.map((workspace) => (
|
||||||
<div key={workspace.id} className="row" aria-current={workspace.id === focusedWorkspaceId ? "true" : undefined} style={{ padding: "8px 0", borderBottom: "1px solid var(--border-soft)", background: workspace.id === focusedWorkspaceId ? "var(--accent-soft)" : undefined }}>
|
<div key={workspace.id} className="row" aria-current={workspace.id === focusedWorkspaceId ? "true" : undefined} style={{ padding: "8px 0", borderBottom: "1px solid var(--border-soft)", background: workspace.id === focusedWorkspaceId ? "var(--accent-soft)" : undefined }}>
|
||||||
<div style={{ minWidth: 0 }}>
|
<div style={{ minWidth: 0 }}>
|
||||||
|
|||||||
@@ -72,6 +72,11 @@ test("the pinned CEO conversation is the Messages front door", async () => {
|
|||||||
// The message itself says where the reply stands.
|
// The message itself says where the reply stands.
|
||||||
await expect(chatLog.locator(".chat-message", { hasText: "How are we tracking against the goal?" }).first()).toContainText("CEO will reply; queued.");
|
await expect(chatLog.locator(".chat-message", { hasText: "How are we tracking against the goal?" }).first()).toContainText("CEO will reply; queued.");
|
||||||
|
|
||||||
|
// Each settings section says what a change there affects.
|
||||||
|
await page.getByRole("button", { name: "Settings" }).click();
|
||||||
|
await expect(page.getByRole("note").filter({ hasText: "Applies to FlowPilot only." })).toBeVisible();
|
||||||
|
await expect(page.getByRole("note").filter({ hasText: "Applies to every company in this app." })).toBeVisible();
|
||||||
|
|
||||||
// A goal's pace is measured against its target date; without one, no pace is claimed.
|
// A goal's pace is measured against its target date; without one, no pace is claimed.
|
||||||
await page.getByRole("button", { name: "Home" }).click();
|
await page.getByRole("button", { name: "Home" }).click();
|
||||||
await expect(page.getByText("no target date")).toBeVisible();
|
await expect(page.getByText("no target date")).toBeVisible();
|
||||||
|
|||||||
Reference in new issue
Block a user