feat: add dashboard layout sort

This commit is contained in:
moonrailgun 2023-10-17 00:23:49 +08:00
parent 0e669a2ca1
commit 54af2874e4
7 changed files with 168 additions and 36 deletions

View File

@ -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",

View File

@ -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

View File

@ -23,10 +23,11 @@ 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)
createdAt DateTime @default(now()) @db.Timestamptz(6) dashboardOrder String[]
updatedAt DateTime @updatedAt @db.Timestamptz(6) createdAt DateTime @default(now()) @db.Timestamptz(6)
updatedAt DateTime @updatedAt @db.Timestamptz(6)
users WorkspacesOnUsers[] users WorkspacesOnUsers[]
websites Website[] websites Website[]

View File

@ -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,40 +27,108 @@ 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>
<div>
{websites.map((website, i) => (
<Fragment key={website.id}>
{i !== 0 && <Divider />}
<WebsiteOverview {isEditLayout ? (
website={website} <SortableList
actions={ className="space-y-2"
<> lockAxis="y"
<Button onSortEnd={handleSortEnd}
type="primary" >
size="large" {websiteList.map((website) => (
onClick={() => { <SortableItem key={website.id}>
navigate(`/website/${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>
View Details <ArrowRightOutlined /> </SortableItem>
</Button> ))}
</> </SortableList>
} ) : (
/> <div>
</Fragment> {websiteList.map((website, i) => (
))} <Fragment key={website.id}>
</div> {i !== 0 && <Divider />}
<WebsiteOverview
website={website}
actions={
<>
<Button
type="primary"
size="large"
onClick={() => {
navigate(`/website/${website.id}`);
}}
>
View Details <ArrowRightOutlined />
</Button>
</>
}
/>
</Fragment>
))}
</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,
};
}

View File

@ -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,
}, },
}, },
}, },

View File

@ -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,

View 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,
},
});
}),
});