feat: add dashboard layout sort
This commit is contained in:
parent
0e669a2ca1
commit
54af2874e4
@ -26,6 +26,7 @@
|
|||||||
"@trpc/server": "^10.38.4",
|
"@trpc/server": "^10.38.4",
|
||||||
"@types/uuid": "^9.0.3",
|
"@types/uuid": "^9.0.3",
|
||||||
"antd": "^5.9.3",
|
"antd": "^5.9.3",
|
||||||
|
"array-move": "^3.0.1",
|
||||||
"axios": "^1.5.0",
|
"axios": "^1.5.0",
|
||||||
"badge-maker": "^3.3.1",
|
"badge-maker": "^3.3.1",
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
@ -58,6 +59,7 @@
|
|||||||
"puppeteer": "^21.3.8",
|
"puppeteer": "^21.3.8",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
|
"react-easy-sort": "^1.5.3",
|
||||||
"react-router": "^6.15.0",
|
"react-router": "^6.15.0",
|
||||||
"react-router-dom": "^6.15.0",
|
"react-router-dom": "^6.15.0",
|
||||||
"request-ip": "^3.3.0",
|
"request-ip": "^3.3.0",
|
||||||
|
@ -31,6 +31,9 @@ dependencies:
|
|||||||
antd:
|
antd:
|
||||||
specifier: ^5.9.3
|
specifier: ^5.9.3
|
||||||
version: 5.9.3(react-dom@18.2.0)(react@18.2.0)
|
version: 5.9.3(react-dom@18.2.0)(react@18.2.0)
|
||||||
|
array-move:
|
||||||
|
specifier: ^3.0.1
|
||||||
|
version: 3.0.1
|
||||||
axios:
|
axios:
|
||||||
specifier: ^1.5.0
|
specifier: ^1.5.0
|
||||||
version: 1.5.0
|
version: 1.5.0
|
||||||
@ -127,6 +130,9 @@ dependencies:
|
|||||||
react-dom:
|
react-dom:
|
||||||
specifier: ^18.2.0
|
specifier: ^18.2.0
|
||||||
version: 18.2.0(react@18.2.0)
|
version: 18.2.0(react@18.2.0)
|
||||||
|
react-easy-sort:
|
||||||
|
specifier: ^1.5.3
|
||||||
|
version: 1.5.3(react-dom@18.2.0)(react@18.2.0)
|
||||||
react-router:
|
react-router:
|
||||||
specifier: ^6.15.0
|
specifier: ^6.15.0
|
||||||
version: 6.15.0(react@18.2.0)
|
version: 6.15.0(react@18.2.0)
|
||||||
@ -2417,6 +2423,11 @@ packages:
|
|||||||
resolution: {integrity: sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ==}
|
resolution: {integrity: sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/array-move@3.0.1:
|
||||||
|
resolution: {integrity: sha512-H3Of6NIn2nNU1gsVDqDnYKY/LCdWvCMMOWifNGhKcVQgiZ6nOek39aESOvro6zmueP07exSl93YLvkN4fZOkSg==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/array-tree-filter@2.1.0:
|
/array-tree-filter@2.1.0:
|
||||||
resolution: {integrity: sha512-4ROwICNlNw/Hqa9v+rk5h22KjmzB1JGTMVKP2AKJBOCgb0yL0ASf0+YvCcLNNwquOHNX48jkeZIJ3a+oOQqKcw==}
|
resolution: {integrity: sha512-4ROwICNlNw/Hqa9v+rk5h22KjmzB1JGTMVKP2AKJBOCgb0yL0ASf0+YvCcLNNwquOHNX48jkeZIJ3a+oOQqKcw==}
|
||||||
dev: false
|
dev: false
|
||||||
@ -5753,6 +5764,19 @@ packages:
|
|||||||
scheduler: 0.23.0
|
scheduler: 0.23.0
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/react-easy-sort@1.5.3(react-dom@18.2.0)(react@18.2.0):
|
||||||
|
resolution: {integrity: sha512-d5DVqmlqGfpRcE2blMkZ/H8AvsY6KZmUG5nOeUrV5INWMbqDIXTRBkkBnJvNOQT6mupAjl4wokgQcur5zRMYxw==}
|
||||||
|
engines: {node: '>=16'}
|
||||||
|
peerDependencies:
|
||||||
|
react: '>=16.4.0'
|
||||||
|
react-dom: '>=16.4.0'
|
||||||
|
dependencies:
|
||||||
|
array-move: 3.0.1
|
||||||
|
react: 18.2.0
|
||||||
|
react-dom: 18.2.0(react@18.2.0)
|
||||||
|
tslib: 2.0.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
/react-is@16.13.1:
|
/react-is@16.13.1:
|
||||||
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
|
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
|
||||||
dev: false
|
dev: false
|
||||||
@ -6577,6 +6601,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==}
|
resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/tslib@2.0.1:
|
||||||
|
resolution: {integrity: sha512-SgIkNheinmEBgx1IUNirK0TUD4X9yjjBRTqqjggWCU3pUEqIk3/Uwl3yRixYKT6WjQuGiwDv4NomL3wqRCj+CQ==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/tslib@2.6.2:
|
/tslib@2.6.2:
|
||||||
resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==}
|
resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==}
|
||||||
dev: false
|
dev: false
|
||||||
|
@ -25,6 +25,7 @@ model User {
|
|||||||
model Workspace {
|
model Workspace {
|
||||||
id String @id @unique @default(cuid()) @db.VarChar(30)
|
id String @id @unique @default(cuid()) @db.VarChar(30)
|
||||||
name String @db.VarChar(100)
|
name String @db.VarChar(100)
|
||||||
|
dashboardOrder String[]
|
||||||
createdAt DateTime @default(now()) @db.Timestamptz(6)
|
createdAt DateTime @default(now()) @db.Timestamptz(6)
|
||||||
updatedAt DateTime @updatedAt @db.Timestamptz(6)
|
updatedAt DateTime @updatedAt @db.Timestamptz(6)
|
||||||
|
|
||||||
|
@ -1,19 +1,24 @@
|
|||||||
import React, { Fragment } from 'react';
|
import React, { Fragment, useMemo, useState } from 'react';
|
||||||
import { ArrowRightOutlined, EditOutlined } from '@ant-design/icons';
|
import { ArrowRightOutlined, EditOutlined } from '@ant-design/icons';
|
||||||
import { Button, Divider } from 'antd';
|
import { Button, Divider } from 'antd';
|
||||||
import { WebsiteOverview } from '../components/website/WebsiteOverview';
|
import { WebsiteOverview } from '../components/website/WebsiteOverview';
|
||||||
import { useCurrentWorkspaceId } from '../store/user';
|
import { useCurrentWorkspace } from '../store/user';
|
||||||
import { Loading } from '../components/Loading';
|
import { Loading } from '../components/Loading';
|
||||||
import { useWorspaceWebsites } from '../api/model/website';
|
import { useWorspaceWebsites } from '../api/model/website';
|
||||||
import { NoWorkspaceTip } from '../components/NoWorkspaceTip';
|
import { NoWorkspaceTip } from '../components/NoWorkspaceTip';
|
||||||
import { useNavigate } from 'react-router';
|
import { useNavigate } from 'react-router';
|
||||||
|
import { useEvent } from '../hooks/useEvent';
|
||||||
|
import arrayMove from 'array-move';
|
||||||
|
import SortableList, { SortableItem } from 'react-easy-sort';
|
||||||
|
import { defaultErrorHandler, defaultSuccessHandler, trpc } from '../api/trpc';
|
||||||
|
|
||||||
export const Dashboard: React.FC = React.memo(() => {
|
export const Dashboard: React.FC = React.memo(() => {
|
||||||
const workspaceId = useCurrentWorkspaceId()!;
|
const workspace = useCurrentWorkspace();
|
||||||
const { isLoading, websites } = useWorspaceWebsites(workspaceId);
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const [isEditLayout, setIsEditLayout] = useState(false);
|
||||||
|
const { isLoading, websiteList, handleSortEnd } = useDashboardWebsiteList();
|
||||||
|
|
||||||
if (!workspaceId) {
|
if (!workspace) {
|
||||||
return <NoWorkspaceTip />;
|
return <NoWorkspaceTip />;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -22,17 +27,37 @@ export const Dashboard: React.FC = React.memo(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="py-4">
|
||||||
<div className="h-24 flex items-center">
|
<div className="h-20 flex items-center">
|
||||||
<div className="text-2xl flex-1">Dashboard</div>
|
<div className="text-2xl flex-1">Dashboard</div>
|
||||||
<div>
|
<div>
|
||||||
<Button icon={<EditOutlined />} size="large">
|
<Button
|
||||||
Edit
|
icon={<EditOutlined />}
|
||||||
|
size="large"
|
||||||
|
onClick={() => setIsEditLayout((state) => !state)}
|
||||||
|
>
|
||||||
|
{isEditLayout ? 'Done' : 'Edit'}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{isEditLayout ? (
|
||||||
|
<SortableList
|
||||||
|
className="space-y-2"
|
||||||
|
lockAxis="y"
|
||||||
|
onSortEnd={handleSortEnd}
|
||||||
|
>
|
||||||
|
{websiteList.map((website) => (
|
||||||
|
<SortableItem key={website.id}>
|
||||||
|
<div className="overflow-hidden h-14 w-full border border-black border-opacity-20 flex justify-center items-center rounded-lg bg-white cursor-move">
|
||||||
|
{website.name}
|
||||||
|
</div>
|
||||||
|
</SortableItem>
|
||||||
|
))}
|
||||||
|
</SortableList>
|
||||||
|
) : (
|
||||||
<div>
|
<div>
|
||||||
{websites.map((website, i) => (
|
{websiteList.map((website, i) => (
|
||||||
<Fragment key={website.id}>
|
<Fragment key={website.id}>
|
||||||
{i !== 0 && <Divider />}
|
{i !== 0 && <Divider />}
|
||||||
|
|
||||||
@ -55,7 +80,55 @@ export const Dashboard: React.FC = React.memo(() => {
|
|||||||
</Fragment>
|
</Fragment>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
Dashboard.displayName = 'Dashboard';
|
Dashboard.displayName = 'Dashboard';
|
||||||
|
|
||||||
|
function useDashboardWebsiteList() {
|
||||||
|
const workspace = useCurrentWorkspace();
|
||||||
|
const workspaceId = workspace.id;
|
||||||
|
const { isLoading, websites } = useWorspaceWebsites(workspaceId);
|
||||||
|
const [dashboardOrder, setDashboardOrder] = useState(
|
||||||
|
workspace.dashboardOrder
|
||||||
|
);
|
||||||
|
const updateDashboardOrderMutation =
|
||||||
|
trpc.workspace.updateDashboardOrder.useMutation({
|
||||||
|
onSuccess: defaultSuccessHandler,
|
||||||
|
onError: defaultErrorHandler,
|
||||||
|
});
|
||||||
|
|
||||||
|
const websiteList = useMemo(
|
||||||
|
() =>
|
||||||
|
websites.sort((a, b) => {
|
||||||
|
const aIndex = dashboardOrder.findIndex((item) => item === a.id);
|
||||||
|
const bIndex = dashboardOrder.findIndex((item) => item === b.id);
|
||||||
|
|
||||||
|
// In both cases, if in the sorted list, they are sorted according to the sorted list.
|
||||||
|
// If not in the sorted list, put it first
|
||||||
|
return aIndex - bIndex;
|
||||||
|
}),
|
||||||
|
[websites, dashboardOrder]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleSortEnd = useEvent((oldIndex: number, newIndex: number) => {
|
||||||
|
const newOrder = arrayMove(
|
||||||
|
websiteList.map((w) => w.id),
|
||||||
|
oldIndex,
|
||||||
|
newIndex
|
||||||
|
);
|
||||||
|
setDashboardOrder(newOrder);
|
||||||
|
|
||||||
|
updateDashboardOrderMutation.mutate({
|
||||||
|
workspaceId,
|
||||||
|
dashboardOrder: newOrder,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
isLoading,
|
||||||
|
websiteList,
|
||||||
|
handleSortEnd,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
@ -28,6 +28,7 @@ const createUserSelect = {
|
|||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
name: true,
|
name: true,
|
||||||
|
dashboardOrder: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
workspaces: {
|
workspaces: {
|
||||||
@ -37,6 +38,7 @@ const createUserSelect = {
|
|||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
name: true,
|
name: true,
|
||||||
|
dashboardOrder: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -3,9 +3,11 @@ import { notificationRouter } from './notification';
|
|||||||
import { websiteRouter } from './website';
|
import { websiteRouter } from './website';
|
||||||
import { monitorRouter } from './monitor';
|
import { monitorRouter } from './monitor';
|
||||||
import { userRouter } from './user';
|
import { userRouter } from './user';
|
||||||
|
import { workspaceRouter } from './workspace';
|
||||||
|
|
||||||
export const appRouter = router({
|
export const appRouter = router({
|
||||||
user: userRouter,
|
user: userRouter,
|
||||||
|
workspace: workspaceRouter,
|
||||||
website: websiteRouter,
|
website: websiteRouter,
|
||||||
notification: notificationRouter,
|
notification: notificationRouter,
|
||||||
monitor: monitorRouter,
|
monitor: monitorRouter,
|
||||||
|
24
src/server/trpc/routers/workspace.ts
Normal file
24
src/server/trpc/routers/workspace.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { router, workspaceOwnerProcedure } from '../trpc';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import { prisma } from '../../model/_client';
|
||||||
|
|
||||||
|
export const workspaceRouter = router({
|
||||||
|
updateDashboardOrder: workspaceOwnerProcedure
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
dashboardOrder: z.array(z.string()),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.mutation(async ({ input }) => {
|
||||||
|
const { workspaceId, dashboardOrder } = input;
|
||||||
|
|
||||||
|
await prisma.workspace.update({
|
||||||
|
where: {
|
||||||
|
id: workspaceId,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
dashboardOrder,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
});
|
Loading…
Reference in New Issue
Block a user