Merge pull request #289 from fantasticit/fix/20250211

Fix/20250211
This commit is contained in:
fantasticit
2025-02-16 12:54:21 +08:00
committed by GitHub
44 changed files with 827 additions and 143 deletions
+14 -17
View File
@@ -4,15 +4,12 @@ client:
assetPrefix: '/'
apiUrl: 'http://localhost:5002/api'
collaborationUrl: 'ws://localhost:5003'
# 以下为页面 meta 配置
seoAppName: '云策文档'
seoDescription: '云策文档是一款开源知识管理工具。通过独立的知识库空间,结构化地组织在线协作文档,实现知识的积累与沉淀,促进知识的复用与流通。'
seoKeywords: '云策文档,协作,文档,fantasticit,https://github.com/fantasticit/think'
# 预先连接的来源,空格分割(比如图片存储服务器)
dnsPrefetch: '//wipi.oss-cn-shanghai.aliyuncs.com'
# 站点地址(如:https://think.codingit.cn/),一定要设置,否则会出现 cookie、跨域等问题
siteUrl: 'http://localhost:5001'
# 一定要设置,否则会出现 cookie、跨域等问题,注意如果网页部署在 https://think.codingit.cn,那这里的值应当为 .codingit.cn(没有 think)
siteUrl:
'http://localhost:5001'
# 一定要设置,否则会出现 cookie、跨域等问题,注意如果网页部署在 https://think.codingit.cn,那这里的值应当为 .codingit.cn(没有 think)
siteDomain: ''
server:
@@ -58,18 +55,18 @@ oss:
# 线上更改为服务端地址(如:https://api.codingit.cn)
server: 'http://localhost:5002'
s3:
enable: true
enable: false
config:
# isp 提供商,可选 minio,s3,aliyun,tencent
cloudisp: 'minio'
accessKeyId: ''
secretAccessKey: ''
bucket: ''
region: ''
# 仅cloudisp 的值为 minio/s3 时配置,其它提供商为空
forcePathStyle: false
# 仅cloudisp 的值为 minio 时配置,其它提供商为空
endpoint: ''
# isp 提供商,可选 minio,s3,aliyun,tencent
cloudisp: 'minio'
accessKeyId: ''
secretAccessKey: ''
bucket: ''
region: ''
# 仅cloudisp 的值为 minio/s3 时配置,其它提供商为空
forcePathStyle: false
# 仅cloudisp 的值为 minio 时配置,其它提供商为空
endpoint: ''
# jwt 配置
jwt:
+12
View File
@@ -0,0 +1,12 @@
# 以下为前台动态渲染的配置,修改后,无需打包,重启服务即可
dynamic:
# 输入 logo 的图片地址
logo: ''
appName: '云策文档'
appDescription: '云策文档是一款开源知识管理工具。通过独立的知识库空间,结构化地组织在线协作文档,实现知识的积累与沉淀,促进知识的复用与流通。'
appKeywords: '云策文档,协作,文档,fantasticit,https://github.com/fantasticit/think'
# 版权信息,可以是 HTML
copyrightInformation: '<p>Develop by fantasticit</p>'
# 外部跳转按钮配置
externalButtonText: 'Github'
externalButtonURL: 'https://github.com/fantasticit/think'
+1 -3
View File
@@ -25,9 +25,7 @@ const nextConfig = semi({
COLLABORATION_API_URL: config.client.collaborationUrl,
ENABLE_OSS_S3: config.oss.s3.enable,
DNS_PREFETCH: (config.client.dnsPrefetch || '').split(' '),
SEO_APPNAME: config.client.seoAppName,
SEO_DESCRIPTION: config.client.seoDescription,
SEO_KEYWORDS: config.client.seoKeywords,
SITE_URL: config.client.siteUrl,
},
webpack: (config, { dev, isServer }) => {
config.resolve.plugins.push(new TsconfigPathsPlugin());
@@ -0,0 +1,141 @@
import React, { useCallback, useRef, useState } from 'react';
import { Avatar, Button, Form, Image, Modal, Space, Table } from '@douyinfe/semi-ui';
import { FormApi } from '@douyinfe/semi-ui/lib/es/form';
import { AdType } from '@think/domains';
import { DataRender } from 'components/data-render';
import { Upload } from 'components/upload';
import { useAd } from 'data/ad';
const { Option } = Form.Select;
const AdTypeTextMap = {
[AdType.shareDocCover]: '分享文档主图位',
[AdType.shareDocAside]: '分享文档侧边位',
};
const columns = [
{
title: '类型',
dataIndex: 'type',
render: (type) => {
return AdTypeTextMap[type];
},
},
{
title: '广告图',
dataIndex: 'cover',
render: (url) => {
return (
<div>
<Image width={180} height={60} src={url}></Image>
</div>
);
},
},
{
title: '广告链接',
dataIndex: 'url',
},
];
export const Ad = () => {
const { data, loading, error, createAd, deleteAd } = useAd();
const [visible, toggleVisible] = useState(false);
const $form = useRef<FormApi>();
const [currentCover, setCurrentCover] = useState('');
const setCover = useCallback((url) => {
$form.current.setValue('cover', url);
setCurrentCover(url);
}, []);
const handleOk = useCallback(() => {
$form.current.validate().then((values) => {
if (!values.email) {
delete values.email;
}
createAd(values).then(() => {
$form.current.reset();
setCover('');
toggleVisible(false);
});
});
}, [createAd, setCover, toggleVisible]);
return (
<DataRender
loading={loading}
error={error}
normalContent={() => (
<div style={{ marginTop: 16 }}>
<Button
htmlType="submit"
type="primary"
theme="solid"
onClick={() => {
toggleVisible(true);
}}
style={{ margin: '16px 0' }}
>
添加
</Button>
<Modal
title="添加广告"
visible={visible}
onCancel={() => {
toggleVisible(false);
}}
style={{ maxWidth: '96vw' }}
onOk={handleOk}
>
<Form
initValues={{ type: AdType.shareDocCover, cover: '', url: '' }}
getFormApi={(formApi) => ($form.current = formApi)}
labelPosition="left"
>
<Form.Select field="type" label={{ text: '类型' }}>
<Option value={AdType.shareDocCover}>{AdTypeTextMap[AdType.shareDocCover]}</Option>
<Option value={AdType.shareDocAside}>{AdTypeTextMap[AdType.shareDocAside]}</Option>
</Form.Select>
<Form.Slot label="图片">
<Space align="end">
<Avatar src={currentCover} shape="square"></Avatar>
<Upload onOK={setCover} />
</Space>
</Form.Slot>
<Form.Input label="链接" field="url" style={{ width: '100%' }} placeholder="请输入跳转链接"></Form.Input>
</Form>
</Modal>
<Table
columns={[
...columns,
{
title: '操作',
dataIndex: 'id',
render: (id) => {
return (
<Button
type="danger"
onClick={() => {
deleteAd(id);
}}
>
删除
</Button>
);
},
},
]}
dataSource={data}
pagination={false}
/>
</div>
)}
/>
);
};
@@ -2,6 +2,7 @@ import React from 'react';
import { TabPane, Tabs } from '@douyinfe/semi-ui';
import { Ad } from './ad';
import { Mail } from './mail';
import { System } from './system';
@@ -13,6 +14,7 @@ interface IProps {
const TitleMap = {
base: '系统管理',
mail: '邮箱服务',
ad: '广告管理',
};
export const SystemConfig: React.FC<IProps> = ({ tab, onNavigate }) => {
@@ -25,6 +27,10 @@ export const SystemConfig: React.FC<IProps> = ({ tab, onNavigate }) => {
<TabPane tab={TitleMap['mail']} itemKey="mail">
<Mail />
</TabPane>
<TabPane tab={TitleMap['ad']} itemKey="ad">
<Ad />
</TabPane>
</Tabs>
);
};
@@ -35,6 +35,8 @@ export const Mail = () => {
closeIcon={null}
/>
{/*
// @ts-ignore */}
<Form initValues={data} onChange={onFormChange} onSubmit={onFinish}>
<Form.Input
field="emailServiceHost"
@@ -30,6 +30,8 @@ export const System = () => {
error={error}
normalContent={() => (
<div style={{ marginTop: 16 }}>
{/*
// @ts-ignore */}
<Form labelPosition="left" initValues={data} onChange={onFormChange} onSubmit={onFinish}>
<Form.Switch
field="isSystemLocked"
+15 -7
View File
@@ -1,18 +1,26 @@
import { IconLikeHeart } from '@douyinfe/semi-icons';
import { Space, Typography } from '@douyinfe/semi-ui';
import { RuntimeConfig } from 'hooks/use-runtime-config';
const { Text } = Typography;
export const Author = () => {
const config = RuntimeConfig.useHook();
return (
<div style={{ padding: '16px 0', textAlign: 'center' }}>
<Text>
<Space>
Develop by
<Text link={{ href: 'https://github.com/fantasticit/think' }}>fantasticit</Text>
with <IconLikeHeart style={{ color: 'red' }} />
</Space>
</Text>
{config.copyrightInformation ? (
<div dangerouslySetInnerHTML={{ __html: config.copyrightInformation }}></div>
) : (
<Text>
<Space>
Develop by
<Text link={{ href: 'https://github.com/fantasticit/think' }}>fantasticit</Text>
with <IconLikeHeart style={{ color: 'red' }} />
</Space>
</Text>
)}
</div>
);
};
@@ -1,4 +1,5 @@
.wrap {
position: relative;
display: flex;
flex-direction: column;
height: 100%;
@@ -8,6 +9,23 @@
position: sticky;
}
.adWrap {
max-height: 180px;
overflow: hidden;
}
.adAsideWrap {
position: absolute;
bottom: 1rem;
left: 1rem;
width: 180px;
height: 70px;
>div {
height: 100%;
}
}
.contentWrap {
flex: 1;
overflow: hidden;
@@ -1,28 +1,40 @@
import { Breadcrumb, Button, Form, Layout, Nav, Skeleton, Space, Tooltip, Typography } from '@douyinfe/semi-ui';
import { IconRoute } from '@douyinfe/semi-icons';
import { FormApi } from '@douyinfe/semi-ui/lib/es/form';
import { DataRender } from 'components/data-render';
import { Divider } from 'components/divider';
import { DocumentStyle } from 'components/document/style';
import { LogoImage, LogoText } from 'components/logo';
import { Seo } from 'components/seo';
import { Theme } from 'components/theme';
import { User } from 'components/user';
import { usePublicDocumentDetail } from 'data/document';
import { useMount } from 'hooks/use-mount';
import { IsOnMobile } from 'hooks/use-on-mobile';
import { SecureDocumentIllustration } from 'illustrations/secure-document';
import Link from 'next/link';
import React, { useCallback, useMemo, useRef } from 'react';
import { createPortal } from 'react-dom';
import { CollaborationEditor } from 'tiptap/editor';
import {
Breadcrumb,
Button,
Form,
Layout,
Nav,
Skeleton,
Space,
Tooltip,
Typography,
Carousel,
} from "@douyinfe/semi-ui";
import { IconRoute } from "@douyinfe/semi-icons";
import { FormApi } from "@douyinfe/semi-ui/lib/es/form";
import { DataRender } from "components/data-render";
import { Divider } from "components/divider";
import { DocumentStyle } from "components/document/style";
import { LogoImage, LogoText } from "components/logo";
import { Seo } from "components/seo";
import { Theme } from "components/theme";
import { User } from "components/user";
import { usePublicDocumentDetail } from "data/document";
import { useMount } from "hooks/use-mount";
import { IsOnMobile } from "hooks/use-on-mobile";
import { SecureDocumentIllustration } from "illustrations/secure-document";
import Link from "next/link";
import React, { useCallback, useMemo, useRef } from "react";
import { createPortal } from "react-dom";
import { CollaborationEditor } from "tiptap/editor";
import { Author } from '../author';
import styles from './index.module.scss';
import Router from 'next/router';
import { useRouterQuery } from 'hooks/use-router-query';
import { IDocument, IWiki } from '@think/domains';
import { DocumentFullscreen } from 'components/document/fullscreen';
import { Author } from "../author";
import styles from "./index.module.scss";
import Router from "next/router";
import { useRouterQuery } from "hooks/use-router-query";
import { AdType, IDocument, IWiki } from "@think/domains";
import { DocumentFullscreen } from "components/document/fullscreen";
import { useAd } from "data/ad";
const { Header } = Layout;
const { Text } = Typography;
@@ -32,18 +44,25 @@ interface IProps {
hideLogo?: boolean;
}
export const DocumentPublicReader: React.FC<IProps> = ({ documentId, hideLogo = false }) => {
export const DocumentPublicReader: React.FC<IProps> = ({
documentId,
hideLogo = false,
}) => {
const $form = useRef<FormApi>();
const mounted = useMount();
const { wikiId: currentWikiId } = useRouterQuery<{ wikiId: IWiki['id']; documentId: IDocument['id'] }>();
const { wikiId: currentWikiId } = useRouterQuery<{
wikiId: IWiki["id"];
documentId: IDocument["id"];
}>();
const { data, loading, error, query } = usePublicDocumentDetail(documentId);
const { data: ads } = useAd();
const { isMobile } = IsOnMobile.useHook();
const renderAuthor = useCallback(
(element) => {
if (!document) return null;
const target = element && element.querySelector('.ProseMirror .title');
const target = element && element.querySelector(".ProseMirror .title");
if (target) {
return createPortal(<Author document={data} />, target);
@@ -62,27 +81,42 @@ export const DocumentPublicReader: React.FC<IProps> = ({ documentId, hideLogo =
const toPublicWikiOrDocumentURL = useCallback(() => {
Router.push({
pathname: currentWikiId ? '/share/document/[documentId]' : '/share/wiki/[wikiId]/document/[documentId]',
query: currentWikiId ? { documentId } : { wikiId: data.wikiId, documentId },
pathname: currentWikiId
? "/share/document/[documentId]"
: "/share/wiki/[wikiId]/document/[documentId]",
query: currentWikiId
? { documentId }
: { wikiId: data.wikiId, documentId },
});
}, [currentWikiId, documentId, data]);
console.log(ads || []);
const content = useMemo(() => {
if (error) {
// @ts-ignore
if (error.statusCode === 400) {
return (
<div>
<Seo title={'输入密码后查看'} />
<Seo title={"输入密码后查看"} />
<Form
style={{ width: 320, maxWidth: 'calc(100vw - 160px)', margin: '10vh auto' }}
initValues={{ password: '' }}
style={{
width: 320,
maxWidth: "calc(100vw - 160px)",
margin: "10vh auto",
}}
initValues={{ password: "" }}
getFormApi={(formApi) => ($form.current = formApi)}
labelPosition="left"
onSubmit={handleOk}
layout="horizontal"
>
<Form.Input autofocus label="密码" field="password" placeholder="请输入密码" />
<Form.Input
autofocus
label="密码"
field="password"
placeholder="请输入密码"
/>
<Button type="primary" theme="solid" htmlType="submit">
提交
</Button>
@@ -94,16 +128,16 @@ export const DocumentPublicReader: React.FC<IProps> = ({ documentId, hideLogo =
return (
<div
style={{
margin: '10%',
display: 'flex',
justifyContent: 'center',
flexDirection: 'column',
alignItems: 'center',
margin: "10%",
display: "flex",
justifyContent: "center",
flexDirection: "column",
alignItems: "center",
}}
>
<SecureDocumentIllustration />
<Text style={{ marginTop: 12 }} type="danger">
{(error && (error as Error).message) || '未知错误'}
{(error && (error as Error).message) || "未知错误"}
</Text>
</div>
);
@@ -144,9 +178,14 @@ export const DocumentPublicReader: React.FC<IProps> = ({ documentId, hideLogo =
}
footer={
<Space>
{!isMobile && data && <DocumentFullscreen data={data}/>}
<Tooltip content={currentWikiId ? '独立模式' : '嵌入模式'}>
<Button theme="borderless" type="tertiary" icon={<IconRoute />} onClick={toPublicWikiOrDocumentURL} />
{!isMobile && data && <DocumentFullscreen data={data} />}
<Tooltip content={currentWikiId ? "独立模式" : "嵌入模式"}>
<Button
theme="borderless"
type="tertiary"
icon={<IconRoute />}
onClick={toPublicWikiOrDocumentURL}
/>
</Tooltip>
<DocumentStyle />
@@ -158,11 +197,20 @@ export const DocumentPublicReader: React.FC<IProps> = ({ documentId, hideLogo =
<DataRender
loading={loading}
error={error}
loadingContent={<Skeleton active placeholder={<Skeleton.Title style={{ width: 80 }} />} loading={true} />}
loadingContent={
<Skeleton
active
placeholder={<Skeleton.Title style={{ width: 80 }} />}
loading={true}
/>
}
normalContent={() => (
<Breadcrumb>
<Breadcrumb.Item>
<Link href="/share/wiki/[wikiId]" as={`/share/wiki/${data.wikiId}`}>
<Link
href="/share/wiki/[wikiId]"
as={`/share/wiki/${data.wikiId}`}
>
<a>{data?.wiki?.name}</a>
</Link>
</Breadcrumb.Item>
@@ -172,9 +220,38 @@ export const DocumentPublicReader: React.FC<IProps> = ({ documentId, hideLogo =
/>
</Nav>
</Header>
<div className={styles.adWrap}>
{(ads || [])
.filter((ad) => ad.type === AdType.shareDocCover)
.map((ad) => {
return (
<div>
<a href={ad.url} target="_blank">
<img width={"100%"} height={"auto"} src={ad.cover} />
</a>
</div>
);
})}
</div>
<div className={styles.contentWrap} id="js-tocs-container">
{content}
</div>
<div className={styles.adAsideWrap}>
<Carousel autoPlay>
{(ads || [])
.filter((ad) => ad.type === AdType.shareDocAside)
.map((ad) => {
return (
<div>
<a href={ad.url} target="_blank">
<img width={"100%"} height={"auto"} src={ad.cover} />
</a>
</div>
);
})}
</Carousel>
</div>
</Layout>
);
};
+25 -18
View File
@@ -1,43 +1,50 @@
import { Typography } from '@douyinfe/semi-ui';
import { RuntimeConfig } from 'hooks/use-runtime-config';
import Link from 'next/link';
import styles from './index.module.scss';
const { Text } = Typography;
export const LogoName = process.env.SEO_APPNAME;
export const LogoImage = () => {
const config = RuntimeConfig.useHook();
return (
<Link href={'/'} as={'/'}>
<a style={{ width: 36, height: 36 }}>
<svg
xmlns="http://www.w3.org/2000/svg"
version="1.0"
width="36"
height="36"
viewBox="0 0 512 512"
preserveAspectRatio="xMidYMid meet"
>
<g
transform="translate(0.000000,512.000000) scale(0.100000,-0.100000)"
fill="var(--semi-color-primary)"
stroke="none"
{config.logo ? (
<img src={config.logo} />
) : (
<svg
xmlns="http://www.w3.org/2000/svg"
version="1.0"
width="36"
height="36"
viewBox="0 0 512 512"
preserveAspectRatio="xMidYMid meet"
>
<path d="M880 4799 c-85 -8 -208 -39 -235 -59 -5 -4 -28 -13 -49 -20 -54 -17 -179 -142 -196 -196 -7 -21 -16 -43 -20 -49 -25 -36 -51 -144 -60 -250 -14 -169 -13 -3182 1 -3335 9 -100 37 -215 59 -245 4 -5 13 -28 20 -49 17 -54 142 -179 196 -196 21 -7 44 -16 49 -20 36 -25 144 -51 250 -60 164 -13 3180 -13 3335 0 101 9 211 36 245 60 6 4 28 13 49 20 54 17 179 142 196 196 7 21 16 44 20 49 22 30 50 145 59 245 14 153 15 3166 1 3335 -9 106 -35 214 -60 250 -4 6 -13 28 -20 49 -17 54 -142 179 -196 196 -21 7 -43 16 -49 20 -30 22 -145 50 -245 59 -139 13 -3205 13 -3350 0z m2418 -1532 c281 -282 512 -516 512 -521 0 -9 -27 -39 -174 -196 -39 -42 -109 -118 -156 -170 -47 -52 -87 -97 -90 -100 -3 -3 -43 -45 -90 -95 -46 -49 -116 -124 -154 -165 -39 -42 -105 -113 -146 -159 -41 -46 -91 -100 -110 -120 -45 -48 -248 -266 -286 -309 -29 -31 -35 -33 -156 -47 -69 -8 -220 -19 -334 -25 -115 -6 -238 -15 -274 -20 -82 -12 -520 -34 -527 -27 -6 7 16 328 27 389 5 26 14 116 20 200 6 84 15 178 20 208 5 30 14 120 20 200 15 211 18 226 54 263 35 36 281 267 366 343 30 27 68 63 85 79 17 17 87 82 155 145 68 63 140 131 160 150 20 19 81 76 136 125 54 50 135 124 179 166 200 188 209 196 240 198 6 1 241 -230 523 -512z" />
</g>
</svg>
<g
transform="translate(0.000000,512.000000) scale(0.100000,-0.100000)"
fill="var(--semi-color-primary)"
stroke="none"
>
<path d="M880 4799 c-85 -8 -208 -39 -235 -59 -5 -4 -28 -13 -49 -20 -54 -17 -179 -142 -196 -196 -7 -21 -16 -43 -20 -49 -25 -36 -51 -144 -60 -250 -14 -169 -13 -3182 1 -3335 9 -100 37 -215 59 -245 4 -5 13 -28 20 -49 17 -54 142 -179 196 -196 21 -7 44 -16 49 -20 36 -25 144 -51 250 -60 164 -13 3180 -13 3335 0 101 9 211 36 245 60 6 4 28 13 49 20 54 17 179 142 196 196 7 21 16 44 20 49 22 30 50 145 59 245 14 153 15 3166 1 3335 -9 106 -35 214 -60 250 -4 6 -13 28 -20 49 -17 54 -142 179 -196 196 -21 7 -43 16 -49 20 -30 22 -145 50 -245 59 -139 13 -3205 13 -3350 0z m2418 -1532 c281 -282 512 -516 512 -521 0 -9 -27 -39 -174 -196 -39 -42 -109 -118 -156 -170 -47 -52 -87 -97 -90 -100 -3 -3 -43 -45 -90 -95 -46 -49 -116 -124 -154 -165 -39 -42 -105 -113 -146 -159 -41 -46 -91 -100 -110 -120 -45 -48 -248 -266 -286 -309 -29 -31 -35 -33 -156 -47 -69 -8 -220 -19 -334 -25 -115 -6 -238 -15 -274 -20 -82 -12 -520 -34 -527 -27 -6 7 16 328 27 389 5 26 14 116 20 200 6 84 15 178 20 208 5 30 14 120 20 200 15 211 18 226 54 263 35 36 281 267 366 343 30 27 68 63 85 79 17 17 87 82 155 145 68 63 140 131 160 150 20 19 81 76 136 125 54 50 135 124 179 166 200 188 209 196 240 198 6 1 241 -230 523 -512z" />
</g>
</svg>
)}
</a>
</Link>
);
};
export const LogoText = () => {
const config = RuntimeConfig.useHook();
return (
<Link href={'/'} as={'/'}>
<a className={styles.wrap}>
<Text>{LogoName}</Text>
<Text>{config?.appName}</Text>
</a>
</Link>
);
+12 -6
View File
@@ -1,21 +1,27 @@
import React, { useEffect } from 'react';
import React, { useCallback, useEffect } from 'react';
import { Helmet } from 'react-helmet';
import { RuntimeConfig } from 'hooks/use-runtime-config';
interface IProps {
title: string;
needTitleSuffix?: boolean;
}
const buildTitle = (title) => `${title} - 云策文档`;
export const Seo: React.FC<IProps> = ({ title, needTitleSuffix = true }) => {
const config = RuntimeConfig.useHook();
const buildTitle = useCallback(() => {
return `${title} - ${config?.appName}`;
}, [title, config?.appName]);
useEffect(() => {
window.document.title = needTitleSuffix ? buildTitle(title) : title;
}, [title, needTitleSuffix]);
window.document.title = needTitleSuffix ? buildTitle() : title;
}, [title, needTitleSuffix, buildTitle]);
return (
<Helmet>
<title>{needTitleSuffix ? buildTitle(title) : title}</title>
<title>{needTitleSuffix ? buildTitle() : title}</title>
</Helmet>
);
};
+42
View File
@@ -0,0 +1,42 @@
import { useCallback } from 'react';
import { useQuery } from 'react-query';
import { AdApiDefinition, IAd } from '@think/domains';
import { HttpClient } from 'services/http-client';
export const useAd = () => {
const { data, error, isLoading, refetch } = useQuery(AdApiDefinition.getAll.client(), () =>
HttpClient.request<IAd>({
method: AdApiDefinition.getAll.method,
url: AdApiDefinition.getAll.client(),
})
);
const createAd = useCallback(
async (data: Partial<IAd>) => {
const ret = await HttpClient.request<IAd>({
method: AdApiDefinition.create.method,
url: AdApiDefinition.create.client(),
data,
});
refetch();
return ret;
},
[refetch]
);
const deleteAd = useCallback(
async (id: IAd['id']) => {
const ret = await HttpClient.request<IAd>({
method: AdApiDefinition.deleteById.method,
url: AdApiDefinition.deleteById.client(id),
});
refetch();
return ret;
},
[refetch]
);
return { data, error, loading: isLoading, refresh: refetch, createAd, deleteAd };
};
@@ -0,0 +1,21 @@
import { useState } from 'react';
import { createGlobalHook } from './create-global-hook';
export type IRuntimeConfig = {
logo?: string;
appName?: string;
appDescription?: string;
appKeywords?: string;
copyrightInformation?: string;
externalButtonText?: string;
externalButtonURL?: string;
};
const useRuntimeConfig = (defaultConfig: IRuntimeConfig = {}) => {
const [config, setConfig] = useState(defaultConfig);
return config;
};
export const RuntimeConfig = createGlobalHook<IRuntimeConfig, IRuntimeConfig>(useRuntimeConfig);
+26 -18
View File
@@ -11,6 +11,7 @@ import { preloadTiptapResources } from 'tiptap/preload';
import { isMobile } from 'helpers/env';
import { DocumentVersionControl } from 'hooks/use-document-version';
import { IsOnMobile } from 'hooks/use-on-mobile';
import { IRuntimeConfig, RuntimeConfig } from 'hooks/use-runtime-config';
import { Theme } from 'hooks/use-theme';
import App from 'next/app';
import Head from 'next/head';
@@ -19,7 +20,7 @@ import 'viewerjs/dist/viewer.css';
import 'styles/globals.scss';
import 'thirtypart/array-prototype-at';
class MyApp extends App<{ isMobile: boolean }> {
class MyApp extends App<{ isMobile: boolean; runtimeConfig: IRuntimeConfig }> {
state = {
queryClient: new QueryClient({
defaultOptions: {
@@ -36,13 +37,19 @@ class MyApp extends App<{ isMobile: boolean }> {
static getInitialProps = async ({ Component, ctx }) => {
const request = ctx?.req;
const getPagePropsPromise = Component.getInitialProps ? Component.getInitialProps(ctx) : Promise.resolve({});
const [pageProps] = await Promise.all([getPagePropsPromise]).catch((err) => {
return [{}];
const [pageProps, runtimeConfig] = await Promise.all([
getPagePropsPromise,
fetch(`${process.env.SITE_URL}/api/config`).then((res) => {
return res.json();
}),
]).catch((err) => {
return [{}, {}];
});
return {
pageProps,
isMobile: isMobile(request?.headers['user-agent']),
runtimeConfig,
};
};
@@ -57,7 +64,7 @@ class MyApp extends App<{ isMobile: boolean }> {
}
render() {
const { Component, pageProps, isMobile } = this.props;
const { Component, pageProps, isMobile, runtimeConfig } = this.props;
const { queryClient } = this.state;
return (
@@ -68,24 +75,23 @@ class MyApp extends App<{ isMobile: boolean }> {
content="minimum-scale=1, initial-scale=1, width=device-width, shrink-to-fit=no, user-scalable=no, viewport-fit=cover"
/>
<meta charSet="utf-8" />
<meta name="description" content={process.env.SEO_DESCRIPTION} />
<meta name="keywords" content={process.env.SEO_KEYWORDS}></meta>
<meta name="application-name" content={process.env.SEO_APPNAME} />
<meta name="description" content={runtimeConfig.appDescription} />
<meta name="keywords" content={runtimeConfig.appKeywords}></meta>
<meta name="application-name" content={runtimeConfig.appName} />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
<meta name="apple-mobile-web-app-title" content={process.env.SEO_APPNAME} />
<meta name="description" content={process.env.SEO_DESCRIPTION} />
<meta name="apple-mobile-web-app-title" content={runtimeConfig.appName} />
<meta name="description" content={runtimeConfig.appDescription} />
<meta name="format-detection" content="telephone=no" />
<meta name="mobile-web-app-capable" content="yes" />
<meta name="msapplication-TileColor" content="#ffffff" />
<meta name="msapplication-tap-highlight" content="no" />
<meta name="theme-color" content="#ffffff" />
<meta property="og:type" content="website" />
<meta property="og:title" content={process.env.SEO_APPNAME} />
<meta property="og:description" content={process.env.SEO_DESCRIPTION} />
<meta property="og:site_name" content={process.env.SEO_APPNAME} />
<meta property="og:title" content={runtimeConfig.appName} />
<meta property="og:description" content={runtimeConfig.appDescription} />
<meta property="og:site_name" content={runtimeConfig.appName} />
<link rel="manifest" href="/manifest.json" />
<link rel="apple-touch-icon" href="/icon192.png" />
{((process.env.DNS_PREFETCH || []) as string[]).map((url) => (
<link key={url} rel="dns-prefetch" href={url} />
))}
@@ -94,11 +100,13 @@ class MyApp extends App<{ isMobile: boolean }> {
<QueryClientProvider client={queryClient}>
<Hydrate state={pageProps.dehydratedState}>
<Theme.Provider>
<IsOnMobile.Provider initialState={isMobile}>
<DocumentVersionControl.Provider initialState={false}>
<Component {...pageProps} />
</DocumentVersionControl.Provider>
</IsOnMobile.Provider>
<RuntimeConfig.Provider initialState={runtimeConfig}>
<IsOnMobile.Provider initialState={isMobile}>
<DocumentVersionControl.Provider initialState={false}>
<Component {...pageProps} />
</DocumentVersionControl.Provider>
</IsOnMobile.Provider>
</RuntimeConfig.Provider>
</Theme.Provider>
</Hydrate>
</QueryClientProvider>
+7
View File
@@ -0,0 +1,7 @@
import { getRuntimeConfig } from '@think/config';
import type { NextApiRequest, NextApiResponse } from 'next';
export default function handler(req: NextApiRequest, res: NextApiResponse<Record<string, unknown>>) {
res.status(200).json(getRuntimeConfig() as Record<string, unknown>);
}
+13 -10
View File
@@ -4,6 +4,7 @@ import { Button, Typography } from '@douyinfe/semi-ui';
import { Seo } from 'components/seo';
import { toLogin, useUser } from 'data/user';
import { RuntimeConfig } from 'hooks/use-runtime-config';
import { TeamWorkIllustration } from 'illustrations/team-work';
import { SingleColumnLayout } from 'layouts/single-column';
import type { NextPage } from 'next';
@@ -14,6 +15,8 @@ import styles from './index.module.scss';
const { Title, Paragraph } = Typography;
const Page: NextPage = () => {
const config = RuntimeConfig.useHook();
const { user } = useUser();
const start = useCallback(() => {
@@ -24,9 +27,9 @@ const Page: NextPage = () => {
}
}, [user]);
const toGithub = useCallback(() => {
window.open('https://github.com/fantasticit/think');
}, []);
const toExternal = useCallback(() => {
window.open(config.externalButtonURL);
}, [config.externalButtonURL]);
return (
<SingleColumnLayout>
@@ -36,18 +39,18 @@ const Page: NextPage = () => {
<div className={styles.content}>
<div>
<div>
<Title style={{ marginBottom: 12 }}>云策文档</Title>
<Paragraph type="tertiary">
云策文档是一款开源知识管理工具。通过独立的知识库空间,结构化地组织在线协作文档,实现知识的积累与沉淀,促进知识的复用与流通。
</Paragraph>
<Title style={{ marginBottom: 12 }}>{config?.appName}</Title>
<Paragraph type="tertiary">{config?.appDescription}</Paragraph>
</div>
<div style={{ margin: '32px 0' }}>
<Button theme="solid" onClick={start}>
开始使用
</Button>
<Button style={{ marginLeft: 12 }} onClick={toGithub}>
Github
</Button>
{config.externalButtonText && (
<Button style={{ marginLeft: 12 }} onClick={toExternal}>
{config.externalButtonText}
</Button>
)}
</div>
</div>
</div>
@@ -12,12 +12,12 @@ import cls from 'classnames';
import { Banner } from 'components/banner';
import { CommentEditor } from 'components/document/comments';
import { ImageViewer } from 'components/image-viewer';
import { LogoName } from 'components/logo';
import { getRandomColor } from 'helpers/color';
import { isAndroid, isIOS } from 'helpers/env';
import { useDocumentStyle } from 'hooks/use-document-style';
import { useNetwork } from 'hooks/use-network';
import { IsOnMobile } from 'hooks/use-on-mobile';
import { RuntimeConfig } from 'hooks/use-runtime-config';
import { useToggle } from 'hooks/use-toggle';
import { CollaborationKit } from '../kit';
@@ -47,6 +47,7 @@ export const EditorInstance = forwardRef((props: IProps, ref) => {
renderInEditorPortal,
onTitleUpdate,
} = props;
const config = RuntimeConfig.useHook();
const $headerContainer = useRef<HTMLDivElement>();
const $mainContainer = useRef<HTMLDivElement>();
const { isMobile } = IsOnMobile.useHook();
@@ -110,7 +111,7 @@ export const EditorInstance = forwardRef((props: IProps, ref) => {
const listener = (event: KeyboardEvent) => {
if ((event.ctrlKey || event.metaKey) && event.keyCode == 83) {
event.preventDefault();
Toast.info(`${LogoName}会实时保存你的数据,无需手动保存。`);
Toast.info(`${config.appName}会实时保存你的数据,无需手动保存。`);
return false;
}
};
@@ -120,7 +121,7 @@ export const EditorInstance = forwardRef((props: IProps, ref) => {
return () => {
window.document.removeEventListener('keydown', listener);
};
}, []);
}, [config.appName]);
// 监听键盘收起、打开
useEffect(() => {
@@ -168,6 +169,7 @@ export const EditorInstance = forwardRef((props: IProps, ref) => {
<>
{(!online || status === 'disconnected') && (
<Banner
// @ts-ignore
type="warning"
description={
editable
@@ -179,6 +181,7 @@ export const EditorInstance = forwardRef((props: IProps, ref) => {
{/* FIXME:需要菜单栏但是无法编辑,则认为进入了编辑模式但是没有编辑权限,也许有更好的判断 */}
{!editable && menubar && (
// @ts-ignore
<Banner type="warning" description="您没有编辑权限,暂不能编辑该文档。" closeable={false} />
)}
+1
View File
@@ -1 +1,2 @@
export declare function getConfig(): Record<string, unknown>;
export declare function getRuntimeConfig(): Record<string, unknown>;
+14 -2
View File
@@ -1,6 +1,6 @@
"use strict";
exports.__esModule = true;
exports.getConfig = void 0;
exports.getRuntimeConfig = exports.getConfig = void 0;
var fs = require("fs-extra");
var yaml = require("js-yaml");
var path = require("path");
@@ -9,8 +9,8 @@ var FILE_ENV_NAME = {
test: 'test',
production: 'prod'
};
var env = process.env.NODE_ENV || 'development';
function getConfig() {
var env = process.env.NODE_ENV || 'development';
var filePath = path.join(__dirname, '../../../config', "".concat(FILE_ENV_NAME[env], ".yaml"));
if (!fs.existsSync(filePath)) {
throw new Error("Can not find config file: ".concat(filePath));
@@ -18,3 +18,15 @@ function getConfig() {
return yaml.load(fs.readFileSync(filePath, 'utf8'));
}
exports.getConfig = getConfig;
function getRuntimeConfig() {
var env = process.env.NODE_ENV || 'development';
var filePath = path.join(__dirname, '../../../../../../', "dynamic.yaml");
if (!fs.existsSync(filePath)) {
filePath = path.join(__dirname, '../../../config', "dynamic.yaml");
}
if (!fs.existsSync(filePath)) {
throw new Error("Can not find config file: ".concat(filePath));
}
return (yaml.load(fs.readFileSync(filePath, 'utf8')).dynamic || {});
}
exports.getRuntimeConfig = getRuntimeConfig;
+4 -1
View File
@@ -21,5 +21,8 @@
],
"dependencies": {
"js-yaml": "^4.1.0"
},
"devDependencies": {
"typescript": "^4.8.4"
}
}
}
+18 -2
View File
@@ -8,9 +8,9 @@ const FILE_ENV_NAME = {
production: 'prod',
};
const env = process.env.NODE_ENV || 'development';
export function getConfig() {
const env = process.env.NODE_ENV || 'development';
const filePath = path.join(__dirname, '../../../config', `${FILE_ENV_NAME[env]}.yaml`);
if (!fs.existsSync(filePath)) {
@@ -19,3 +19,19 @@ export function getConfig() {
return yaml.load(fs.readFileSync(filePath, 'utf8')) as Record<string, unknown>;
}
export function getRuntimeConfig() {
const env = process.env.NODE_ENV || 'development';
let filePath = path.join(__dirname, '../../../../../../', `dynamic.yaml`);
if (!fs.existsSync(filePath)) {
filePath = path.join(__dirname, '../../../config', `dynamic.yaml`);
}
if (!fs.existsSync(filePath)) {
throw new Error(`Can not find config file: ${filePath}`);
}
return (yaml.load(fs.readFileSync(filePath, 'utf8')).dynamic || {}) as Record<string, unknown>;
}
+2 -2
View File
@@ -19,6 +19,6 @@
"src"
],
"devDependencies": {
"typescript": "^4.5.5"
"typescript": "^4.8.4"
}
}
}
+23
View File
@@ -0,0 +1,23 @@
export declare const AdApiDefinition: {
/**
* 获取用户
*/
getAll: {
method: "get";
server: "";
client: () => string;
};
create: {
method: "post";
server: "";
client: () => string;
};
/**
* 获取组织内加星的知识库
*/
deleteById: {
method: "delete";
server: ":id";
client: (id: any) => string;
};
};
+26
View File
@@ -0,0 +1,26 @@
"use strict";
exports.__esModule = true;
exports.AdApiDefinition = void 0;
exports.AdApiDefinition = {
/**
* 获取用户
*/
getAll: {
method: 'get',
server: '',
client: function () { return '/ad'; }
},
create: {
method: 'post',
server: '',
client: function () { return '/ad'; }
},
/**
* 获取组织内加星的知识库
*/
deleteById: {
method: 'delete',
server: ':id',
client: function (id) { return "/ad/".concat(id); }
}
};
+1
View File
@@ -1,3 +1,4 @@
export * from './ad';
export * from './user';
export * from './wiki';
export * from './document';
+6 -1
View File
@@ -1,7 +1,11 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
@@ -10,6 +14,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
};
exports.__esModule = true;
__exportStar(require("./ad"), exports);
__exportStar(require("./user"), exports);
__exportStar(require("./wiki"), exports);
__exportStar(require("./document"), exports);
+5 -1
View File
@@ -1,7 +1,11 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
+10
View File
@@ -0,0 +1,10 @@
export declare enum AdType {
shareDocCover = "shareDocCover",
shareDocAside = "shareDocAside"
}
export interface IAd {
id: string;
type: AdType;
cover: string;
url: string;
}
+8
View File
@@ -0,0 +1,8 @@
"use strict";
exports.__esModule = true;
exports.AdType = void 0;
var AdType;
(function (AdType) {
AdType["shareDocCover"] = "shareDocCover";
AdType["shareDocAside"] = "shareDocAside";
})(AdType = exports.AdType || (exports.AdType = {}));
+1
View File
@@ -1,3 +1,4 @@
export * from './ad';
export * from './user';
export * from './wiki';
export * from './document';
+6 -1
View File
@@ -1,7 +1,11 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
@@ -10,6 +14,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
};
exports.__esModule = true;
__exportStar(require("./ad"), exports);
__exportStar(require("./user"), exports);
__exportStar(require("./wiki"), exports);
__exportStar(require("./document"), exports);
+2 -2
View File
@@ -19,6 +19,6 @@
"src"
],
"devDependencies": {
"typescript": "^4.5.5"
"typescript": "^4.8.4"
}
}
}
+25
View File
@@ -0,0 +1,25 @@
export const AdApiDefinition = {
/**
* 获取用户
*/
getAll: {
method: 'get' as const,
server: '' as const,
client: () => '/ad',
},
create: {
method: 'post' as const,
server: '' as const,
client: () => '/ad',
},
/**
* 获取组织内加星的知识库
*/
deleteById: {
method: 'delete' as const,
server: ':id' as const,
client: (id) => `/ad/${id}`,
},
};
+1
View File
@@ -1,3 +1,4 @@
export * from './ad';
export * from './user';
export * from './wiki';
export * from './document';
+11
View File
@@ -0,0 +1,11 @@
export enum AdType {
shareDocCover = 'shareDocCover',
shareDocAside = 'shareDocAside',
}
export interface IAd {
id: string;
type: AdType;
cover: string;
url: string;
}
+1
View File
@@ -1,3 +1,4 @@
export * from './ad';
export * from './user';
export * from './wiki';
export * from './document';
+4
View File
@@ -6,6 +6,7 @@ import { LoggerModule } from 'nestjs-pino';
import { getConfig } from '@think/config';
import { AdEntity } from '@entities/ad.entity';
import { AuthEntity } from '@entities/auth.entity';
import { CommentEntity } from '@entities/comment.entity';
import { DocumentEntity } from '@entities/document.entity';
@@ -18,6 +19,7 @@ import { UserEntity } from '@entities/user.entity';
import { WikiEntity } from '@entities/wiki.entity';
import { IS_PRODUCTION } from '@helpers/env.helper';
import { getLogFileName, ONE_DAY } from '@helpers/log.helper';
import { AdModule } from '@modules/ad.module';
import { AuthModule } from '@modules/auth.module';
import { CommentModule } from '@modules/comment.module';
import { DocumentModule } from '@modules/document.module';
@@ -46,6 +48,7 @@ const ENTITIES = [
MessageEntity,
TemplateEntity,
SystemEntity,
AdEntity,
];
const MODULES = [
@@ -62,6 +65,7 @@ const MODULES = [
ViewModule,
VerifyModule,
SystemModule,
AdModule,
];
@Module({
@@ -0,0 +1,48 @@
import {
Body,
ClassSerializerInterceptor,
Controller,
Delete,
Get,
HttpCode,
HttpStatus,
Param,
Post,
Request,
UseGuards,
UseInterceptors,
} from '@nestjs/common';
import { AdApiDefinition } from '@think/domains';
import { AdDto } from '@dtos/ad.dto';
import { JwtGuard } from '@guard/jwt.guard';
import { AdService } from '@services/ad.service';
@Controller('ad')
export class AdController {
constructor(private readonly adService: AdService) {}
@UseInterceptors(ClassSerializerInterceptor)
@Post(AdApiDefinition.create.server)
@HttpCode(HttpStatus.CREATED)
@UseGuards(JwtGuard)
async addAd(@Request() req, @Body() data: AdDto) {
return await this.adService.addAd(req.user, data);
}
@UseInterceptors(ClassSerializerInterceptor)
@Get(AdApiDefinition.getAll.server)
@HttpCode(HttpStatus.OK)
async getAds() {
return await this.adService.getAds();
}
@UseInterceptors(ClassSerializerInterceptor)
@Delete(AdApiDefinition.deleteById.server)
@HttpCode(HttpStatus.OK)
@UseGuards(JwtGuard)
async deleteAd(@Request() req, @Param('id') id) {
return await this.adService.deleteAd(req.user, id);
}
}
+17
View File
@@ -0,0 +1,17 @@
import { AdType } from '@think/domains';
import { IsNotEmpty, IsString } from 'class-validator';
export class AdDto {
@IsString({ message: '广告位类型错误(正确类型为:String)' })
@IsNotEmpty({ message: '广告位不能为空' })
type: AdType;
@IsString({ message: '广告封面图类型错误(正确类型为:String)' })
@IsNotEmpty({ message: '广告封面图不能为空' })
cover: string;
@IsString({ message: '广告跳转链接类型错误(正确类型为:String)' })
@IsNotEmpty({ message: '广告跳转链接不能为空' })
url: string;
}
+37
View File
@@ -0,0 +1,37 @@
import { AdType } from '@think/domains';
import { Column, CreateDateColumn, Entity, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm';
@Entity('ad')
export class AdEntity {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({
type: 'enum',
enum: AdType,
default: AdType.shareDocAside,
comment: '权限类型',
})
public type: AdType;
@Column({ type: 'mediumtext', comment: '广告封面图' })
cover: string;
@Column({ type: 'mediumtext', comment: '跳转链接', default: null, charset: 'utf8mb4' })
url: string;
@CreateDateColumn({
type: 'timestamp',
name: 'createdAt',
comment: '创建时间',
})
createdAt: Date;
@UpdateDateColumn({
type: 'timestamp',
name: 'updatedAt',
comment: '更新时间',
})
updatedAt: Date;
}
+16
View File
@@ -0,0 +1,16 @@
import { forwardRef, Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AdController } from '@controllers/ad.controller';
import { AdEntity } from '@entities/ad.entity';
import { AdService } from '@services/ad.service';
import { UserModule } from './user.module';
@Module({
imports: [TypeOrmModule.forFeature([AdEntity]), forwardRef(() => UserModule)],
providers: [AdService],
exports: [AdService],
controllers: [AdController],
})
export class AdModule {}
@@ -0,0 +1,47 @@
import { forwardRef, HttpException, HttpStatus, Inject, Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { AdDto } from '@dtos/ad.dto';
import { AdEntity } from '@entities/ad.entity';
import { UserEntity } from '@entities/user.entity';
import { Repository } from 'typeorm';
import { UserService } from './user.service';
@Injectable()
export class AdService {
constructor(
@Inject(forwardRef(() => UserService))
private readonly userService: UserService,
@InjectRepository(AdEntity)
private readonly adRepo: Repository<AdEntity>
) {}
private async check(user: UserEntity) {
const currentUser = await this.userService.findById(user.id);
if (!currentUser.isSystemAdmin) {
throw new HttpException('您无权操作', HttpStatus.FORBIDDEN);
}
}
async addAd(user: UserEntity, dto: AdDto) {
await this.check(user);
const data = await this.adRepo.create(dto);
const res = await this.adRepo.save(data);
return res;
}
async getAds() {
const data = await this.adRepo.find();
return data;
}
async deleteAd(user: UserEntity, id: string) {
await this.check(user);
return await this.adRepo.delete(id);
}
}
+8 -4
View File
@@ -424,18 +424,22 @@ importers:
js-yaml:
specifier: ^4.1.0
version: 4.1.0
devDependencies:
typescript:
specifier: ^4.8.4
version: 4.8.4
packages/constants:
devDependencies:
typescript:
specifier: ^4.5.5
version: 4.5.5
specifier: ^4.8.4
version: 4.8.4
packages/domains:
devDependencies:
typescript:
specifier: ^4.5.5
version: 4.5.5
specifier: ^4.8.4
version: 4.8.4
packages/server:
dependencies: