diff --git a/package.json b/package.json index b115a63..42a462b 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c3aa936..e71d96e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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 diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 467f22a..2375a0a 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -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[] diff --git a/src/client/pages/Dashboard.tsx b/src/client/pages/Dashboard.tsx index ccb79f4..99d24c6 100644 --- a/src/client/pages/Dashboard.tsx +++ b/src/client/pages/Dashboard.tsx @@ -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 ; } @@ -22,40 +27,108 @@ export const Dashboard: React.FC = React.memo(() => { } return ( -
-
+
+
Dashboard
-
-
- {websites.map((website, i) => ( - - {i !== 0 && } - - - - } - /> - - ))} -
+ {isEditLayout ? ( + + {websiteList.map((website) => ( + +
+ {website.name} +
+
+ ))} +
+ ) : ( +
+ {websiteList.map((website, i) => ( + + {i !== 0 && } + + + + + } + /> + + ))} +
+ )}
); }); 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, + }; +} diff --git a/src/server/model/user.ts b/src/server/model/user.ts index 913cca6..5f1e3a5 100644 --- a/src/server/model/user.ts +++ b/src/server/model/user.ts @@ -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, }, }, }, diff --git a/src/server/trpc/routers/index.ts b/src/server/trpc/routers/index.ts index 65e8bfc..c419132 100644 --- a/src/server/trpc/routers/index.ts +++ b/src/server/trpc/routers/index.ts @@ -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, diff --git a/src/server/trpc/routers/workspace.ts b/src/server/trpc/routers/workspace.ts new file mode 100644 index 0000000..4b91f4a --- /dev/null +++ b/src/server/trpc/routers/workspace.ts @@ -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, + }, + }); + }), +});