diff --git a/apps/web/components/linear/linear-issue-watch-dialog.tsx b/apps/web/components/linear/linear-issue-watch-dialog.tsx
index 051905fa7d..7e2b561a3b 100644
--- a/apps/web/components/linear/linear-issue-watch-dialog.tsx
+++ b/apps/web/components/linear/linear-issue-watch-dialog.tsx
@@ -4,7 +4,6 @@ import { useState, useEffect, useCallback, useMemo } from "react";
import { Button } from "@kandev/ui/button";
import { Separator } from "@kandev/ui/separator";
import { Label } from "@kandev/ui/label";
-import { Input } from "@kandev/ui/input";
import {
Dialog,
DialogContent,
@@ -13,7 +12,6 @@ import {
DialogDescription,
DialogFooter,
} from "@kandev/ui/dialog";
-import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@kandev/ui/select";
import { IconInfoCircle } from "@tabler/icons-react";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@kandev/ui/tooltip";
import { CliModeIcon } from "@/components/cli-mode-icon";
@@ -25,36 +23,26 @@ import {
ScriptEditor,
computeEditorHeight,
} from "@/components/settings/profile-edit/script-editor";
-import {
- LabelMultiSelect,
- PriorityMultiSelect,
- SettingsFields,
- StateMultiSelect,
- useTeamsAndStates,
-} from "./linear-issue-watch-fields";
-import { LINEAR_ISSUE_WATCH_PLACEHOLDERS } from "./linear-issue-watch-placeholders";
+import { SettingsFields } from "./linear-issue-watch-fields";
+import { FilterFields, SelectField } from "./linear-issue-watch-filter-fields";
+import { linearIssueWatchPlaceholders } from "./linear-issue-watch-placeholders";
import { STEP_DEFAULT, STEP_DEFAULT_LABEL, resolveProfileId } from "@/lib/watcher-profile-default";
import { WatcherRepositoryFields } from "@/components/watcher-repository-fields";
import { clearWorkspaceScopedForm } from "@/lib/watcher-repository-default";
import {
- ASSIGNED_ANY,
- CREATOR_ANY,
type FormState,
- type LinearPriority,
buildWatchPayload,
- creatorPlaceholder,
formStateFromWatch,
isWatchFormReady,
makeEmptyForm,
- userOptionLabel,
} from "./linear-issue-watch-form";
import type {
CreateLinearIssueWatchInput,
LinearIssueWatch,
- LinearTeam,
- LinearUser,
UpdateLinearIssueWatchInput,
} from "@/lib/types/linear";
+import { useTranslation } from "react-i18next";
+import type { TFunction } from "i18next";
type Props = {
open: boolean;
@@ -82,254 +70,9 @@ function useFormData(workspaceId: string) {
return { workflows, agentProfiles, allExecutorProfiles };
}
-type SelectFieldItem = { id: string; label: string; icon?: React.ReactNode };
-
-function SelectField(props: {
- label: string;
- description?: string;
- value: string;
- onChange: (v: string) => void;
- placeholder: string;
- items: SelectFieldItem[];
- disabled?: boolean;
-}) {
- return (
-
-
- {props.description &&
{props.description}
}
-
-
- );
-}
-
-function FilterFields({
- form,
- setForm,
-}: {
- form: FormState;
- setForm: React.Dispatch>;
-}) {
- const { teams, states, labels, users, loadingStates, loadingLabels, loadingUsers } =
- useTeamsAndStates(form.workspaceId, form.teamKey);
- const toggleState = useCallback(
- (id: string) =>
- setForm((p) => ({
- ...p,
- stateIds: p.stateIds.includes(id)
- ? p.stateIds.filter((s) => s !== id)
- : [...p.stateIds, id],
- })),
- [setForm],
- );
- const toggleLabel = useCallback(
- (id: string) =>
- setForm((p) => ({
- ...p,
- labelIds: p.labelIds.includes(id)
- ? p.labelIds.filter((l) => l !== id)
- : [...p.labelIds, id],
- })),
- [setForm],
- );
- const togglePriority = useCallback(
- (priority: LinearPriority) =>
- setForm((p) => ({
- ...p,
- priorities: p.priorities.includes(priority)
- ? p.priorities.filter((x) => x !== priority)
- : [...p.priorities, priority],
- })),
- [setForm],
- );
-
- return (
- <>
-
-
-
-
-
- Click to toggle. Matches issues at ANY of the selected priorities.
-
-
-
-
-
-
- {form.teamKey
- ? "Click states to toggle. Empty matches every state on the team."
- : "Pick a team to choose specific workflow states."}
-
-
-
-
-
-
- {form.teamKey
- ? "Click to toggle. Matches ANY of the selected labels."
- : "Pick a team to choose specific labels."}
-
-
-
-
-
- >
- );
-}
-
-type FormSetter = React.Dispatch>;
-
-function TeamRow({
- form,
- setForm,
- teams,
-}: {
- form: FormState;
- setForm: FormSetter;
- teams: LinearTeam[];
-}) {
- return (
-
- setForm((p) => ({ ...p, teamKey: v, stateIds: [], labelIds: [], creatorId: "" }))
- }
- placeholder="(any team)"
- items={teams.map((t) => ({ id: t.key, label: `${t.name} (${t.key})` }))}
- />
- );
-}
-
-function AssigneeAndCreatorRow({
- form,
- setForm,
- users,
- loadingUsers,
-}: {
- form: FormState;
- setForm: FormSetter;
- users: LinearUser[];
- loadingUsers: boolean;
-}) {
- return (
-
- setForm((p) => ({ ...p, assigned: v === ASSIGNED_ANY ? "" : v }))}
- placeholder="(any)"
- items={[
- { id: ASSIGNED_ANY, label: "(any)" },
- { id: "me", label: "Me" },
- { id: "unassigned", label: "Unassigned" },
- ]}
- />
- setForm((p) => ({ ...p, creatorId: v === CREATOR_ANY ? "" : v }))}
- placeholder={creatorPlaceholder(form.teamKey, loadingUsers)}
- items={[
- { id: CREATOR_ANY, label: "(any)" },
- ...users.map((u) => ({ id: u.id, label: userOptionLabel(u) })),
- ]}
- disabled={!form.teamKey || loadingUsers}
- />
-
- );
-}
-
-function EstimateRow({ form, setForm }: { form: FormState; setForm: FormSetter }) {
- return (
-
-
-
-
Lower bound in points (optional).
-
setForm((p) => ({ ...p, estimateMin: e.target.value }))}
- min={0}
- step="0.5"
- placeholder="e.g. 1"
- />
-
-
-
-
Upper bound in points (optional).
-
setForm((p) => ({ ...p, estimateMax: e.target.value }))}
- min={0}
- step="0.5"
- placeholder="e.g. 5"
- />
-
-
- );
-}
-
-function QueryField({ form, setForm }: { form: FormState; setForm: FormSetter }) {
- return (
-
-
-
- Free-text match across title and description (optional).
-
-
setForm((p) => ({ ...p, query: e.target.value }))}
- placeholder="auth bug"
- />
-
- );
-}
-
function PlaceholdersHelp() {
+ const { t } = useTranslation();
+ const placeholders = useMemo(() => linearIssueWatchPlaceholders(t), [t]);
return (
@@ -337,9 +80,9 @@ function PlaceholdersHelp() {
- Available placeholders:
+ {t("linear:availablePlaceholders")}
- {LINEAR_ISSUE_WATCH_PLACEHOLDERS.map((p) => (
+ {placeholders.map((p) => (
-
{`{{${p.key}}}`}{" "}
{p.description}
@@ -353,14 +96,22 @@ function PlaceholdersHelp() {
}
function PromptField({ value, onChange }: { value: string; onChange: (v: string) => void }) {
+ const { t } = useTranslation();
+ // Memoized because `ScriptEditor` keys its Monaco completion-provider
+ // registration on `placeholders` identity. This used to be a module-scope
+ // const, so it was stable for free; now that it is built from `t`, a fresh
+ // array on every render would re-register the provider on every keystroke.
+ const placeholders = useMemo(() => linearIssueWatchPlaceholders(t), [t]);
return (
-
+
- The prompt sent to the agent for each new issue. Type {"{{"} to insert placeholders.
+ {/* The `{{` token is passed as a value so it never reaches the catalog,
+ where i18next would interpolate it away. */}
+ {t("linear:promptFieldHelp", { token: "{{" })}
@@ -385,14 +136,15 @@ function WorkspacePicker({
onChange: (v: string) => void;
disabled?: boolean;
}) {
+ const { t } = useTranslation();
const workspaces = useAppStore((s) => s.workspaces.items);
return (
({ id: w.id, label: w.name }))}
disabled={disabled}
/>
@@ -406,22 +158,23 @@ function AutomationFields({
form: FormState;
setForm: React.Dispatch>;
}) {
+ const { t } = useTranslation();
const { workflows, agentProfiles, allExecutorProfiles } = useFormData(form.workspaceId);
const { steps, loading: stepsLoading } = useWorkflowSteps(form.workflowId);
return (
<>
setForm((p) => ({ ...p, workflowId: v, workflowStepId: "" }))}
- placeholder="Select workflow"
+ placeholder={t("linear:selectWorkflow")}
items={workflows.map((w) => ({ id: w.id, label: w.name }))}
/>
setForm((p) => ({ ...p, workflowStepId: v }))}
placeholder={stepPlaceholder(form.workflowId, stepsLoading, steps.length)}
@@ -440,8 +193,8 @@ function AutomationFields({
/>
setForm((p) => ({ ...p, agentProfileId: resolveProfileId(v) }))}
placeholder={STEP_DEFAULT_LABEL}
@@ -455,8 +208,8 @@ function AutomationFields({
]}
/>
setForm((p) => ({ ...p, executorProfileId: resolveProfileId(v) }))}
placeholder={STEP_DEFAULT_LABEL}
@@ -470,9 +223,9 @@ function AutomationFields({
);
}
-function savingLabel(saving: boolean, isEdit: boolean): string {
- if (saving) return "Saving…";
- return isEdit ? "Update" : "Create";
+function savingLabel(t: TFunction, saving: boolean, isEdit: boolean): string {
+ if (saving) return t("linear:saving");
+ return isEdit ? t("linear:update") : t("linear:create");
}
export function LinearIssueWatchDialog({
@@ -483,6 +236,7 @@ export function LinearIssueWatchDialog({
onCreate,
onUpdate,
}: Props) {
+ const { t } = useTranslation();
const activeWorkspaceId = useAppStore((s) => s.workspaces.activeId);
const [saving, setSaving] = useState(false);
const [form, setForm] = useState(() => makeEmptyForm(workspaceId ?? ""));
@@ -520,12 +274,10 @@ export function LinearIssueWatchDialog({