Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,13 @@ import { createContext, useContextSelector } from "use-context-selector";
* Internal dependencies.
*/
import { RISK_STATUSES, type RiskStatus } from "./constants";
import type { RiskFilters, RiskItem, RiskVisibleColumns } from "./types";
import type {
RiskFilters,
RiskItem,
RiskSort,
RiskVisibleColumns,
UserDetails,
} from "./types";

export interface RisksContextProps {
state: {
Expand All @@ -16,14 +22,17 @@ export interface RisksContextProps {
error: unknown;
filters: RiskFilters;
visibleColumns: RiskVisibleColumns;
sort: RiskSort | null;
isCreateRiskOpen: boolean;
editRiskName: string | null;
createRiskInitialStatus: RiskStatus | "";
deleteRiskName: string | null;
allOwnersWithDetails: Record<string, UserDetails | undefined>;
};
Comment thread
PraveenKum11 marked this conversation as resolved.
actions: {
setFilters: (filters: Partial<RiskFilters>) => void;
setVisibleColumns: (cols: Partial<RiskVisibleColumns>) => void;
setSort: (sort: RiskSort | null) => void;
updateRiskStatus: (name: string, status: RiskStatus) => Promise<void>;
openCreateRisk: () => void;
closeCreateRisk: () => void;
Expand Down Expand Up @@ -54,14 +63,17 @@ export const RisksContext = createContext<RisksContextProps>({
advanced: [],
},
visibleColumns: defaultVisibleColumns,
sort: null,
isCreateRiskOpen: false,
editRiskName: null,
createRiskInitialStatus: "",
deleteRiskName: null,
allOwnersWithDetails: {},
},
actions: {
setFilters: noop,
setVisibleColumns: noop,
setSort: noop,
updateRiskStatus: async () => {},
openCreateRisk: noop,
closeCreateRisk: noop,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,24 @@ import { Plus } from "lucide-react";
* Internal dependencies.
*/
import { useRisks } from "./context";
import { RisksToolbar } from "./toolbar/toolbar";

export function RisksHeader() {
const openCreateRisk = useRisks((c) => c.actions.openCreateRisk);

return (
<div className="flex items-center justify-between mb-3.5">
<h1 className="text-xl font-semibold text-ink-gray-8">Risks</h1>
<Button
variant="solid"
label="Create"
iconLeft={() => <Plus />}
onClick={openCreateRisk}
/>
</div>
<>
<div className="flex items-center justify-between mb-3.5">
<h1 className="text-xl font-semibold text-ink-gray-8">Risks</h1>
<Button
variant="solid"
label="Create"
iconLeft={() => <Plus />}
onClick={openCreateRisk}
/>
</div>

<RisksToolbar />
</>
Comment thread
PraveenKum11 marked this conversation as resolved.
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
* External dependencies.
*/
import { useSearchParams } from "react-router-dom";
import { mergeClassNames as cn } from "@next-pms/design-system";
import { Spinner } from "@next-pms/design-system/components";

/**
* Internal dependencies.
Expand Down Expand Up @@ -31,6 +33,7 @@ function RisksContent() {
const deleteRiskName = useRisks((c) => c.state.deleteRiskName);
const closeCreateRisk = useRisks((c) => c.actions.closeCreateRisk);
const closeDeleteRisk = useRisks((c) => c.actions.closeDeleteRisk);
const isLoading = useRisks((c) => c.state.isLoading);

return (
<>
Expand All @@ -46,9 +49,21 @@ function RisksContent() {
{riskId ? (
<RiskDetailView riskId={riskId} />
) : (
<div className="flex flex-col h-full">
<div className="relative flex flex-col h-full">
<RisksHeader />
{activeView === "kanban" ? <RisksKanbanView /> : <RisksListView />}
<div
className={cn("flex flex-col flex-1 min-h-0", {
"opacity-50 transition-opacity duration-150": isLoading,
})}
>
{activeView === "kanban" ? <RisksKanbanView /> : <RisksListView />}
</div>
{isLoading && (
<Spinner
isFull
className="absolute top-0 left-0 w-full h-full cursor-wait"
/>
)}
</div>
)}
</>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,33 +34,27 @@ export function RisksKanbanView() {
(c) => c.actions.openCreateRiskWithStatus,
);
const toast = useToasts();
const [localData, setLocalData] = useState<RiskItem[]>(data);

useEffect(() => {
if (!data || data.length === 0) return;
setLocalData(data);
}, [data]);

const [items, setItems] = useState<RiskIdsByStatus>(emptyGroups);

const byId = useMemo(() => {
const map = new Map<string, RiskItem>();
for (const risk of data) {
for (const risk of localData) {
map.set(risk.name, risk);
}
return map;
}, [data]);
}, [localData]);

useEffect(() => {
setItems((current) => {
const fromServer = groupIdsByStatus(data);
const merged = {} as RiskIdsByStatus;
for (const status of RISK_STATUSES) {
const serverSet = new Set(fromServer[status]);
// Preserve existing drag-and-drop order; drop items that moved away
const kept = current[status].filter((id) => serverSet.has(id));
const keptSet = new Set(kept);
// Append any items newly added on the server side
const added = fromServer[status].filter((id) => !keptSet.has(id));
merged[status as RiskStatus] = [...kept, ...added];
}
return merged;
});
}, [data]);
if (!localData) return;
setItems(groupIdsByStatus(localData));
}, [localData]);

return (
<DragDropProvider<RiskDragData, RiskDraggable, RiskDroppable>
Expand All @@ -84,7 +78,7 @@ export function RisksKanbanView() {
try {
await updateRiskStatus(riskId, newStatus);
} catch {
setItems(groupIdsByStatus(data));
setItems(groupIdsByStatus(localData));
const risk = byId.get(riskId);
toast.error(
`Error updating status for ${risk?.risk_category ?? riskId}`,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/**
* External dependencies.
*/
import { useEffect, useState } from "react";
import { Accordion } from "@base-ui/react/accordion";

/**
Expand All @@ -9,12 +10,21 @@ import { Accordion } from "@base-ui/react/accordion";
import { RISK_LIST_COLUMNS } from "../constants";
import { useRisks } from "../context";
import { RiskGroup } from "./listViewGroup";
import { RiskItem } from "../types";

export function RisksListView() {
const data = useRisks((c) => c.state.data);
const [localData, setLocalData] = useState<RiskItem[]>(data);

const openRisks = data.filter((r) => !r.status || r.status !== "Mitigated");
const mitigatedRisks = data.filter((r) => r.status === "Mitigated");
useEffect(() => {
if (!data || data.length === 0) return;
setLocalData(data);
}, [data]);

const openRisks = localData.filter(
(r) => !r.status || r.status !== "Mitigated",
);
const mitigatedRisks = localData.filter((r) => r.status === "Mitigated");

return (
<div className="flex flex-col">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { useFrappeUpdateDoc } from "frappe-react-sdk";
*/
import { RISK_DETAIL_PARAM, RISK_STATUSES, type RiskStatus } from "./constants";
import { RisksContext, type RisksContextProps } from "./context";
import type { RiskFilters, RiskVisibleColumns } from "./types";
import type { RiskFilters, RiskSort, RiskVisibleColumns } from "./types";
import { useRisksData } from "./useRisksData";

const defaultFilters: RiskFilters = {
Expand All @@ -35,9 +35,16 @@ export function RisksProvider({ children }: PropsWithChildren) {
RiskStatus | ""
>("");
const [deleteRiskName, setDeleteRiskName] = useState<string | null>(null);
const [sort, setSortState] = useState<RiskSort | null>(null);
const [, setSearchParams] = useSearchParams();

const { data, isLoading, error, mutate: refreshRiskList } = useRisksData();
const {
data,
isLoading,
error,
mutate: refreshRiskList,
allOwnersWithDetails,
} = useRisksData(filters, sort);

const { updateDoc } = useFrappeUpdateDoc();

Expand All @@ -52,6 +59,11 @@ export function RisksProvider({ children }: PropsWithChildren) {
[],
);

const setSort = useCallback((s: RiskSort | null) => {
setSortState(s);
void refreshRiskList();
}, []);

const updateRiskStatus = useCallback(
async (name: string, status: RiskStatus) => {
await updateDoc("Risk", name, { status });
Expand Down Expand Up @@ -114,14 +126,17 @@ export function RisksProvider({ children }: PropsWithChildren) {
error,
filters,
visibleColumns,
sort,
isCreateRiskOpen,
editRiskName,
createRiskInitialStatus,
deleteRiskName,
allOwnersWithDetails,
},
actions: {
setFilters,
setVisibleColumns,
setSort,
updateRiskStatus,
openCreateRisk,
closeCreateRisk,
Expand All @@ -139,12 +154,15 @@ export function RisksProvider({ children }: PropsWithChildren) {
error,
filters,
visibleColumns,
sort,
isCreateRiskOpen,
editRiskName,
createRiskInitialStatus,
deleteRiskName,
allOwnersWithDetails,
setFilters,
setVisibleColumns,
setSort,
updateRiskStatus,
openCreateRisk,
closeCreateRisk,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/**
* External dependencies.
*/
import { useMemo } from "react";
import { MultiSelect } from "@rtcamp/frappe-ui-react";
import type { MultiSelectOption } from "@rtcamp/frappe-ui-react";

/**
* Internal dependencies.
*/
import { RISK_STATUSES } from "../constants";
import type { RiskStatus } from "../constants";
import { RiskStatusBadge } from "../riskStatusBadge";
import type { RiskVisibleColumns } from "../types";

interface ColumnsDropdownProps {
visibleColumns: RiskVisibleColumns;
setVisibleColumns: (partial: Partial<RiskVisibleColumns>) => void;
}

const COLUMN_OPTIONS: MultiSelectOption[] = RISK_STATUSES.map((status) => ({
value: status,
label: status,
}));

export function ColumnsDropdown({
visibleColumns,
setVisibleColumns,
}: ColumnsDropdownProps) {
const selectedValues = useMemo(
() =>
RISK_STATUSES.filter((status) => visibleColumns[status]).map(
(status) => status,
),
[visibleColumns],
);

const handleChange = (newValues: string[]) => {
const partial = Object.fromEntries(
RISK_STATUSES.map((status) => [status, newValues.includes(status)]),
) as unknown as RiskVisibleColumns;
setVisibleColumns(partial);
};

return (
<div>
<MultiSelect
options={COLUMN_OPTIONS}
value={selectedValues}
triggerLabel="Columns"
hideSearch
onChange={handleChange}
renderOption={(option) => (
<RiskStatusBadge status={option.value as RiskStatus} />
)}
popupClassName="w-full"
/>
</div>
);
}
Loading
Loading