diff --git a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/AddQuestionButton.tsx b/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/AddQuestionButton.tsx index ad446619a63..1a6d8ea4743 100644 --- a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/AddQuestionButton.tsx +++ b/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/AddQuestionButton.tsx @@ -1,11 +1,15 @@ "use client"; -import { getQuestionDefaults, questionTypes, universalQuestionPresets } from "@/app/lib/questions"; import { createId } from "@paralleldrive/cuid2"; import * as Collapsible from "@radix-ui/react-collapsible"; import { PlusIcon } from "lucide-react"; import { useState } from "react"; import { cn } from "@formbricks/lib/cn"; +import { + getQuestionDefaults, + questionTypes, + universalQuestionPresets, +} from "@formbricks/lib/utils/questions"; import { TProduct } from "@formbricks/types/product"; interface AddQuestionButtonProps { diff --git a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/EditorCardMenu.tsx b/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/EditorCardMenu.tsx index 032588216d7..0ddea6ac1d4 100644 --- a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/EditorCardMenu.tsx +++ b/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/EditorCardMenu.tsx @@ -1,10 +1,10 @@ "use client"; -import { QUESTIONS_ICON_MAP, QUESTIONS_NAME_MAP, getQuestionDefaults } from "@/app/lib/questions"; import { createId } from "@paralleldrive/cuid2"; import { ArrowDownIcon, ArrowUpIcon, CopyIcon, EllipsisIcon, TrashIcon } from "lucide-react"; import { useState } from "react"; import { cn } from "@formbricks/lib/cn"; +import { QUESTIONS_ICON_MAP, QUESTIONS_NAME_MAP, getQuestionDefaults } from "@formbricks/lib/utils/questions"; import { TProduct } from "@formbricks/types/product"; import { TSurvey, diff --git a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/QuestionCard.tsx b/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/QuestionCard.tsx index 7f841c117ad..8fc5c21de8f 100644 --- a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/QuestionCard.tsx +++ b/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/QuestionCard.tsx @@ -2,13 +2,13 @@ import { RankingQuestionForm } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/RankingQuestionForm"; import { formatTextWithSlashes } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/lib/util"; -import { QUESTIONS_ICON_MAP, getTSurveyQuestionTypeEnumName } from "@/app/lib/questions"; import { useSortable } from "@dnd-kit/sortable"; import { CSS } from "@dnd-kit/utilities"; import * as Collapsible from "@radix-ui/react-collapsible"; import { ChevronDownIcon, ChevronRightIcon, GripIcon } from "lucide-react"; import { useState } from "react"; import { cn } from "@formbricks/lib/cn"; +import { QUESTIONS_ICON_MAP, getTSurveyQuestionTypeEnumName } from "@formbricks/lib/utils/questions"; import { recallToHeadline } from "@formbricks/lib/utils/recall"; import { TAttributeClass } from "@formbricks/types/attribute-classes"; import { TProduct } from "@formbricks/types/product"; diff --git a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/SurveyMenuBar.tsx b/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/SurveyMenuBar.tsx index 66699dd914b..a55efa2da1d 100644 --- a/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/SurveyMenuBar.tsx +++ b/apps/web/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/components/SurveyMenuBar.tsx @@ -86,6 +86,13 @@ export const SurveyMenuBar = ({ }; }, [localSurvey, survey]); + const clearSurveyLocalStorage = () => { + if (typeof localStorage !== "undefined") { + localStorage.removeItem(`${localSurvey.id}-columnOrder`); + localStorage.removeItem(`${localSurvey.id}-columnVisibility`); + } + }; + const containsEmptyTriggers = useMemo(() => { if (localSurvey.type === "link") return false; @@ -233,6 +240,7 @@ export const SurveyMenuBar = ({ } const segment = await handleSegmentUpdate(); + clearSurveyLocalStorage(); const updatedSurveyResponse = await updateSurveyAction({ ...localSurvey, segment }); setIsSurveySaving(false); @@ -278,6 +286,7 @@ export const SurveyMenuBar = ({ } const status = localSurvey.runOnDate ? "scheduled" : "inProgress"; const segment = await handleSegmentUpdate(); + clearSurveyLocalStorage(); await updateSurveyAction({ ...localSurvey, diff --git a/apps/web/app/(app)/environments/[environmentId]/(people)/attributes/loading.tsx b/apps/web/app/(app)/environments/[environmentId]/(people)/attributes/loading.tsx index 987ddec86da..33d0fe85b1f 100644 --- a/apps/web/app/(app)/environments/[environmentId]/(people)/attributes/loading.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/(people)/attributes/loading.tsx @@ -1,4 +1,4 @@ -import { PeopleSecondaryNavigation } from "@/app/(app)/environments/[environmentId]/(people)/people/components/PeopleSecondaryNavigation"; +import { PersonSecondaryNavigation } from "@/app/(app)/environments/[environmentId]/(people)/people/components/PersonSecondaryNavigation"; import { TagIcon } from "lucide-react"; import { PageContentWrapper } from "@formbricks/ui/PageContentWrapper"; import { PageHeader } from "@formbricks/ui/PageHeader"; @@ -8,7 +8,7 @@ const Loading = () => { <> - +
diff --git a/apps/web/app/(app)/environments/[environmentId]/(people)/attributes/page.tsx b/apps/web/app/(app)/environments/[environmentId]/(people)/attributes/page.tsx index b012f5263eb..3ed401747f0 100644 --- a/apps/web/app/(app)/environments/[environmentId]/(people)/attributes/page.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/(people)/attributes/page.tsx @@ -1,4 +1,4 @@ -import { PeopleSecondaryNavigation } from "@/app/(app)/environments/[environmentId]/(people)/people/components/PeopleSecondaryNavigation"; +import { PersonSecondaryNavigation } from "@/app/(app)/environments/[environmentId]/(people)/people/components/PersonSecondaryNavigation"; import { CircleHelpIcon } from "lucide-react"; import { Metadata } from "next"; import { notFound } from "next/navigation"; @@ -42,7 +42,7 @@ const Page = async ({ params }) => { return ( - + diff --git a/apps/web/app/(app)/environments/[environmentId]/(people)/people/[personId]/components/DeletePersonButton.tsx b/apps/web/app/(app)/environments/[environmentId]/(people)/people/[personId]/components/DeletePersonButton.tsx index 0899fdd8036..a8a3ee70256 100644 --- a/apps/web/app/(app)/environments/[environmentId]/(people)/people/[personId]/components/DeletePersonButton.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/(people)/people/[personId]/components/DeletePersonButton.tsx @@ -1,11 +1,11 @@ "use client"; -import { deletePersonAction } from "@/app/(app)/environments/[environmentId]/(people)/people/[personId]/actions"; import { TrashIcon } from "lucide-react"; import { useRouter } from "next/navigation"; import { useState } from "react"; import toast from "react-hot-toast"; import { getFormattedErrorMessage } from "@formbricks/lib/actionClient/helper"; +import { deletePersonAction } from "@formbricks/ui/DataTable/actions"; import { DeleteDialog } from "@formbricks/ui/DeleteDialog"; interface DeletePersonButtonProps { diff --git a/apps/web/app/(app)/environments/[environmentId]/(people)/people/actions.ts b/apps/web/app/(app)/environments/[environmentId]/(people)/people/actions.ts new file mode 100644 index 00000000000..47c9215ca0b --- /dev/null +++ b/apps/web/app/(app)/environments/[environmentId]/(people)/people/actions.ts @@ -0,0 +1,43 @@ +"use server"; + +import { z } from "zod"; +import { authenticatedActionClient } from "@formbricks/lib/actionClient"; +import { checkAuthorization } from "@formbricks/lib/actionClient/utils"; +import { getAttributes } from "@formbricks/lib/attribute/service"; +import { getOrganizationIdFromEnvironmentId } from "@formbricks/lib/organization/utils"; +import { getPeople } from "@formbricks/lib/person/service"; +import { ZId } from "@formbricks/types/common"; + +const ZGetPersonsAction = z.object({ + environmentId: ZId, + page: z.number(), +}); + +export const getPersonsAction = authenticatedActionClient + .schema(ZGetPersonsAction) + .action(async ({ ctx, parsedInput }) => { + await checkAuthorization({ + userId: ctx.user.id, + organizationId: await getOrganizationIdFromEnvironmentId(parsedInput.environmentId), + rules: ["environment", "read"], + }); + + return getPeople(parsedInput.environmentId, parsedInput.page); + }); + +const ZGetPersonAttributesAction = z.object({ + environmentId: ZId, + personId: ZId, +}); + +export const getPersonAttributesAction = authenticatedActionClient + .schema(ZGetPersonAttributesAction) + .action(async ({ ctx, parsedInput }) => { + await checkAuthorization({ + userId: ctx.user.id, + organizationId: await getOrganizationIdFromEnvironmentId(parsedInput.environmentId), + rules: ["environment", "read"], + }); + + return getAttributes(parsedInput.personId); + }); diff --git a/apps/web/app/(app)/environments/[environmentId]/(people)/people/components/PersonCard.tsx b/apps/web/app/(app)/environments/[environmentId]/(people)/people/components/PersonCard.tsx deleted file mode 100644 index 3f927f88eea..00000000000 --- a/apps/web/app/(app)/environments/[environmentId]/(people)/people/components/PersonCard.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import Link from "next/link"; -import React from "react"; -import { getAttributes } from "@formbricks/lib/attribute/service"; -import { getPersonIdentifier } from "@formbricks/lib/person/utils"; -import { TPerson } from "@formbricks/types/people"; -import { PersonAvatar } from "@formbricks/ui/Avatars"; - -export const PersonCard = async ({ person }: { person: TPerson }) => { - const attributes = await getAttributes(person.id); - - return ( - -
-
-
-
- -
-
-
- {getPersonIdentifier({ id: person.id, userId: person.userId }, attributes)} -
-
-
-
-
-
{person.userId}
-
-
-
{attributes.email}
-
-
- - ); -}; diff --git a/apps/web/app/(app)/environments/[environmentId]/(people)/people/components/PersonDataView.tsx b/apps/web/app/(app)/environments/[environmentId]/(people)/people/components/PersonDataView.tsx new file mode 100644 index 00000000000..300521a0213 --- /dev/null +++ b/apps/web/app/(app)/environments/[environmentId]/(people)/people/components/PersonDataView.tsx @@ -0,0 +1,107 @@ +"use client"; + +import { + getPersonAttributesAction, + getPersonsAction, +} from "@/app/(app)/environments/[environmentId]/(people)/people/actions"; +import { PersonTable } from "@/app/(app)/environments/[environmentId]/(people)/people/components/PersonTable"; +import { useEffect, useState } from "react"; +import React from "react"; +import { TEnvironment } from "@formbricks/types/environment"; +import { TPerson, TPersonTableData } from "@formbricks/types/people"; + +interface PersonDataViewProps { + environment: TEnvironment; + personCount: number; + itemsPerPage: number; +} + +export const PersonDataView = ({ environment, personCount, itemsPerPage }: PersonDataViewProps) => { + const [persons, setPersons] = useState([]); + const [personTableData, setPersonTableData] = useState([]); + const [pageNumber, setPageNumber] = useState(1); + const [totalPersons, setTotalPersons] = useState(0); + const [isDataLoaded, setIsDataLoaded] = useState(false); + const [hasMore, setHasMore] = useState(false); + const [loadingNextPage, setLoadingNextPage] = useState(false); + + useEffect(() => { + setTotalPersons(personCount); + setHasMore(pageNumber < Math.ceil(personCount / itemsPerPage)); + + const fetchData = async () => { + try { + const getPersonActionData = await getPersonsAction({ + environmentId: environment.id, + page: pageNumber, + }); + if (getPersonActionData?.data) { + setPersons(getPersonActionData.data); + } + } catch (error) { + console.error("Error fetching people data:", error); + } + }; + + fetchData(); + }, [pageNumber]); + + // Fetch additional person attributes and update table data + useEffect(() => { + const fetchAttributes = async () => { + const updatedPersonTableData = await Promise.all( + persons.map(async (person) => { + const attributes = await getPersonAttributesAction({ + environmentId: environment.id, + personId: person.id, + }); + return { + createdAt: person.createdAt, + personId: person.id, + userId: person.userId, + email: attributes?.data?.email ?? "", + attributes: attributes?.data ?? {}, + }; + }) + ); + setPersonTableData(updatedPersonTableData); + setIsDataLoaded(true); + }; + + if (persons.length > 0) { + fetchAttributes(); + } + }, [persons]); + + const fetchNextPage = async () => { + if (hasMore && !loadingNextPage) { + setLoadingNextPage(true); + const getPersonsActionData = await getPersonsAction({ + environmentId: environment.id, + page: pageNumber, + }); + if (getPersonsActionData?.data) { + const newData = getPersonsActionData.data; + setPersons((prevPersonsData) => [...prevPersonsData, ...newData]); + } + setPageNumber((prevPage) => prevPage + 1); + setHasMore(pageNumber + 1 < Math.ceil(totalPersons / itemsPerPage)); + setLoadingNextPage(false); + } + }; + + const deletePersons = (personIds: string[]) => { + setPersons(persons.filter((p) => !personIds.includes(p.id))); + }; + + return ( + + ); +}; diff --git a/apps/web/app/(app)/environments/[environmentId]/(people)/people/components/PeopleSecondaryNavigation.tsx b/apps/web/app/(app)/environments/[environmentId]/(people)/people/components/PersonSecondaryNavigation.tsx similarity index 90% rename from apps/web/app/(app)/environments/[environmentId]/(people)/people/components/PeopleSecondaryNavigation.tsx rename to apps/web/app/(app)/environments/[environmentId]/(people)/people/components/PersonSecondaryNavigation.tsx index 3df8dd3dfed..cfe21a81b37 100644 --- a/apps/web/app/(app)/environments/[environmentId]/(people)/people/components/PeopleSecondaryNavigation.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/(people)/people/components/PersonSecondaryNavigation.tsx @@ -2,17 +2,17 @@ import { getProductByEnvironmentId } from "@formbricks/lib/product/service"; import { TProductConfigChannel } from "@formbricks/types/product"; import { SecondaryNavigation } from "@formbricks/ui/SecondaryNavigation"; -interface PeopleSegmentsTabsProps { +interface PersonSecondaryNavigationProps { activeId: string; environmentId?: string; loading?: boolean; } -export const PeopleSecondaryNavigation = async ({ +export const PersonSecondaryNavigation = async ({ activeId, environmentId, loading, -}: PeopleSegmentsTabsProps) => { +}: PersonSecondaryNavigationProps) => { let currentProductChannel: TProductConfigChannel = null; if (!loading && environmentId) { diff --git a/apps/web/app/(app)/environments/[environmentId]/(people)/people/components/PersonTable.tsx b/apps/web/app/(app)/environments/[environmentId]/(people)/people/components/PersonTable.tsx new file mode 100644 index 00000000000..0848f81aef2 --- /dev/null +++ b/apps/web/app/(app)/environments/[environmentId]/(people)/people/components/PersonTable.tsx @@ -0,0 +1,238 @@ +import { generatePersonTableColumns } from "@/app/(app)/environments/[environmentId]/(people)/people/components/PersonTableColumn"; +import { + DndContext, + type DragEndEvent, + KeyboardSensor, + MouseSensor, + TouchSensor, + closestCenter, + useSensor, + useSensors, +} from "@dnd-kit/core"; +import { restrictToHorizontalAxis } from "@dnd-kit/modifiers"; +import { SortableContext, arrayMove, horizontalListSortingStrategy } from "@dnd-kit/sortable"; +import { VisibilityState, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table"; +import { useRouter } from "next/navigation"; +import { useEffect, useMemo, useState } from "react"; +import { cn } from "@formbricks/lib/cn"; +import { TPersonTableData } from "@formbricks/types/people"; +import { Button } from "@formbricks/ui/Button"; +import { DataTableHeader, DataTableSettingsModal, DataTableToolbar } from "@formbricks/ui/DataTable"; +import { Skeleton } from "@formbricks/ui/Skeleton"; +import { Table, TableBody, TableCell, TableHeader, TableRow } from "@formbricks/ui/Table"; + +interface PersonTableProps { + data: TPersonTableData[]; + fetchNextPage: () => void; + hasMore: boolean; + deletePersons: (personIds: string[]) => void; + isDataLoaded: boolean; + environmentId: string; +} + +export const PersonTable = ({ + data, + fetchNextPage, + hasMore, + deletePersons, + isDataLoaded, + environmentId, +}: PersonTableProps) => { + const [columnVisibility, setColumnVisibility] = useState({}); + const [columnOrder, setColumnOrder] = useState([]); + const [isTableSettingsModalOpen, setIsTableSettingsModalOpen] = useState(false); + const [isExpanded, setIsExpanded] = useState(null); + const [rowSelection, setRowSelection] = useState({}); + const router = useRouter(); + // Generate columns + const columns = useMemo(() => generatePersonTableColumns(isExpanded ?? false), [isExpanded]); + + // Load saved settings from localStorage + useEffect(() => { + const savedColumnOrder = localStorage.getItem(`${environmentId}-columnOrder`); + const savedColumnVisibility = localStorage.getItem(`${environmentId}-columnVisibility`); + const savedExpandedSettings = localStorage.getItem(`${environmentId}-rowExpand`); + if (savedColumnOrder && JSON.parse(savedColumnOrder).length > 0) { + setColumnOrder(JSON.parse(savedColumnOrder)); + } else { + setColumnOrder(table.getAllLeafColumns().map((d) => d.id)); + } + + if (savedColumnVisibility) { + setColumnVisibility(JSON.parse(savedColumnVisibility)); + } + if (savedExpandedSettings !== null) { + setIsExpanded(JSON.parse(savedExpandedSettings)); + } + }, [environmentId]); + + // Save settings to localStorage when they change + useEffect(() => { + if (columnOrder.length > 0) { + localStorage.setItem(`${environmentId}-columnOrder`, JSON.stringify(columnOrder)); + } + if (Object.keys(columnVisibility).length > 0) { + localStorage.setItem(`${environmentId}-columnVisibility`, JSON.stringify(columnVisibility)); + } + + if (isExpanded !== null) { + localStorage.setItem(`${environmentId}-rowExpand`, JSON.stringify(isExpanded)); + } + }, [columnOrder, columnVisibility, isExpanded, environmentId]); + + // Initialize DnD sensors + const sensors = useSensors( + useSensor(MouseSensor, {}), + useSensor(TouchSensor, {}), + useSensor(KeyboardSensor, {}) + ); + + // Memoize table data and columns + const tableData: TPersonTableData[] = useMemo( + () => (!isDataLoaded ? Array(10).fill({}) : data), + [data, isDataLoaded] + ); + const tableColumns = useMemo( + () => + !isDataLoaded + ? columns.map((column) => ({ + ...column, + cell: () => ( + +
+
+ ), + })) + : columns, + [columns, data] + ); + + // React Table instance + const table = useReactTable({ + data: tableData, + columns: tableColumns, + getRowId: (originalRow) => originalRow.personId, + getCoreRowModel: getCoreRowModel(), + onColumnVisibilityChange: setColumnVisibility, + onRowSelectionChange: setRowSelection, + onColumnOrderChange: setColumnOrder, + columnResizeMode: "onChange", + columnResizeDirection: "ltr", + manualPagination: true, + defaultColumn: { size: 300 }, + state: { + columnOrder, + columnVisibility, + rowSelection, + columnPinning: { + left: ["select", "createdAt"], + }, + }, + }); + + // Handle column drag end + const handleDragEnd = (event: DragEndEvent) => { + const { active, over } = event; + if (active && over && active.id !== over.id) { + setColumnOrder((prevOrder) => { + const oldIndex = prevOrder.indexOf(active.id as string); + const newIndex = prevOrder.indexOf(over.id as string); + return arrayMove(prevOrder, oldIndex, newIndex); + }); + } + }; + + return ( +
+ + +
+
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + + {headerGroup.headers.map((header) => ( + + ))} + + + ))} + + + + {table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + { + if (cell.column.id === "select") return; + router.push(`/environments/${environmentId}/people/${row.id}`); + }} + className={cn( + "border-slate-300 bg-white shadow-none group-hover:bg-slate-100", + row.getIsSelected() && "bg-slate-100", + { + "border-r": !cell.column.getIsLastColumn(), + "border-l": !cell.column.getIsFirstColumn(), + } + )}> +
+ {flexRender(cell.column.columnDef.cell, cell.getContext())} +
+
+ ))} +
+ ))} + {table.getRowModel().rows.length === 0 && ( + + + No results. + + + )} +
+
+
+
+ + {data && hasMore && data.length > 0 && ( +
+ +
+ )} + + +
+
+ ); +}; diff --git a/apps/web/app/(app)/environments/[environmentId]/(people)/people/components/PersonTableColumn.tsx b/apps/web/app/(app)/environments/[environmentId]/(people)/people/components/PersonTableColumn.tsx new file mode 100644 index 00000000000..51efcd1f22a --- /dev/null +++ b/apps/web/app/(app)/environments/[environmentId]/(people)/people/components/PersonTableColumn.tsx @@ -0,0 +1,83 @@ +"use client"; + +import { ColumnDef } from "@tanstack/react-table"; +import { cn } from "@formbricks/lib/cn"; +import { TPersonTableData } from "@formbricks/types/people"; +import { getSelectionColumn } from "@formbricks/ui/DataTable"; + +export const generatePersonTableColumns = (isExpanded: boolean): ColumnDef[] => { + const dateColumn: ColumnDef = { + accessorKey: "createdAt", + header: () => "Date", + size: 200, + cell: ({ row }) => { + const isoDateString = row.original.createdAt; + const date = new Date(isoDateString); + + const formattedDate = date.toLocaleString(undefined, { + year: "numeric", + month: "long", + day: "numeric", + }); + + const formattedTime = date.toLocaleString(undefined, { + hour: "2-digit", + minute: "2-digit", + hour12: false, + }); + + return ( +
+

{formattedDate}

+

{formattedTime}

+
+ ); + }, + }; + + const userColumn: ColumnDef = { + accessorKey: "user", + header: "User", + cell: ({ row }) => { + const personId = row.original.personId; + return

{personId}

; + }, + }; + + const userIdColumn: ColumnDef = { + accessorKey: "userId", + header: "User ID", + cell: ({ row }) => { + const userId = row.original.userId; + return

{userId}

; + }, + }; + + const emailColumn: ColumnDef = { + accessorKey: "email", + header: "Email", + }; + + const attributesColumn: ColumnDef = { + accessorKey: "attributes", + header: "Attributes", + cell: ({ row }) => { + const attributes = row.original.attributes; + + // Handle cases where attributes are missing or empty + if (!attributes || Object.keys(attributes).length === 0) return null; + + return ( +
+ {Object.entries(attributes).map(([key, value]) => ( +
+
{key}
:
{value}
+
+ ))} +
+ ); + }, + }; + + return [getSelectionColumn(), dateColumn, userColumn, userIdColumn, emailColumn, attributesColumn]; +}; diff --git a/apps/web/app/(app)/environments/[environmentId]/(people)/people/components/pagination.tsx b/apps/web/app/(app)/environments/[environmentId]/(people)/people/components/pagination.tsx deleted file mode 100644 index e4241f2d8a3..00000000000 --- a/apps/web/app/(app)/environments/[environmentId]/(people)/people/components/pagination.tsx +++ /dev/null @@ -1,55 +0,0 @@ -export const Pagination = ({ environmentId, currentPage, totalItems, itemsPerPage }) => { - const totalPages = Math.ceil(totalItems / itemsPerPage); - - const previousPageLink = - currentPage === 1 ? "#" : `/environments/${environmentId}/people?page=${currentPage - 1}`; - const nextPageLink = - currentPage === totalPages ? "#" : `/environments/${environmentId}/people?page=${currentPage + 1}`; - - return ( - - ); -}; diff --git a/apps/web/app/(app)/environments/[environmentId]/(people)/people/loading.tsx b/apps/web/app/(app)/environments/[environmentId]/(people)/people/loading.tsx index 84d10b5a8b2..9bd184d5360 100644 --- a/apps/web/app/(app)/environments/[environmentId]/(people)/people/loading.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/(people)/people/loading.tsx @@ -1,4 +1,4 @@ -import { PeopleSecondaryNavigation } from "@/app/(app)/environments/[environmentId]/(people)/people/components/PeopleSecondaryNavigation"; +import { PersonSecondaryNavigation } from "@/app/(app)/environments/[environmentId]/(people)/people/components/PersonSecondaryNavigation"; import { PageContentWrapper } from "@formbricks/ui/PageContentWrapper"; import { PageHeader } from "@formbricks/ui/PageHeader"; @@ -7,7 +7,7 @@ const Loading = () => { <> - +
diff --git a/apps/web/app/(app)/environments/[environmentId]/(people)/people/page.tsx b/apps/web/app/(app)/environments/[environmentId]/(people)/people/page.tsx index 2a4ff2eadd0..f0204b65f6e 100644 --- a/apps/web/app/(app)/environments/[environmentId]/(people)/people/page.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/(people)/people/page.tsx @@ -1,42 +1,20 @@ -import { PeopleSecondaryNavigation } from "@/app/(app)/environments/[environmentId]/(people)/people/components/PeopleSecondaryNavigation"; +import { PersonDataView } from "@/app/(app)/environments/[environmentId]/(people)/people/components/PersonDataView"; +import { PersonSecondaryNavigation } from "@/app/(app)/environments/[environmentId]/(people)/people/components/PersonSecondaryNavigation"; import { CircleHelpIcon } from "lucide-react"; import { ITEMS_PER_PAGE } from "@formbricks/lib/constants"; import { getEnvironment } from "@formbricks/lib/environment/service"; -import { getPeople, getPeopleCount } from "@formbricks/lib/person/service"; -import { TPerson } from "@formbricks/types/people"; +import { getPersonCount } from "@formbricks/lib/person/service"; import { Button } from "@formbricks/ui/Button"; -import { EmptySpaceFiller } from "@formbricks/ui/EmptySpaceFiller"; import { PageContentWrapper } from "@formbricks/ui/PageContentWrapper"; import { PageHeader } from "@formbricks/ui/PageHeader"; -import { Pagination } from "@formbricks/ui/Pagination"; -import { PersonCard } from "./components/PersonCard"; -const Page = async ({ - params, - searchParams, -}: { - params: { environmentId: string }; - searchParams: { [key: string]: string | string[] | undefined }; -}) => { - const pageNumber = searchParams.page ? parseInt(searchParams.page as string) : 1; - const [environment, totalPeople] = await Promise.all([ - getEnvironment(params.environmentId), - getPeopleCount(params.environmentId), - ]); +const Page = async ({ params }: { params: { environmentId: string } }) => { + const environment = await getEnvironment(params.environmentId); + const personCount = await getPersonCount(params.environmentId); + if (!environment) { throw new Error("Environment not found"); } - const maxPageNumber = Math.ceil(totalPeople / ITEMS_PER_PAGE); - let hidePagination = false; - - let people: TPerson[] = []; - - if (pageNumber < 1 || pageNumber > maxPageNumber) { - people = []; - hidePagination = true; - } else { - people = await getPeople(params.environmentId, pageNumber); - } const HowToAddPeopleButton = (