- {
sort={{ field: "name", order: "ASC" }}
actions={
-
+
-
+
+
+ ++ ++ +++ +++
+ )}
+ />
+
{findDealLabel(dealStages, stage)}
diff --git a/src/components/atomic-crm/deals/DealList.tsx b/src/components/atomic-crm/deals/DealList.tsx
index 51f6c257..46aeb1b0 100644
--- a/src/components/atomic-crm/deals/DealList.tsx
+++ b/src/components/atomic-crm/deals/DealList.tsx
@@ -1,5 +1,5 @@
import { useGetIdentity, useListContext } from "ra-core";
-import { matchPath, useLocation } from "react-router";
+import { Link, matchPath, useLocation } from "react-router";
import { AutocompleteInput } from "@/components/admin/autocomplete-input";
import { CreateButton } from "@/components/admin/create-button";
import { ExportButton } from "@/components/admin/export-button";
@@ -18,25 +18,32 @@ import { DealEmpty } from "./DealEmpty";
import { DealListContent } from "./DealListContent";
import { DealShow } from "./DealShow";
import { OnlyMineInput } from "./OnlyMineInput";
+import { useIsMobile } from "@/hooks/use-mobile";
+import { Button } from "@/components/ui/button";
+import { Plus } from "lucide-react";
const DealList = () => {
const { identity } = useGetIdentity();
const { dealCategories } = useConfigurationContext();
+ const isMobile = useIsMobile();
if (!identity) return null;
- const dealFilters = [
- ,
-
-
- ,
- ({ id: type, name: type }))}
- />,
- ,
- ];
+ const dealFilters = [ ];
+
+ if (!isMobile) {
+ dealFilters.push(
+
+
+ ,
+ ({ id: type, name: type }))}
+ />,
+ ,
+ );
+ }
return (
{
title={false}
sort={{ field: "index", order: "DESC" }}
filters={dealFilters}
- actions={ }
+ actions={isMobile ? false : }
pagination={null}
>
+
);
};
From 124c2d22ab91ee8340a4a3039735d661cf559187 Mon Sep 17 00:00:00 2001
From: Gildas Garcia <1122076+djhi@users.noreply.github.com>
Date: Wed, 26 Nov 2025 09:16:15 +0100
Subject: [PATCH 07/34] Formatting
---
src/components/atomic-crm/companies/CompanyList.tsx | 6 +-----
src/components/atomic-crm/layout/Layout.tsx | 5 ++++-
2 files changed, 5 insertions(+), 6 deletions(-)
diff --git a/src/components/atomic-crm/companies/CompanyList.tsx b/src/components/atomic-crm/companies/CompanyList.tsx
index 04492043..6d45c47c 100644
--- a/src/components/atomic-crm/companies/CompanyList.tsx
+++ b/src/components/atomic-crm/companies/CompanyList.tsx
@@ -72,11 +72,7 @@ const CompanyListLayout = () => {
{isMobile ? null : }
- {isMobile ? (
-
- ) : (
-
- )}
+ {isMobile ? : }
);
diff --git a/src/components/atomic-crm/layout/Layout.tsx b/src/components/atomic-crm/layout/Layout.tsx
index afe962a5..5393de7e 100644
--- a/src/components/atomic-crm/layout/Layout.tsx
+++ b/src/components/atomic-crm/layout/Layout.tsx
@@ -9,7 +9,10 @@ import Header from "./Header";
export const Layout = ({ children }: { children: ReactNode }) => (
<>
-
+
}>
{children}
From 871a9064aac7861caadfcce025e4f4aeaa4f1288 Mon Sep 17 00:00:00 2001
From: Gildas Garcia <1122076+djhi@users.noreply.github.com>
Date: Wed, 26 Nov 2025 09:49:07 +0100
Subject: [PATCH 08/34] Improve contact list
---
.../atomic-crm/contacts/ContactListContent.tsx | 13 ++++++-------
1 file changed, 6 insertions(+), 7 deletions(-)
diff --git a/src/components/atomic-crm/contacts/ContactListContent.tsx b/src/components/atomic-crm/contacts/ContactListContent.tsx
index 917799c3..59987803 100644
--- a/src/components/atomic-crm/contacts/ContactListContent.tsx
+++ b/src/components/atomic-crm/contacts/ContactListContent.tsx
@@ -58,13 +58,13 @@ export const ContactListContent = () => {
/>
)}
-
+
{`${contact.first_name} ${contact.last_name ?? ""}`}
-
+
{contact.title}
{contact.title && contact.company_id != null && " at "}
@@ -79,17 +79,16 @@ export const ContactListContent = () => {
)}
{contact.nb_tasks ? (
-
+
{contact.nb_tasks} task{contact.nb_tasks > 1 ? "s" : ""}
) : null}
-
-
+ {isSmall ? null : }
- {contact.last_seen && (
-
+ {contact.last_seen && !isSmall && (
+
Date: Wed, 26 Nov 2025 10:21:09 +0100
Subject: [PATCH 09/34] Improve contact show view for mobile
---
.../atomic-crm/companies/CompanyList.tsx | 1 +
.../atomic-crm/contacts/ContactAside.tsx | 50 +++--
.../atomic-crm/contacts/ContactShow.tsx | 172 ++++++++++++++----
src/components/atomic-crm/tasks/AddTask.tsx | 10 +-
4 files changed, 172 insertions(+), 61 deletions(-)
diff --git a/src/components/atomic-crm/companies/CompanyList.tsx b/src/components/atomic-crm/companies/CompanyList.tsx
index 6d45c47c..358c70b2 100644
--- a/src/components/atomic-crm/companies/CompanyList.tsx
+++ b/src/components/atomic-crm/companies/CompanyList.tsx
@@ -105,6 +105,7 @@ const CompanyMobileList = () => (
);
+
/**
* Necessary until we have a render prop on ReferenceManyCountBase
*/
diff --git a/src/components/atomic-crm/contacts/ContactAside.tsx b/src/components/atomic-crm/contacts/ContactAside.tsx
index de816eaa..9aeede9a 100644
--- a/src/components/atomic-crm/contacts/ContactAside.tsx
+++ b/src/components/atomic-crm/contacts/ContactAside.tsx
@@ -22,7 +22,6 @@ import { ContactMergeButton } from "./ContactMergeButton";
import { ExportVCardButton } from "./ExportVCardButton";
export const ContactAside = ({ link = "edit" }: { link?: "edit" | "show" }) => {
- const { contactGender } = useConfigurationContext();
const record = useRecordContext();
if (!record) return null;
@@ -36,6 +35,35 @@ export const ContactAside = ({ link = "edit" }: { link?: "edit" | "show" }) => {
)}
+
+
+
+
+
+
+
+
+
+ {link !== "edit" && (
+
+
+
+
+ )}
+
+ );
+};
+
+export const ContactDetails = () => {
+ const record = useRecordContext();
+ const { contactGender } = useConfigurationContext();
+ if (!record) return null;
+ return (
+ <>
@@ -129,25 +157,7 @@ export const ContactAside = ({ link = "edit" }: { link?: "edit" | "show" }) => {
-
-
-
-
-
-
-
-
- {link !== "edit" && (
-
-
-
-
- )}
-
+ >
);
};
diff --git a/src/components/atomic-crm/contacts/ContactShow.tsx b/src/components/atomic-crm/contacts/ContactShow.tsx
index 67b3ec1a..69783cbe 100644
--- a/src/components/atomic-crm/contacts/ContactShow.tsx
+++ b/src/components/atomic-crm/contacts/ContactShow.tsx
@@ -8,7 +8,16 @@ import { CompanyAvatar } from "../companies/CompanyAvatar";
import { NoteCreate, NotesIterator } from "../notes";
import type { Contact } from "../types";
import { Avatar } from "./Avatar";
-import { ContactAside } from "./ContactAside";
+import { ContactAside, ContactDetails } from "./ContactAside";
+import { useIsMobile } from "@/hooks/use-mobile";
+import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
+import { AddTask } from "../tasks/AddTask";
+import { TasksIterator } from "../tasks/TasksIterator";
+import { useRecordContext } from "ra-core";
+import { useReferenceManyFieldController } from "ra-core";
+import { Button } from "@/components/ui/button";
+import { Link } from "react-router";
+import { Edit } from "lucide-react";
export const ContactShow = () => (
@@ -18,59 +27,144 @@ export const ContactShow = () => (
const ContactShowContent = () => {
const { record, isPending } = useShowContext();
+ const isMobile = useIsMobile();
if (isPending || !record) return null;
return (
-
+
-
- {record.first_name} {record.last_name}
-
-
- {record.title}
- {record.title && record.company_id != null && " at "}
- {record.company_id != null && (
-
-
-
-
- )}
+
+
+ {record.first_name} {record.last_name}
+
+
+
+
+ {record.title}
+
+ {record.title && record.company_id != null && " at "}
+ {record.company_id != null && (
+
+
+
+
+ )}
+
-
-
-
-
-
+ {isMobile ? null : (
+
+
+
+
+
+ )}
-
- }
- >
-
-
+ {isMobile ? (
+
+
+ Notes
+
+
+
+ Contact details
+
+
+
+ }
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ) : (
+
+ }
+ >
+
+
+ )}
-
+ {isMobile ? null : }
);
};
+
+/**
+ * Necessary until we have a render prop on ReferenceManyCountBase
+ */
+const TasksCount = () => {
+ const record = useRecordContext();
+ const { isLoading, error, total } = useReferenceManyFieldController({
+ page: 1,
+ perPage: 1,
+ record,
+ reference: "tasks",
+ target: "contact_id",
+ });
+
+ const body = isLoading
+ ? ""
+ : error
+ ? "error"
+ : `${total} ${total === 1 ? "task" : "tasks"}`;
+ return {body};
+};
diff --git a/src/components/atomic-crm/tasks/AddTask.tsx b/src/components/atomic-crm/tasks/AddTask.tsx
index d5f4e04e..9f0748ec 100644
--- a/src/components/atomic-crm/tasks/AddTask.tsx
+++ b/src/components/atomic-crm/tasks/AddTask.tsx
@@ -34,6 +34,8 @@ import {
import { contactOptionText } from "../misc/ContactOption";
import { useConfigurationContext } from "../root/ConfigurationContext";
+import { useIsMobile } from "@/hooks/use-mobile";
+import { cn } from "@/lib/utils";
export const AddTask = ({
selectContact,
@@ -49,6 +51,7 @@ export const AddTask = ({
const { taskTypes } = useConfigurationContext();
const contact = useRecordContext();
const [open, setOpen] = useState(false);
+ const isMobile = useIsMobile();
const handleOpen = () => {
setOpen(true);
};
@@ -93,9 +96,12 @@ export const AddTask = ({
+
}
text={contactNote.text}
/>
diff --git a/src/components/atomic-crm/activity/ActivityLogDealCreated.tsx b/src/components/atomic-crm/activity/ActivityLogDealCreated.tsx
index a8d60232..2e92108d 100644
--- a/src/components/atomic-crm/activity/ActivityLogDealCreated.tsx
+++ b/src/components/atomic-crm/activity/ActivityLogDealCreated.tsx
@@ -1,9 +1,11 @@
import { Link } from "react-router";
import type { RaRecord } from "ra-core";
-import { RelativeDate } from "../misc/RelativeDate";
import type { ActivityDealCreated } from "../types";
import { useActivityLogContext } from "./ActivityLogContext";
+import { ActivityLogHeader } from "./ActivityLogHeader";
+import { ReferenceField } from "@/components/admin";
+import { SaleName } from "../sales/SaleName";
type ActivityLogDealCreatedProps = {
activity: RaRecord & ActivityDealCreated;
@@ -15,27 +17,21 @@ export function ActivityLogDealCreated({
const context = useActivityLogContext();
const { deal } = activity;
return (
-
-
-
-
-
- Sales ID: {activity.sales_id}
- {" "}
- added deal {deal.name}{" "}
- {context !== "company" && (
- <>
- to company {activity.company_id}{" "}
-
- >
- )}
-
- {context === "company" && (
-
-
-
- )}
-
-
+ }
+ activity={activity}
+ >
+
+
+
+
+
+ added deal {deal.name}
+ {context !== "company" && (
+ <>
+ to company {activity.company_id}
+ >
+ )}
+
);
}
diff --git a/src/components/atomic-crm/activity/ActivityLogDealNoteCreated.tsx b/src/components/atomic-crm/activity/ActivityLogDealNoteCreated.tsx
index 3810498b..e1c611d3 100644
--- a/src/components/atomic-crm/activity/ActivityLogDealNoteCreated.tsx
+++ b/src/components/atomic-crm/activity/ActivityLogDealNoteCreated.tsx
@@ -2,11 +2,11 @@ import type { RaRecord } from "ra-core";
import { ReferenceField } from "@/components/admin/reference-field";
import { CompanyAvatar } from "../companies/CompanyAvatar";
-import { RelativeDate } from "../misc/RelativeDate";
import { SaleName } from "../sales/SaleName";
import type { ActivityDealNoteCreated } from "../types";
import { useActivityLogContext } from "./ActivityLogContext";
import { ActivityLogNote } from "./ActivityLogNote";
+import { ActivityLogHeader } from "./ActivityLogHeader";
type ActivityLogDealNoteCreatedProps = {
activity: RaRecord & ActivityDealNoteCreated;
@@ -20,63 +20,59 @@ export function ActivityLogDealNoteCreated({
return (
-
+
-
+
+
+
+ }
+ activity={activity}
+ >
+
+
-
-
-
-
-
- added a note about deal
-
- {context !== "company" && (
- <>
- {" at "}
+ added a note about deal
+
+ {context !== "company" && (
+ <>
+ {" at "}
+
-
- {" "}
- >
- )}
-
-
- {context === "company" && (
-
-
-
+ source="company_id"
+ reference="companies"
+ link="show"
+ />
+ {" "}
+ >
)}
-
+
}
text={dealNote.text}
/>
diff --git a/src/components/atomic-crm/activity/ActivityLogHeader.tsx b/src/components/atomic-crm/activity/ActivityLogHeader.tsx
new file mode 100644
index 00000000..9e82fbe3
--- /dev/null
+++ b/src/components/atomic-crm/activity/ActivityLogHeader.tsx
@@ -0,0 +1,39 @@
+import { useIsMobile } from "@/hooks/use-mobile";
+import { RelativeDate } from "../misc/RelativeDate";
+import type { Activity } from "../types";
+import { useActivityLogContext } from "./ActivityLogContext";
+
+export const ActivityLogHeader = ({
+ activity,
+ avatar,
+ children,
+}: {
+ activity: Activity;
+ avatar: React.ReactNode;
+ children: React.ReactNode;
+}) => {
+ const context = useActivityLogContext();
+ const isMobile = useIsMobile();
+
+ return (
+
+
+ {avatar}
+
+
+ {children}
+ {isMobile ? (
+ <>
+ at
+ >
+ ) : null}
+
+ {context === "company" && !isMobile && (
+
+
+
+ )}
+
+
+ );
+};
diff --git a/src/components/atomic-crm/companies/CompanyAside.tsx b/src/components/atomic-crm/companies/CompanyAside.tsx
index d8265379..0eb9bf77 100644
--- a/src/components/atomic-crm/companies/CompanyAside.tsx
+++ b/src/components/atomic-crm/companies/CompanyAside.tsx
@@ -42,7 +42,7 @@ export const CompanyAside = ({ link = "edit" }: CompanyAsideProps) => {
);
};
-const CompanyInfo = ({ record }: { record: Company }) => {
+export const CompanyInfo = ({ record }: { record: Company }) => {
if (!record.website && !record.linkedin_url && !record.phone_number) {
return null;
}
@@ -86,7 +86,7 @@ const CompanyInfo = ({ record }: { record: Company }) => {
);
};
-const ContextInfo = ({ record }: { record: Company }) => {
+export const ContextInfo = ({ record }: { record: Company }) => {
if (!record.revenue && !record.id) {
return null;
}
@@ -117,7 +117,7 @@ const ContextInfo = ({ record }: { record: Company }) => {
);
};
-const AddressInfo = ({ record }: { record: Company }) => {
+export const AddressInfo = ({ record }: { record: Company }) => {
if (!record.address && !record.city && !record.zipcode && !record.stateAbbr) {
return null;
}
@@ -133,7 +133,7 @@ const AddressInfo = ({ record }: { record: Company }) => {
);
};
-const AdditionalInfo = ({ record }: { record: Company }) => {
+export const AdditionalInfo = ({ record }: { record: Company }) => {
if (
!record.created_at &&
!record.sales_id &&
diff --git a/src/components/atomic-crm/companies/CompanyList.tsx b/src/components/atomic-crm/companies/CompanyList.tsx
index 358c70b2..4b46bd80 100644
--- a/src/components/atomic-crm/companies/CompanyList.tsx
+++ b/src/components/atomic-crm/companies/CompanyList.tsx
@@ -86,7 +86,7 @@ const CompanyMobileList = () => (
diff --git a/src/components/atomic-crm/companies/CompanyShow.tsx b/src/components/atomic-crm/companies/CompanyShow.tsx
index 5c064050..bd572552 100644
--- a/src/components/atomic-crm/companies/CompanyShow.tsx
+++ b/src/components/atomic-crm/companies/CompanyShow.tsx
@@ -26,8 +26,16 @@ import { findDealLabel } from "../deals/deal";
import { Status } from "../misc/Status";
import { useConfigurationContext } from "../root/ConfigurationContext";
import type { Company, Contact, Deal } from "../types";
-import { CompanyAside } from "./CompanyAside";
+import {
+ AdditionalInfo,
+ AddressInfo,
+ CompanyAside,
+ CompanyInfo,
+ ContextInfo,
+} from "./CompanyAside";
import { CompanyAvatar } from "./CompanyAvatar";
+import { useIsMobile } from "@/hooks/use-mobile";
+import { cn } from "@/lib/utils";
export const CompanyShow = () => (
@@ -38,6 +46,7 @@ export const CompanyShow = () => (
const CompanyShowContent = () => {
const { record, isPending } = useShowContext();
const navigate = useNavigate();
+ const isMobile = useIsMobile();
// Get tab from URL or default to "activity"
const tabMatch = useMatch("/companies/:id/show/:tab");
@@ -57,14 +66,19 @@ const CompanyShowContent = () => {
return (
-
-
+
+
{record.name}
-
+
Activity
{record.nb_contacts
@@ -80,6 +94,9 @@ const CompanyShowContent = () => {
: `${record.nb_deals} deals`}
) : null}
+ {isMobile ? (
+ About
+ ) : null}
@@ -122,17 +139,24 @@ const CompanyShowContent = () => {
) : null}
+
+
+
+
+
+
-
+ {isMobile ? null : }
);
};
const ContactsIterator = () => {
const location = useLocation();
+ const isMobile = useIsMobile();
const { data: contacts, error, isPending } = useListContext();
if (isPending || error) return null;
@@ -162,11 +186,15 @@ const ContactsIterator = () => {
contact.nb_tasks > 1 ? "s" : ""
}`
: ""}
-
-
+ {isMobile ? null : (
+ <>
+
+
+ >
+ )}
- {contact.last_seen && (
+ {!isMobile && contact.last_seen && (
last activity {formatDistance(contact.last_seen, now)} ago{" "}
@@ -211,9 +239,9 @@ const DealsIterator = () => {
-
+
{deal.name}
{findDealLabel(dealStages, deal.stage)},{" "}
@@ -227,7 +255,7 @@ const DealsIterator = () => {
{deal.category ? `, ${deal.category}` : ""}
-
+
last activity {formatDistance(deal.updated_at, now)} ago{" "}
diff --git a/src/components/atomic-crm/contacts/ContactShow.tsx b/src/components/atomic-crm/contacts/ContactShow.tsx
index 69783cbe..9569ac80 100644
--- a/src/components/atomic-crm/contacts/ContactShow.tsx
+++ b/src/components/atomic-crm/contacts/ContactShow.tsx
@@ -33,8 +33,8 @@ const ContactShowContent = () => {
return (
-
-
+
+
@@ -87,7 +87,7 @@ const ContactShowContent = () => {
{isMobile ? (
-
+
Notes
diff --git a/src/components/atomic-crm/dashboard/Dashboard.tsx b/src/components/atomic-crm/dashboard/Dashboard.tsx
index bd47b5a0..cc6b175c 100644
--- a/src/components/atomic-crm/dashboard/Dashboard.tsx
+++ b/src/components/atomic-crm/dashboard/Dashboard.tsx
@@ -59,7 +59,7 @@ export const Dashboard = () => {
{totalDeal ? : null}
-
+
Tasks
Activity
From 1aba9d6615638c10bcbef93f49638e5a527daa30 Mon Sep 17 00:00:00 2001
From: Gildas Garcia <1122076+djhi@users.noreply.github.com>
Date: Wed, 26 Nov 2025 11:26:20 +0100
Subject: [PATCH 11/34] Update registry
---
registry.json | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/registry.json b/registry.json
index a4bd0cb3..f356b4d1 100644
--- a/registry.json
+++ b/registry.json
@@ -634,6 +634,10 @@
"path": "src/components/atomic-crm/activity/ActivityLogIterator.tsx",
"type": "registry:component"
},
+ {
+ "path": "src/components/atomic-crm/activity/ActivityLogHeader.tsx",
+ "type": "registry:component"
+ },
{
"path": "src/components/atomic-crm/activity/ActivityLogDealNoteCreated.tsx",
"type": "registry:component"
From 304111889c867dbaf6c3520eb3d39d56c9adc622 Mon Sep 17 00:00:00 2001
From: Gildas Garcia <1122076+djhi@users.noreply.github.com>
Date: Wed, 26 Nov 2025 14:51:32 +0100
Subject: [PATCH 12/34] Improve deal views for mobile
---
.../atomic-crm/companies/CompanyList.tsx | 37 +--
.../atomic-crm/contacts/ContactShow.tsx | 46 ++-
.../atomic-crm/deals/DealCreate.tsx | 48 +--
src/components/atomic-crm/deals/DealEdit.tsx | 58 +++-
src/components/atomic-crm/deals/DealList.tsx | 24 +-
src/components/atomic-crm/deals/DealShow.tsx | 276 ++++++++++++------
src/components/atomic-crm/deals/index.ts | 15 +
.../atomic-crm/misc/ReferenceManyCount.tsx | 45 +++
src/components/atomic-crm/root/CRM.tsx | 10 +-
9 files changed, 369 insertions(+), 190 deletions(-)
create mode 100644 src/components/atomic-crm/misc/ReferenceManyCount.tsx
diff --git a/src/components/atomic-crm/companies/CompanyList.tsx b/src/components/atomic-crm/companies/CompanyList.tsx
index 4b46bd80..ff5ae232 100644
--- a/src/components/atomic-crm/companies/CompanyList.tsx
+++ b/src/components/atomic-crm/companies/CompanyList.tsx
@@ -1,4 +1,4 @@
-import { useGetIdentity, useListContext } from "ra-core";
+import { RecordsIterator, useGetIdentity, useListContext } from "ra-core";
import { CreateButton } from "@/components/admin/create-button";
import { ExportButton } from "@/components/admin/export-button";
import { List } from "@/components/admin/list";
@@ -15,10 +15,8 @@ import { Button } from "@/components/ui/button";
import { Link } from "react-router";
import { Plus } from "lucide-react";
import { CompanyAvatar } from "./CompanyAvatar";
-import { RecordsIterator } from "ra-core";
import { Card } from "@/components/ui/card";
-import { useRecordContext } from "ra-core";
-import { useReferenceManyFieldController } from "ra-core";
+import { ReferenceManyCount } from "../misc/ReferenceManyCount";
export const CompanyList = () => {
const { identity } = useGetIdentity();
@@ -94,7 +92,15 @@ const CompanyMobileList = () => (
-
+ (
+ <>
+ {total} {total === 1 ? "deal" : "deals"}
+ >
+ )}
+ />
@@ -106,27 +112,6 @@ const CompanyMobileList = () => (
);
-/**
- * Necessary until we have a render prop on ReferenceManyCountBase
- */
-const DealsCount = () => {
- const record = useRecordContext();
- const { isLoading, error, total } = useReferenceManyFieldController({
- page: 1,
- perPage: 1,
- record,
- reference: "deals",
- target: "company_id",
- });
-
- const body = isLoading
- ? ""
- : error
- ? "error"
- : `${total} ${total === 1 ? "deal" : "deals"}`;
- return {body};
-};
-
const CompanyListActions = () => {
const isMobile = useIsMobile();
diff --git a/src/components/atomic-crm/contacts/ContactShow.tsx b/src/components/atomic-crm/contacts/ContactShow.tsx
index 9569ac80..fafbd0ab 100644
--- a/src/components/atomic-crm/contacts/ContactShow.tsx
+++ b/src/components/atomic-crm/contacts/ContactShow.tsx
@@ -13,11 +13,10 @@ import { useIsMobile } from "@/hooks/use-mobile";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { AddTask } from "../tasks/AddTask";
import { TasksIterator } from "../tasks/TasksIterator";
-import { useRecordContext } from "ra-core";
-import { useReferenceManyFieldController } from "ra-core";
import { Button } from "@/components/ui/button";
import { Link } from "react-router";
import { Edit } from "lucide-react";
+import { ReferenceManyCount } from "../misc/ReferenceManyCount";
export const ContactShow = () => (
@@ -88,9 +87,27 @@ const ContactShowContent = () => {
{isMobile ? (
- Notes
+
+ (
+ <>
+ {total?.toString()} {total === 1 ? "note" : "notes"}
+ >
+ )}
+ />
+
-
+ (
+ <>
+ {total?.toString()} {total === 1 ? "task" : "tasks"}
+ >
+ )}
+ />
Contact details
@@ -147,24 +164,3 @@ const ContactShowContent = () => {
);
};
-
-/**
- * Necessary until we have a render prop on ReferenceManyCountBase
- */
-const TasksCount = () => {
- const record = useRecordContext();
- const { isLoading, error, total } = useReferenceManyFieldController({
- page: 1,
- perPage: 1,
- record,
- reference: "tasks",
- target: "contact_id",
- });
-
- const body = isLoading
- ? ""
- : error
- ? "error"
- : `${total} ${total === 1 ? "task" : "tasks"}`;
- return {body};
-};
diff --git a/src/components/atomic-crm/deals/DealCreate.tsx b/src/components/atomic-crm/deals/DealCreate.tsx
index 323b6033..a4ca50ab 100644
--- a/src/components/atomic-crm/deals/DealCreate.tsx
+++ b/src/components/atomic-crm/deals/DealCreate.tsx
@@ -7,7 +7,7 @@ import {
useRedirect,
type GetListResult,
} from "ra-core";
-import { Create } from "@/components/admin/create";
+import { Create, CreateProps } from "@/components/admin/create";
import { SaveButton } from "@/components/admin/form";
import { FormToolbar } from "@/components/admin/simple-form";
import { Dialog, DialogContent } from "@/components/ui/dialog";
@@ -16,14 +16,9 @@ import type { Deal } from "../types";
import { DealInputs } from "./DealInputs";
export const DealCreate = ({ open }: { open: boolean }) => {
- const redirect = useRedirect();
const dataProvider = useDataProvider();
+ const redirect = useRedirect();
const { data: allDeals } = useListContext();
-
- const handleClose = () => {
- redirect("/deals");
- };
-
const queryClient = useQueryClient();
const onSuccess = async (deal: Deal) => {
@@ -70,26 +65,35 @@ export const DealCreate = ({ open }: { open: boolean }) => {
redirect("/deals");
};
- const { identity } = useGetIdentity();
+ const handleClose = () => {
+ redirect("/deals");
+ };
return (
);
};
+
+export const DealCreatePage = (props: Partial) => {
+ const { identity } = useGetIdentity();
+ return (
+
+
+
+ );
+};
diff --git a/src/components/atomic-crm/deals/DealEdit.tsx b/src/components/atomic-crm/deals/DealEdit.tsx
index f61beb63..812df221 100644
--- a/src/components/atomic-crm/deals/DealEdit.tsx
+++ b/src/components/atomic-crm/deals/DealEdit.tsx
@@ -55,27 +55,57 @@ export const DealEdit = ({ open, id }: { open: boolean; id?: string }) => {
);
};
-function EditHeader() {
+export const DealEditPage = () => {
+ const redirect = useRedirect();
+ const notify = useNotify();
+ return (
+ {
+ notify("Deal updated");
+ redirect(`show`, undefined, undefined, undefined, {
+ _scrollToTop: false,
+ });
+ },
+ }}
+ >
+
+
+
+ );
+};
+
+function EditTitle() {
const deal = useRecordContext();
if (!deal) {
return null;
}
return (
-
-
-
-
-
-
- Edit {deal.name} deal
-
-
-
-
+
+
+
+
+
+ Edit {deal.name} deal
+
+
+
+
+ );
+}
+
+function EditHeader() {
+ return (
+
+
);
}
diff --git a/src/components/atomic-crm/deals/DealList.tsx b/src/components/atomic-crm/deals/DealList.tsx
index 46aeb1b0..f1ca7adb 100644
--- a/src/components/atomic-crm/deals/DealList.tsx
+++ b/src/components/atomic-crm/deals/DealList.tsx
@@ -73,9 +73,12 @@ const DealList = () => {
const DealLayout = () => {
const location = useLocation();
- const matchCreate = matchPath("/deals/create", location.pathname);
- const matchShow = matchPath("/deals/:id/show", location.pathname);
- const matchEdit = matchPath("/deals/:id", location.pathname);
+ const isMobile = useIsMobile();
+ const matchCreate =
+ !isMobile && matchPath("/deals/create", location.pathname);
+ const matchShow =
+ !isMobile && matchPath("/deals/:id/show", location.pathname);
+ const matchEdit = !isMobile && matchPath("/deals/:id", location.pathname);
const { data, isPending, filterValues } = useListContext();
const hasFilters = filterValues && Object.keys(filterValues).length > 0;
@@ -85,7 +88,10 @@ const DealLayout = () => {
return (
<>
-
+
>
@@ -96,8 +102,14 @@ const DealLayout = () => {
-
-
+
+
);
};
diff --git a/src/components/atomic-crm/deals/DealShow.tsx b/src/components/atomic-crm/deals/DealShow.tsx
index 7c0f02fc..63652679 100644
--- a/src/components/atomic-crm/deals/DealShow.tsx
+++ b/src/components/atomic-crm/deals/DealShow.tsx
@@ -1,6 +1,6 @@
import { useMutation } from "@tanstack/react-query";
import { format, isValid } from "date-fns";
-import { Archive, ArchiveRestore } from "lucide-react";
+import { Archive, ArchiveRestore, Edit } from "lucide-react";
import {
ShowBase,
useDataProvider,
@@ -27,6 +27,10 @@ import { useConfigurationContext } from "../root/ConfigurationContext";
import type { Deal } from "../types";
import { ContactList } from "./ContactList";
import { findDealLabel } from "./deal";
+import { useIsMobile } from "@/hooks/use-mobile";
+import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
+import { ReferenceManyCount } from "../misc/ReferenceManyCount";
+import { Link } from "react-router";
export const DealShow = ({ open, id }: { open: boolean; id?: string }) => {
const redirect = useRedirect();
@@ -37,18 +41,20 @@ export const DealShow = ({ open, id }: { open: boolean; id?: string }) => {
return (
);
};
+export const DealShowPage = ({ id }: { id?: string }) => (
+
+
+
+);
+
const DealShowContent = () => {
- const { dealStages } = useConfigurationContext();
+ const isMobile = useIsMobile();
const record = useRecordContext();
if (!record) return null;
@@ -67,112 +73,190 @@ const DealShowContent = () => {
{record.name}
+
-
- {record.archived_at ? (
- <>
-
-
- >
- ) : (
- <>
-
-
- >
- )}
-
-
-
-
-
-
- Expected closing date
-
-
-
- {isValid(new Date(record.expected_closing_date))
- ? format(new Date(record.expected_closing_date), "PP")
- : "Invalid date"}
-
- {new Date(record.expected_closing_date) < new Date() ? (
- Past
- ) : null}
-
-
-
-
-
- Budget
-
-
- {record.amount.toLocaleString("en-US", {
- notation: "compact",
- style: "currency",
- currency: "USD",
- currencyDisplay: "narrowSymbol",
- minimumSignificantDigits: 3,
- })}
-
-
-
- {record.category && (
-
-
- Category
-
- {record.category}
+ {isMobile ? null : (
+
+ {record.archived_at ? (
+ <>
+
+
+ >
+ ) : (
+ <>
+
+
+ >
+ )}
)}
-
-
-
- Stage
-
-
- {findDealLabel(dealStages, record.stage)}
-
-
- {!!record.contact_ids?.length && (
-
-
-
- Contacts
-
+ {isMobile ? (
+
+
+
+ (
+ <>
+ {total?.toString()} {total === 1 ? "note" : "notes"}
+ >
+ )}
+ />
+
+
+ (
+ <>
+ {total?.toString()}{" "}
+ {total === 1 ? "contact" : "contacts"}
+ >
+ )}
+ />
+
+ About
+
+
+ }
+ >
+
+
+
+
+
+
+
+
+
+ ) : (
+ <>
+
+ {!!record.contact_ids?.length && (
+
+
+
+ Contacts
+
+
+
+
+
+
+ )}
+
+ {record.description && (
+
+
+ Description
+
+ {record.description}
+
+ )}
+
+
+
+ }
+ >
+
+
-
+ >
)}
+
+
+ >
+ );
+};
- {record.description && (
-
-
- Description
-
- {record.description}
-
- )}
+const DealDetails = () => {
+ const { dealStages } = useConfigurationContext();
+ const record = useRecordContext();
-
-
- }
- >
-
-
-
+ if (!record) return null;
+ return (
+
+
+
+ Expected closing date
+
+
+
+ {isValid(new Date(record.expected_closing_date))
+ ? format(new Date(record.expected_closing_date), "PP")
+ : "Invalid date"}
+
+ {new Date(record.expected_closing_date) < new Date() ? (
+ Past
+ ) : null}
- >
+
+
+
+ Budget
+
+
+ {record.amount.toLocaleString("en-US", {
+ notation: "compact",
+ style: "currency",
+ currency: "USD",
+ currencyDisplay: "narrowSymbol",
+ minimumSignificantDigits: 3,
+ })}
+
+
+
+ {record.category && (
+
+
+ Category
+
+ {record.category}
+
+ )}
+
+
+
+ Stage
+
+
+ {findDealLabel(dealStages, record.stage)}
+
+
+
);
};
diff --git a/src/components/atomic-crm/deals/index.ts b/src/components/atomic-crm/deals/index.ts
index 82086f28..35cf4b55 100644
--- a/src/components/atomic-crm/deals/index.ts
+++ b/src/components/atomic-crm/deals/index.ts
@@ -1,6 +1,21 @@
import * as React from "react";
+
const DealList = React.lazy(() => import("./DealList"));
+const DealEdit = React.lazy(() =>
+ import("./DealEdit").then(({ DealEditPage }) => ({ default: DealEditPage })),
+);
+const DealShow = React.lazy(() =>
+ import("./DealShow").then(({ DealShowPage }) => ({ default: DealShowPage })),
+);
+const DealCreate = React.lazy(() =>
+ import("./DealCreate").then(({ DealCreatePage }) => ({
+ default: DealCreatePage,
+ })),
+);
export default {
list: DealList,
+ edit: DealEdit,
+ show: DealShow,
+ create: DealCreate,
};
diff --git a/src/components/atomic-crm/misc/ReferenceManyCount.tsx b/src/components/atomic-crm/misc/ReferenceManyCount.tsx
new file mode 100644
index 00000000..07cfd5d2
--- /dev/null
+++ b/src/components/atomic-crm/misc/ReferenceManyCount.tsx
@@ -0,0 +1,45 @@
+import {
+ ReferenceManyCountBaseProps,
+ useTimeout,
+ useReferenceManyFieldController,
+} from "ra-core";
+
+export const ReferenceManyCount = (
+ props: ReferenceManyCountBaseProps & {
+ render: (total: number | undefined) => React.ReactNode;
+ },
+) => {
+ const { loading, error, offline, render, timeout = 1000, ...rest } = props;
+ const oneSecondHasPassed = useTimeout(timeout);
+
+ const {
+ isPaused,
+ isPending,
+ error: fetchError,
+ total,
+ } = useReferenceManyFieldController({
+ ...rest,
+ page: 1,
+ perPage: 1,
+ });
+ const shouldRenderLoading =
+ isPending && !isPaused && loading !== undefined && loading !== false;
+ const shouldRenderOffline =
+ isPending && isPaused && offline !== undefined && offline !== false;
+ const shouldRenderError =
+ !isPending && fetchError && error !== undefined && error !== false;
+
+ return (
+ <>
+ {shouldRenderLoading
+ ? oneSecondHasPassed
+ ? loading
+ : null
+ : shouldRenderOffline
+ ? offline
+ : shouldRenderError
+ ? error
+ : render(total)}
+ >
+ );
+};
diff --git a/src/components/atomic-crm/root/CRM.tsx b/src/components/atomic-crm/root/CRM.tsx
index 6a87ee16..ef70a318 100644
--- a/src/components/atomic-crm/root/CRM.tsx
+++ b/src/components/atomic-crm/root/CRM.tsx
@@ -39,6 +39,7 @@ import {
} from "./defaultConfiguration";
import { i18nProvider } from "./i18nProvider";
import { StartPage } from "../login/StartPage.tsx";
+import { useIsMobile } from "@/hooks/use-mobile.ts";
export type CRMProps = {
dataProvider?: DataProvider;
@@ -102,6 +103,7 @@ export const CRM = ({
disableTelemetry,
...rest
}: CRMProps) => {
+ const isMobile = useIsMobile();
useEffect(() => {
if (
disableTelemetry ||
@@ -153,7 +155,13 @@ export const CRM = ({
} />
-
+
From 993f358b68584e396d666adf75cd15a1cf67503c Mon Sep 17 00:00:00 2001
From: Gildas Garcia <1122076+djhi@users.noreply.github.com>
Date: Wed, 26 Nov 2025 14:57:51 +0100
Subject: [PATCH 13/34] Update registry
---
registry.json | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/registry.json b/registry.json
index f356b4d1..f56fec91 100644
--- a/registry.json
+++ b/registry.json
@@ -338,6 +338,10 @@
"path": "src/components/atomic-crm/misc/RelativeDate.tsx",
"type": "registry:component"
},
+ {
+ "path": "src/components/atomic-crm/misc/ReferenceManyCount.tsx",
+ "type": "registry:component"
+ },
{
"path": "src/components/atomic-crm/misc/ImageEditorField.tsx",
"type": "registry:component"
From f346bd866f0562780bb9a8ca14552275abef8cb1 Mon Sep 17 00:00:00 2001
From: Gildas Garcia <1122076+djhi@users.noreply.github.com>
Date: Wed, 26 Nov 2025 16:36:49 +0100
Subject: [PATCH 14/34] Introduce infinite loading for contact list
---
.../atomic-crm/contacts/ContactList.tsx | 53 ++---
.../atomic-crm/misc/InfinitePagination.tsx | 117 +++++++++++
src/components/atomic-crm/misc/SearchForm.tsx | 0
src/components/ui/item.tsx | 193 ++++++++++++++++++
src/components/ui/separator.tsx | 2 +-
5 files changed, 339 insertions(+), 26 deletions(-)
create mode 100644 src/components/atomic-crm/misc/InfinitePagination.tsx
create mode 100644 src/components/atomic-crm/misc/SearchForm.tsx
create mode 100644 src/components/ui/item.tsx
diff --git a/src/components/atomic-crm/contacts/ContactList.tsx b/src/components/atomic-crm/contacts/ContactList.tsx
index 4d648afb..106d33d7 100644
--- a/src/components/atomic-crm/contacts/ContactList.tsx
+++ b/src/components/atomic-crm/contacts/ContactList.tsx
@@ -8,7 +8,7 @@ import {
import { BulkActionsToolbar } from "@/components/admin/bulk-actions-toolbar";
import { CreateButton } from "@/components/admin/create-button";
import { ExportButton } from "@/components/admin/export-button";
-import { List } from "@/components/admin/list";
+import { List, ListView } from "@/components/admin/list";
import { SortButton } from "@/components/admin/sort-button";
import { Card } from "@/components/ui/card";
@@ -19,10 +19,11 @@ import { ContactListContent } from "./ContactListContent";
import { ContactListFilter } from "./ContactListFilter";
import { TopToolbar } from "../layout/TopToolbar";
import { useIsMobile } from "@/hooks/use-mobile";
-import { SearchInput } from "@/components/admin";
import { Button } from "@/components/ui/button";
import { Plus } from "lucide-react";
import { Link } from "react-router";
+import { InfiniteListBase } from "ra-core";
+import { InfinitePagination } from "../misc/InfinitePagination";
export const ContactList = () => {
const { identity } = useGetIdentity();
@@ -30,6 +31,31 @@ export const ContactList = () => {
if (!identity) return null;
+ if (isMobile) {
+ return (
+
+ } actions={false}>
+
+
+
+
+ );
+ }
+
return (
{
perPage={25}
sort={{ field: "last_seen", order: "DESC" }}
exporter={exporter}
- filters={
- isMobile
- ? [
- ,
- ]
- : undefined
- }
>
-
);
};
diff --git a/src/components/atomic-crm/misc/InfinitePagination.tsx b/src/components/atomic-crm/misc/InfinitePagination.tsx
new file mode 100644
index 00000000..77c3fe77
--- /dev/null
+++ b/src/components/atomic-crm/misc/InfinitePagination.tsx
@@ -0,0 +1,117 @@
+import * as React from "react";
+import { useEffect, useRef } from "react";
+import {
+ useInfinitePaginationContext,
+ useListContext,
+ useEvent,
+} from "ra-core";
+import { Item, ItemContent, ItemMedia, ItemTitle } from "@/components/ui/item";
+import { Spinner } from "@/components/admin/spinner";
+
+/**
+ * A pagination component that loads more results when the user scrolls to the bottom of the list.
+ *
+ * Used as the default pagination component in the component.
+ *
+ * @example
+ * import { InfiniteList, InfinitePagination, Datagrid, TextField } from 'react-admin';
+ *
+ * const PostList = () => (
+ * }>
+ *
+ *
+ *
+ *
+ *
+ * );
+ */
+export const InfinitePagination = ({
+ offline = null,
+ options = defaultOptions,
+}: InfinitePaginationProps) => {
+ const { isPaused, isPending } = useListContext();
+ const { fetchNextPage, hasNextPage, isFetchingNextPage } =
+ useInfinitePaginationContext();
+
+ if (!fetchNextPage) {
+ throw new Error(
+ "InfinitePagination must be used inside an InfinitePaginationContext, usually created by . You cannot use it as child of a component.",
+ );
+ }
+
+ const [hasRequestedNextPage, setHasRequestedNextPage] = React.useState(false);
+ const observerElem = useRef(null);
+ const handleObserver = useEvent<[IntersectionObserverEntry[]], void>(
+ (entries) => {
+ const [target] = entries;
+ if (target.isIntersecting && hasNextPage && !isFetchingNextPage) {
+ setHasRequestedNextPage(true);
+ fetchNextPage();
+ }
+ },
+ );
+
+ useEffect(() => {
+ // Whenever the query is unpaused, reset the requested next page state
+ if (!isPaused) {
+ setHasRequestedNextPage(false);
+ }
+ }, [isPaused]);
+
+ useEffect(() => {
+ const element = observerElem.current;
+ if (!element) return;
+ const observer = new IntersectionObserver(handleObserver, options);
+ observer.observe(element);
+ return () => observer.unobserve(element);
+ }, [
+ fetchNextPage,
+ hasNextPage,
+ handleObserver,
+ options,
+ isPending,
+ isFetchingNextPage,
+ ]);
+
+ if (isPending) return null;
+
+ const showOffline =
+ isPaused &&
+ hasNextPage &&
+ hasRequestedNextPage &&
+ offline !== false &&
+ offline !== undefined;
+
+ return (
+
+ {showOffline ? (
+ offline
+ ) : isFetchingNextPage && hasNextPage ? (
+ -
+
+
+
+
+ Loading...
+
+
+ ) : (
+ -
+
+
+
+
+
+
+
+ )}
+
+ );
+};
+
+const defaultOptions = { threshold: 0 };
+
+export interface InfinitePaginationProps {
+ offline?: React.ReactNode;
+ options?: IntersectionObserverInit;
+}
diff --git a/src/components/atomic-crm/misc/SearchForm.tsx b/src/components/atomic-crm/misc/SearchForm.tsx
new file mode 100644
index 00000000..e69de29b
diff --git a/src/components/ui/item.tsx b/src/components/ui/item.tsx
new file mode 100644
index 00000000..d97de21e
--- /dev/null
+++ b/src/components/ui/item.tsx
@@ -0,0 +1,193 @@
+import * as React from "react"
+import { Slot } from "@radix-ui/react-slot"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@/lib/utils"
+import { Separator } from "@/components/ui/separator"
+
+function ItemGroup({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function ItemSeparator({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+const itemVariants = cva(
+ "group/item flex items-center border border-transparent text-sm rounded-md transition-colors [a]:hover:bg-accent/50 [a]:transition-colors duration-100 flex-wrap outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
+ {
+ variants: {
+ variant: {
+ default: "bg-transparent",
+ outline: "border-border",
+ muted: "bg-muted/50",
+ },
+ size: {
+ default: "p-4 gap-4 ",
+ sm: "py-3 px-4 gap-2.5",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ size: "default",
+ },
+ }
+)
+
+function Item({
+ className,
+ variant = "default",
+ size = "default",
+ asChild = false,
+ ...props
+}: React.ComponentProps<"div"> &
+ VariantProps & { asChild?: boolean }) {
+ const Comp = asChild ? Slot : "div"
+ return (
+
+ )
+}
+
+const itemMediaVariants = cva(
+ "flex shrink-0 items-center justify-center gap-2 group-has-[[data-slot=item-description]]/item:self-start [&_svg]:pointer-events-none group-has-[[data-slot=item-description]]/item:translate-y-0.5",
+ {
+ variants: {
+ variant: {
+ default: "bg-transparent",
+ icon: "size-8 border rounded-sm bg-muted [&_svg:not([class*='size-'])]:size-4",
+ image:
+ "size-10 rounded-sm overflow-hidden [&_img]:size-full [&_img]:object-cover",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ },
+ }
+)
+
+function ItemMedia({
+ className,
+ variant = "default",
+ ...props
+}: React.ComponentProps<"div"> & VariantProps) {
+ return (
+
+ )
+}
+
+function ItemContent({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function ItemTitle({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function ItemDescription({ className, ...props }: React.ComponentProps<"p">) {
+ return (
+ a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4",
+ className
+ )}
+ {...props}
+ />
+ )
+}
+
+function ItemActions({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function ItemHeader({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function ItemFooter({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+export {
+ Item,
+ ItemMedia,
+ ItemContent,
+ ItemActions,
+ ItemGroup,
+ ItemSeparator,
+ ItemTitle,
+ ItemDescription,
+ ItemHeader,
+ ItemFooter,
+}
diff --git a/src/components/ui/separator.tsx b/src/components/ui/separator.tsx
index 3cf4f89b..bb3ad74c 100644
--- a/src/components/ui/separator.tsx
+++ b/src/components/ui/separator.tsx
@@ -11,7 +11,7 @@ function Separator({
}: React.ComponentProps) {
return (
Date: Mon, 1 Dec 2025 10:02:55 +0100
Subject: [PATCH 15/34] Fix active menu in mobile navigation
---
registry.json | 8 ++++++++
src/components/atomic-crm/layout/Header.tsx | 10 +++++++++-
2 files changed, 17 insertions(+), 1 deletion(-)
diff --git a/registry.json b/registry.json
index f56fec91..057e0848 100644
--- a/registry.json
+++ b/registry.json
@@ -334,6 +334,10 @@
"path": "src/components/atomic-crm/misc/Status.tsx",
"type": "registry:component"
},
+ {
+ "path": "src/components/atomic-crm/misc/SearchForm.tsx",
+ "type": "registry:component"
+ },
{
"path": "src/components/atomic-crm/misc/RelativeDate.tsx",
"type": "registry:component"
@@ -342,6 +346,10 @@
"path": "src/components/atomic-crm/misc/ReferenceManyCount.tsx",
"type": "registry:component"
},
+ {
+ "path": "src/components/atomic-crm/misc/InfinitePagination.tsx",
+ "type": "registry:component"
+ },
{
"path": "src/components/atomic-crm/misc/ImageEditorField.tsx",
"type": "registry:component"
diff --git a/src/components/atomic-crm/layout/Header.tsx b/src/components/atomic-crm/layout/Header.tsx
index ea741be8..94d10f75 100644
--- a/src/components/atomic-crm/layout/Header.tsx
+++ b/src/components/atomic-crm/layout/Header.tsx
@@ -59,6 +59,7 @@ const Header = () => {
const MobileHeader = ({ currentPath }: { currentPath: string | boolean }) => {
const { title } = useConfigurationContext();
const [open, setOpen] = useState(false);
+ console.log({ currentPath });
return (
@@ -77,18 +78,20 @@ const MobileHeader = ({ currentPath }: { currentPath: string | boolean }) => {
element?.focus()}
onClick={() => setOpen(false)}
+ ref={(element) => element?.focus()}
/>
{
{
) => {
+ console.log({ props });
return (
Date: Mon, 1 Dec 2025 10:25:38 +0100
Subject: [PATCH 16/34] Improve company list for mobile
---
.../atomic-crm/companies/CompanyList.tsx | 54 ++++++++++---------
1 file changed, 29 insertions(+), 25 deletions(-)
diff --git a/src/components/atomic-crm/companies/CompanyList.tsx b/src/components/atomic-crm/companies/CompanyList.tsx
index ff5ae232..a1f5725c 100644
--- a/src/components/atomic-crm/companies/CompanyList.tsx
+++ b/src/components/atomic-crm/companies/CompanyList.tsx
@@ -1,7 +1,12 @@
-import { RecordsIterator, useGetIdentity, useListContext } from "ra-core";
+import {
+ InfiniteListBase,
+ RecordsIterator,
+ useGetIdentity,
+ useListContext,
+} from "ra-core";
import { CreateButton } from "@/components/admin/create-button";
import { ExportButton } from "@/components/admin/export-button";
-import { List } from "@/components/admin/list";
+import { List, ListView } from "@/components/admin/list";
import { ListPagination } from "@/components/admin/list-pagination";
import { SortButton } from "@/components/admin/sort-button";
@@ -17,11 +22,33 @@ import { Plus } from "lucide-react";
import { CompanyAvatar } from "./CompanyAvatar";
import { Card } from "@/components/ui/card";
import { ReferenceManyCount } from "../misc/ReferenceManyCount";
+import { InfinitePagination } from "../misc/InfinitePagination";
export const CompanyList = () => {
const { identity } = useGetIdentity();
const isMobile = useIsMobile();
if (!identity) return null;
+
+ if (isMobile) {
+ return (
+
+ } actions={false}>
+
+
+
+
+ );
+ }
return (
{
sort={{ field: "name", order: "ASC" }}
actions={ }
pagination={ }
- filters={
- isMobile
- ? [
- ,
- ]
- : undefined
- }
>
-
);
};
From c6373f2282d312d82fe4be30cd5eb916230f7519 Mon Sep 17 00:00:00 2001
From: Gildas Garcia <1122076+djhi@users.noreply.github.com>
Date: Mon, 1 Dec 2025 10:27:57 +0100
Subject: [PATCH 17/34] Cleanup
---
src/components/atomic-crm/companies/CompanyList.tsx | 12 ++++++------
src/components/atomic-crm/contacts/ContactList.tsx | 10 +++++-----
2 files changed, 11 insertions(+), 11 deletions(-)
diff --git a/src/components/atomic-crm/companies/CompanyList.tsx b/src/components/atomic-crm/companies/CompanyList.tsx
index a1f5725c..c80555d6 100644
--- a/src/components/atomic-crm/companies/CompanyList.tsx
+++ b/src/components/atomic-crm/companies/CompanyList.tsx
@@ -9,18 +9,18 @@ import { ExportButton } from "@/components/admin/export-button";
import { List, ListView } from "@/components/admin/list";
import { ListPagination } from "@/components/admin/list-pagination";
import { SortButton } from "@/components/admin/sort-button";
+import { useIsMobile } from "@/hooks/use-mobile";
+import { TextField } from "@/components/admin";
+import { Button } from "@/components/ui/button";
+import { Card } from "@/components/ui/card";
+import { Link } from "react-router";
+import { Plus } from "lucide-react";
import { TopToolbar } from "../layout/TopToolbar";
import { CompanyEmpty } from "./CompanyEmpty";
import { CompanyListFilter } from "./CompanyListFilter";
import { ImageList } from "./GridList";
-import { useIsMobile } from "@/hooks/use-mobile";
-import { SearchInput, TextField } from "@/components/admin";
-import { Button } from "@/components/ui/button";
-import { Link } from "react-router";
-import { Plus } from "lucide-react";
import { CompanyAvatar } from "./CompanyAvatar";
-import { Card } from "@/components/ui/card";
import { ReferenceManyCount } from "../misc/ReferenceManyCount";
import { InfinitePagination } from "../misc/InfinitePagination";
diff --git a/src/components/atomic-crm/contacts/ContactList.tsx b/src/components/atomic-crm/contacts/ContactList.tsx
index 106d33d7..1ab36259 100644
--- a/src/components/atomic-crm/contacts/ContactList.tsx
+++ b/src/components/atomic-crm/contacts/ContactList.tsx
@@ -1,6 +1,7 @@
import jsonExport from "jsonexport/dist";
import {
downloadCSV,
+ InfiniteListBase,
useGetIdentity,
useListContext,
type Exporter,
@@ -11,6 +12,10 @@ import { ExportButton } from "@/components/admin/export-button";
import { List, ListView } from "@/components/admin/list";
import { SortButton } from "@/components/admin/sort-button";
import { Card } from "@/components/ui/card";
+import { useIsMobile } from "@/hooks/use-mobile";
+import { Button } from "@/components/ui/button";
+import { Plus } from "lucide-react";
+import { Link } from "react-router";
import type { Company, Contact, Sale, Tag } from "../types";
import { ContactEmpty } from "./ContactEmpty";
@@ -18,11 +23,6 @@ import { ContactImportButton } from "./ContactImportButton";
import { ContactListContent } from "./ContactListContent";
import { ContactListFilter } from "./ContactListFilter";
import { TopToolbar } from "../layout/TopToolbar";
-import { useIsMobile } from "@/hooks/use-mobile";
-import { Button } from "@/components/ui/button";
-import { Plus } from "lucide-react";
-import { Link } from "react-router";
-import { InfiniteListBase } from "ra-core";
import { InfinitePagination } from "../misc/InfinitePagination";
export const ContactList = () => {
From c51327ca5f7f8256250d2d590ea7cfb3e2ff9fa4 Mon Sep 17 00:00:00 2001
From: Gildas Garcia <1122076+djhi@users.noreply.github.com>
Date: Mon, 1 Dec 2025 12:10:00 +0100
Subject: [PATCH 18/34] Cleanup
---
src/components/atomic-crm/layout/Header.tsx | 2 --
src/components/atomic-crm/misc/InfinitePagination.tsx | 3 ---
2 files changed, 5 deletions(-)
diff --git a/src/components/atomic-crm/layout/Header.tsx b/src/components/atomic-crm/layout/Header.tsx
index 94d10f75..59c6f959 100644
--- a/src/components/atomic-crm/layout/Header.tsx
+++ b/src/components/atomic-crm/layout/Header.tsx
@@ -59,7 +59,6 @@ const Header = () => {
const MobileHeader = ({ currentPath }: { currentPath: string | boolean }) => {
const { title } = useConfigurationContext();
const [open, setOpen] = useState(false);
- console.log({ currentPath });
return (
@@ -212,7 +211,6 @@ const NavigationLink = ({
isActive: boolean;
} & LinkProps &
RefAttributes) => {
- console.log({ props });
return (
) : (
-
-
-
-
From 8a470efbe088951551d1e9aea1177f6d4f013d8e Mon Sep 17 00:00:00 2001
From: Gildas Garcia <1122076+djhi@users.noreply.github.com>
Date: Mon, 1 Dec 2025 12:10:37 +0100
Subject: [PATCH 19/34] Improve filtering on mobile (contacts and companies)
---
registry.json | 4 +
.../atomic-crm/companies/CompanyList.tsx | 1 +
.../companies/CompanyListFilter.tsx | 12 +--
.../atomic-crm/contacts/ContactList.tsx | 1 +
.../atomic-crm/contacts/ContactListFilter.tsx | 12 +--
.../atomic-crm/misc/ResponsiveFilters.tsx | 80 +++++++++++++++++++
6 files changed, 94 insertions(+), 16 deletions(-)
create mode 100644 src/components/atomic-crm/misc/ResponsiveFilters.tsx
diff --git a/registry.json b/registry.json
index 057e0848..95659461 100644
--- a/registry.json
+++ b/registry.json
@@ -338,6 +338,10 @@
"path": "src/components/atomic-crm/misc/SearchForm.tsx",
"type": "registry:component"
},
+ {
+ "path": "src/components/atomic-crm/misc/ResponsiveFilters.tsx",
+ "type": "registry:component"
+ },
{
"path": "src/components/atomic-crm/misc/RelativeDate.tsx",
"type": "registry:component"
diff --git a/src/components/atomic-crm/companies/CompanyList.tsx b/src/components/atomic-crm/companies/CompanyList.tsx
index c80555d6..9eb35590 100644
--- a/src/components/atomic-crm/companies/CompanyList.tsx
+++ b/src/components/atomic-crm/companies/CompanyList.tsx
@@ -33,6 +33,7 @@ export const CompanyList = () => {
return (
} actions={false}>
+
);
From 16f47dc0d820cea6b3f548f9f335f2b0ed792de8 Mon Sep 17 00:00:00 2001
From: Gildas Garcia <1122076+djhi@users.noreply.github.com>
Date: Mon, 1 Dec 2025 16:40:39 +0100
Subject: [PATCH 21/34] Update lock file
---
package-lock.json | 57 +++++++++++++++++++++--------------------------
1 file changed, 26 insertions(+), 31 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index 5f95cde9..7423fff7 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -170,7 +170,6 @@
"integrity": "sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@ampproject/remapping": "^2.2.0",
"@babel/code-frame": "^7.27.1",
@@ -771,7 +770,6 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=12"
},
@@ -1794,6 +1792,7 @@
"integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==",
"license": "MIT",
"optional": true,
+ "peer": true,
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.5",
"@jridgewell/trace-mapping": "^0.3.25"
@@ -1845,7 +1844,6 @@
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
"dev": true,
"license": "MIT",
- "peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
@@ -2082,7 +2080,6 @@
"integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==",
"dev": true,
"license": "MIT",
- "peer": true,
"engines": {
"node": "^14.21.3 || >=16"
},
@@ -3842,7 +3839,8 @@
"resolved": "https://registry.npmjs.org/@storybook/global/-/global-5.0.0.tgz",
"integrity": "sha512-FcOqPAXACP0I3oJ/ws6/rrPT9WGhu915Cg8D02a9YxLo0DE9zI+a9A5gRGvmQ09fiWPukqI8ZAEoQEdWUKMQdQ==",
"dev": true,
- "license": "MIT"
+ "license": "MIT",
+ "peer": true
},
"node_modules/@supabase/auth-js": {
"version": "2.71.1",
@@ -4414,6 +4412,7 @@
"integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@babel/code-frame": "^7.10.4",
"@babel/runtime": "^7.12.5",
@@ -4434,6 +4433,7 @@
"integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==",
"dev": true,
"license": "Apache-2.0",
+ "peer": true,
"dependencies": {
"dequal": "^2.0.3"
}
@@ -4443,7 +4443,8 @@
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
"integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
"dev": true,
- "license": "MIT"
+ "license": "MIT",
+ "peer": true
},
"node_modules/@testing-library/jest-dom": {
"version": "6.8.0",
@@ -4471,6 +4472,7 @@
"integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==",
"dev": true,
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=12",
"npm": ">=6"
@@ -4512,7 +4514,8 @@
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
"integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
"dev": true,
- "license": "MIT"
+ "license": "MIT",
+ "peer": true
},
"node_modules/@types/babel__core": {
"version": "7.20.5",
@@ -4707,7 +4710,6 @@
"integrity": "sha512-cMoR+FoAf/Jyq6+Df2/Z41jISvGZZ2eTlnsaJRptmZ76Caldwy1odD4xTr/gNV9VLj0AWgg/nmkevIyUfIIq5w==",
"devOptional": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"csstype": "^3.0.2"
}
@@ -4718,7 +4720,6 @@
"integrity": "sha512-qXRuZaOsAdXKFyOhRBg6Lqqc0yay13vN7KrIg4L7N4aaHN68ma9OK3NE1BoDFgFOTfM7zg+3/8+2n8rLUH3OKQ==",
"devOptional": true,
"license": "MIT",
- "peer": true,
"peerDependencies": {
"@types/react": "^19.0.0"
}
@@ -4791,7 +4792,6 @@
"integrity": "sha512-gTtSdWX9xiMPA/7MV9STjJOOYtWwIJIYxkQxnSV1U3xcE+mnJSH3f6zI0RYP+ew66WSlZ5ed+h0VCxsvdC1jJg==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.41.0",
"@typescript-eslint/types": "8.41.0",
@@ -5173,7 +5173,6 @@
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"devOptional": true,
"license": "MIT",
- "peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -5353,6 +5352,7 @@
"integrity": "sha512-aVNobHnJqLiUelTaHat9DZ1qM2w0C0Eym4LPI/3JxOnSokGVdsl1T1kN7TFvsEAD8G47A6VKQ0TVHqbBnYMJlQ==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"open": "^8.0.4"
},
@@ -5425,7 +5425,6 @@
}
],
"license": "MIT",
- "peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.8.19",
"caniuse-lite": "^1.0.30001751",
@@ -5445,7 +5444,8 @@
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
"license": "MIT",
- "optional": true
+ "optional": true,
+ "peer": true
},
"node_modules/bytes": {
"version": "3.1.2",
@@ -6354,7 +6354,6 @@
"integrity": "sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g==",
"hasInstallScript": true,
"license": "MIT",
- "peer": true,
"bin": {
"esbuild": "bin/esbuild"
},
@@ -6396,6 +6395,7 @@
"integrity": "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"debug": "^4.3.4"
},
@@ -6438,7 +6438,6 @@
"integrity": "sha512-RNCHRX5EwdrESy3Jc9o8ie8Bog+PeYvvSR8sDGoZxNFTvZ4dlxUB3WzQ3bQMztFrSRODGrLLj8g6OFuGY/aiQg==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.12.1",
@@ -8322,6 +8321,7 @@
"integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
"dev": true,
"license": "MIT",
+ "peer": true,
"bin": {
"lz-string": "bin/bin.js"
}
@@ -9292,7 +9292,6 @@
"integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
"dev": true,
"license": "MIT",
- "peer": true,
"bin": {
"prettier": "bin/prettier.cjs"
},
@@ -9309,6 +9308,7 @@
"integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"ansi-regex": "^5.0.1",
"ansi-styles": "^5.0.0",
@@ -9324,6 +9324,7 @@
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
"dev": true,
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=10"
},
@@ -9336,7 +9337,8 @@
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
"dev": true,
- "license": "MIT"
+ "license": "MIT",
+ "peer": true
},
"node_modules/pretty-ms": {
"version": "9.3.0",
@@ -9478,7 +9480,6 @@
"resolved": "https://registry.npmjs.org/ra-core/-/ra-core-5.13.1.tgz",
"integrity": "sha512-7pjERvKNtFaeTg2OvZS2uKUQt3G4OnZFfZLKrGpc4dzLzWj7CRwG+7YCcc/Q69yw2fhA7eJCBJtTfJl+EEncCw==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@tanstack/react-query": "^5.83.0",
"date-fns": "^3.6.0",
@@ -9615,7 +9616,6 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.1.1.tgz",
"integrity": "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==",
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -9637,7 +9637,6 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.1.tgz",
"integrity": "sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==",
"license": "MIT",
- "peer": true,
"dependencies": {
"scheduler": "^0.26.0"
},
@@ -9679,7 +9678,6 @@
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.66.0.tgz",
"integrity": "sha512-xXBqsWGKrY46ZqaHDo+ZUYiMUgi8suYu5kdrS20EG8KiL7VRQitEbNjm+UcrDYrNi1YLyfpmAeGjCZYXLT9YBw==",
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=18.0.0"
},
@@ -9782,7 +9780,6 @@
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.9.6.tgz",
"integrity": "sha512-Y1tUp8clYRXpfPITyuifmSoE2vncSME18uVLgaqyxh9H35JWpIfzHo+9y3Fzh5odk/jxPW29IgLgzcdwxGqyNA==",
"license": "MIT",
- "peer": true,
"dependencies": {
"cookie": "^1.0.1",
"set-cookie-parser": "^2.6.0"
@@ -9884,8 +9881,7 @@
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
- "license": "MIT",
- "peer": true
+ "license": "MIT"
},
"node_modules/require-directory": {
"version": "2.1.1",
@@ -9946,7 +9942,6 @@
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.50.0.tgz",
"integrity": "sha512-/Zl4D8zPifNmyGzJS+3kVoyXeDeT/GrsJM94sACNg9RtUE0hrHa1bNPtRSrfHTMH5HjRzce6K7rlTh3Khiw+pw==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@types/estree": "1.0.8"
},
@@ -10246,7 +10241,6 @@
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
"dev": true,
"license": "MIT",
- "peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
@@ -10428,6 +10422,7 @@
"integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
"license": "MIT",
"optional": true,
+ "peer": true,
"dependencies": {
"buffer-from": "^1.0.0",
"source-map": "^0.6.0"
@@ -10485,6 +10480,7 @@
"integrity": "sha512-Sm+qP3iGb/QKx/jTYdfE0mIeTmA2HF+5k9fD70S9oOJq3F9UdW8MLgs+5PE+E/xAfDjZU4OWAKEOyA6EYIvQHg==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@storybook/global": "^5.0.0",
"@testing-library/jest-dom": "^6.6.3",
@@ -10521,6 +10517,7 @@
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
"dev": true,
"license": "ISC",
+ "peer": true,
"bin": {
"semver": "bin/semver.js"
},
@@ -10783,6 +10780,7 @@
"integrity": "sha512-nIVck8DK+GM/0Frwd+nIhZ84pR/BX7rmXMfYwyg+Sri5oGVE99/E3KvXqpC2xHFxyqXyGHTKBSioxxplrO4I4w==",
"license": "BSD-2-Clause",
"optional": true,
+ "peer": true,
"dependencies": {
"@jridgewell/source-map": "^0.3.3",
"acorn": "^8.15.0",
@@ -10801,7 +10799,8 @@
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
"license": "MIT",
- "optional": true
+ "optional": true,
+ "peer": true
},
"node_modules/tiny-invariant": {
"version": "1.3.3",
@@ -10861,7 +10860,6 @@
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=12"
},
@@ -11085,7 +11083,6 @@
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
"dev": true,
"license": "Apache-2.0",
- "peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -11300,7 +11297,6 @@
"resolved": "https://registry.npmjs.org/vite/-/vite-7.1.4.tgz",
"integrity": "sha512-X5QFK4SGynAeeIt+A7ZWnApdUyHYm+pzv/8/A57LqSGcI88U6R6ipOs3uCesdc6yl7nl+zNO0t8LmqAdXcQihw==",
"license": "MIT",
- "peer": true,
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.5.0",
@@ -11431,7 +11427,6 @@
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=12"
},
From ced4abd1ebf4b666d5d189c35709fde1e0acd8a8 Mon Sep 17 00:00:00 2001
From: Gildas Garcia <1122076+djhi@users.noreply.github.com>
Date: Mon, 1 Dec 2025 16:45:05 +0100
Subject: [PATCH 22/34] Formatting
---
.../atomic-crm/activity/ActivityLogDealCreated.tsx | 6 +-----
1 file changed, 1 insertion(+), 5 deletions(-)
diff --git a/src/components/atomic-crm/activity/ActivityLogDealCreated.tsx b/src/components/atomic-crm/activity/ActivityLogDealCreated.tsx
index 2e92108d..1d19ae03 100644
--- a/src/components/atomic-crm/activity/ActivityLogDealCreated.tsx
+++ b/src/components/atomic-crm/activity/ActivityLogDealCreated.tsx
@@ -27,11 +27,7 @@ export function ActivityLogDealCreated({
added deal {deal.name}
- {context !== "company" && (
- <>
- to company {activity.company_id}
- >
- )}
+ {context !== "company" && <> to company {activity.company_id}>}
);
}
From b800b39cb8f1e89eb19581af075b52504d55e003 Mon Sep 17 00:00:00 2001
From: Gildas Garcia <1122076+djhi@users.noreply.github.com>
Date: Mon, 1 Dec 2025 18:04:23 +0100
Subject: [PATCH 23/34] Improve deal edition actions
---
.../atomic-crm/deals/ArchiveButton.tsx | 56 ++++++++++
src/components/atomic-crm/deals/DealEdit.tsx | 42 ++++++-
src/components/atomic-crm/deals/DealShow.tsx | 103 ++----------------
.../atomic-crm/deals/UnarchiveButton.tsx | 60 ++++++++++
4 files changed, 167 insertions(+), 94 deletions(-)
create mode 100644 src/components/atomic-crm/deals/ArchiveButton.tsx
create mode 100644 src/components/atomic-crm/deals/UnarchiveButton.tsx
diff --git a/src/components/atomic-crm/deals/ArchiveButton.tsx b/src/components/atomic-crm/deals/ArchiveButton.tsx
new file mode 100644
index 00000000..1b03c8bb
--- /dev/null
+++ b/src/components/atomic-crm/deals/ArchiveButton.tsx
@@ -0,0 +1,56 @@
+import type { ComponentProps, MouseEvent } from "react";
+import { Archive } from "lucide-react";
+import {
+ useNotify,
+ useRecordContext,
+ useRedirect,
+ useRefresh,
+ useUpdate,
+} from "ra-core";
+import { cn } from "@/lib/utils";
+import { Button } from "@/components/ui/button";
+import type { Deal } from "../types";
+
+export const ArchiveButton = ({
+ record: _recordProp,
+ className,
+ onClick,
+ ...props
+}: { record?: Deal } & ComponentProps) => {
+ const redirect = useRedirect();
+ const notify = useNotify();
+ const refresh = useRefresh();
+ const record = useRecordContext(props);
+ const [update] = useUpdate(undefined, undefined, {
+ onSuccess: () => {
+ redirect("list", "deals");
+ notify("Deal archived", { type: "info", undoable: false });
+ refresh();
+ },
+ onError: () => {
+ notify("Error: deal not archived", { type: "error" });
+ },
+ });
+ const handleClick = (event: MouseEvent) => {
+ update("deals", {
+ id: record?.id,
+ data: { archived_at: new Date().toISOString() },
+ previousData: record,
+ });
+ if (onClick) onClick(event);
+ };
+
+ return (
+
+
+ Archive
+
+ );
+};
diff --git a/src/components/atomic-crm/deals/DealEdit.tsx b/src/components/atomic-crm/deals/DealEdit.tsx
index 812df221..74ace94c 100644
--- a/src/components/atomic-crm/deals/DealEdit.tsx
+++ b/src/components/atomic-crm/deals/DealEdit.tsx
@@ -16,6 +16,16 @@ import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog";
import { CompanyAvatar } from "../companies/CompanyAvatar";
import type { Deal } from "../types";
import { DealInputs } from "./DealInputs";
+import { ArchiveButton } from "./ArchiveButton";
+import { UnarchiveButton } from "./UnarchiveButton";
+import { useIsMobile } from "@/hooks/use-mobile";
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuTrigger,
+} from "@/components/ui/dropdown-menu";
+import { MoreVertical } from "lucide-react";
export const DealEdit = ({ open, id }: { open: boolean; id?: string }) => {
const redirect = useRedirect();
@@ -81,6 +91,8 @@ export const DealEditPage = () => {
function EditTitle() {
const deal = useRecordContext();
+ const isMobile = useIsMobile();
+
if (!deal) {
return null;
}
@@ -92,6 +104,30 @@ function EditTitle() {
Edit {deal.name} deal
+ {isMobile ? (
+
+
+
+ Actions
+
+
+
+
+
+
+
+ {deal.archived_at ? (
+
+
+
+ ) : (
+
+
+
+ )}
+
+
+ ) : null}
@@ -111,10 +147,14 @@ function EditHeader() {
}
function EditToolbar() {
+ const record = useRecordContext();
+ const isMobile = useIsMobile();
+ if (!record) return null;
+
return (
-
+ {isMobile ? null : }
diff --git a/src/components/atomic-crm/deals/DealShow.tsx b/src/components/atomic-crm/deals/DealShow.tsx
index 63652679..abb49a61 100644
--- a/src/components/atomic-crm/deals/DealShow.tsx
+++ b/src/components/atomic-crm/deals/DealShow.tsx
@@ -1,15 +1,9 @@
-import { useMutation } from "@tanstack/react-query";
import { format, isValid } from "date-fns";
-import { Archive, ArchiveRestore, Edit } from "lucide-react";
-import {
- ShowBase,
- useDataProvider,
- useNotify,
- useRecordContext,
- useRedirect,
- useRefresh,
- useUpdate,
-} from "ra-core";
+import { ShowBase, useRecordContext, useRedirect } from "ra-core";
+import { Edit } from "lucide-react";
+import { Link } from "react-router";
+
+import { useIsMobile } from "@/hooks/use-mobile";
import { DeleteButton } from "@/components/admin/delete-button";
import { EditButton } from "@/components/admin/edit-button";
import { ReferenceArrayField } from "@/components/admin/reference-array-field";
@@ -19,6 +13,7 @@ import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Dialog, DialogContent } from "@/components/ui/dialog";
import { Separator } from "@/components/ui/separator";
+import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { CompanyAvatar } from "../companies/CompanyAvatar";
import { NoteCreate } from "../notes/NoteCreate";
@@ -27,10 +22,9 @@ import { useConfigurationContext } from "../root/ConfigurationContext";
import type { Deal } from "../types";
import { ContactList } from "./ContactList";
import { findDealLabel } from "./deal";
-import { useIsMobile } from "@/hooks/use-mobile";
-import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { ReferenceManyCount } from "../misc/ReferenceManyCount";
-import { Link } from "react-router";
+import { UnarchiveButton } from "./UnarchiveButton";
+import { ArchiveButton } from "./ArchiveButton";
export const DealShow = ({ open, id }: { open: boolean; id?: string }) => {
const redirect = useRedirect();
@@ -91,12 +85,12 @@ const DealShowContent = () => {
>
{record.archived_at ? (
<>
-
+
>
) : (
<>
-
+
>
)}
@@ -265,80 +259,3 @@ const ArchivedTitle = () => (
Archived Deal
);
-
-const ArchiveButton = ({ record }: { record: Deal }) => {
- const [update] = useUpdate();
- const redirect = useRedirect();
- const notify = useNotify();
- const refresh = useRefresh();
- const handleClick = () => {
- update(
- "deals",
- {
- id: record.id,
- data: { archived_at: new Date().toISOString() },
- previousData: record,
- },
- {
- onSuccess: () => {
- redirect("list", "deals");
- notify("Deal archived", { type: "info", undoable: false });
- refresh();
- },
- onError: () => {
- notify("Error: deal not archived", { type: "error" });
- },
- },
- );
- };
-
- return (
-
-
- Archive
-
- );
-};
-
-const UnarchiveButton = ({ record }: { record: Deal }) => {
- const dataProvider = useDataProvider();
- const redirect = useRedirect();
- const notify = useNotify();
- const refresh = useRefresh();
-
- const { mutate } = useMutation({
- mutationFn: () => dataProvider.unarchiveDeal(record),
- onSuccess: () => {
- redirect("list", "deals");
- notify("Deal unarchived", {
- type: "info",
- undoable: false,
- });
- refresh();
- },
- onError: () => {
- notify("Error: deal not unarchived", { type: "error" });
- },
- });
-
- const handleClick = () => {
- mutate();
- };
-
- return (
-
-
- Send back to the board
-
- );
-};
diff --git a/src/components/atomic-crm/deals/UnarchiveButton.tsx b/src/components/atomic-crm/deals/UnarchiveButton.tsx
new file mode 100644
index 00000000..963f0a95
--- /dev/null
+++ b/src/components/atomic-crm/deals/UnarchiveButton.tsx
@@ -0,0 +1,60 @@
+import type { ComponentProps, MouseEvent } from "react";
+import { ArchiveRestore } from "lucide-react";
+import { useMutation } from "@tanstack/react-query";
+import {
+ useDataProvider,
+ useNotify,
+ useRecordContext,
+ useRedirect,
+ useRefresh,
+} from "ra-core";
+import { Button } from "@/components/ui/button";
+import { cn } from "@/lib/utils";
+import type { Deal } from "../types";
+
+export const UnarchiveButton = ({
+ record: _recordProp,
+ className,
+ onClick,
+ ...props
+}: { record?: Deal } & ComponentProps) => {
+ const dataProvider = useDataProvider();
+ const redirect = useRedirect();
+ const notify = useNotify();
+ const refresh = useRefresh();
+ const record = useRecordContext(props);
+
+ const { mutate } = useMutation({
+ mutationFn: () => dataProvider.unarchiveDeal(record),
+ onSuccess: () => {
+ redirect("list", "deals");
+ notify("Deal unarchived", {
+ type: "info",
+ undoable: false,
+ });
+ refresh();
+ },
+ onError: () => {
+ notify("Error: deal not unarchived", { type: "error" });
+ },
+ });
+
+ const handleClick = (event: MouseEvent) => {
+ mutate();
+ if (onClick) onClick(event);
+ };
+
+ return (
+
+
+ Send back to the board
+
+ );
+};
From 68294e33771f881e0f4f30097f7b223169172e69 Mon Sep 17 00:00:00 2001
From: Gildas Garcia <1122076+djhi@users.noreply.github.com>
Date: Mon, 1 Dec 2025 18:13:31 +0100
Subject: [PATCH 24/34] Add contact status on mobile contact list
---
registry.json | 8 ++++++++
src/components/atomic-crm/contacts/ContactListContent.tsx | 7 +++++--
2 files changed, 13 insertions(+), 2 deletions(-)
diff --git a/registry.json b/registry.json
index 5b8d21c2..d8236ba9 100644
--- a/registry.json
+++ b/registry.json
@@ -416,6 +416,10 @@
"path": "src/components/atomic-crm/deals/deal.ts",
"type": "registry:component"
},
+ {
+ "path": "src/components/atomic-crm/deals/UnarchiveButton.tsx",
+ "type": "registry:component"
+ },
{
"path": "src/components/atomic-crm/deals/OnlyMineInput.tsx",
"type": "registry:component"
@@ -464,6 +468,10 @@
"path": "src/components/atomic-crm/deals/ContactList.tsx",
"type": "registry:component"
},
+ {
+ "path": "src/components/atomic-crm/deals/ArchiveButton.tsx",
+ "type": "registry:component"
+ },
{
"path": "src/components/atomic-crm/dashboard/Welcome.tsx",
"type": "registry:component"
diff --git a/src/components/atomic-crm/contacts/ContactListContent.tsx b/src/components/atomic-crm/contacts/ContactListContent.tsx
index 59987803..237ca9f3 100644
--- a/src/components/atomic-crm/contacts/ContactListContent.tsx
+++ b/src/components/atomic-crm/contacts/ContactListContent.tsx
@@ -60,8 +60,11 @@ export const ContactListContent = () => {
-
- {`${contact.first_name} ${contact.last_name ?? ""}`}
+
+
+ {`${contact.first_name} ${contact.last_name ?? ""}`}
+
+ {isSmall ? : null}
From 988282d8fa97c69bf43c7cd65a0f8498b911677f Mon Sep 17 00:00:00 2001
From: Gildas Garcia <1122076+djhi@users.noreply.github.com>
Date: Tue, 2 Dec 2025 09:38:07 +0100
Subject: [PATCH 25/34] Improve company list
---
.../atomic-crm/companies/CompanyList.tsx | 32 +++++++++++--------
1 file changed, 18 insertions(+), 14 deletions(-)
diff --git a/src/components/atomic-crm/companies/CompanyList.tsx b/src/components/atomic-crm/companies/CompanyList.tsx
index 9eb35590..9f01630b 100644
--- a/src/components/atomic-crm/companies/CompanyList.tsx
+++ b/src/components/atomic-crm/companies/CompanyList.tsx
@@ -93,21 +93,25 @@ const CompanyMobileList = () => (
>
-
-
-
-
- (
- <>
- {total} {total === 1 ? "deal" : "deals"}
- >
- )}
- />
-
+
+
+ (
+
+ {total} {total === 1 ? "deal" : "deals"}
+
+ )}
+ />
+
From aa422445230f7a094073c3b1d8c3a87b18794b0f Mon Sep 17 00:00:00 2001
From: Gildas Garcia <1122076+djhi@users.noreply.github.com>
Date: Thu, 4 Dec 2025 14:26:28 +0100
Subject: [PATCH 26/34] Remove create task entry in dashboard menu
---
src/components/atomic-crm/dashboard/Dashboard.tsx | 9 ---------
1 file changed, 9 deletions(-)
diff --git a/src/components/atomic-crm/dashboard/Dashboard.tsx b/src/components/atomic-crm/dashboard/Dashboard.tsx
index cc6b175c..38afd6de 100644
--- a/src/components/atomic-crm/dashboard/Dashboard.tsx
+++ b/src/components/atomic-crm/dashboard/Dashboard.tsx
@@ -91,15 +91,6 @@ export const Dashboard = () => {
Deal
-
-
- New
- Task
-
-
Date: Thu, 4 Dec 2025 14:59:18 +0100
Subject: [PATCH 27/34] Remove card wrapper in mobile views
---
src/components/atomic-crm/companies/CompanyCreate.tsx | 5 +++--
src/components/atomic-crm/contacts/ContactCreate.tsx | 4 ++--
src/components/atomic-crm/contacts/ContactEdit.tsx | 4 ++--
3 files changed, 7 insertions(+), 6 deletions(-)
diff --git a/src/components/atomic-crm/companies/CompanyCreate.tsx b/src/components/atomic-crm/companies/CompanyCreate.tsx
index b2b35084..5aa05f88 100644
--- a/src/components/atomic-crm/companies/CompanyCreate.tsx
+++ b/src/components/atomic-crm/companies/CompanyCreate.tsx
@@ -8,6 +8,7 @@ import { CompanyInputs } from "./CompanyInputs";
export const CompanyCreate = () => {
const { identity } = useGetIdentity();
+
return (
{
- {
title={false}
sort={{ field: "index", order: "DESC" }}
filters={dealFilters}
- actions={
- {record.first_name} {record.last_name} -
-+ {record.first_name} {record.last_name} +
+ +{record.name}
Edit {deal.name} deal
-Edit {deal.name} deal
{record.name}
+{record.description}
+{record.description}
-- {
perPage={25}
sort={{ field: "last_seen", order: "DESC" }}
exporter={exporter}
- filters={
- isMobile
- ? [
-
- component.",
+ );
+ }
+
+ const [hasRequestedNextPage, setHasRequestedNextPage] = React.useState(false);
+ const observerElem = useRef(null);
+ const handleObserver = useEvent<[IntersectionObserverEntry[]], void>(
+ (entries) => {
+ const [target] = entries;
+ if (target.isIntersecting && hasNextPage && !isFetchingNextPage) {
+ setHasRequestedNextPage(true);
+ fetchNextPage();
+ }
+ },
+ );
+
+ useEffect(() => {
+ // Whenever the query is unpaused, reset the requested next page state
+ if (!isPaused) {
+ setHasRequestedNextPage(false);
+ }
+ }, [isPaused]);
+
+ useEffect(() => {
+ const element = observerElem.current;
+ if (!element) return;
+ const observer = new IntersectionObserver(handleObserver, options);
+ observer.observe(element);
+ return () => observer.unobserve(element);
+ }, [
+ fetchNextPage,
+ hasNextPage,
+ handleObserver,
+ options,
+ isPending,
+ isFetchingNextPage,
+ ]);
+
+ if (isPending) return null;
+
+ const showOffline =
+ isPaused &&
+ hasNextPage &&
+ hasRequestedNextPage &&
+ offline !== false &&
+ offline !== undefined;
+
+ return (
+
-
+
+ ++ + +Loading... +
+ ) : (
+ -
+
+ ++ + + +
+ )}
+ -
-
- - From 8a470efbe088951551d1e9aea1177f6d4f013d8e Mon Sep 17 00:00:00 2001 From: Gildas Garcia <1122076+djhi@users.noreply.github.com> Date: Mon, 1 Dec 2025 12:10:37 +0100 Subject: [PATCH 19/34] Improve filtering on mobile (contacts and companies) --- registry.json | 4 + .../atomic-crm/companies/CompanyList.tsx | 1 + .../companies/CompanyListFilter.tsx | 12 +-- .../atomic-crm/contacts/ContactList.tsx | 1 + .../atomic-crm/contacts/ContactListFilter.tsx | 12 +-- .../atomic-crm/misc/ResponsiveFilters.tsx | 80 +++++++++++++++++++ 6 files changed, 94 insertions(+), 16 deletions(-) create mode 100644 src/components/atomic-crm/misc/ResponsiveFilters.tsx diff --git a/registry.json b/registry.json index 057e0848..95659461 100644 --- a/registry.json +++ b/registry.json @@ -338,6 +338,10 @@ "path": "src/components/atomic-crm/misc/SearchForm.tsx", "type": "registry:component" }, + { + "path": "src/components/atomic-crm/misc/ResponsiveFilters.tsx", + "type": "registry:component" + }, { "path": "src/components/atomic-crm/misc/RelativeDate.tsx", "type": "registry:component" diff --git a/src/components/atomic-crm/companies/CompanyList.tsx b/src/components/atomic-crm/companies/CompanyList.tsx index c80555d6..9eb35590 100644 --- a/src/components/atomic-crm/companies/CompanyList.tsx +++ b/src/components/atomic-crm/companies/CompanyList.tsx @@ -33,6 +33,7 @@ export const CompanyList = () => { return (} actions={false}> +
a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4", + className + )} + {...props} + /> + ) +} + +function ItemActions({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function ItemHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( + + ) +} + +function ItemFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( + + ) +} + +export { + Item, + ItemMedia, + ItemContent, + ItemActions, + ItemGroup, + ItemSeparator, + ItemTitle, + ItemDescription, + ItemHeader, + ItemFooter, +} diff --git a/src/components/ui/separator.tsx b/src/components/ui/separator.tsx index 3cf4f89b..bb3ad74c 100644 --- a/src/components/ui/separator.tsx +++ b/src/components/ui/separator.tsx @@ -11,7 +11,7 @@ function Separator({ }: React.ComponentProps- {
sort={{ field: "name", order: "ASC" }}
actions={