Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions apps/web/src/hooks/use-jira-integration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,3 +100,27 @@ export function useRequestJiraResync() {
queryClient.invalidateQueries({ queryKey: KEYS.jiraIntegration(org.id) }),
});
}

/**
* Say what one of the board's columns means to Studio.
*
* Invalidates the board read, not the integration: the roles live on the
* columns that read carries, and the settings screen and the board itself are
* looking at the same cached list.
*/
export function useSetColumnRole() {
const { locator } = useProjectContext();
const studio = useStudioTools();
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (input: { columnKey: string; role: string | null }) =>
await studio.call("TASK_BOARD_COLUMN_ROLE_SET", {
columnKey: input.columnKey,
role: input.role as "in_review" | "archived" | null,
}),
onSettled: () =>
queryClient.invalidateQueries({
queryKey: KEYS.taskBoardItems(locator),
}),
});
}
8 changes: 8 additions & 0 deletions apps/web/src/i18n/en/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,14 @@ export const settings = {
"settings.jira.boardSearchPlaceholder": "Search boards…",
"settings.jira.noBoardsMatch": "No board matches that search",
"settings.jira.loadingBoards": "Loading boards…",
"settings.jira.rolesLabel": "What each column means",
"settings.jira.rolesDescription":
"Your board's columns come from Jira. Tell Studio which of them is where review happens, and which retires a card — most columns mean nothing to it, and that is fine.",
"settings.jira.roleNone": "Nothing special",
"settings.jira.roleInReview": "Under review",
"settings.jira.roleArchived": "Archive",
"settings.jira.noColumnsYet":
"No columns yet — they arrive with the next sync.",
"settings.jira.mappingLabel": "Column mapping",
"settings.jira.mappingDescription":
"Map the board's columns onto this board's lanes. Columns marked “Don't sync” never appear here.",
Expand Down
8 changes: 8 additions & 0 deletions apps/web/src/i18n/pt-br/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,14 @@ export const settings = {
"settings.jira.boardSearchPlaceholder": "Buscar boards…",
"settings.jira.noBoardsMatch": "Nenhum board corresponde à busca",
"settings.jira.loadingBoards": "Carregando boards…",
"settings.jira.rolesLabel": "O que cada coluna significa",
"settings.jira.rolesDescription":
"As colunas do seu board vêm do Jira. Diga ao Studio qual delas é onde a revisão acontece, e qual arquiva um card — a maioria não significa nada pra ele, e tudo bem.",
"settings.jira.roleNone": "Nada em especial",
"settings.jira.roleInReview": "Em revisão",
"settings.jira.roleArchived": "Arquivo",
"settings.jira.noColumnsYet":
"Nenhuma coluna ainda — elas chegam na próxima sincronização.",
"settings.jira.mappingLabel": "Mapeamento de colunas",
"settings.jira.mappingDescription":
"Mapeie as colunas do board pras lanes deste quadro. Colunas marcadas como “Não sincronizar” nunca aparecem aqui.",
Expand Down
93 changes: 90 additions & 3 deletions apps/web/src/views/settings/jira.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
* account.
*/

import { useTaskBoardItems } from "@/hooks/use-task-board-items";
import { useState } from "react";
import { toast } from "sonner";
import { Button } from "@decocms/ui/components/button.tsx";
Expand Down Expand Up @@ -56,6 +57,7 @@ import {
import { useT } from "@/i18n/use-t.ts";
import type { TranslationKey } from "@/i18n/en";
import {
useSetColumnRole,
type JiraIntegration,
useDeleteJiraIntegration,
useJiraBoardColumns,
Expand Down Expand Up @@ -248,6 +250,78 @@ function SyncStatusLine({ integration }: { integration: JiraIntegration }) {
);
}

/** No role — the honest default for a column nobody has told us about. */
const NO_ROLE = "__none__";

/**
* What each of the board's own columns means to Studio.
*
* Replaces the status mapping for an org whose board is its own. The mapping
* asked a team to restate, lane by lane, something their tracker already knew;
* this asks the one thing it could not know — which of THEIR columns is where
* review happens, and which retires a card. Most columns mean nothing to us,
* and leaving them that way is the safe answer rather than a gap to fill.
*/
function ColumnRoleRows() {
const t = useT();
const { columns, isLoading } = useTaskBoardItems();
const setRole = useSetColumnRole();

if (isLoading) return <Skeleton className="h-24 w-full mt-3" />;
if (columns.length === 0) {
return (
<p className="mt-3 text-xs text-muted-foreground">
{t("settings.jira.noColumnsYet")}
</p>
);
}

return (
<div className="flex flex-col mt-3">
{columns.map((column) => (
<div
key={column.key}
className="flex items-center justify-between gap-4 py-2.5 border-b border-border/60 last:border-b-0"
>
<span className="min-w-0 truncate text-sm">{column.title}</span>
<Select
value={column.role ?? NO_ROLE}
onValueChange={(value) =>
setRole.mutate(
{
columnKey: column.key,
role: value === NO_ROLE ? null : value,
},
{
onError: (err) =>
toast.error(
errorMessage(err, t("settings.jira.saveFailed")),
),
},
)
}
>
<SelectTrigger className="w-44 shrink-0">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value={NO_ROLE}>
{t("settings.jira.roleNone")}
</SelectItem>
<SelectItem value="in_review">
{t("settings.jira.roleInReview")}
</SelectItem>
<SelectItem value="archived">
{t("settings.jira.roleArchived")}
</SelectItem>
</SelectContent>
</Select>
</div>
))}
</div>
);
}

function ColumnMappingRows({ integration }: { integration: JiraIntegration }) {
const t = useT();
const upsert = useUpsertJiraIntegration();
Expand Down Expand Up @@ -505,12 +579,25 @@ function BoardRow({ integration }: { integration: JiraIntegration }) {

function MappingRow({ integration }: { integration: JiraIntegration }) {
const t = useT();
const orgOwnedColumns = useOrgFlag("org_board_columns");
return (
<SettingsCardItem
title={t("settings.jira.mappingLabel")}
description={t("settings.jira.mappingDescription")}
title={
orgOwnedColumns
? t("settings.jira.rolesLabel")
: t("settings.jira.mappingLabel")
}
description={
orgOwnedColumns
? t("settings.jira.rolesDescription")
: t("settings.jira.mappingDescription")
}
>
<ColumnMappingRows integration={integration} />
{orgOwnedColumns ? (
<ColumnRoleRows />
) : (
<ColumnMappingRows integration={integration} />
)}
</SettingsCardItem>
);
}
Expand Down
Loading