diff --git a/src/components/common/TeamSelect.test.tsx b/src/components/common/TeamSelect.test.tsx
new file mode 100644
index 0000000..9990c0c
--- /dev/null
+++ b/src/components/common/TeamSelect.test.tsx
@@ -0,0 +1,54 @@
+import { describe, expect, it, vi } from "vitest";
+import { screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { renderWithProviders } from "@/test/test-utils";
+import type { Team } from "@/types/team";
+import { TeamSelect } from "./TeamSelect";
+
+const personalTeam = { id: "team-personal", name: "Personal team", is_personal: true } as Team;
+const sharedTeam = { id: "team-shared", name: "Shared team", is_personal: false } as Team;
+
+describe("TeamSelect", () => {
+ it("renders nothing for a single team", () => {
+ const { container } = renderWithProviders(
+ ,
+ );
+
+ expect(container).toBeEmptyDOMElement();
+ });
+
+ it("renders an error even without a selector", () => {
+ // A failed /teams load leaves no teams to choose from, so the error is the
+ // only thing explaining why the form will not submit.
+ renderWithProviders( );
+
+ expect(screen.getByText("Team is required")).toBeInTheDocument();
+ });
+
+ it("reports the chosen team", async () => {
+ const onChange = vi.fn();
+ renderWithProviders(
+ ,
+ );
+
+ await userEvent.setup().click(screen.getByRole("combobox", { name: /^team/i }));
+ await userEvent.setup().click(screen.getByRole("option", { name: "Shared team" }));
+
+ expect(onChange).toHaveBeenCalledWith(sharedTeam.id);
+ });
+
+ it("marks the field invalid when in error", () => {
+ renderWithProviders(
+ ,
+ );
+
+ const select = screen.getByRole("combobox", { name: /^team/i });
+ expect(select).toHaveAttribute("aria-invalid", "true");
+ expect(select).toHaveAccessibleDescription("Team is required");
+ });
+});
diff --git a/src/components/common/TeamSelect.tsx b/src/components/common/TeamSelect.tsx
new file mode 100644
index 0000000..6351402
--- /dev/null
+++ b/src/components/common/TeamSelect.tsx
@@ -0,0 +1,78 @@
+import { useIntl } from "react-intl";
+
+import { Label } from "@/components/ui/label";
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select";
+import type { Team } from "@/types/team";
+
+interface TeamSelectProps {
+ /** Teams the caller belongs to, from `useTeams()`. */
+ teams: Team[];
+ value?: string;
+ onChange: (teamId: string) => void;
+ /** Validation message for the field, rendered below the select. */
+ error?: string;
+ /** Element id for the select, so each form can scope it. */
+ id?: string;
+}
+
+/**
+ * Team picker for `team`-visibility records.
+ *
+ * Renders nothing when the caller has fewer than two teams: everyone belongs to
+ * at least their own personal team, so a single-team caller has no choice to
+ * make and the form scopes to that team silently (see `resolveTeamId`). The
+ * exception is an error — shown even without a selector, so a failed `/teams`
+ * load explains itself instead of leaving the submit button inert.
+ */
+export function TeamSelect({ teams, value, onChange, error, id = "team" }: TeamSelectProps) {
+ const intl = useIntl();
+ const errorId = `${id}-error`;
+
+ if (teams.length < 2) {
+ return error ? (
+
+ {error}
+
+ ) : null;
+ }
+
+ return (
+
+
+ {intl.formatMessage({ id: "common.team.label" })}{" "}
+
+ {intl.formatMessage({ id: "common.required" })}
+
+
+
+
+
+
+
+ {teams.map((team) => (
+
+ {team.name}
+
+ ))}
+
+
+ {error && (
+
+ {error}
+
+ )}
+
+ );
+}
diff --git a/src/components/mcp-servers/AdvancedSettings.test.tsx b/src/components/mcp-servers/AdvancedSettings.test.tsx
index 6cdec5b..3e1fdb6 100644
--- a/src/components/mcp-servers/AdvancedSettings.test.tsx
+++ b/src/components/mcp-servers/AdvancedSettings.test.tsx
@@ -1,6 +1,7 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
-import { renderWithProviders as render, screen } from "@/test/test-utils";
+import { renderWithProviders as render, screen, waitFor } from "@/test/test-utils";
import userEvent from "@testing-library/user-event";
+import { api } from "@/api/client";
import * as AuthContextModule from "@/auth/AuthContext";
import { AdvancedSettings } from "./AdvancedSettings";
@@ -8,7 +9,22 @@ vi.mock("@/auth/AuthContext", () => ({
useAuthContext: vi.fn(),
}));
+vi.mock("@/api/client", () => ({
+ api: { get: vi.fn() },
+}));
+
const mockUseAuthContext = vi.mocked(AuthContextModule.useAuthContext);
+const mockGet = vi.mocked(api.get);
+
+const personalTeam = { id: "team-personal", name: "Personal team", is_personal: true };
+const sharedTeam = { id: "team-shared", name: "Shared team", is_personal: false };
+
+/** Answers `GET /teams` with the given teams; everything else stays empty. */
+function mockTeams(teams: Array>) {
+ mockGet.mockImplementation((path: string) =>
+ path === "/teams" ? Promise.resolve({ teams }) : Promise.resolve([]),
+ );
+}
type AdvancedSettingsProps = Parameters[0];
@@ -81,6 +97,7 @@ const makeProps = (overrides: Partial = {}): AdvancedSett
describe("AdvancedSettings", () => {
beforeEach(() => {
vi.clearAllMocks();
+ mockTeams([personalTeam]);
mockUseAuthContext.mockReturnValue(makeAuthContext());
});
@@ -155,17 +172,28 @@ describe("AdvancedSettings", () => {
expect(onTeamIdChange).toHaveBeenCalledWith("");
});
- it("clears teamId when selectedTeamId becomes null while visibility is team", () => {
- mockUseAuthContext.mockReturnValue(makeAuthContext(null));
+ it("falls back to the caller's own team on a switch to All teams", () => {
+ mockUseAuthContext.mockReturnValue(makeAuthContext("team-A"));
const onTeamIdChange = vi.fn();
+ const { rerender } = render(
+ ,
+ );
+ onTeamIdChange.mockClear();
- render(
+ mockUseAuthContext.mockReturnValue(makeAuthContext(null));
+ rerender(
,
);
- expect(onTeamIdChange).toHaveBeenCalledWith("");
+ // "All teams" is not a scope a server can be created in, so the form
+ // falls back rather than leaving it unscoped.
+ return waitFor(() => {
+ expect(onTeamIdChange).toHaveBeenCalledWith(personalTeam.id);
+ });
});
it("does not call onTeamIdChange when visibility is not team and teamId is already empty", () => {
@@ -208,32 +236,32 @@ describe("AdvancedSettings", () => {
});
});
- describe("team visibility — hint message", () => {
- it("shows 'scoped to currently selected team' when visibility is team and a team is selected", () => {
- mockUseAuthContext.mockReturnValue(makeAuthContext("team-A"));
-
- render( );
+ describe("team visibility — selector", () => {
+ it("stays hidden for a single team", async () => {
+ render( );
- expect(screen.getByText(/scoped to your currently selected team/i)).toBeInTheDocument();
+ await waitFor(() => {
+ expect(screen.queryByRole("combobox", { name: /^team/i })).not.toBeInTheDocument();
+ });
});
- it("shows 'please select a team' when visibility is team but no team is selected", () => {
- mockUseAuthContext.mockReturnValue(makeAuthContext(null));
+ it("lists the caller's teams", async () => {
+ mockTeams([personalTeam, sharedTeam]);
- render( );
+ render( );
- expect(screen.getByText(/please select a team using the team switcher/i)).toBeInTheDocument();
+ const teamSelect = await screen.findByRole("combobox", { name: /^team/i });
+ expect(teamSelect).toHaveTextContent("Personal team");
});
- it("does not show either team hint when visibility is not team", () => {
- mockUseAuthContext.mockReturnValue(makeAuthContext("team-A"));
+ it("stays hidden when visibility is not team", async () => {
+ mockTeams([personalTeam, sharedTeam]);
render( );
- expect(screen.queryByText(/scoped to your currently selected team/i)).not.toBeInTheDocument();
- expect(
- screen.queryByText(/please select a team using the team switcher/i),
- ).not.toBeInTheDocument();
+ await waitFor(() => {
+ expect(screen.queryByRole("combobox", { name: /^team/i })).not.toBeInTheDocument();
+ });
});
});
diff --git a/src/components/mcp-servers/AdvancedSettings.tsx b/src/components/mcp-servers/AdvancedSettings.tsx
index bd53ec3..5c5f501 100644
--- a/src/components/mcp-servers/AdvancedSettings.tsx
+++ b/src/components/mcp-servers/AdvancedSettings.tsx
@@ -1,4 +1,4 @@
-import { useEffect } from "react";
+import { useEffect, useState } from "react";
import { useIntl } from "react-intl";
import { Info, TriangleAlert } from "lucide-react";
import { Textarea } from "@/components/ui/textarea";
@@ -18,8 +18,10 @@ import { CustomHeadersAuth, type CustomHeader } from "@/components/mcp-servers/C
import { OAuth2Auth } from "@/components/mcp-servers/OAuth2Auth";
import { QueryParameterAuth } from "@/components/mcp-servers/QueryParameterAuth";
import { useAuthContext } from "@/auth/AuthContext";
+import { resolveTeamId, useTeams } from "@/hooks/useTeams";
import type { Visibility } from "@/types/server";
import { VisibilityInfoPopover } from "@/components/common/VisibilityInfoPopover";
+import { TeamSelect } from "@/components/common/TeamSelect";
export type { CustomHeader };
@@ -30,6 +32,8 @@ interface AdvancedSettingsProps {
onVisibilityChange: (value: Visibility) => void;
teamId: string;
onTeamIdChange: (value: string) => void;
+ /** Validation message for the team field, shown on the selector. */
+ teamError?: string;
authType: AuthType;
onAuthTypeChange: (value: AuthType) => void;
basicAuthUsername: string;
@@ -81,6 +85,7 @@ export function AdvancedSettings({
onVisibilityChange,
teamId,
onTeamIdChange,
+ teamError,
authType,
onAuthTypeChange,
basicAuthUsername,
@@ -127,17 +132,32 @@ export function AdvancedSettings({
oauthErrors,
}: AdvancedSettingsProps) {
const { selectedTeamId } = useAuthContext();
+ const { teams } = useTeams();
const intl = useIntl();
+ const [pickedInForm, setPickedInForm] = useState(false);
+
+ // The sidebar switcher stays authoritative (#5077) until the caller picks a
+ // team in the selector below. "All teams" is not a scope a server can be
+ // created in, so it resolves to the caller's own team rather than leaving the
+ // server unscoped.
useEffect(() => {
- if (visibility === "team") {
- if ((selectedTeamId ?? "") !== teamId) {
- onTeamIdChange(selectedTeamId ?? "");
- }
- } else if (teamId) {
- onTeamIdChange("");
+ if (visibility !== "team") {
+ if (teamId) onTeamIdChange("");
+ return;
+ }
+ if (pickedInForm) return;
+
+ const resolved = resolveTeamId(teams, selectedTeamId);
+ if (resolved && resolved !== teamId) {
+ onTeamIdChange(resolved);
}
- }, [visibility, selectedTeamId, teamId, onTeamIdChange]);
+ }, [visibility, selectedTeamId, teams, teamId, pickedInForm, onTeamIdChange]);
+
+ const handleTeamChange = (nextTeamId: string) => {
+ setPickedInForm(true);
+ onTeamIdChange(nextTeamId);
+ };
const renderAuthContent = () => {
switch (authType) {
@@ -234,15 +254,18 @@ export function AdvancedSettings({
- {visibility === "team" && (
-
- {selectedTeamId
- ? "This server will be scoped to your currently selected team"
- : "Please select a team using the team switcher in the sidebar"}
-
- )}
+ {visibility === "team" && (
+
+ )}
+
{/* Authentication type */}
diff --git a/src/components/mcp-servers/MCPServerForm.test.tsx b/src/components/mcp-servers/MCPServerForm.test.tsx
index fd80af2..2d2f470 100644
--- a/src/components/mcp-servers/MCPServerForm.test.tsx
+++ b/src/components/mcp-servers/MCPServerForm.test.tsx
@@ -59,6 +59,12 @@ const server = setupServer(
http.get("/api/prompts", () => {
return HttpResponse.json([]);
}),
+ // Everyone belongs to at least their own personal team.
+ http.get("/api/teams", () => {
+ return HttpResponse.json({
+ teams: [{ id: "team-personal", name: "Personal team", is_personal: true }],
+ });
+ }),
);
beforeAll(() => server.listen({ onUnhandledRequest: "warn" }));
@@ -269,18 +275,44 @@ describe("MCPServerForm", () => {
expect(screen.getByText("CA certificate")).toBeInTheDocument();
});
- it("shows team-switcher hint when Team visibility is selected and no team is active", async () => {
- const user = userEvent.setup();
- renderWithRouter( );
+ describe("team visibility", () => {
+ const selectTeamVisibility = async () => {
+ const user = userEvent.setup();
+ renderWithRouter( );
- await user.click(screen.getByRole("button", { name: /Advanced settings/i }));
- await user.click(screen.getByRole("combobox", { name: /visibility/i }));
- await user.click(screen.getByRole("option", { name: /^Team$/i }));
+ await user.click(screen.getByRole("button", { name: /Advanced settings/i }));
+ await user.click(screen.getByRole("combobox", { name: /visibility/i }));
+ await user.click(screen.getByRole("option", { name: /^Team$/i }));
+ };
- // AuthProvider returns selectedTeamId: null (unauthenticated), so the sidebar prompt appears
- expect(
- screen.getByText(/please select a team using the team switcher in the sidebar/i),
- ).toBeInTheDocument();
+ it("hides the selector for one team", async () => {
+ // The default /api/teams handler returns a single, personal team.
+ await selectTeamVisibility();
+
+ await waitFor(() => {
+ expect(screen.queryByRole("combobox", { name: /^team/i })).not.toBeInTheDocument();
+ });
+ expect(
+ screen.queryByText(/team selection is required when visibility is set to team/i),
+ ).not.toBeInTheDocument();
+ });
+
+ it("shows the selector for several teams", async () => {
+ server.use(
+ http.get("/api/teams", () =>
+ HttpResponse.json({
+ teams: [
+ { id: "team-personal", name: "Personal team", is_personal: true },
+ { id: "team-shared", name: "Shared team", is_personal: false },
+ ],
+ }),
+ ),
+ );
+
+ await selectTeamVisibility();
+
+ expect(await screen.findByRole("combobox", { name: /^team/i })).toBeInTheDocument();
+ });
});
});
diff --git a/src/components/mcp-servers/MCPServerForm.tsx b/src/components/mcp-servers/MCPServerForm.tsx
index 9d63516..086785e 100644
--- a/src/components/mcp-servers/MCPServerForm.tsx
+++ b/src/components/mcp-servers/MCPServerForm.tsx
@@ -307,6 +307,7 @@ export function MCPServerForm({ isOpen, onToggle, serverId, onSuccess }: MCPServ
onVisibilityChange={setVisibility}
teamId={teamId}
onTeamIdChange={setTeamId}
+ teamError={errors.teamId}
authType={authType}
onAuthTypeChange={setAuthType}
basicAuthUsername={authUsername}
diff --git a/src/components/prompts/PromptForm.test.tsx b/src/components/prompts/PromptForm.test.tsx
index 5a59abc..f8c4c62 100644
--- a/src/components/prompts/PromptForm.test.tsx
+++ b/src/components/prompts/PromptForm.test.tsx
@@ -19,10 +19,21 @@ vi.mock("@/auth/AuthContext", () => ({
useAuthContext: vi.fn(),
}));
+const mockGet = vi.mocked(api.get);
const mockPost = vi.mocked(api.post);
const mockPut = vi.mocked(api.put);
const mockUseAuthContext = vi.mocked(useAuthContext);
+const personalTeam = { id: "team-personal", name: "Personal team", is_personal: true };
+const sharedTeam = { id: "team-shared", name: "Shared team", is_personal: false };
+
+/** Answers `GET /teams` with the given teams; everything else stays empty. */
+function mockTeams(teams: Array>) {
+ mockGet.mockImplementation((path: string) =>
+ path === "/teams" ? Promise.resolve({ teams }) : Promise.resolve([]),
+ );
+}
+
function renderPromptForm(props?: {
onToggle?: () => void;
onSuccess?: () => void;
@@ -69,6 +80,7 @@ describe("PromptForm", () => {
vi.clearAllMocks();
mockPost.mockReset();
mockPut.mockReset();
+ mockTeams([personalTeam]);
mockUseAuthContext.mockReturnValue({
selectedTeamId: null,
user: null,
@@ -174,52 +186,85 @@ describe("PromptForm", () => {
expect(screen.getByRole("button", { name: "Add prompt" })).toBeEnabled();
});
- it("requires an active team when visibility is set to team", async () => {
- renderPromptForm();
- const user = await fillRequiredFields();
-
- await user.click(screen.getByRole("combobox", { name: /visibility/i }));
- await user.click(screen.getByRole("option", { name: /^Team$/i }));
-
- expect(
- screen.getByText("Please select a team using the team switcher in the sidebar"),
- ).toBeInTheDocument();
- expect(
- screen.getByText("Team selection is required when visibility is set to team"),
- ).toBeInTheDocument();
-
- await user.click(screen.getByRole("button", { name: "Add prompt" }));
-
- expect(mockPost).not.toHaveBeenCalled();
- });
-
- it("explains that team prompts use the currently selected sidebar team", async () => {
- mockUseAuthContext.mockReturnValue({
- selectedTeamId: "team-123",
- user: null,
- isAuthenticated: true,
- isLoading: false,
- login: vi.fn(),
- logout: vi.fn(),
- setSelectedTeamId: vi.fn(),
- permissions: [],
- permissionsLoading: false,
- permissionsError: false,
- hasPermission: () => true,
+ describe("team visibility", () => {
+ it("scopes to the only team without asking", async () => {
+ renderPromptForm();
+ const user = await fillRequiredFields();
+
+ await user.click(screen.getByRole("combobox", { name: /visibility/i }));
+ await user.click(screen.getByRole("option", { name: /^Team$/i }));
+
+ // One team means no choice to make: no selector, and above all no error.
+ expect(screen.queryByRole("combobox", { name: /^team/i })).not.toBeInTheDocument();
+ expect(
+ screen.queryByText("Team selection is required when visibility is set to team"),
+ ).not.toBeInTheDocument();
+
+ await user.click(screen.getByRole("button", { name: "Add prompt" }));
+
+ await waitFor(() => {
+ expect(mockPost).toHaveBeenCalledWith(
+ "/prompts",
+ expect.objectContaining({ team_id: personalTeam.id, visibility: "team" }),
+ expect.anything(),
+ );
+ });
});
- renderPromptForm();
- const user = userEvent.setup();
-
- await user.click(screen.getByRole("combobox", { name: /visibility/i }));
- await user.click(screen.getByRole("option", { name: /^Team$/i }));
+ it("offers a selector for several teams", async () => {
+ mockTeams([sharedTeam, personalTeam]);
+ renderPromptForm();
+ const user = await fillRequiredFields();
+
+ await user.click(screen.getByRole("combobox", { name: /visibility/i }));
+ await user.click(screen.getByRole("option", { name: /^Team$/i }));
+
+ const teamSelect = await screen.findByRole("combobox", { name: /^team/i });
+ // Defaults to the personal team rather than an empty required field.
+ expect(teamSelect).toHaveTextContent("Personal team");
+
+ await user.click(teamSelect);
+ await user.click(screen.getByRole("option", { name: "Shared team" }));
+ await user.click(screen.getByRole("button", { name: "Add prompt" }));
+
+ await waitFor(() => {
+ expect(mockPost).toHaveBeenCalledWith(
+ "/prompts",
+ expect.objectContaining({ team_id: sharedTeam.id, visibility: "team" }),
+ expect.anything(),
+ );
+ });
+ });
- expect(
- screen.getByText("This prompt will be scoped to your currently selected team"),
- ).toBeInTheDocument();
- expect(
- screen.queryByText("Team selection is required when visibility is set to team"),
- ).not.toBeInTheDocument();
+ it("defaults to the sidebar's active team", async () => {
+ mockTeams([personalTeam, sharedTeam]);
+ mockUseAuthContext.mockReturnValue({
+ selectedTeamId: sharedTeam.id,
+ user: null,
+ isAuthenticated: true,
+ isLoading: false,
+ login: vi.fn(),
+ logout: vi.fn(),
+ setSelectedTeamId: vi.fn(),
+ permissions: [],
+ permissionsLoading: false,
+ permissionsError: false,
+ hasPermission: () => true,
+ });
+
+ renderPromptForm();
+ const user = userEvent.setup();
+
+ await user.click(screen.getByRole("combobox", { name: /visibility/i }));
+ await user.click(screen.getByRole("option", { name: /^Team$/i }));
+
+ expect(await screen.findByRole("combobox", { name: /^team/i })).toHaveTextContent(
+ "Shared team",
+ );
+ expect(
+ screen.queryByText("Team selection is required when visibility is set to team"),
+ ).not.toBeInTheDocument();
+ });
});
it("calls onToggle when cancel is clicked", async () => {
diff --git a/src/components/prompts/PromptForm.tsx b/src/components/prompts/PromptForm.tsx
index 0e0afda..f359285 100644
--- a/src/components/prompts/PromptForm.tsx
+++ b/src/components/prompts/PromptForm.tsx
@@ -21,6 +21,7 @@ import { getTagDisplay } from "@/components/gateways/utils";
import type { PromptRead } from "@/generated/types";
import type { Visibility } from "@/types/server";
import { VisibilityInfoPopover } from "@/components/common/VisibilityInfoPopover";
+import { TeamSelect } from "@/components/common/TeamSelect";
interface PromptFormProps {
isOpen: boolean;
@@ -73,10 +74,7 @@ export function PromptForm({ isOpen, onToggle, onSuccess, prompt }: PromptFormPr
if (!isOpen) return null;
- const visibilityHintId = form.visibility === "team" ? "prompt-visibility-team-hint" : undefined;
- const visibilityErrorId = form.errors.visibility ? "prompt-visibility-error" : undefined;
- const visibilityDescribedBy =
- [visibilityHintId, visibilityErrorId].filter(Boolean).join(" ") || undefined;
+ const visibilityDescribedBy = form.errors.visibility ? "prompt-visibility-error" : undefined;
return (
@@ -181,15 +179,6 @@ export function PromptForm({ isOpen, onToggle, onSuccess, prompt }: PromptFormPr
- {form.visibility === "team" && (
-
- {intl.formatMessage({
- id: form.teamId
- ? "prompts.add.visibility.team.selectedHint"
- : "prompts.add.visibility.team.selectFromSidebarHint",
- })}
-
- )}
{form.errors.visibility && (
{form.errors.visibility}
@@ -197,6 +186,16 @@ export function PromptForm({ isOpen, onToggle, onSuccess, prompt }: PromptFormPr
)}
+ {form.visibility === "team" && (
+
+ )}
+
({
}),
}));
+vi.mock("@/api/client", () => ({
+ api: { get: vi.fn().mockResolvedValue([]) },
+}));
+
+const mockGet = vi.mocked(api.get);
+
+const personalTeam = { id: "team-personal", name: "Personal team", is_personal: true };
+const sharedTeam = { id: "team-shared", name: "Shared team", is_personal: false };
+
+/** Answers `GET /teams` with the given teams; everything else stays empty. */
+function mockTeams(teams: Array>) {
+ mockGet.mockImplementation((path: string) =>
+ path === "/teams" ? Promise.resolve({ teams }) : Promise.resolve([]),
+ );
+}
+
const defaultProps = {
visibility: "public" as const,
onVisibilityChange: vi.fn(),
@@ -162,9 +179,28 @@ describe("ToolAdvancedSettings", () => {
expect(screen.queryByRole("button", { name: /Add header/i })).toBeNull();
});
- it("shows team hint when visibility is team and selectedTeamId is set", () => {
- renderWithProviders( );
- expect(screen.getByText(/currently selected team/i)).toBeTruthy();
+ describe("team visibility", () => {
+ it("renders no selector for a single team", async () => {
+ mockTeams([personalTeam]);
+
+ renderWithProviders( );
+
+ await waitFor(() => {
+ expect(screen.queryByRole("combobox", { name: /^team/i })).not.toBeInTheDocument();
+ });
+ });
+
+ it("renders a selector for several teams", async () => {
+ mockTeams([personalTeam, sharedTeam]);
+
+ renderWithProviders(
+ ,
+ );
+
+ expect(await screen.findByRole("combobox", { name: /^team/i })).toHaveTextContent(
+ "Shared team",
+ );
+ });
});
it("calls onResponseFilterChange when response filter changes", () => {
diff --git a/src/components/tools/ToolAdvancedSettings.tsx b/src/components/tools/ToolAdvancedSettings.tsx
index cfcc67b..c9ffb20 100644
--- a/src/components/tools/ToolAdvancedSettings.tsx
+++ b/src/components/tools/ToolAdvancedSettings.tsx
@@ -1,4 +1,4 @@
-import { useEffect } from "react";
+import { useEffect, useState } from "react";
import { useIntl } from "react-intl";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
@@ -16,8 +16,10 @@ import { BasicAuth } from "@/components/mcp-servers/BasicAuth";
import { ToolBearerTokenAuth } from "@/components/tools/ToolBearerTokenAuth";
import { CustomHeadersAuth, type CustomHeader } from "@/components/mcp-servers/CustomHeadersAuth";
import { useAuthContext } from "@/auth/AuthContext";
+import { resolveTeamId, useTeams } from "@/hooks/useTeams";
import type { Visibility } from "@/types/server";
import { VisibilityInfoPopover } from "@/components/common/VisibilityInfoPopover";
+import { TeamSelect } from "@/components/common/TeamSelect";
export type { CustomHeader };
@@ -28,6 +30,8 @@ interface ToolAdvancedSettingsProps {
onVisibilityChange: (value: Visibility) => void;
teamId: string;
onTeamIdChange: (value: string) => void;
+ /** Validation message for the team field, shown on the selector. */
+ teamError?: string;
authType: AuthType;
onAuthTypeChange: (value: AuthType) => void;
basicAuthUsername: string;
@@ -51,6 +55,7 @@ export function ToolAdvancedSettings({
onVisibilityChange,
teamId,
onTeamIdChange,
+ teamError,
authType,
onAuthTypeChange,
basicAuthUsername,
@@ -70,17 +75,31 @@ export function ToolAdvancedSettings({
}: ToolAdvancedSettingsProps) {
const intl = useIntl();
const { selectedTeamId } = useAuthContext();
+ const { teams } = useTeams();
const tagSuggestions = useTagSuggestions();
+ const [pickedInForm, setPickedInForm] = useState(false);
+
+ // The sidebar switcher stays authoritative until the caller picks a team in
+ // the selector below. "All teams" is not a scope a tool can be created in, so
+ // it resolves to the caller's own team rather than leaving the tool unscoped.
useEffect(() => {
- if (visibility === "team") {
- if (selectedTeamId && !teamId) {
- onTeamIdChange(selectedTeamId);
- }
- } else if (teamId) {
- onTeamIdChange("");
+ if (visibility !== "team") {
+ if (teamId) onTeamIdChange("");
+ return;
+ }
+ if (pickedInForm) return;
+
+ const resolved = resolveTeamId(teams, selectedTeamId);
+ if (resolved && resolved !== teamId) {
+ onTeamIdChange(resolved);
}
- }, [visibility, selectedTeamId, teamId, onTeamIdChange]);
+ }, [visibility, selectedTeamId, teams, teamId, pickedInForm, onTeamIdChange]);
+
+ const handleTeamChange = (nextTeamId: string) => {
+ setPickedInForm(true);
+ onTeamIdChange(nextTeamId);
+ };
const renderAuthContent = () => {
switch (authType) {
case "none":
@@ -141,15 +160,18 @@ export function ToolAdvancedSettings({
- {visibility === "team" && (
-
- {selectedTeamId
- ? "This tool will be scoped to your currently selected team"
- : "Please select a team using the team switcher in the sidebar"}
-
- )}
+ {visibility === "team" && (
+
+ )}
+
{/* Authentication type */}
{
expect(onDescriptionChange).toHaveBeenCalledWith("My tool");
});
- it("shows team scope hint when visibility=team and selectedTeamId is set", () => {
- renderWithProviders( );
- expect(
- screen.getByText(/This tool will be scoped to your currently selected team/i),
- ).toBeTruthy();
- });
-
it("calls onTeamIdChange('') when visibility changes away from team", () => {
const onTeamIdChange = vi.fn();
renderWithProviders(
diff --git a/src/components/tools/ToolForm.tsx b/src/components/tools/ToolForm.tsx
index a8a8f81..58546bf 100644
--- a/src/components/tools/ToolForm.tsx
+++ b/src/components/tools/ToolForm.tsx
@@ -496,6 +496,7 @@ export function ToolForm({ isOpen, onToggle, onSuccess, tool }: ToolFormProps) {
onVisibilityChange={setVisibility}
teamId={teamId}
onTeamIdChange={setTeamId}
+ teamError={errors.teamId}
authType={authType}
onAuthTypeChange={setAuthType}
basicAuthUsername={authUsername}
diff --git a/src/hooks/usePromptForm.test.ts b/src/hooks/usePromptForm.test.ts
index da30dcc..01f0433 100644
--- a/src/hooks/usePromptForm.test.ts
+++ b/src/hooks/usePromptForm.test.ts
@@ -9,6 +9,7 @@ import { usePromptForm } from "./usePromptForm";
vi.mock("@/api/client", () => ({
api: {
+ get: vi.fn(),
post: vi.fn(),
put: vi.fn(),
},
@@ -18,10 +19,21 @@ vi.mock("@/auth/AuthContext", () => ({
useAuthContext: vi.fn(),
}));
+const mockGet = vi.mocked(api.get);
const mockPost = vi.mocked(api.post);
const mockPut = vi.mocked(api.put);
const mockUseAuthContext = vi.mocked(useAuthContext);
+const personalTeam = { id: "team-personal", name: "Personal team", is_personal: true };
+const sharedTeam = { id: "team-shared", name: "Shared team", is_personal: false };
+
+/** Answers `GET /teams` with the given teams; everything else stays empty. */
+function mockTeams(teams: Array>) {
+ mockGet.mockImplementation((path: string) =>
+ path === "/teams" ? Promise.resolve({ teams }) : Promise.resolve([]),
+ );
+}
+
const wrapper = ({ children }: { children: ReactNode }) =>
createElement(
IntlProvider,
@@ -56,6 +68,7 @@ describe("usePromptForm", () => {
vi.clearAllMocks();
mockPost.mockReset();
mockPut.mockReset();
+ mockTeams([personalTeam]);
mockAuth();
});
@@ -161,49 +174,82 @@ describe("usePromptForm", () => {
expect(result.current.getFormData().prompt.description).toHaveLength(500);
});
- it("shows a team visibility error immediately when no team is selected", () => {
- const { result } = renderHook(() => usePromptForm());
-
- act(() => {
- result.current.setName("Team prompt");
- result.current.setTemplate("Hello {{ name }}");
- result.current.setVisibility("team");
+ describe("team visibility", () => {
+ it("resolves the only team without a sidebar selection", async () => {
+ const { result } = renderHook(() => usePromptForm());
+
+ act(() => {
+ result.current.setName("Team prompt");
+ result.current.setTemplate("Hello {{ name }}");
+ result.current.setVisibility("team");
+ });
+
+ // A single-team caller is never asked to choose, so choosing "team"
+ // visibility must not flag the field.
+ await waitFor(() => {
+ expect(result.current.teamId).toBe(personalTeam.id);
+ });
+ expect(result.current.errors.teamId).toBeUndefined();
+ expect(result.current.isValid).toBe(true);
});
- expect(result.current.errors.visibility).toBe(
- "Team selection is required when visibility is set to team",
- );
- expect(result.current.teamId).toBeUndefined();
- });
+ it("defaults to the personal team, and honours an override", async () => {
+ mockTeams([sharedTeam, personalTeam]);
+ const { result } = renderHook(() => usePromptForm());
- it("clears the team visibility error and exposes teamId when a team becomes selected", async () => {
- const { result, rerender } = renderHook(() => usePromptForm());
+ act(() => {
+ result.current.setVisibility("team");
+ });
- act(() => {
- result.current.setVisibility("team");
+ await waitFor(() => {
+ expect(result.current.teams).toHaveLength(2);
+ });
+ expect(result.current.teamId).toBe(personalTeam.id);
+
+ act(() => {
+ result.current.setTeamId(sharedTeam.id);
+ });
+
+ expect(result.current.teamId).toBe(sharedTeam.id);
});
- expect(result.current.errors.visibility).toBeDefined();
+ it("prefers the sidebar's active team", async () => {
+ mockAuth(sharedTeam.id);
+ mockTeams([personalTeam, sharedTeam]);
+ const { result } = renderHook(() => usePromptForm());
- mockAuth("team-123");
- rerender();
+ act(() => {
+ result.current.setVisibility("team");
+ });
- await waitFor(() => {
- expect(result.current.errors.visibility).toBeUndefined();
- expect(result.current.teamId).toBe("team-123");
+ await waitFor(() => {
+ expect(result.current.teamId).toBe(sharedTeam.id);
+ });
+ expect(result.current.errors.teamId).toBeUndefined();
});
- });
- it("does not set a team visibility error when the sidebar already has a team selected", () => {
- mockAuth("team-123");
- const { result } = renderHook(() => usePromptForm());
+ it("requires a team only on submit", async () => {
+ mockTeams([]);
+ const { result } = renderHook(() => usePromptForm());
- act(() => {
- result.current.setVisibility("team");
- });
+ act(() => {
+ result.current.setName("Team prompt");
+ result.current.setTemplate("Hello {{ name }}");
+ result.current.setVisibility("team");
+ });
- expect(result.current.errors.visibility).toBeUndefined();
- expect(result.current.teamId).toBe("team-123");
+ // Not before the user tries to submit, though.
+ expect(result.current.errors.teamId).toBeUndefined();
+
+ await act(async () => {
+ await result.current.handleSubmit(fakeSubmit());
+ });
+
+ expect(result.current.errors.teamId).toBe(
+ "Team selection is required when visibility is set to team",
+ );
+ expect(mockPost).not.toHaveBeenCalled();
+ });
});
it("submits valid prompt data and calls onSuccess", async () => {
@@ -277,7 +323,7 @@ describe("usePromptForm", () => {
);
});
- it("maps API team_id field errors onto visibility", async () => {
+ it("maps API team_id field errors onto the team field", async () => {
mockAuth("team-123");
const error = new Error("HTTP 422") as Error & {
body?: { field?: string; message?: string };
@@ -302,7 +348,7 @@ describe("usePromptForm", () => {
expect(mockPost).toHaveBeenCalled();
await waitFor(() => {
- expect(result.current.errors.visibility).toBe("Team is not available");
+ expect(result.current.errors.teamId).toBe("Team is not available");
});
});
diff --git a/src/hooks/usePromptForm.ts b/src/hooks/usePromptForm.ts
index b93909a..b80d7ca 100644
--- a/src/hooks/usePromptForm.ts
+++ b/src/hooks/usePromptForm.ts
@@ -1,8 +1,9 @@
-import { useCallback, useEffect, useMemo, useState, type FormEvent } from "react";
+import { useCallback, useMemo, useState, type FormEvent } from "react";
import { useIntl } from "react-intl";
import { z } from "zod";
import { useAuthContext } from "@/auth/AuthContext";
import { useQuery } from "@/hooks/useQuery";
+import { resolveTeamId, useTeams } from "@/hooks/useTeams";
import { promptsApi } from "@/api/prompts";
import { parseApiError } from "@/lib/errorUtils";
import { sanitizeString } from "@/lib/sanitize";
@@ -14,6 +15,7 @@ import type {
} from "@/generated/types";
import type { PromptFormErrors } from "@/types/prompts";
import type { Visibility } from "@/types/server";
+import type { Team } from "@/types/team";
interface PromptFormValues {
name: string;
@@ -34,8 +36,8 @@ export interface PromptFormInitialValues {
tags?: string[];
/**
* The prompt's existing team (edit mode). When set, a `team`-visibility edit
- * keeps this team instead of forcing the caller to (re)select one in the
- * sidebar, so editing a team prompt never silently reassigns or blocks it.
+ * keeps this team instead of resolving a default, so editing a team prompt
+ * never silently reassigns it.
*/
teamId?: string | null;
}
@@ -65,6 +67,8 @@ export interface UsePromptFormReturn {
name: string;
visibility: Visibility;
teamId?: string;
+ /** Teams the caller belongs to, for the in-form selector. */
+ teams: Team[];
template: string;
arguments: string;
description: string;
@@ -74,6 +78,7 @@ export interface UsePromptFormReturn {
isSubmitting: boolean;
setName: (value: string) => void;
setVisibility: (value: Visibility) => void;
+ setTeamId: (value: string) => void;
setTemplate: (value: string) => void;
setArguments: (value: string) => void;
setDescription: (value: string) => void;
@@ -142,7 +147,7 @@ const createPromptFormSchema = (intl: ReturnType, templateRequir
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: intl.formatMessage({ id: "prompts.add.error.teamRequired" }),
- path: ["visibility"],
+ path: ["teamId"],
});
}
})
@@ -179,8 +184,14 @@ function getApiFieldError(error: unknown): PromptFormErrors | null {
const body = (error as { body?: { field?: string; message?: string } | null }).body;
if (!body?.field || !body.message) return null;
- const field = body.field === "team_id" ? "visibility" : body.field;
- if (field === "name" || field === "visibility" || field === "template" || field === "arguments") {
+ const field = body.field === "team_id" ? "teamId" : body.field;
+ if (
+ field === "name" ||
+ field === "visibility" ||
+ field === "teamId" ||
+ field === "template" ||
+ field === "arguments"
+ ) {
return { [field]: body.message };
}
@@ -212,12 +223,12 @@ export function usePromptForm(options: UsePromptFormOptions = {}): UsePromptForm
const [tags, setTagsState] = useState(initialValues?.tags ?? initialState.tags);
const [errors, setErrors] = useState({});
const [isUpdating, setIsUpdating] = useState(false);
- // In edit mode, keep the prompt's own team; only fall back to the sidebar
- // selection (create mode, or when switching a non-team prompt to team).
- const initialTeamId = initialValues?.teamId ?? undefined;
- const resolveTeamId = (vis: Visibility): string | undefined =>
- vis === "team" ? (initialTeamId ?? selectedTeamId ?? undefined) : undefined;
- const teamId = resolveTeamId(visibility);
+ const { teams } = useTeams();
+ // The prompt's own team in edit mode, or the caller's in-form choice; either
+ // one wins over the resolved default so an edit never reassigns the prompt.
+ const [chosenTeamId, setChosenTeamId] = useState(initialValues?.teamId ?? undefined);
+ const teamId =
+ visibility === "team" ? resolveTeamId(teams, selectedTeamId, chosenTeamId) : undefined;
const { execute: createPrompt, isLoading: isCreating } = useQuery<
PromptRead,
CreatePromptPayload
@@ -252,7 +263,7 @@ export function usePromptForm(options: UsePromptFormOptions = {}): UsePromptForm
if (field === "visibility") {
nextValues.teamId =
- value === "team" ? (initialTeamId ?? selectedTeamId ?? undefined) : undefined;
+ value === "team" ? resolveTeamId(teams, selectedTeamId, chosenTeamId) : undefined;
}
const result = schema.safeParse({
@@ -279,7 +290,7 @@ export function usePromptForm(options: UsePromptFormOptions = {}): UsePromptForm
return nextErrors;
});
},
- [getFormValues, schema, selectedTeamId, initialTeamId],
+ [getFormValues, schema, selectedTeamId, teams, chosenTeamId],
);
const updateField = useCallback(
@@ -308,19 +319,28 @@ export function usePromptForm(options: UsePromptFormOptions = {}): UsePromptForm
(value: string) => updateField("name", value, setNameState),
[updateField],
);
- const setVisibility = useCallback(
- (value: Visibility) => {
- setVisibilityState(value);
- setErrors((current) => {
- if (!current.submit) return current;
- const nextErrors = { ...current };
- delete nextErrors.submit;
- return nextErrors;
- });
- validateField("visibility", value);
- },
- [validateField],
- );
+ const setVisibility = useCallback((value: Visibility) => {
+ setVisibilityState(value);
+ setErrors((current) => {
+ const nextErrors = { ...current };
+ delete nextErrors.submit;
+ delete nextErrors.visibility;
+ // Choosing "team" is not itself a mistake — the team requirement is
+ // raised on submit, never as a reaction to picking the level.
+ delete nextErrors.teamId;
+ return nextErrors;
+ });
+ }, []);
+ const setTeamId = useCallback((value: string) => {
+ setChosenTeamId(value || undefined);
+ setErrors((current) => {
+ if (!current.submit && !current.teamId) return current;
+ const nextErrors = { ...current };
+ delete nextErrors.submit;
+ if (value) delete nextErrors.teamId;
+ return nextErrors;
+ });
+ }, []);
const setTemplate = useCallback(
(value: string) => updateField("template", value, setTemplateState),
[updateField],
@@ -361,6 +381,7 @@ export function usePromptForm(options: UsePromptFormOptions = {}): UsePromptForm
setArgumentsState(initialState.arguments);
setDescriptionState(initialState.description);
setTagsState(initialState.tags);
+ setChosenTeamId(undefined);
setErrors({});
}, []);
@@ -430,18 +451,13 @@ export function usePromptForm(options: UsePromptFormOptions = {}): UsePromptForm
[createPrompt, getFormData, getUpdateData, intl, promptId, resetForm, validateForm],
);
- useEffect(() => {
- if (visibility === "team") {
- validateField("visibility", visibility);
- }
- }, [validateField, visibility]);
-
const isValid = useMemo(() => schema.safeParse(getFormValues()).success, [getFormValues, schema]);
return {
name,
visibility,
teamId,
+ teams,
template,
arguments: argumentsValue,
description,
@@ -451,6 +467,7 @@ export function usePromptForm(options: UsePromptFormOptions = {}): UsePromptForm
isSubmitting,
setName,
setVisibility,
+ setTeamId,
setTemplate,
setArguments,
setDescription,
diff --git a/src/hooks/useTeams.test.ts b/src/hooks/useTeams.test.ts
new file mode 100644
index 0000000..bf34a85
--- /dev/null
+++ b/src/hooks/useTeams.test.ts
@@ -0,0 +1,82 @@
+import { renderHook, waitFor } from "@testing-library/react";
+import { beforeEach, describe, expect, it, vi } from "vitest";
+import { api } from "@/api/client";
+import type { Team } from "@/types/team";
+import { resolveTeamId, useTeams } from "./useTeams";
+
+vi.mock("@/api/client", () => ({
+ api: { get: vi.fn() },
+}));
+
+const mockGet = vi.mocked(api.get);
+
+const personalTeam = { id: "team-personal", name: "Personal team", is_personal: true } as Team;
+const sharedTeam = { id: "team-shared", name: "Shared team", is_personal: false } as Team;
+
+describe("useTeams", () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ it("exposes the caller's teams", async () => {
+ mockGet.mockResolvedValue({ teams: [personalTeam] });
+ const { result } = renderHook(() => useTeams());
+
+ await waitFor(() => {
+ expect(result.current.teams).toEqual([personalTeam]);
+ });
+ });
+
+ it("requires no selection for a single team", async () => {
+ mockGet.mockResolvedValue({ teams: [personalTeam] });
+ const { result } = renderHook(() => useTeams());
+
+ await waitFor(() => {
+ expect(result.current.teams).toHaveLength(1);
+ });
+ expect(result.current.requiresSelection).toBe(false);
+ });
+
+ it("requires a selection beyond one team", async () => {
+ mockGet.mockResolvedValue({ teams: [personalTeam, sharedTeam] });
+ const { result } = renderHook(() => useTeams());
+
+ await waitFor(() => {
+ expect(result.current.requiresSelection).toBe(true);
+ });
+ });
+
+ it("yields no teams when the request fails", async () => {
+ mockGet.mockRejectedValue(new Error("boom"));
+ const { result } = renderHook(() => useTeams());
+
+ await waitFor(() => {
+ expect(result.current.isLoading).toBe(false);
+ });
+ expect(result.current.teams).toEqual([]);
+ });
+});
+
+describe("resolveTeamId", () => {
+ it("prefers an explicit team over every default", () => {
+ expect(resolveTeamId([personalTeam, sharedTeam], personalTeam.id, sharedTeam.id)).toBe(
+ sharedTeam.id,
+ );
+ });
+
+ it("falls back to the sidebar's active team", () => {
+ expect(resolveTeamId([personalTeam, sharedTeam], sharedTeam.id)).toBe(sharedTeam.id);
+ });
+
+ it("falls back to the personal team", () => {
+ expect(resolveTeamId([sharedTeam, personalTeam], null)).toBe(personalTeam.id);
+ });
+
+ it("falls back to the first team when none is personal", () => {
+ expect(resolveTeamId([sharedTeam], null)).toBe(sharedTeam.id);
+ });
+
+ it("resolves nothing without teams", () => {
+ expect(resolveTeamId([], null)).toBeUndefined();
+ });
+});
diff --git a/src/hooks/useTeams.ts b/src/hooks/useTeams.ts
new file mode 100644
index 0000000..2693ae8
--- /dev/null
+++ b/src/hooks/useTeams.ts
@@ -0,0 +1,47 @@
+import { useMemo } from "react";
+import { useQuery } from "@/hooks/useQuery";
+import type { Team, TeamsResponse } from "@/types/team";
+
+export interface UseTeamsResult {
+ teams: Team[];
+ isLoading: boolean;
+ /**
+ * Whether the caller has to choose a team explicitly. Every user belongs to
+ * at least their own personal team, so a single-team caller is never asked:
+ * forms scope to that team implicitly and render no selector.
+ */
+ requiresSelection: boolean;
+}
+
+/** The teams the caller belongs to, for scoping `team`-visibility records. */
+export function useTeams(): UseTeamsResult {
+ const { data, isLoading } = useQuery("/teams");
+ const teams = useMemo(() => data?.teams ?? [], [data?.teams]);
+
+ return { teams, isLoading, requiresSelection: teams.length > 1 };
+}
+
+/**
+ * Resolves the team a `team`-visibility record belongs to, in priority order:
+ *
+ * 1. `explicitTeamId` — the record's own team (edit mode) or an in-form choice.
+ * 2. The sidebar's active team, when the switcher is not on "All teams".
+ * 3. The caller's personal team, else the first team they belong to.
+ *
+ * Step 3 is what keeps a single-team caller from ever being asked to pick: the
+ * sidebar defaults to "All teams" every session, and sending them off to the
+ * switcher to choose the only team they have is a dead end. It also gives
+ * multi-team callers a sane default in the in-form selector rather than an
+ * empty required field behind a disabled submit button.
+ */
+export function resolveTeamId(
+ teams: Team[],
+ selectedTeamId: string | null,
+ explicitTeamId?: string | null,
+): string | undefined {
+ if (explicitTeamId) return explicitTeamId;
+ if (selectedTeamId) return selectedTeamId;
+ if (teams.length === 0) return undefined;
+
+ return (teams.find((team) => team.is_personal) ?? teams[0]).id;
+}
diff --git a/src/i18n/locales/en-US/common.json b/src/i18n/locales/en-US/common.json
index e2d5dab..eea306b 100644
--- a/src/i18n/locales/en-US/common.json
+++ b/src/i18n/locales/en-US/common.json
@@ -52,5 +52,8 @@
"common.theme.dark": "Dark mode",
"common.theme.system": "System theme",
"common.theme.toggle": "Toggle theme",
- "common.button.remove": "Remove"
+ "common.button.remove": "Remove",
+ "common.required": "*",
+ "common.team.label": "Team",
+ "common.team.placeholder": "Select a team"
}
diff --git a/src/i18n/locales/en-US/prompts.json b/src/i18n/locales/en-US/prompts.json
index 6a02765..82ba2bc 100644
--- a/src/i18n/locales/en-US/prompts.json
+++ b/src/i18n/locales/en-US/prompts.json
@@ -78,8 +78,6 @@
"prompts.add.placeholder.tags": "e.g., greeting,template,conversation (comma-separated)",
"prompts.add.visibility.public": "Internal",
"prompts.add.visibility.team": "Team",
- "prompts.add.visibility.team.selectedHint": "This prompt will be scoped to your currently selected team",
- "prompts.add.visibility.team.selectFromSidebarHint": "Please select a team using the team switcher in the sidebar",
"prompts.add.visibility.private": "Private",
"prompts.add.button.submit": "Add prompt",
"prompts.edit.pageTitle": "Edit prompt",
diff --git a/src/i18n/locales/es-ES/common.json b/src/i18n/locales/es-ES/common.json
index cbbff44..358e243 100644
--- a/src/i18n/locales/es-ES/common.json
+++ b/src/i18n/locales/es-ES/common.json
@@ -52,5 +52,8 @@
"common.theme.dark": "Modo oscuro",
"common.theme.system": "Tema del sistema",
"common.theme.toggle": "Cambiar tema",
- "common.button.remove": "Eliminar"
+ "common.button.remove": "Eliminar",
+ "common.required": "*",
+ "common.team.label": "Equipo",
+ "common.team.placeholder": "Seleccione un equipo"
}
diff --git a/src/i18n/locales/es-ES/prompts.json b/src/i18n/locales/es-ES/prompts.json
index 2b3b87e..1b2c94a 100644
--- a/src/i18n/locales/es-ES/prompts.json
+++ b/src/i18n/locales/es-ES/prompts.json
@@ -78,8 +78,6 @@
"prompts.add.placeholder.tags": "ej., saludo,plantilla,conversación (separados por comas)",
"prompts.add.visibility.public": "Interno",
"prompts.add.visibility.team": "Equipo",
- "prompts.add.visibility.team.selectedHint": "Este prompt se limitará al equipo seleccionado actualmente",
- "prompts.add.visibility.team.selectFromSidebarHint": "Seleccione un equipo usando el selector de equipos en la barra lateral",
"prompts.add.visibility.private": "Privado",
"prompts.add.button.submit": "Añadir prompt",
"prompts.edit.pageTitle": "Editar prompt",
diff --git a/src/i18n/locales/pt-BR/common.json b/src/i18n/locales/pt-BR/common.json
index 017a5b6..e3600d4 100644
--- a/src/i18n/locales/pt-BR/common.json
+++ b/src/i18n/locales/pt-BR/common.json
@@ -52,5 +52,8 @@
"common.theme.dark": "Modo escuro",
"common.theme.system": "Tema do sistema",
"common.theme.toggle": "Alternar tema",
- "common.button.remove": "Remover"
+ "common.button.remove": "Remover",
+ "common.required": "*",
+ "common.team.label": "Equipe",
+ "common.team.placeholder": "Selecione uma equipe"
}
diff --git a/src/i18n/locales/pt-BR/prompts.json b/src/i18n/locales/pt-BR/prompts.json
index f0d96dc..cec757b 100644
--- a/src/i18n/locales/pt-BR/prompts.json
+++ b/src/i18n/locales/pt-BR/prompts.json
@@ -78,8 +78,6 @@
"prompts.add.placeholder.tags": "ex., saudação,modelo,conversa (separados por vírgula)",
"prompts.add.visibility.public": "Interno",
"prompts.add.visibility.team": "Equipe",
- "prompts.add.visibility.team.selectedHint": "Este prompt será limitado à equipe selecionada atualmente",
- "prompts.add.visibility.team.selectFromSidebarHint": "Selecione uma equipe usando o seletor de equipes na barra lateral",
"prompts.add.visibility.private": "Privado",
"prompts.add.button.submit": "Adicionar prompt",
"prompts.edit.pageTitle": "Editar prompt",