From 3a07a201956534995b945f2810dbc3c1407fdd93 Mon Sep 17 00:00:00 2001 From: nick-delirium Date: Mon, 17 Feb 2025 14:43:07 +0100 Subject: [PATCH] ui: drag and drop table comp, separate item page --- .../Activity/EventDetailsModal.tsx | 8 +- .../DataManagement/Activity/Page.tsx | 149 ++++++++++++------ .../DataManagement/DataItemPage.tsx | 112 +++++++++++++ .../DataManagement/UsersEvents/EventPage.tsx | 122 ++++---------- frontend/app/components/shared/DNDTable.tsx | 100 ++++++++++++ frontend/app/layout/SideMenu.tsx | 3 +- 6 files changed, 348 insertions(+), 146 deletions(-) create mode 100644 frontend/app/components/DataManagement/DataItemPage.tsx create mode 100644 frontend/app/components/shared/DNDTable.tsx diff --git a/frontend/app/components/DataManagement/Activity/EventDetailsModal.tsx b/frontend/app/components/DataManagement/Activity/EventDetailsModal.tsx index 50e183070..2106b1439 100644 --- a/frontend/app/components/DataManagement/Activity/EventDetailsModal.tsx +++ b/frontend/app/components/DataManagement/Activity/EventDetailsModal.tsx @@ -10,14 +10,14 @@ function EventDetailsModal({ ev, onClose }: { ev: EventData, onClose: () => void label: 'All Properties', value: 'all', }, + { + label: 'Openreplay Properties', + value: 'default', + }, { label: 'Custom Properties', value: 'custom', }, - { - label: 'Default Properties', - value: 'default', - } ] const views = [ diff --git a/frontend/app/components/DataManagement/Activity/Page.tsx b/frontend/app/components/DataManagement/Activity/Page.tsx index cb56599d1..c087da5cf 100644 --- a/frontend/app/components/DataManagement/Activity/Page.tsx +++ b/frontend/app/components/DataManagement/Activity/Page.tsx @@ -1,8 +1,7 @@ import React from 'react'; import { EventsList, FilterList } from 'Shared/Filters/FilterList'; -import { Table, Dropdown } from 'antd'; +import { Dropdown, Button } from 'antd'; import { MoreOutlined } from '@ant-design/icons'; -import { numberWithCommas } from 'App/utils'; import OutsideClickDetectingDiv from 'Shared/OutsideClickDetectingDiv'; import ColumnsModal from 'Components/DataManagement/Activity/ColumnsModal'; import Event from './data/Event'; @@ -11,10 +10,12 @@ import EventDetailsModal from './EventDetailsModal'; import { useQuery } from '@tanstack/react-query'; import Select from 'Shared/Select'; import { Link } from 'react-router-dom'; -import { dataManagement, withSiteId } from 'App/routes' +import { dataManagement, withSiteId } from 'App/routes'; import { observer } from 'mobx-react-lite'; import { useStore } from 'App/mstore'; -import FullPagination from "Shared/FullPagination"; +import FullPagination from 'Shared/FullPagination'; +import AnimatedSVG from 'Shared/AnimatedSVG'; +import DndTable from 'Shared/DNDTable'; const limit = 100; @@ -61,27 +62,11 @@ const fetcher = async ( }); }; -function ActivityPage() { - const { projectsStore } = useStore() - const siteId = projectsStore.activeSiteId; +const columnOrderKey = '$__activity_columns_order__$'; - const [page, setPage] = React.useState(1); - const [hiddenCols, setHiddenCols] = React.useState([]); - const { data, isPending } = useQuery({ - queryKey: ['data', 'events', page], - queryFn: () => fetcher(page), - initialData: { list: [], total: 0 }, - }); - const { list, total } = data; - const appliedFilter = { filters: [] }; - const onAddFilter = () => {}; - const onUpdateFilter = () => {}; - const onRemoveFilter = () => {}; - const onChangeEventsOrder = () => {}; - const saveRequestPayloads = () => {}; - const onFilterMove = () => {}; - const [editCols, setEditCols] = React.useState(false); - const { showModal, hideModal } = useModal(); +function ActivityPage() { + const { projectsStore } = useStore(); + const siteId = projectsStore.activeSiteId; const dropdownItems = [ { @@ -90,7 +75,6 @@ function ActivityPage() { onClick: () => setTimeout(() => setEditCols(true), 1), }, ]; - const columns = [ { title: 'Event Name', @@ -100,9 +84,7 @@ function ActivityPage() { sorter: (a, b) => a.name.localeCompare(b.name), render: (text, row) => (
- {row.$_isAutoCapture && ( - [auto] - )} + {row.$_isAutoCapture && [a]} {row.name}
), @@ -164,7 +146,53 @@ function ActivityPage() { }, ]; + const [page, setPage] = React.useState(1); + const [cols, setCols] = React.useState(columns); + const [hiddenCols, setHiddenCols] = React.useState([]); + const { data, isPending } = useQuery({ + queryKey: ['data', 'events', page], + queryFn: () => fetcher(page), + initialData: { list: [], total: 0 }, + }); + const { list, total } = data; + const appliedFilter = { filters: [] }; + const onAddFilter = () => {}; + const onUpdateFilter = () => {}; + const onRemoveFilter = () => {}; + const onChangeEventsOrder = () => {}; + const saveRequestPayloads = () => {}; + const onFilterMove = () => {}; + const [editCols, setEditCols] = React.useState(false); + const { showModal, hideModal } = useModal(); + React.useEffect(() => { + if (hiddenCols.length) { + setCols((cols) => + cols.map((col) => ({ + ...col, + hidden: hiddenCols.includes(col.key), + })) + ); + } + }, [hiddenCols]); + React.useEffect(() => { + const savedColumnOrder = localStorage.getItem(columnOrderKey); + if (savedColumnOrder) { + const keys = savedColumnOrder.split(','); + setCols((cols) => { + return cols.sort((a, b) => { + return keys.indexOf(a.key) - keys.indexOf(b.key); + }); + }); + } + }, []); + + const onOrderChange = (newCols) => { + const order = newCols.map((col) => col.key).join(','); + localStorage.setItem(columnOrderKey, order); + + setCols(newCols); + }; const onPageChange = (page: number) => { setPage(page); @@ -177,10 +205,6 @@ function ActivityPage() { }); }; - const shownCols = columns.map((col) => ({ - ...col, - hidden: hiddenCols.includes(col.key), - })); const onUpdateVisibleCols = (cols: string[]) => { setHiddenCols((_) => { return columns @@ -271,23 +295,50 @@ function ActivityPage() { }} /> - ({ - onClick: () => onItemClick(record), - })} - dataSource={list} - pagination={false} - columns={shownCols} - /> - + {total === 0 ? ( +
+ +
+
+ No results in the{' '} +
+ +
+ + +
+ ) : ( +
+ {value} +
setIsEdit(true)} + > + +
+
+ )} +
+
+ ); +} + +export default DataItemPage; diff --git a/frontend/app/components/DataManagement/UsersEvents/EventPage.tsx b/frontend/app/components/DataManagement/UsersEvents/EventPage.tsx index cfa255d70..05b70c48a 100644 --- a/frontend/app/components/DataManagement/UsersEvents/EventPage.tsx +++ b/frontend/app/components/DataManagement/UsersEvents/EventPage.tsx @@ -5,6 +5,7 @@ import Event from 'Components/DataManagement/Activity/data/Event'; import { Triangle } from '../Activity/EventDetailsModal'; import cn from 'classnames'; import { EditOutlined } from '@ant-design/icons'; +import DataItemPage from '../DataItemPage'; const testAutoEv = new Event({ name: 'auto test ev', @@ -30,104 +31,41 @@ function EventPage() { label: 'All Properties', value: 'all', }, + { + label: 'Openreplay Properties', + value: 'default', + }, { label: 'Custom Properties', value: 'custom', }, - { - label: 'Default Properties', - value: 'default', - } - ] - return ( -
- -
-
-
- {testAutoEv.name} -
-
- Play Sessions - -
-
- null} fieldName={'Display Name'} value={testAutoEv.displayName} /> - null} fieldName={'Description'} value={testAutoEv.description} /> - null} fieldName={'30 Day Volume'} value={testAutoEv.monthVolume} /> -
+ ]; -
-
-
Event Properties
- setTab(v)} /> + const evWithFields = { + ...testAutoEv, + fields: [ + { name: 'User ID', value: testAutoEv.defaultFields.userId }, + { name: 'User Location', value: testAutoEv.defaultFields.userLocation }, + { + name: 'User Environment', + value: testAutoEv.defaultFields.userEnvironment, + }, + ], + }; + return ( + +
+
Event Properties
+ setTab(v)} /> +
-
-
+ } + /> ); } -function EditableField({ - onSave, - fieldName, - value, -}: { - onSave: (value: string) => void - fieldName: string - value: string -}) { - const [isEdit, setIsEdit] = React.useState(false); - return ( -
-
- {fieldName} -
-
- {isEdit ? ( -
- -
- - -
- ) : ( -
- {value} -
setIsEdit(true)}> - -
-
- )} -
-
- ); -} - -export default EventPage \ No newline at end of file +export default EventPage; diff --git a/frontend/app/components/shared/DNDTable.tsx b/frontend/app/components/shared/DNDTable.tsx new file mode 100644 index 000000000..5c58cd3f2 --- /dev/null +++ b/frontend/app/components/shared/DNDTable.tsx @@ -0,0 +1,100 @@ +import React, { useRef, useState, useCallback } from 'react'; +import { Table } from 'antd'; +import { useDrag, useDrop, DndProvider } from 'react-dnd'; +import { HTML5Backend } from 'react-dnd-html5-backend'; +import { GripVertical } from 'lucide-react'; + +const type = 'COLUMN'; + +function DraggableHeaderCell({ index, moveColumn, children, ...rest }) { + const ref = useRef(null); + const [{ isDragging }, drag] = useDrag({ + type, + item: { index }, + collect: (monitor) => ({ + isDragging: monitor.isDragging(), + }), + }); + const [, drop] = useDrop({ + accept: type, + hover: (item, monitor) => { + if (!ref.current) return; + const dragIndex = item.index; + if (dragIndex === index) return; + const hoverBoundingRect = ref.current.getBoundingClientRect(); + const hoverMiddleX = + (hoverBoundingRect.right - hoverBoundingRect.left) / 2; + const clientOffset = monitor.getClientOffset(); + const hoverClientX = clientOffset.x - hoverBoundingRect.left; + if (dragIndex < index && hoverClientX < hoverMiddleX) return; + if (dragIndex > index && hoverClientX > hoverMiddleX) return; + moveColumn(dragIndex, index); + item.index = index; + }, + }); + drag(drop(ref)); + return ( +
+ ); +} + +const DNDTable = ({ columns: initCols, onOrderChange, ...tableProps }) => { + const [cols, setCols] = useState(initCols); + + const moveColumn = useCallback( + (dragIndex, hoverIndex) => { + const updated = [...cols]; + const [removed] = updated.splice(dragIndex, 1); + updated.splice(hoverIndex, 0, removed); + setCols(updated); + onOrderChange?.(updated); + }, + [cols, onOrderChange] + ); + + const components = { + header: { + cell: (cellProps) => { + const i = cols.findIndex((c) => c.key === cellProps['data-col-key']); + const isOptionsCell = cellProps['data-col-key'] === '$__opts__$'; + return !isOptionsCell && i > -1 ? ( + + ) : ( +
+
+
+ +
+ {children} +
+
+ ); + }, + }, + }; + + const mergedCols = cols.map((col) => ({ + ...col, + onHeaderCell: () => ({ 'data-col-key': col.key }), + })); + + return ( + + + + ); +}; + +export default DNDTable; diff --git a/frontend/app/layout/SideMenu.tsx b/frontend/app/layout/SideMenu.tsx index 9b7f7384c..93ee0d691 100644 --- a/frontend/app/layout/SideMenu.tsx +++ b/frontend/app/layout/SideMenu.tsx @@ -108,7 +108,8 @@ function SideMenu(props: Props) { item.key === MENU.ALERTS && modules.includes(MODULES.ALERTS), item.key === MENU.USABILITY_TESTS && modules.includes(MODULES.USABILITY_TESTS), item.isAdmin && !isAdmin, - item.isEnterprise && !isEnterprise + item.isEnterprise && !isEnterprise, + (item.key === MENU.ACTIVITY || item.key === MENU.USERS_EVENTS) && isMobile ].some((cond) => cond); return { ...item, hidden: isHidden };