feat: add dashboard layout sort
This commit is contained in:
parent
0e669a2ca1
commit
54af2874e4
@ -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",
|
||||
|
@ -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
|
||||
|
@ -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[]
|
||||
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -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,
|
||||
|
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