Просмотр исходного кода

Merge pull request #1656 from visuddhinanda/agile

文章可以从另一篇文章派生
visuddhinanda 2 лет назад
Родитель
Сommit
72ec64484d

+ 3 - 1
dashboard/src/Router.tsx

@@ -109,6 +109,7 @@ import StudioTermList from "./pages/studio/term/list";
 import StudioArticle from "./pages/studio/article";
 import StudioArticleList from "./pages/studio/article/list";
 import StudioArticleEdit from "./pages/studio/article/edit";
+import StudioArticleCreate from "./pages/studio/article/create";
 
 import StudioAnthology from "./pages/studio/anthology";
 import StudioAnthologyList from "./pages/studio/anthology/list";
@@ -303,7 +304,8 @@ const Widget = () => {
 
           <Route path="article" element={<StudioArticle />}>
             <Route path="list" element={<StudioArticleList />} />
-            <Route path=":articleId/edit" element={<StudioArticleEdit />} />
+            <Route path="edit/:articleId" element={<StudioArticleEdit />} />
+            <Route path="create" element={<StudioArticleCreate />} />
           </Route>
 
           <Route path="anthology" element={<StudioAnthology />}>

+ 2 - 0
dashboard/src/components/api/Article.ts

@@ -100,6 +100,7 @@ export interface IArticleDataResponse {
   to?: number;
   mode?: string;
   paraId?: string;
+  parent_uid?: string;
   channels?: string;
 }
 export interface IArticleResponse {
@@ -121,6 +122,7 @@ export interface IArticleCreateRequest {
   lang: string;
   studio: string;
   anthologyId?: string;
+  parentId?: string;
 }
 
 export interface IAnthologyCreateRequest {

+ 110 - 49
dashboard/src/components/article/ArticleCreate.tsx

@@ -2,77 +2,138 @@ import { useIntl } from "react-intl";
 import {
   ProForm,
   ProFormInstance,
+  ProFormSelect,
   ProFormText,
 } from "@ant-design/pro-components";
-import { message } from "antd";
+import { Alert, Space, message } from "antd";
 
-import { post } from "../../request";
-import { IArticleCreateRequest, IArticleResponse } from "../api/Article";
+import { get, post } from "../../request";
+import {
+  IAnthologyListResponse,
+  IArticleCreateRequest,
+  IArticleDataResponse,
+  IArticleResponse,
+} from "../api/Article";
 import LangSelect from "../general/LangSelect";
-import { useRef } from "react";
+import { useEffect, useRef, useState } from "react";
 
 interface IFormData {
   title: string;
   lang: string;
   studio: string;
   anthologyId?: string;
+  parentId?: string;
 }
 
 interface IWidget {
   studio?: string;
   anthologyId?: string;
+  parentId?: string | null;
+  compact?: boolean;
   onSuccess?: Function;
 }
-const ArticleCreateWidget = ({ studio, anthologyId, onSuccess }: IWidget) => {
+const ArticleCreateWidget = ({
+  studio,
+  anthologyId,
+  parentId,
+  compact = true,
+  onSuccess,
+}: IWidget) => {
   const intl = useIntl();
   const formRef = useRef<ProFormInstance>();
+  const [parent, setParent] = useState<IArticleDataResponse>();
+  console.log("parentId", parentId);
+  useEffect(() => {
+    if (parentId) {
+      get<IArticleResponse>(`/v2/article/${parentId}`).then((json) => {
+        console.log("article", json);
 
-  return (
-    <ProForm<IFormData>
-      formRef={formRef}
-      onFinish={async (values: IFormData) => {
-        console.log(values);
-        if (typeof studio === "undefined") {
-          return;
-        }
-        values.studio = studio;
-        values.anthologyId = anthologyId;
-        const res = await post<IArticleCreateRequest, IArticleResponse>(
-          `/v2/article`,
-          values
-        );
-        console.log(res);
-        if (res.ok) {
-          message.success(intl.formatMessage({ id: "flashes.success" }));
-          if (typeof onSuccess !== "undefined") {
-            onSuccess();
-            formRef.current?.resetFields(["title"]);
-          }
-        } else {
-          message.error(res.message);
+        if (json.ok) {
+          setParent(json.data);
         }
-      }}
-    >
-      <ProForm.Group>
-        <ProFormText
-          width="md"
-          name="title"
-          required
-          label={intl.formatMessage({ id: "channel.name" })}
-          rules={[
-            {
-              required: true,
-              message: intl.formatMessage({
-                id: "channel.create.message.noname",
-              }),
-            },
-          ]}
+      });
+    }
+  }, []);
+
+  return (
+    <Space direction="vertical">
+      {parentId ? (
+        <Alert
+          message={`从文章 ${parent?.title} 创建子文章`}
+          type="info"
+          closable
         />
-      </ProForm.Group>
-      <ProForm.Group>
-        <LangSelect />
-      </ProForm.Group>
-    </ProForm>
+      ) : undefined}
+      <ProForm<IFormData>
+        formRef={formRef}
+        onFinish={async (values: IFormData) => {
+          console.log(values);
+          if (typeof studio === "undefined") {
+            return;
+          }
+          values.studio = studio;
+          values.parentId = parentId ? parentId : undefined;
+          const res = await post<IArticleCreateRequest, IArticleResponse>(
+            `/v2/article`,
+            values
+          );
+          console.log(res);
+          if (res.ok) {
+            message.success(intl.formatMessage({ id: "flashes.success" }));
+            if (typeof onSuccess !== "undefined") {
+              onSuccess(res.data);
+              formRef.current?.resetFields(["title"]);
+            }
+          } else {
+            message.error(res.message);
+          }
+        }}
+      >
+        <ProForm.Group>
+          <ProFormText
+            width="md"
+            name="title"
+            required
+            label={intl.formatMessage({ id: "channel.name" })}
+            rules={[
+              {
+                required: true,
+                message: intl.formatMessage({
+                  id: "channel.create.message.noname",
+                }),
+              },
+            ]}
+          />
+        </ProForm.Group>
+        <ProForm.Group>
+          <LangSelect />
+        </ProForm.Group>
+        <ProForm.Group>
+          <ProFormSelect
+            name={"anthologyId"}
+            label={"加入文集"}
+            hidden={compact}
+            width={"md"}
+            showSearch
+            debounceTime={300}
+            request={async ({ keyWords }) => {
+              console.log("keyWord", keyWords);
+              let url = `/v2/anthology?view=studio&view2=my&name=${studio}`;
+              url += keyWords ? "&search=" + keyWords : "";
+              const res = await get<IAnthologyListResponse>(url);
+              const result = res.data.rows.map((item) => {
+                return {
+                  value: item.uid,
+                  label: item.title,
+                };
+              });
+              console.log("json", result);
+              return result;
+            }}
+          />
+        </ProForm.Group>
+      </ProForm>
+    </Space>
   );
 };
 

+ 8 - 1
dashboard/src/components/article/ArticleEdit.tsx

@@ -36,6 +36,7 @@ interface IWidget {
   studioName?: string;
   articleId?: string;
   onReady?: Function;
+  onLoad?: Function;
   onChange?: Function;
 }
 
@@ -43,6 +44,7 @@ const ArticleEditWidget = ({
   studioName,
   articleId,
   onReady,
+  onLoad,
   onChange,
 }: IWidget) => {
   const intl = useIntl();
@@ -120,7 +122,12 @@ const ArticleEditWidget = ({
             mTitle = "无权访问";
           }
           if (typeof onReady !== "undefined") {
-            onReady(mTitle, mReadonly, res.data.studio?.realName);
+            onReady(
+              mTitle,
+              mReadonly,
+              res.data.studio?.realName,
+              res.data.parent_uid
+            );
           }
           return {
             uid: res.data.uid,

+ 5 - 3
dashboard/src/components/article/ArticleList.tsx

@@ -120,7 +120,7 @@ const ArticleListWidget = ({
       icon: <ExclamationCircleOutlined />,
       title:
         intl.formatMessage({
-          id: "message.delete.sure",
+          id: "message.delete.confirm",
         }) +
         intl.formatMessage({
           id: "message.irrevocable",
@@ -195,9 +195,11 @@ const ArticleListWidget = ({
                 <>
                   <div key={1}>
                     <Typography.Link
-                      onClick={() => {
+                      onClick={(
+                        event: React.MouseEvent<HTMLElement, MouseEvent>
+                      ) => {
                         if (typeof onSelect !== "undefined") {
-                          onSelect(row.id, row.title);
+                          onSelect(row.id, row.title, event);
                         }
                       }}
                     >

+ 3 - 0
dashboard/src/components/invite/InviteCreate.tsx

@@ -9,11 +9,13 @@ import { post } from "../../request";
 import { useRef } from "react";
 import { IInviteData } from "../../pages/studio/invite/list";
 import LangSelect from "../general/LangSelect";
+import { dashboardBasePath } from "../../utils";
 
 interface IInviteRequest {
   email: string;
   lang: string;
   studio: string;
+  dashboard?: string;
 }
 interface IInviteResponse {
   ok: boolean;
@@ -45,6 +47,7 @@ const InviteCreateWidget = ({ studio, onCreate }: IWidget) => {
           email: values.email,
           lang: values.lang,
           studio: studio,
+          dashboard: dashboardBasePath(),
         });
         console.log(res);
         if (res.ok) {

+ 31 - 13
dashboard/src/pages/library/article/show.tsx

@@ -48,6 +48,7 @@ import { TResType } from "../../../components/discussion/DiscussionListCard";
 import { modeChange } from "../../../reducers/article-mode";
 import SearchButton from "../../../components/general/SearchButton";
 import ToStudio from "../../../components/auth/ToStudio";
+import { currentUser as _currentUser } from "../../../reducers/current-user";
 
 /**
  * type:
@@ -73,6 +74,7 @@ const Widget = () => {
     useState<IArticleDataResponse>();
 
   const paraChange = useAppSelector(paraParam);
+  const user = useAppSelector(_currentUser);
 
   useEffect(() => {
     if (typeof paraChange === "undefined") {
@@ -154,19 +156,35 @@ const Widget = () => {
           <div style={{ display: "flex" }} key="middle"></div>
           <div style={{ display: "flex" }} key="right">
             {type === "article" && loadedArticleData ? (
-              <Button
-                ghost
-                onClick={(event) => {
-                  const url = `/studio/${loadedArticleData.studio?.realName}/article/${loadedArticleData.uid}/edit`;
-                  if (event.ctrlKey || event.metaKey) {
-                    window.open(fullUrl(url), "_blank");
-                  } else {
-                    navigate(url);
-                  }
-                }}
-              >
-                Edit
-              </Button>
+              <>
+                <Button
+                  ghost
+                  onClick={(event) => {
+                    const url = `/studio/${loadedArticleData.studio?.realName}/article/edit/${loadedArticleData.uid}`;
+                    if (event.ctrlKey || event.metaKey) {
+                      window.open(fullUrl(url), "_blank");
+                    } else {
+                      navigate(url);
+                    }
+                  }}
+                >
+                  Edit
+                </Button>
+                <Button
+                  disabled={user ? false : true}
+                  ghost
+                  onClick={(event) => {
+                    const url = `/studio/${user?.nickName}/article/create?parent=${loadedArticleData.uid}`;
+                    if (event.ctrlKey || event.metaKey) {
+                      window.open(fullUrl(url), "_blank");
+                    } else {
+                      navigate(url);
+                    }
+                  }}
+                >
+                  Fork
+                </Button>
+              </>
             ) : undefined}
             <SearchButton />
             <Divider type="vertical" />

+ 35 - 0
dashboard/src/pages/studio/article/create.tsx

@@ -0,0 +1,35 @@
+import { useNavigate, useParams, useSearchParams } from "react-router-dom";
+
+import { Card, Space } from "antd";
+
+import GoBack from "../../../components/studio/GoBack";
+
+import ArticleCreate from "../../../components/article/ArticleCreate";
+import { IArticleDataResponse } from "../../../components/api/Article";
+
+const Widget = () => {
+  const { studioname } = useParams(); //url 参数
+  const navigate = useNavigate();
+  const [searchParams] = useSearchParams();
+
+  return (
+    <Card
+      title={
+        <Space>
+          <GoBack to={`/studio/${studioname}/article/list`} title={"新建"} />
+        </Space>
+      }
+    >
+      <ArticleCreate
+        studio={studioname}
+        parentId={searchParams.get("parent")}
+        compact={false}
+        onSuccess={(article: IArticleDataResponse) => {
+          navigate(`/studio/${studioname}/article/edit/${article.uid}`);
+        }}
+      />
+    </Card>
+  );
+};
+
+export default Widget;

+ 58 - 24
dashboard/src/pages/studio/article/edit.tsx

@@ -1,43 +1,77 @@
 import { useState } from "react";
 import { useParams } from "react-router-dom";
-
-import { Card, Space } from "antd";
-
+import { Affix, Button, Card, Space } from "antd";
+import { DoubleLeftOutlined, DoubleRightOutlined } from "@ant-design/icons";
 import GoBack from "../../../components/studio/GoBack";
 import ReadonlyLabel from "../../../components/general/ReadonlyLabel";
 
 import ArticleEdit from "../../../components/article/ArticleEdit";
 import ArticleEditTools from "../../../components/article/ArticleEditTools";
+import Article from "../../../components/article/Article";
 
 const Widget = () => {
   const { studioname, articleId } = useParams(); //url 参数
   const [title, setTitle] = useState("loading");
   const [readonly, setReadonly] = useState(false);
-
+  const [parent, setParent] = useState<string>();
+  const [showParent, setShowParent] = useState(false);
   return (
-    <Card
-      title={
-        <Space>
-          <GoBack to={`/studio/${studioname}/article/list`} title={title} />
-          {readonly ? <ReadonlyLabel /> : undefined}
-        </Space>
-      }
-      extra={
-        <ArticleEditTools
-          studioName={studioname}
+    <div style={{ display: "flex" }}>
+      <Card
+        style={{ width: "100%" }}
+        title={
+          <Space>
+            <GoBack to={`/studio/${studioname}/article/list`} title={title} />
+            {readonly ? <ReadonlyLabel /> : undefined}
+          </Space>
+        }
+        extra={
+          <Space>
+            <ArticleEditTools
+              studioName={studioname}
+              articleId={articleId}
+              title={title}
+            />
+            <Button
+              onClick={() => setShowParent((origin) => !origin)}
+              style={{ display: parent ? "inline-block" : "none" }}
+            >
+              源文件
+              {showParent ? <DoubleRightOutlined /> : <DoubleLeftOutlined />}
+            </Button>
+          </Space>
+        }
+      >
+        <ArticleEdit
           articleId={articleId}
-          title={title}
+          onReady={(
+            title: string,
+            readonly: boolean,
+            studioName?: string,
+            parentUid?: string
+          ) => {
+            setTitle(title);
+            setReadonly(readonly);
+            setParent(parentUid);
+          }}
         />
-      }
-    >
-      <ArticleEdit
-        articleId={articleId}
-        onReady={(title: string, readonly: boolean) => {
-          setTitle(title);
-          setReadonly(readonly);
+      </Card>
+      <div
+        style={{
+          width: 1000,
+          display: showParent ? "block" : "none",
         }}
-      />
-    </Card>
+      >
+        <Affix offsetTop={0}>
+          <Article
+            active={true}
+            type={"article"}
+            articleId={parent}
+            mode="read"
+          />
+        </Affix>
+      </div>
+    </div>
   );
 };