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 @@ -89,6 +89,7 @@ const BoardCard = <T extends BoardItemProps = BoardItemProps>({
transition,
transform: CSS.Transform.toString(transform),
};

return (
<>
<div style={style} {...listeners} {...attributes} ref={setNodeRef}>
Expand Down
3 changes: 2 additions & 1 deletion frontend/libs/erxes-ui/src/modules/board/types/boardTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@ import type {
DragOverEvent,
DragStartEvent,
} from '@dnd-kit/core';

import { HTMLAttributes } from 'react';

export type BoardItemProps = {
id: string;
column: string;
column: string | undefined;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify handling of undefined column values.

The type change correctly acknowledges that column can be undefined at runtime. However, this reveals a potential issue in Board.tsx at line 181 where overItem.column as string is used without checking for undefined. The type assertion could mask a bug where undefined is passed to setDragOverBoardColumnId.

Run the following script to verify all usages handle undefined properly:


🏁 Script executed:

#!/bin/bash
# Description: Find all usages of BoardItemProps column property to verify undefined handling

# Search for direct property access of .column
rg -nP --type=ts --type=tsx -C3 '\.column\b' -g '!**/node_modules/**' -g '!**/dist/**'

# Search for BoardItemProps usage patterns
ast-grep --pattern 'const $_ = $_.$column'

Length of output: 27


🏁 Script executed:

#!/bin/bash
# Locate Board.tsx
file=$(fd -e tsx Board.tsx || find . -type f -name 'Board.tsx')
echo "Board.tsx path: $file"

# Show lines 170-190 around line 181 to inspect undefined handling
sed -n '170,190p' "$file"

# Search for all .column accesses in TS/TSX files to verify checks
rg -nP '\.column\b' -C3 -g '*.ts' -g '*.tsx' -g '!**/node_modules/**' -g '!**/dist/**'

Length of output: 13158


Use nullish coalescing instead of a type assertion for overItem.column.
Board.tsx:181: replace

setDragOverBoardColumnId(overItem.column as string);

with

setDragOverBoardColumnId(overItem.column ?? null);
🤖 Prompt for AI Agents
In frontend/libs/erxes-ui/src/modules/board/types/boardTypes.ts around line 12,
the column property is typed as string | undefined which leads to a type
assertion when assigning overItem.column to setDragOverBoardColumnId; change the
column type to allow null (e.g., string | null or string | null | undefined) and
then in Board.tsx replace the type assertion with a nullish coalescing
assignment so you pass overItem.column ?? null to setDragOverBoardColumnId.

sort?: unknown;
} & Record<string, unknown>;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Resizable, Sheet, cn, useQueryState } from 'erxes-ui';
import { Resizable, Sheet, cn } from 'erxes-ui';
import {
SalesDetailLeftSidebar,
SalesDetailTabContent,
Expand All @@ -9,15 +9,20 @@ import { IDeal } from '@/deals/types/deals';
import Overview from './overview/Overview';
import { SalesDetailActions } from './SalesDetailActions';
import { SalesItemDetailHeader } from './SalesItemDetailHeader';
import { dealDetailSheetState } from '@/deals/states/dealDetailSheetState';
import { useAtom } from 'jotai';
import { useDealDetail } from '@/deals/cards/hooks/useDeals';

export const SalesItemDetail = () => {
const [open, setOpen] = useQueryState<string>('salesItemId');
const [activeDealId, setActiveDealId] = useAtom(dealDetailSheetState);

const { deal, loading } = useDealDetail();

return (
<Sheet open={!!open && !loading} onOpenChange={() => setOpen(null)}>
<Sheet
open={!!activeDealId && !loading}
onOpenChange={() => setActiveDealId(null)}
>
<DealsProvider>
<Sheet.View
className={cn(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Select, Spinner, cn } from 'erxes-ui';

import { useDealsContext } from '@/deals/context/DealContext';
import { useDealsEdit } from '@/deals/cards/hooks/useDeals';

const PRIORITY_COLORS: Record<string, string> = {
critical: 'bg-red-500',
Expand All @@ -16,7 +16,7 @@ const Priority = ({
priority: string;
dealId: string;
}) => {
const { editDeals, loading } = useDealsContext();
const { editDeals, loading } = useDealsEdit();

const onChangePriority = (value: string) => {
editDeals({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export const ItemFooter = ({ createdAt, assignedUsers = [] }: Props) => {
</div>
<div className="flex">
<MembersInline.Provider
memberIds={assignedUsers.map((user) => user._id)}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: Redundant fallback for assignedUsers.

If assignedUsers defaults to an empty array, you can remove the fallback to [].

memberIds={(assignedUsers || []).map((user) => user._id)}
>
<MembersInline.Avatar />
</MembersInline.Provider>
Expand Down
118 changes: 50 additions & 68 deletions frontend/plugins/sales_ui/src/modules/deals/cards/hooks/useDeals.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
} from '@/deals/graphql/mutations/DealsMutations';
import {
EnumCursorDirection,
ICursorListResponse,
mergeCursorData,
toast,
useQueryState,
Expand All @@ -16,46 +17,57 @@ import {
GET_DEALS,
GET_DEAL_DETAIL,
} from '@/deals/graphql/queries/DealsQueries';
import { IDeal, IDealList } from '@/deals/types/deals';
import {
MutationHookOptions,
QueryHookOptions,
useMutation,
useQuery,
} from '@apollo/client';
import { useAtom, useAtomValue } from 'jotai';

import { DEAL_LIST_CHANGED } from '~/modules/deals/graphql/subscriptions/dealListChange';
import { IDeal } from '@/deals/types/deals';
import { currentUserState } from 'ui-modules';
import { useAtomValue } from 'jotai';
import { dealCreateDefaultValuesState } from '@/deals/states/dealCreateSheetState';
import { dealDetailSheetState } from '@/deals/states/dealDetailSheetState';
import { useEffect } from 'react';

interface IDealChanged {
salesDealListChanged: {
action: string;
deal: IDeal;
};
}

export const useDeals = (
options?: QueryHookOptions<{ deals: IDealList }>,
options?: QueryHookOptions<ICursorListResponse<IDeal>>,
pipelineId?: string,
) => {
const { data, loading, error, fetchMore, refetch, subscribeToMore } =
useQuery<{
deals: IDealList;
}>(GET_DEALS, {
...options,
variables: {
...options?.variables,
},
});
const { data, loading, fetchMore, subscribeToMore } = useQuery<
ICursorListResponse<IDeal>
>(GET_DEALS, {
...options,
variables: { ...options?.variables },
skip: options?.skip,
fetchPolicy: 'cache-and-network',
onError: (e) => {
toast({
title: 'Error',
description: e.message,
variant: 'destructive',
});
},
});

const currentUser = useAtomValue(currentUserState);
const [qryStrPipelineId] = useQueryState('pipelineId');

const lastPipelineId = pipelineId || qryStrPipelineId || '';

const currentUser = useAtomValue(currentUserState);
const { deals } = data || {};

const { list = [], pageInfo, totalCount = 0 } = deals || {};

const { hasPreviousPage, hasNextPage } = pageInfo || {};
const { list: deals, pageInfo, totalCount } = data?.deals || {};

useEffect(() => {
const unsubscribe = subscribeToMore<any>({
const unsubscribe = subscribeToMore<IDealChanged>({
document: DEAL_LIST_CHANGED,
variables: {
pipelineId: lastPipelineId,
Expand All @@ -80,9 +92,9 @@ export const useDeals = (
}

if (action === 'edit') {
updatedList = currentList.map((item: IDeal) => {
return item._id === deal._id ? { ...item, ...deal } : item;
});
updatedList = currentList.map((item: IDeal) =>
item._id === deal._id ? { ...item, ...deal } : item,
);
}

if (action === 'remove') {
Expand Down Expand Up @@ -145,81 +157,51 @@ export const useDeals = (
};

return {
deals: data?.deals,
loading,
error,
deals,
handleFetchMore,
pageInfo,
hasPreviousPage,
hasNextPage,
refetch,
totalCount,
list,
};
};

export const useDealDetail = (
options?: QueryHookOptions<{ dealDetail: IDeal }>,
) => {
const [_id] = useQueryState('salesItemId');
const [activeDealId] = useAtom(dealDetailSheetState);

const { data, loading, error } = useQuery<{ dealDetail: IDeal }>(
GET_DEAL_DETAIL,
{
...options,
variables: {
...options?.variables,
_id,
_id: activeDealId,
},
skip: !_id,
skip: !activeDealId,
},
);

return { deal: data?.dealDetail, loading, error };
};

export function useDealsEdit(options?: MutationHookOptions<any, any>) {
const [_id] = useQueryState('salesItemId');
const [_id] = useAtom(dealDetailSheetState);

const [editDeals, { loading, error }] = useMutation(EDIT_DEALS, {
...options,
variables: {
...options?.variables,
_id,
},
optimisticResponse: ({ _id, name }) => ({
dealsEdit: { __typename: 'Deal', _id, name },
}),
update: (cache, { data }) => {
const updatedDeal = data?.dealsEdit;
if (!updatedDeal) return;

const existing = cache.readQuery<{ deals: IDealList }>({
query: GET_DEALS,
});
if (!existing?.deals) return;

cache.writeQuery({
query: GET_DEALS,
data: {
deals: {
...existing.deals,
list: existing.deals.list.map((d) =>
d._id === updatedDeal._id ? { ...d, ...updatedDeal } : d,
),
refetchQueries: _id
? [
{
query: GET_DEAL_DETAIL,
variables: { ...options?.variables, _id },
},
},
});
},
refetchQueries: [
{
query: GET_DEAL_DETAIL,
variables: {
...options?.variables,
_id,
},
},
],
]
: [],
awaitRefetchQueries: true,
onCompleted: (...args) => {
toast({
Expand All @@ -245,15 +227,15 @@ export function useDealsEdit(options?: MutationHookOptions<any, any>) {
}

export function useDealsAdd(options?: MutationHookOptions<any, any>) {
const [_id] = useQueryState('salesItemId');
const [stageId] = useQueryState('stageId');
const [_id] = useAtom(dealDetailSheetState);
const [defaultValues] = useAtom(dealCreateDefaultValuesState);

const [addDeals, { loading, error }] = useMutation(ADD_DEALS, {
...options,
variables: {
...options?.variables,
_id,
stageId,
stageId: defaultValues?.stageId,
},
refetchQueries: [
Comment on lines +230 to 240
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (bug_risk): Default values for stageId may be undefined.

Validate that 'defaultValues' and 'defaultValues.stageId' are defined before assigning to mutation variables, or set a fallback value to prevent sending undefined.

Suggested change
const [_id] = useAtom(dealDetailSheetState);
const [defaultValues] = useAtom(dealCreateDefaultValuesState);
const [addDeals, { loading, error }] = useMutation(ADD_DEALS, {
...options,
variables: {
...options?.variables,
_id,
stageId,
stageId: defaultValues?.stageId,
},
refetchQueries: [
const [_id] = useAtom(dealDetailSheetState);
const [defaultValues] = useAtom(dealCreateDefaultValuesState);
const stageId = defaultValues && typeof defaultValues.stageId !== 'undefined'
? defaultValues.stageId
: null; // fallback value, adjust as needed
const [addDeals, { loading, error }] = useMutation(ADD_DEALS, {
...options,
variables: {
...options?.variables,
_id,
stageId,
},
refetchQueries: [

{
Expand Down Expand Up @@ -288,7 +270,7 @@ export function useDealsAdd(options?: MutationHookOptions<any, any>) {
}

export function useDealsRemove(options?: MutationHookOptions<any, any>) {
const [_id] = useQueryState('salesItemId');
const [_id] = useAtom(dealDetailSheetState);

const [removeDeals, { loading, error }] = useMutation(REMOVE_DEALS, {
...options,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { AddCardForm } from '@/deals/cards/components/AddCardForm';
import { Sheet } from 'erxes-ui';
import { dealCreateSheetState } from '@/deals/states/dealCreateSheetState';
import { useAtom } from 'jotai';

export const AddDealSheet = () => {
const [open, setOpen] = useAtom(dealCreateSheetState);

const onOpen = () => {
setOpen(true);
};

const onClose = () => {
setOpen(false);
};

return (
<Sheet open={open} onOpenChange={(open) => (open ? onOpen() : onClose())}>
<Sheet.View
className="sm:max-w-3xl w-full p-0"
onEscapeKeyDown={(e) => {
e.preventDefault();
}}
>
<AddCardForm onCloseSheet={onClose} />
</Sheet.View>
</Sheet>
);
};
Loading