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",
"@types/uuid": "^9.0.3",
"antd": "^5.9.3",
"array-move": "^3.0.1",
"axios": "^1.5.0",
"badge-maker": "^3.3.1",
"bcryptjs": "^2.4.3",
@ -58,6 +59,7 @@
"puppeteer": "^21.3.8",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-easy-sort": "^1.5.3",
"react-router": "^6.15.0",
"react-router-dom": "^6.15.0",
"request-ip": "^3.3.0",

View File

@ -31,6 +31,9 @@ dependencies:
antd:
specifier: ^5.9.3
version: 5.9.3(react-dom@18.2.0)(react@18.2.0)
array-move:
specifier: ^3.0.1
version: 3.0.1
axios:
specifier: ^1.5.0
version: 1.5.0
@ -127,6 +130,9 @@ dependencies:
react-dom:
specifier: ^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:
specifier: ^6.15.0
version: 6.15.0(react@18.2.0)
@ -2417,6 +2423,11 @@ packages:
resolution: {integrity: sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ==}
dev: false
/array-move@3.0.1:
resolution: {integrity: sha512-H3Of6NIn2nNU1gsVDqDnYKY/LCdWvCMMOWifNGhKcVQgiZ6nOek39aESOvro6zmueP07exSl93YLvkN4fZOkSg==}
engines: {node: '>=10'}
dev: false
/array-tree-filter@2.1.0:
resolution: {integrity: sha512-4ROwICNlNw/Hqa9v+rk5h22KjmzB1JGTMVKP2AKJBOCgb0yL0ASf0+YvCcLNNwquOHNX48jkeZIJ3a+oOQqKcw==}
dev: false
@ -5753,6 +5764,19 @@ packages:
scheduler: 0.23.0
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:
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
dev: false
@ -6577,6 +6601,10 @@ packages:
resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==}
dev: false
/tslib@2.0.1:
resolution: {integrity: sha512-SgIkNheinmEBgx1IUNirK0TUD4X9yjjBRTqqjggWCU3pUEqIk3/Uwl3yRixYKT6WjQuGiwDv4NomL3wqRCj+CQ==}
dev: false
/tslib@2.6.2:
resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==}
dev: false

View File

@ -23,10 +23,11 @@ model User {
}
model Workspace {
id String @id @unique @default(cuid()) @db.VarChar(30)
name String @db.VarChar(100)
createdAt DateTime @default(now()) @db.Timestamptz(6)
updatedAt DateTime @updatedAt @db.Timestamptz(6)
id String @id @unique @default(cuid()) @db.VarChar(30)
name String @db.VarChar(100)
dashboardOrder String[]
createdAt DateTime @default(now()) @db.Timestamptz(6)
updatedAt DateTime @updatedAt @db.Timestamptz(6)
users WorkspacesOnUsers[]
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 { Button, Divider } from 'antd';
import { WebsiteOverview } from '../components/website/WebsiteOverview';
import { useCurrentWorkspaceId } from '../store/user';
import { useCurrentWorkspace } from '../store/user';
import { Loading } from '../components/Loading';
import { useWorspaceWebsites } from '../api/model/website';
import { NoWorkspaceTip } from '../components/NoWorkspaceTip';
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(() => {
const workspaceId = useCurrentWorkspaceId()!;
const { isLoading, websites } = useWorspaceWebsites(workspaceId);
const workspace = useCurrentWorkspace();
const navigate = useNavigate();
const [isEditLayout, setIsEditLayout] = useState(false);
const { isLoading, websiteList, handleSortEnd } = useDashboardWebsiteList();
if (!workspaceId) {
if (!workspace) {
return <NoWorkspaceTip />;
}
@ -22,40 +27,108 @@ export const Dashboard: React.FC = React.memo(() => {
}
return (
<div>
<div className="h-24 flex items-center">
<div className="py-4">
<div className="h-20 flex items-center">
<div className="text-2xl flex-1">Dashboard</div>
<div>
<Button icon={<EditOutlined />} size="large">
Edit
<Button
icon={<EditOutlined />}
size="large"
onClick={() => setIsEditLayout((state) => !state)}
>
{isEditLayout ? 'Done' : 'Edit'}
</Button>
</div>
</div>
<div>
{websites.map((website, i) => (
<Fragment key={website.id}>
{i !== 0 && <Divider />}
<WebsiteOverview
website={website}
actions={
<>
<Button
type="primary"
size="large"
onClick={() => {
navigate(`/website/${website.id}`);
}}
>
View Details <ArrowRightOutlined />
</Button>
</>
}
/>
</Fragment>
))}
</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>
{websiteList.map((website, i) => (
<Fragment key={website.id}>
{i !== 0 && <Divider />}
<WebsiteOverview
website={website}
actions={
<>
<Button
type="primary"
size="large"
onClick={() => {
navigate(`/website/${website.id}`);
}}
>
View Details <ArrowRightOutlined />
</Button>
</>
}
/>
</Fragment>
))}
</div>
)}
</div>
);
});
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: {
id: true,
name: true,
dashboardOrder: true,
},
},
workspaces: {
@ -37,6 +38,7 @@ const createUserSelect = {
select: {
id: true,
name: true,
dashboardOrder: true,
},
},
},

View File

@ -3,9 +3,11 @@ import { notificationRouter } from './notification';
import { websiteRouter } from './website';
import { monitorRouter } from './monitor';
import { userRouter } from './user';
import { workspaceRouter } from './workspace';
export const appRouter = router({
user: userRouter,
workspace: workspaceRouter,
website: websiteRouter,
notification: notificationRouter,
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,
},
});
}),
});