From a4c4a6d9e9851f60d6492fac15e98395d713bb68 Mon Sep 17 00:00:00 2001 From: moonrailgun Date: Tue, 5 Sep 2023 15:32:16 +0800 Subject: [PATCH] feat: website info and edit --- src/client/App.tsx | 63 ++++---- src/client/api/model/website.ts | 42 ++++++ src/client/components/ErrorTip.tsx | 6 + src/client/components/TokenLoginContainer.tsx | 27 ++++ src/client/components/WebsiteInfo.tsx | 87 +++++++++++ src/client/components/WebsiteList.tsx | 135 ++++++++++++++++++ src/client/main.tsx | 2 +- src/client/pages/Login.tsx | 13 +- src/client/pages/Settings.tsx | 45 +++++- src/client/pages/Website.tsx | 119 +-------------- src/client/store/user.ts | 8 ++ src/server/middleware/workspace.ts | 3 +- src/server/model/workspace.ts | 34 +++++ src/server/router/workspace.ts | 89 +++++++++++- src/server/tsconfig.json | 1 + 15 files changed, 514 insertions(+), 160 deletions(-) create mode 100644 src/client/components/ErrorTip.tsx create mode 100644 src/client/components/TokenLoginContainer.tsx create mode 100644 src/client/components/WebsiteInfo.tsx create mode 100644 src/client/components/WebsiteList.tsx diff --git a/src/client/App.tsx b/src/client/App.tsx index 81fa01d..bd6ac28 100644 --- a/src/client/App.tsx +++ b/src/client/App.tsx @@ -10,38 +10,51 @@ import { useUserStore } from './store/user'; import { Register } from './pages/Register'; import { QueryClientProvider } from '@tanstack/react-query'; import { queryClient } from './api/cache'; -function App() { +import { TokenLoginContainer } from './components/TokenLoginContainer'; +import React from 'react'; + +export const AppRoutes: React.FC = React.memo(() => { const { info } = useUserStore(); + return ( + + {info ? ( + }> + } /> + } /> + } /> + } /> + } /> + + ) : ( + + } /> + } /> + + )} + + + } + /> + + ); +}); +AppRoutes.displayName = 'AppRoutes'; + +export const App: React.FC = React.memo(() => { return (
- - {info && ( - }> - } /> - } /> - } /> - } /> - } /> - - )} - - } /> - } /> - - - } - /> - + + +
); -} - -export default App; +}); +App.displayName = 'App'; diff --git a/src/client/api/model/website.ts b/src/client/api/model/website.ts index eec40ae..9b22395 100644 --- a/src/client/api/model/website.ts +++ b/src/client/api/model/website.ts @@ -26,6 +26,33 @@ export async function getWorkspaceWebsites( return data.websites; } +export async function getWorkspaceWebsiteInfo( + workspaceId: string, + websiteId: string +): Promise { + const { data } = await request.get(`/api/workspace/website/${websiteId}`, { + params: { + workspaceId, + }, + }); + + return data.website; +} + +export async function updateWorkspaceWebsiteInfo( + workspaceId: string, + websiteId: string, + info: { name: string; domain: string } +) { + await request.post(`/api/workspace/website/${websiteId}`, { + workspaceId, + name: info.name, + domain: info.domain, + }); + + queryClient.resetQueries(['websites', workspaceId]); +} + export function useWorspaceWebsites(workspaceId: string) { const { data: websites = [], isLoading } = useQuery( ['websites', workspaceId], @@ -37,6 +64,21 @@ export function useWorspaceWebsites(workspaceId: string) { return { websites, isLoading }; } +export function useWorkspaceWebsiteInfo( + workspaceId: string, + websiteId: string +) { + const { data: website = null, isLoading } = useQuery( + ['website', workspaceId, websiteId], + () => { + return getWorkspaceWebsiteInfo(workspaceId, websiteId); + }, + { cacheTime: 0 } + ); + + return { website, isLoading }; +} + export function refreshWorkspaceWebsites(workspaceId: string) { queryClient.refetchQueries(['websites', workspaceId]); } diff --git a/src/client/components/ErrorTip.tsx b/src/client/components/ErrorTip.tsx new file mode 100644 index 0000000..881e09a --- /dev/null +++ b/src/client/components/ErrorTip.tsx @@ -0,0 +1,6 @@ +import React from 'react'; + +export const ErrorTip: React.FC = React.memo(() => { + return
An unexpected error has occurred
; +}); +ErrorTip.displayName = 'ErrorTip'; diff --git a/src/client/components/TokenLoginContainer.tsx b/src/client/components/TokenLoginContainer.tsx new file mode 100644 index 0000000..0a91d9c --- /dev/null +++ b/src/client/components/TokenLoginContainer.tsx @@ -0,0 +1,27 @@ +import React, { useEffect, useState } from 'react'; +import { getJWT } from '../api/auth'; +import { loginWithToken } from '../api/model/user'; +import { Loading } from './Loading'; + +export const TokenLoginContainer: React.FC = + React.memo((props) => { + const [loading, setLoading] = useState(true); + + useEffect(() => { + const token = getJWT(); + if (token) { + loginWithToken().then(() => { + setLoading(false); + }); + } else { + setLoading(false); + } + }, []); + + if (loading) { + return ; + } + + return <>{props.children}; + }); +TokenLoginContainer.displayName = 'TokenLoginContainer'; diff --git a/src/client/components/WebsiteInfo.tsx b/src/client/components/WebsiteInfo.tsx new file mode 100644 index 0000000..465b927 --- /dev/null +++ b/src/client/components/WebsiteInfo.tsx @@ -0,0 +1,87 @@ +import { Button, Form, Input, message } from 'antd'; +import React from 'react'; +import { useParams } from 'react-router'; +import { + updateWorkspaceWebsiteInfo, + useWorkspaceWebsiteInfo, +} from '../api/model/website'; +import { useRequest } from '../hooks/useRequest'; +import { useCurrentWorkspaceId } from '../store/user'; +import { ErrorTip } from './ErrorTip'; +import { Loading } from './Loading'; +import { NoWorkspaceTip } from './NoWorkspaceTip'; + +export const WebsiteInfo: React.FC = React.memo(() => { + const { websiteId } = useParams<{ + websiteId: string; + }>(); + const currentWorkspaceId = useCurrentWorkspaceId(); + const { website, isLoading } = useWorkspaceWebsiteInfo( + currentWorkspaceId!, + websiteId! + ); + + const [, handleSave] = useRequest( + async (values: { name: string; domain: string }) => { + await updateWorkspaceWebsiteInfo(currentWorkspaceId!, websiteId!, { + name: values.name, + domain: values.domain, + }); + + message.success('Save Success'); + } + ); + + if (!currentWorkspaceId) { + return ; + } + + if (!websiteId) { + return ; + } + + if (isLoading) { + return ; + } + + if (!website) { + return ; + } + + return ( +
+
+
Website Info
+
+ +
+
+ + + + + + + + + + + + + +
+
+
+ ); +}); +WebsiteInfo.displayName = 'WebsiteInfo'; diff --git a/src/client/components/WebsiteList.tsx b/src/client/components/WebsiteList.tsx new file mode 100644 index 0000000..7aed08f --- /dev/null +++ b/src/client/components/WebsiteList.tsx @@ -0,0 +1,135 @@ +import { + BarChartOutlined, + EditOutlined, + PlusOutlined, +} from '@ant-design/icons'; +import { Button, Form, Input, Modal, Table } from 'antd'; +import { ColumnsType } from 'antd/es/table'; +import React, { useMemo, useState } from 'react'; +import { + addWorkspaceWebsite, + refreshWorkspaceWebsites, + useWorspaceWebsites, + WebsiteInfo, +} from '../api/model/website'; +import { Loading } from './Loading'; +import { NoWorkspaceTip } from './NoWorkspaceTip'; +import { useRequest } from '../hooks/useRequest'; +import { useUserStore } from '../store/user'; +import { useEvent } from '../hooks/useEvent'; +import { useNavigate } from 'react-router'; + +export const WebsiteList: React.FC = React.memo(() => { + const [isModalOpen, setIsModalOpen] = useState(false); + const currentWorkspace = useUserStore( + (state) => state.info?.currentWorkspace + ); + const [form] = Form.useForm(); + + const [{ loading }, handleAddWebsite] = useRequest(async () => { + await form.validateFields(); + const values = form.getFieldsValue(); + + await addWorkspaceWebsite(currentWorkspace!.id, values.name, values.domain); + refreshWorkspaceWebsites(currentWorkspace!.id); + setIsModalOpen(false); + + form.resetFields(); + }); + + if (!currentWorkspace) { + return ; + } + + return ( +
+
+
Websites
+
+ +
+
+ + + + handleAddWebsite()} + onCancel={() => setIsModalOpen(false)} + > +
+ + + + + + +
+
+
+ ); +}); +WebsiteList.displayName = 'WebsiteList'; + +const WebsiteListTable: React.FC<{ workspaceId: string }> = React.memo( + (props) => { + const { websites, isLoading } = useWorspaceWebsites(props.workspaceId); + const navigate = useNavigate(); + + const handleEdit = useEvent((websiteId) => { + console.log(`/settings/website/${websiteId}`); + navigate(`/settings/website/${websiteId}`); + }); + + const columns = useMemo((): ColumnsType => { + return [ + { + dataIndex: 'name', + title: 'Name', + }, + { + dataIndex: 'domain', + title: 'Domain', + }, + { + key: 'action', + render: (record) => { + return ( +
+ + +
+ ); + }, + }, + ] as ColumnsType; + }, []); + + if (isLoading) { + return ; + } + + return ; + } +); +WebsiteListTable.displayName = 'WebsiteListTable'; diff --git a/src/client/main.tsx b/src/client/main.tsx index 424bf74..8734aa8 100644 --- a/src/client/main.tsx +++ b/src/client/main.tsx @@ -3,7 +3,7 @@ import './index.css'; import React from 'react'; import ReactDOM from 'react-dom/client'; -import App from './App'; +import { App } from './App'; ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( diff --git a/src/client/pages/Login.tsx b/src/client/pages/Login.tsx index 39e2ed3..c5898bd 100644 --- a/src/client/pages/Login.tsx +++ b/src/client/pages/Login.tsx @@ -1,9 +1,7 @@ import { Button, Form, Input, Typography } from 'antd'; -import React, { useEffect } from 'react'; +import React from 'react'; import { model } from '../api/model'; import { useNavigate } from 'react-router'; -import { loginWithToken } from '../api/model/user'; -import { getJWT } from '../api/auth'; import { useRequest } from '../hooks/useRequest'; export const Login: React.FC = React.memo(() => { @@ -14,15 +12,6 @@ export const Login: React.FC = React.memo(() => { navigate('/dashboard'); }); - useEffect(() => { - const token = getJWT(); - if (token) { - loginWithToken().then(() => { - navigate('/dashboard'); - }); - } - }, []); - return (
diff --git a/src/client/pages/Settings.tsx b/src/client/pages/Settings.tsx index 344a5ea..3f955df 100644 --- a/src/client/pages/Settings.tsx +++ b/src/client/pages/Settings.tsx @@ -1,6 +1,49 @@ +import { Menu, MenuProps } from 'antd'; import React from 'react'; +import { Routes, Route, useLocation, useNavigate } from 'react-router-dom'; +import { WebsiteInfo } from '../components/WebsiteInfo'; +import { WebsiteList } from '../components/WebsiteList'; +import { useEvent } from '../hooks/useEvent'; export const Settings: React.FC = React.memo(() => { - return
Settings
; + const navigate = useNavigate(); + const { pathname } = useLocation(); + + const items: MenuProps['items'] = [ + { + key: 'websites', + label: 'Websites', + }, + ]; + + const onClick: MenuProps['onClick'] = useEvent((e) => { + navigate(`/settings/${e.key}`); + }); + + const selectedKey = + (items.find((item) => pathname.startsWith(`/settings/${item?.key}`)) + ?.key as string) ?? 'websites'; + + return ( +
+
+ + {pathname} +
+
+ + } /> + } /> + } /> + +
+
+ ); }); Settings.displayName = 'Settings'; diff --git a/src/client/pages/Website.tsx b/src/client/pages/Website.tsx index b5af3c9..9b56fad 100644 --- a/src/client/pages/Website.tsx +++ b/src/client/pages/Website.tsx @@ -1,120 +1,7 @@ -import { - BarChartOutlined, - EditOutlined, - PlusOutlined, -} from '@ant-design/icons'; -import { Button, Form, Input, Modal, Table } from 'antd'; -import { ColumnsType } from 'antd/es/table'; -import React, { useMemo, useState } from 'react'; -import { - addWorkspaceWebsite, - refreshWorkspaceWebsites, - useWorspaceWebsites, - WebsiteInfo, -} from '../api/model/website'; -import { Loading } from '../components/Loading'; -import { NoWorkspaceTip } from '../components/NoWorkspaceTip'; -import { useRequest } from '../hooks/useRequest'; -import { useUserStore } from '../store/user'; +import React from 'react'; +import { WebsiteList } from '../components/WebsiteList'; export const Website: React.FC = React.memo(() => { - const [isModalOpen, setIsModalOpen] = useState(false); - const currentWorkspace = useUserStore( - (state) => state.info?.currentWorkspace - ); - const [form] = Form.useForm(); - - const [{ loading }, handleAddWebsite] = useRequest(async () => { - await form.validateFields(); - const values = form.getFieldsValue(); - - await addWorkspaceWebsite(currentWorkspace!.id, values.name, values.domain); - refreshWorkspaceWebsites(currentWorkspace!.id); - setIsModalOpen(false); - - form.resetFields(); - }); - - if (!currentWorkspace) { - return ; - } - - return ( -
-
-
Websites
-
- -
-
- - - - handleAddWebsite()} - onCancel={() => setIsModalOpen(false)} - > -
- - - - - - - -
-
- ); + return ; }); Website.displayName = 'Website'; - -const WebsiteList: React.FC<{ workspaceId: string }> = React.memo((props) => { - const { websites, isLoading } = useWorspaceWebsites(props.workspaceId); - - const columns = useMemo((): ColumnsType => { - return [ - { - dataIndex: 'name', - title: 'Name', - }, - { - dataIndex: 'domain', - title: 'Domain', - }, - { - key: 'action', - render: () => { - return ( -
- - -
- ); - }, - }, - ]; - }, []); - - if (isLoading) { - return ; - } - - return
; -}); -WebsiteList.displayName = 'WebsiteList'; diff --git a/src/client/store/user.ts b/src/client/store/user.ts index efafce1..3a28a78 100644 --- a/src/client/store/user.ts +++ b/src/client/store/user.ts @@ -22,3 +22,11 @@ export function setUserInfo(info: UserLoginInfo) { info, }); } + +export function useCurrentWorkspaceId() { + const currentWorkspaceId = useUserStore( + (state) => state.info?.currentWorkspace?.id + ); + + return currentWorkspaceId; +} diff --git a/src/server/middleware/workspace.ts b/src/server/middleware/workspace.ts index 634d928..79bc259 100644 --- a/src/server/middleware/workspace.ts +++ b/src/server/middleware/workspace.ts @@ -3,7 +3,8 @@ import { checkIsWorkspaceUser } from '../model/workspace'; export function workspacePermission(): Handler { return async (req, res, next) => { - const workspaceId = req.body.workspaceId ?? req.query.workspaceId; + const workspaceId = + req.body.workspaceId ?? req.query.workspaceId ?? req.params.workspaceId; if (!workspaceId) { throw new Error('Cannot find workspace id'); diff --git a/src/server/model/workspace.ts b/src/server/model/workspace.ts index 4ec74f0..509d3e4 100644 --- a/src/server/model/workspace.ts +++ b/src/server/model/workspace.ts @@ -35,6 +35,40 @@ export async function getWorkspaceWebsites(workspaceId: string) { return workspace?.websites ?? []; } +export async function getWorkspaceWebsiteInfo( + workspaceId: string, + websiteId: string +) { + const websiteInfo = await prisma.website.findUnique({ + where: { + id: websiteId, + workspaceId, + }, + }); + + return websiteInfo; +} + +export async function updateWorkspaceWebsiteInfo( + workspaceId: string, + websiteId: string, + name: string, + domain: string +) { + const websiteInfo = await prisma.website.update({ + where: { + id: websiteId, + workspaceId, + }, + data: { + name, + domain, + }, + }); + + return websiteInfo; +} + export async function addWorkspaceWebsite( workspaceId: string, name: string, diff --git a/src/server/router/workspace.ts b/src/server/router/workspace.ts index 1311dfe..c462b58 100644 --- a/src/server/router/workspace.ts +++ b/src/server/router/workspace.ts @@ -1,8 +1,13 @@ import { Router } from 'express'; import { auth } from '../middleware/auth'; -import { body, query, validate } from '../middleware/validate'; +import { body, param, query, validate } from '../middleware/validate'; import { workspacePermission } from '../middleware/workspace'; -import { addWorkspaceWebsite, getWorkspaceWebsites } from '../model/workspace'; +import { + addWorkspaceWebsite, + getWorkspaceWebsiteInfo, + getWorkspaceWebsites, + updateWorkspaceWebsiteInfo, +} from '../model/workspace'; export const workspaceRouter = Router(); @@ -34,8 +39,16 @@ workspaceRouter.post( .withMessage('workspaceId should be string') .isUUID() .withMessage('workspaceId should be UUID'), - body('name').isString().withMessage('name should be a string'), - body('domain').isURL().withMessage('domain should be URL') + body('name') + .isString() + .withMessage('name should be string') + .isLength({ max: 100 }) + .withMessage('length should be under 100'), + body('domain') + .isURL() + .withMessage('domain should be URL') + .isLength({ max: 500 }) + .withMessage('length should be under 500') ), auth(), workspacePermission(), @@ -47,3 +60,71 @@ workspaceRouter.post( res.json({ website }); } ); + +workspaceRouter.get( + '/website/:websiteId', + validate( + query('workspaceId') + .isString() + .withMessage('workspaceId should be string') + .isUUID() + .withMessage('workspaceId should be UUID'), + param('websiteId') + .isString() + .withMessage('workspaceId should be string') + .isUUID() + .withMessage('workspaceId should be UUID') + ), + auth(), + workspacePermission(), + async (req, res) => { + const workspaceId = req.query.workspaceId as string; + const websiteId = req.params.websiteId; + + const website = await getWorkspaceWebsiteInfo(workspaceId, websiteId); + + res.json({ website }); + } +); + +workspaceRouter.post( + '/website/:websiteId', + validate( + body('workspaceId') + .isString() + .withMessage('workspaceId should be string') + .isUUID() + .withMessage('workspaceId should be UUID'), + param('websiteId') + .isString() + .withMessage('workspaceId should be string') + .isUUID() + .withMessage('workspaceId should be UUID'), + body('name') + .isString() + .withMessage('name should be string') + .isLength({ max: 100 }) + .withMessage('length should be under 100'), + body('domain') + .isURL() + .withMessage('domain should be URL') + .isLength({ max: 500 }) + .withMessage('length should be under 500') + ), + auth(), + workspacePermission(), + async (req, res) => { + const workspaceId = req.query.workspaceId as string; + const websiteId = req.params.websiteId; + const { name, domain } = req.body; + + const website = await updateWorkspaceWebsiteInfo( + workspaceId, + websiteId, + name, + domain + ); + + res.json({ website }); + } +); diff --git a/src/server/tsconfig.json b/src/server/tsconfig.json index 6f83eb6..63e20a2 100644 --- a/src/server/tsconfig.json +++ b/src/server/tsconfig.json @@ -1,3 +1,4 @@ { "extends": "../../tsconfig.json", + "include": ["./**/*"] }