diff --git a/apps/code/src/renderer/features/inbox/components/InboxSignalsTab.tsx b/apps/code/src/renderer/features/inbox/components/InboxSignalsTab.tsx index 726e6f177..7dd1e379d 100644 --- a/apps/code/src/renderer/features/inbox/components/InboxSignalsTab.tsx +++ b/apps/code/src/renderer/features/inbox/components/InboxSignalsTab.tsx @@ -1,5 +1,6 @@ import { useOptionalAuthenticatedClient } from "@features/auth/hooks/authClient"; import { useCurrentUser } from "@features/auth/hooks/authQueries"; +import { InboxBoardView } from "@features/inbox/components/board/InboxBoardView"; import { SelectReportPane, SkeletonBackdrop, @@ -21,6 +22,7 @@ import { } from "@features/inbox/hooks/useInboxReports"; import { useSignalSourceConfigs } from "@features/inbox/hooks/useSignalSourceConfigs"; import { useInboxReportSelectionStore } from "@features/inbox/stores/inboxReportSelectionStore"; +import { useInboxSignalsBoardDetailStore } from "@features/inbox/stores/inboxSignalsBoardDetailStore"; import { useInboxSignalsFilterStore } from "@features/inbox/stores/inboxSignalsFilterStore"; import { useInboxSignalsSidebarStore } from "@features/inbox/stores/inboxSignalsSidebarStore"; import { useInboxSourcesDialogStore } from "@features/inbox/stores/inboxSourcesDialogStore"; @@ -66,6 +68,7 @@ export function InboxSignalsTab() { const sortDirection = useInboxSignalsFilterStore((s) => s.sortDirection); const searchQuery = useInboxSignalsFilterStore((s) => s.searchQuery); const statusFilter = useInboxSignalsFilterStore((s) => s.statusFilter); + const viewMode = useInboxSignalsFilterStore((s) => s.viewMode); const sourceProductFilter = useInboxSignalsFilterStore( (s) => s.sourceProductFilter, ); @@ -444,6 +447,56 @@ export function InboxSignalsTab() { }; }, [sidebarIsResizing, setSidebarWidth, setSidebarIsResizing]); + // ── Board-mode detail pane resize ────────────────────────────────────── + const boardDetailWidth = useInboxSignalsBoardDetailStore((s) => s.width); + const boardDetailIsResizing = useInboxSignalsBoardDetailStore( + (s) => s.isResizing, + ); + const setBoardDetailWidth = useInboxSignalsBoardDetailStore( + (s) => s.setWidth, + ); + const setBoardDetailIsResizing = useInboxSignalsBoardDetailStore( + (s) => s.setIsResizing, + ); + const boardContainerRef = useRef(null); + + const handleBoardDetailResizeMouseDown = useCallback( + (e: React.MouseEvent) => { + e.preventDefault(); + setBoardDetailIsResizing(true); + document.body.style.cursor = "col-resize"; + document.body.style.userSelect = "none"; + }, + [setBoardDetailIsResizing], + ); + + useEffect(() => { + const handleMouseMove = (e: MouseEvent) => { + if (!boardDetailIsResizing || !boardContainerRef.current) return; + const rect = boardContainerRef.current.getBoundingClientRect(); + const containerWidth = rect.width; + const maxWidth = Math.max(420, containerWidth * 0.7); + const newWidth = Math.max( + 420, + Math.min(maxWidth, rect.right - e.clientX), + ); + setBoardDetailWidth(newWidth); + }; + const handleMouseUp = () => { + if (boardDetailIsResizing) { + setBoardDetailIsResizing(false); + document.body.style.cursor = ""; + document.body.style.userSelect = ""; + } + }; + document.addEventListener("mousemove", handleMouseMove); + document.addEventListener("mouseup", handleMouseUp); + return () => { + document.removeEventListener("mousemove", handleMouseMove); + document.removeEventListener("mouseup", handleMouseUp); + }; + }, [boardDetailIsResizing, setBoardDetailWidth, setBoardDetailIsResizing]); + // ── Discovered-task suggestions (rendered inline at top of list) ─────── const discoveredTasks = useSetupStore((s) => s.discoveredTasks); const hasDiscoveredTasks = discoveredTasks.length > 0; @@ -681,88 +734,69 @@ export function InboxSignalsTab() { // ── Render ────────────────────────────────────────────────────────────── + const detailPaneContent = + selectedReports.length > 1 ? ( + + ) : selectedReport ? ( + + ) : selectedDiscoveredTask ? ( + + ) : ( + + ); + + const hasDetailSelection = + selectedReports.length > 1 || + selectedReport != null || + selectedDiscoveredTask != null; + + const toolbar = ( + setSourcesDialogOpen(true)} + onOpenDismissDialog={openDismissDialogFromToolbar} + isDismissMutationPending={dismissMutationPending} + onReportAction={tracker.signalAction} + /> + ); + return ( <> {showTwoPaneLayout ? ( - - {/* ── Left pane: report list ───────────────────────────────── */} - - - { - const target = e.target as HTMLElement; - if ( - target.closest( - "input, textarea, select, [contenteditable='true']", - ) - ) { - return; - } - if (target.closest("[data-report-id], button")) { - focusListPane(); - } - }} - // Same redirect for focus arriving via keyboard (Tab) — if focus lands - // inside a row element rather than on the container itself, pull it back up. - onFocusCapture={(e) => { - const target = e.target as HTMLElement; - if ( - target.closest( - "input, textarea, select, [contenteditable='true']", - ) - ) { - return; - } - if ( - target !== leftPaneRef.current && - target.closest("[data-report-id], button") - ) { - focusListPane(); - } - }} - > - - setSourcesDialogOpen(true)} - onOpenDismissDialog={openDismissDialogFromToolbar} - isDismissMutationPending={dismissMutationPending} - onReportAction={tracker.signalAction} - /> - - - + + {toolbar} + + + + - - - - - - {/* Resize handle */} + + {hasDetailSelection ? ( + + + {detailPaneContent} + + ) : null} + + + ) : ( + + {/* ── Left pane: report list ───────────────────────────────── */} - + > + + { + const target = e.target as HTMLElement; + if ( + target.closest( + "input, textarea, select, [contenteditable='true']", + ) + ) { + return; + } + if (target.closest("[data-report-id], button")) { + focusListPane(); + } + }} + // Same redirect for focus arriving via keyboard (Tab) — if focus lands + // inside a row element rather than on the container itself, pull it back up. + onFocusCapture={(e) => { + const target = e.target as HTMLElement; + if ( + target.closest( + "input, textarea, select, [contenteditable='true']", + ) + ) { + return; + } + if ( + target !== leftPaneRef.current && + target.closest("[data-report-id], button") + ) { + focusListPane(); + } + }} + > + + {toolbar} + + + + + - {/* ── Right pane: detail ───────────────────────────────── */} - - {selectedReports.length > 1 ? ( - - ) : selectedReport ? ( - - ) : selectedDiscoveredTask ? ( - + + {/* Resize handle */} + - ) : ( - - )} + + + {/* ── Right pane: detail ───────────────────────────────── */} + + {detailPaneContent} + - + ) ) : ( /* ── Full-width empty state with skeleton backdrop ──────── */ diff --git a/apps/code/src/renderer/features/inbox/components/board/InboxBoardView.tsx b/apps/code/src/renderer/features/inbox/components/board/InboxBoardView.tsx new file mode 100644 index 000000000..c377b7428 --- /dev/null +++ b/apps/code/src/renderer/features/inbox/components/board/InboxBoardView.tsx @@ -0,0 +1,395 @@ +import { ReportCardContent } from "@features/inbox/components/utils/ReportCardContent"; +import { SOURCE_PRODUCT_META } from "@features/inbox/components/utils/source-product-icons"; +import { useInboxSignalsFilterStore } from "@features/inbox/stores/inboxSignalsFilterStore"; +import { + type BoardColumnDef, + getBoardColumns, + getReportColumnId, +} from "@features/inbox/utils/inboxBoardGrouping"; +import { + ArrowsClockwiseIcon, + CircleNotchIcon, + FileTextIcon, + WarningIcon, +} from "@phosphor-icons/react"; +import { Box, Button, Flex, Text, Tooltip } from "@radix-ui/themes"; +import type { SignalReport, SignalReportStatus } from "@shared/types"; +import { motion } from "framer-motion"; +import { + type KeyboardEvent, + type MouseEvent, + useEffect, + useMemo, + useRef, +} from "react"; + +function isInteractiveTarget(target: EventTarget | null): boolean { + return ( + target instanceof HTMLElement && + !!target.closest("a, button, input, select, textarea, [role='checkbox']") + ); +} + +function SourceProductIcon({ sourceProducts }: { sourceProducts?: string[] }) { + const firstProduct = sourceProducts?.[0]; + const meta = firstProduct ? SOURCE_PRODUCT_META[firstProduct] : undefined; + + if (!meta) { + return ( + + + + ); + } + + return ( + + + + + + ); +} + +interface InboxBoardCardProps { + report: SignalReport; + isSelected: boolean; + index: number; + onClick: (event: { metaKey: boolean; shiftKey: boolean }) => void; +} + +function InboxBoardCard({ + report, + isSelected, + index, + onClick, +}: InboxBoardCardProps) { + const handleActivate = (e: MouseEvent | KeyboardEvent) => { + if (isInteractiveTarget(e.target)) return; + onClick({ metaKey: e.metaKey, shiftKey: e.shiftKey }); + }; + + return ( + { + e.preventDefault(); + }} + onClick={handleActivate} + onKeyDown={(e: KeyboardEvent) => { + if (isInteractiveTarget(e.target)) return; + if (e.key === "Enter") { + e.preventDefault(); + handleActivate(e); + } + }} + className={[ + "relative isolate cursor-pointer rounded-(--radius-2) border bg-(--color-panel-solid) p-2 text-left transition-colors", + "before:pointer-events-none before:absolute before:inset-0 before:rounded-(--radius-2) before:bg-gray-12 before:opacity-0 hover:before:opacity-[0.05]", + isSelected + ? "border-(--accent-8) ring-(--accent-8) ring-1" + : "border-(--gray-5)", + ].join(" ")} + > + + + + +
+ +
+
+
+ ); +} + +interface BoardLoadMoreTriggerProps { + hasNextPage: boolean; + isFetchingNextPage: boolean; + fetchNextPage: () => void; +} + +function BoardLoadMoreTrigger({ + hasNextPage, + isFetchingNextPage, + fetchNextPage, +}: BoardLoadMoreTriggerProps) { + const ref = useRef(null); + + useEffect(() => { + const el = ref.current; + if (!el || !hasNextPage) return; + + const observer = new IntersectionObserver( + ([entry]) => { + if (entry.isIntersecting && !isFetchingNextPage) { + fetchNextPage(); + } + }, + { threshold: 0 }, + ); + observer.observe(el); + return () => observer.disconnect(); + }, [hasNextPage, isFetchingNextPage, fetchNextPage]); + + if (!hasNextPage && !isFetchingNextPage) return null; + + return ( + + {isFetchingNextPage ? ( + + Loading more… + + ) : null} + + ); +} + +interface InboxBoardColumnProps { + column: BoardColumnDef; + reports: SignalReport[]; + selectedIdSet: Set; + onReportClick: ( + id: string, + event: { metaKey: boolean; shiftKey: boolean }, + ) => void; + loadMoreTrigger: React.ReactNode | null; +} + +function InboxBoardColumn({ + column, + reports, + selectedIdSet, + onReportClick, + loadMoreTrigger, +}: InboxBoardColumnProps) { + const { accent, label } = column; + + return ( + + + + + + {label} + + + + {reports.length} + + + +
+ + {reports.length === 0 ? ( + + + No items + + + ) : ( + reports.map((report, index) => ( + onReportClick(report.id, e)} + /> + )) + )} + {loadMoreTrigger} + +
+
+ ); +} + +interface InboxBoardViewProps { + reports: SignalReport[]; + allReports: SignalReport[]; + isLoading: boolean; + isFetching: boolean; + error: Error | null; + refetch: () => void; + hasNextPage: boolean; + isFetchingNextPage: boolean; + fetchNextPage: () => void; + hasSignalSources: boolean; + searchQuery: string; + hasActiveFilters: boolean; + selectedReportIds: string[]; + onReportClick: ( + id: string, + event: { metaKey: boolean; shiftKey: boolean }, + ) => void; +} + +export function InboxBoardView({ + reports, + allReports, + isLoading, + isFetching, + error, + refetch, + hasNextPage, + isFetchingNextPage, + fetchNextPage, + hasSignalSources, + searchQuery, + hasActiveFilters, + selectedReportIds, + onReportClick, +}: InboxBoardViewProps) { + const statusFilter = useInboxSignalsFilterStore((s) => s.statusFilter); + const groupBy = useInboxSignalsFilterStore((s) => s.boardGroupBy); + + const columns = useMemo( + () => getBoardColumns(groupBy, new Set(statusFilter)), + [groupBy, statusFilter], + ); + + const reportsByColumn = useMemo(() => { + const map = new Map(); + for (const column of columns) { + map.set(column.id, []); + } + for (const report of reports) { + const columnId = getReportColumnId(report, groupBy); + const bucket = map.get(columnId); + if (bucket) bucket.push(report); + } + return map; + }, [reports, columns, groupBy]); + + const selectedIdSet = useMemo( + () => new Set(selectedReportIds), + [selectedReportIds], + ); + + const longestColumnId = useMemo(() => { + let best: string | null = null; + let bestLen = -1; + for (const [id, list] of reportsByColumn) { + if (list.length > bestLen) { + best = id; + bestLen = list.length; + } + } + return best; + }, [reportsByColumn]); + + if (isLoading && allReports.length === 0 && hasSignalSources) { + return ( +
+ + {columns.map((column) => ( + + ))} + +
+ ); + } + + if (error) { + return ( + + + + + Could not load signals + + + + + ); + } + + if (reports.length === 0 && searchQuery.trim()) { + return ( + + + No matching reports + + + ); + } + + if (reports.length === 0 && hasActiveFilters) { + return ( + + + No reports match current filters + + + ); + } + + return ( +
+ + {columns.map((column) => ( + + ) : null + } + /> + ))} + +
+ ); +} diff --git a/apps/code/src/renderer/features/inbox/components/list/BoardGroupByMenu.tsx b/apps/code/src/renderer/features/inbox/components/list/BoardGroupByMenu.tsx new file mode 100644 index 000000000..2d4e76115 --- /dev/null +++ b/apps/code/src/renderer/features/inbox/components/list/BoardGroupByMenu.tsx @@ -0,0 +1,47 @@ +import { + type InboxBoardGroupBy, + useInboxSignalsFilterStore, +} from "@features/inbox/stores/inboxSignalsFilterStore"; +import { boardGroupByLabel } from "@features/inbox/utils/inboxBoardGrouping"; +import { CaretDownIcon, RowsIcon } from "@phosphor-icons/react"; +import { DropdownMenu, Text } from "@radix-ui/themes"; + +const OPTIONS: InboxBoardGroupBy[] = ["actionability", "priority", "status"]; + +export function BoardGroupByMenu() { + const groupBy = useInboxSignalsFilterStore((s) => s.boardGroupBy); + const setGroupBy = useInboxSignalsFilterStore((s) => s.setBoardGroupBy); + const viewMode = useInboxSignalsFilterStore((s) => s.viewMode); + + if (viewMode !== "board") return null; + + return ( + + + + + + Group by + {OPTIONS.map((option) => ( + setGroupBy(option)} + > + {boardGroupByLabel(option)} + + ))} + + + ); +} diff --git a/apps/code/src/renderer/features/inbox/components/list/SignalsToolbar.tsx b/apps/code/src/renderer/features/inbox/components/list/SignalsToolbar.tsx index 1304ea04d..ed8962a4f 100644 --- a/apps/code/src/renderer/features/inbox/components/list/SignalsToolbar.tsx +++ b/apps/code/src/renderer/features/inbox/components/list/SignalsToolbar.tsx @@ -33,8 +33,10 @@ import type { SignalReport } from "@shared/types"; import type { InboxReportActionProperties } from "@shared/types/analytics"; import type { ReactNode } from "react"; import { useState } from "react"; +import { BoardGroupByMenu } from "./BoardGroupByMenu"; import { FilterSortMenu } from "./FilterSortMenu"; import { SuggestedReviewerFilterMenu } from "./SuggestedReviewerFilterMenu"; +import { ViewModeToggle } from "./ViewModeToggle"; interface SignalsToolbarProps { totalCount: number; @@ -532,8 +534,10 @@ export function SignalsToolbar({ {!hideFilters && ( + + )} diff --git a/apps/code/src/renderer/features/inbox/components/list/ViewModeToggle.tsx b/apps/code/src/renderer/features/inbox/components/list/ViewModeToggle.tsx new file mode 100644 index 000000000..3f9fd2de3 --- /dev/null +++ b/apps/code/src/renderer/features/inbox/components/list/ViewModeToggle.tsx @@ -0,0 +1,65 @@ +import { + type InboxViewMode, + useInboxSignalsFilterStore, +} from "@features/inbox/stores/inboxSignalsFilterStore"; +import { KanbanIcon, ListBulletsIcon } from "@phosphor-icons/react"; +import { Tooltip } from "@radix-ui/themes"; +import type { ReactNode } from "react"; + +interface OptionButtonProps { + mode: InboxViewMode; + active: boolean; + label: string; + icon: ReactNode; + onSelect: (mode: InboxViewMode) => void; +} + +function OptionButton({ + mode, + active, + label, + icon, + onSelect, +}: OptionButtonProps) { + return ( + + + + ); +} + +export function ViewModeToggle() { + const viewMode = useInboxSignalsFilterStore((s) => s.viewMode); + const setViewMode = useInboxSignalsFilterStore((s) => s.setViewMode); + + return ( +
+ } + onSelect={setViewMode} + /> + } + onSelect={setViewMode} + /> +
+ ); +} diff --git a/apps/code/src/renderer/features/inbox/stores/inboxSignalsBoardDetailStore.ts b/apps/code/src/renderer/features/inbox/stores/inboxSignalsBoardDetailStore.ts new file mode 100644 index 000000000..bd01434cd --- /dev/null +++ b/apps/code/src/renderer/features/inbox/stores/inboxSignalsBoardDetailStore.ts @@ -0,0 +1,7 @@ +import { createSidebarStore } from "@stores/createSidebarStore"; + +export const useInboxSignalsBoardDetailStore = createSidebarStore({ + name: "inbox-signals-board-detail-storage", + defaultWidth: 560, + defaultOpen: true, +}); diff --git a/apps/code/src/renderer/features/inbox/stores/inboxSignalsFilterStore.ts b/apps/code/src/renderer/features/inbox/stores/inboxSignalsFilterStore.ts index 51338816d..ddad11d4f 100644 --- a/apps/code/src/renderer/features/inbox/stores/inboxSignalsFilterStore.ts +++ b/apps/code/src/renderer/features/inbox/stores/inboxSignalsFilterStore.ts @@ -12,6 +12,10 @@ type SignalSortField = Extract< type SignalSortDirection = "asc" | "desc"; +export type InboxViewMode = "list" | "board"; + +export type InboxBoardGroupBy = "status" | "actionability" | "priority"; + export type SourceProduct = | "session_replay" | "error_tracking" @@ -42,6 +46,8 @@ interface InboxSignalsFilterState { suggestedReviewerFilter: string[]; /** Tracks whether we've seeded the reviewer filter with the current user once. Persisted so the seed only runs on first inbox visit. */ hasInitializedSuggestedReviewerFilter: boolean; + viewMode: InboxViewMode; + boardGroupBy: InboxBoardGroupBy; } interface InboxSignalsFilterActions { @@ -60,6 +66,8 @@ interface InboxSignalsFilterActions { seedSuggestedReviewerFilterWithCurrentUser: (currentUserUuid: string) => void; /** Reset all filters when a deep link arrives so the linked report isn't hidden. */ resetFilters: () => void; + setViewMode: (mode: InboxViewMode) => void; + setBoardGroupBy: (groupBy: InboxBoardGroupBy) => void; } type InboxSignalsFilterStore = InboxSignalsFilterState & @@ -75,6 +83,8 @@ export const useInboxSignalsFilterStore = create()( sourceProductFilter: [], suggestedReviewerFilter: [], hasInitializedSuggestedReviewerFilter: false, + viewMode: "list", + boardGroupBy: "actionability", setSort: (sortField, sortDirection) => set({ sortField, sortDirection }), setSearchQuery: (searchQuery) => set({ searchQuery }), setStatusFilter: (statusFilter) => set({ statusFilter }), @@ -124,6 +134,8 @@ export const useInboxSignalsFilterStore = create()( sourceProductFilter: [], suggestedReviewerFilter: [], }), + setViewMode: (viewMode) => set({ viewMode }), + setBoardGroupBy: (boardGroupBy) => set({ boardGroupBy }), }), { name: "inbox-signals-filter-storage", @@ -135,6 +147,8 @@ export const useInboxSignalsFilterStore = create()( suggestedReviewerFilter: state.suggestedReviewerFilter, hasInitializedSuggestedReviewerFilter: state.hasInitializedSuggestedReviewerFilter, + viewMode: state.viewMode, + boardGroupBy: state.boardGroupBy, }), }, ), diff --git a/apps/code/src/renderer/features/inbox/utils/inboxBoardGrouping.ts b/apps/code/src/renderer/features/inbox/utils/inboxBoardGrouping.ts new file mode 100644 index 000000000..6c18ab441 --- /dev/null +++ b/apps/code/src/renderer/features/inbox/utils/inboxBoardGrouping.ts @@ -0,0 +1,129 @@ +import type { InboxBoardGroupBy } from "@features/inbox/stores/inboxSignalsFilterStore"; +import { inboxStatusLabel } from "@features/inbox/utils/inboxSort"; +import type { + SignalReport, + SignalReportActionability, + SignalReportPriority, + SignalReportStatus, +} from "@shared/types"; + +export interface BoardColumnDef { + id: string; + label: string; + accent: string; +} + +const STATUS_COLUMNS: BoardColumnDef[] = ( + [ + "ready", + "pending_input", + "in_progress", + "failed", + "candidate", + "potential", + ] as SignalReportStatus[] +).map((status) => ({ + id: status, + label: inboxStatusLabel(status), + accent: statusAccent(status), +})); + +function statusAccent(status: SignalReportStatus): string { + switch (status) { + case "ready": + return "var(--green-9)"; + case "pending_input": + return "var(--violet-9)"; + case "in_progress": + return "var(--amber-9)"; + case "candidate": + return "var(--cyan-9)"; + case "potential": + return "var(--gray-9)"; + case "failed": + return "var(--red-9)"; + default: + return "var(--gray-8)"; + } +} + +const ACTIONABILITY_COLUMNS: BoardColumnDef[] = [ + { + id: "immediately_actionable", + label: "Actionable", + accent: "var(--green-9)", + }, + { + id: "requires_human_input", + label: "Needs input", + accent: "var(--amber-9)", + }, + { + id: "not_actionable", + label: "Not actionable", + accent: "var(--gray-9)", + }, + { + id: "pending", + label: "In pipeline", + accent: "var(--violet-9)", + }, +]; + +const PRIORITY_COLUMNS: BoardColumnDef[] = [ + { id: "P0", label: "P0", accent: "var(--red-9)" }, + { id: "P1", label: "P1", accent: "var(--orange-9)" }, + { id: "P2", label: "P2", accent: "var(--amber-9)" }, + { id: "P3", label: "P3", accent: "var(--cyan-9)" }, + { id: "P4", label: "P4", accent: "var(--gray-9)" }, + { id: "unprioritized", label: "Unprioritized", accent: "var(--gray-8)" }, +]; + +export function getBoardColumns( + groupBy: InboxBoardGroupBy, + visibleStatuses?: Set, +): BoardColumnDef[] { + if (groupBy === "status") { + return STATUS_COLUMNS.filter( + (c) => + !visibleStatuses || visibleStatuses.has(c.id as SignalReportStatus), + ); + } + if (groupBy === "actionability") return ACTIONABILITY_COLUMNS; + return PRIORITY_COLUMNS; +} + +export function getReportColumnId( + report: SignalReport, + groupBy: InboxBoardGroupBy, +): string { + if (groupBy === "status") { + return report.status; + } + if (groupBy === "actionability") { + if (report.status !== "ready") return "pending"; + const a: SignalReportActionability | null | undefined = + report.actionability; + if (a === "immediately_actionable") return "immediately_actionable"; + if (a === "requires_human_input") return "requires_human_input"; + if (a === "not_actionable") return "not_actionable"; + return "pending"; + } + // priority + const p: SignalReportPriority | null | undefined = report.priority; + if (p === "P0" || p === "P1" || p === "P2" || p === "P3" || p === "P4") { + return p; + } + return "unprioritized"; +} + +export function boardGroupByLabel(groupBy: InboxBoardGroupBy): string { + switch (groupBy) { + case "status": + return "Status"; + case "actionability": + return "Actionability"; + case "priority": + return "Priority"; + } +}