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

Merge pull request #1919 from visuddhinanda/agile

添加 fork_from 支持
visuddhinanda 2 лет назад
Родитель
Сommit
d5222f95f2
29 измененных файлов с 380 добавлено и 94 удалено
  1. 36 0
      dashboard/src/assets/icon/index.tsx
  2. 1 0
      dashboard/src/components/admin/relation/CaseSelect.tsx
  3. 1 0
      dashboard/src/components/admin/relation/GrammarSelect.tsx
  4. 2 0
      dashboard/src/components/api/Corpus.ts
  5. 19 10
      dashboard/src/components/channel/ChannelSentDiff.tsx
  6. 3 0
      dashboard/src/components/channel/CopyToModal.tsx
  7. 3 0
      dashboard/src/components/channel/CopyToStep.tsx
  8. 30 1
      dashboard/src/components/corpus/SentHistory.tsx
  9. 1 0
      dashboard/src/components/corpus/SentHistoryModal.tsx
  10. 1 0
      dashboard/src/components/dict/Community.tsx
  11. 1 0
      dashboard/src/components/dict/WordCard.tsx
  12. 17 18
      dashboard/src/components/general/TimeShow.tsx
  13. 1 0
      dashboard/src/components/template/SentEdit.tsx
  14. 102 11
      dashboard/src/components/template/SentEdit/EditInfo.tsx
  15. 1 0
      dashboard/src/components/template/SentEdit/SentCanRead.tsx
  16. 14 0
      dashboard/src/components/template/SentEdit/SentCell.tsx
  17. 7 2
      dashboard/src/components/template/SentEdit/SentCellEditable.tsx
  18. 1 0
      dashboard/src/components/template/SentEdit/SentContent.tsx
  19. 9 0
      dashboard/src/components/template/SentEdit/SentEditMenu.tsx
  20. 2 0
      dashboard/src/components/template/Wbw/WbwCase.tsx
  21. 4 1
      dashboard/src/components/template/Wbw/WbwDetailParent2.tsx
  22. 1 0
      dashboard/src/components/template/Wbw/WbwMeaningSelect.tsx
  23. 55 47
      dashboard/src/components/template/Wbw/WbwPali.tsx
  24. 2 0
      dashboard/src/components/template/Wbw/WbwParent2.tsx
  25. 1 0
      dashboard/src/components/template/Wbw/WbwWord.tsx
  26. 61 3
      dashboard/src/components/template/WbwSent.tsx
  27. 1 1
      dashboard/src/components/template/cs_para_map.ts
  28. 1 0
      dashboard/src/pages/admin/nissaya-ending/list.tsx
  29. 2 0
      dashboard/src/pages/admin/relation/list.tsx

+ 36 - 0
dashboard/src/assets/icon/index.tsx

@@ -641,6 +641,38 @@ const VideoSvg = () => (
     ></path>
   </svg>
 );
+
+const MergeSvg2 = () => (
+  <svg
+    viewBox="0 0 1024 1024"
+    version="1.1"
+    xmlns="http://www.w3.org/2000/svg"
+    p-id="6789"
+    width="1em"
+    height="1em"
+  >
+    <path
+      d="M180.878 119.396c-38.864 0-70.446 31.584-70.446 70.412s31.582 70.446 70.446 70.446c38.794 0 70.376-31.618 70.376-70.446S219.674 119.396 180.878 119.396z"
+      fill="currentColor"
+      p-id="6790"
+    ></path>
+    <path
+      d="M180.81 775.618c-38.868 0-70.448 31.582-70.448 70.446 0 38.794 31.58 70.376 70.448 70.376 38.864 0 70.446-31.582 70.446-70.376C251.256 807.2 219.674 775.618 180.81 775.618z"
+      fill="currentColor"
+      p-id="6791"
+    ></path>
+    <path
+      d="M843.192 775.618c-38.794 0-70.376 31.582-70.376 70.446 0 38.794 31.582 70.376 70.376 70.376 38.866 0 70.448-31.582 70.448-70.376C913.638 807.2 882.056 775.618 843.192 775.618z"
+      fill="currentColor"
+      p-id="6792"
+    ></path>
+    <path
+      d="M898.232 677.578l0-272.72c0-148.28-120.656-268.902-268.902-268.902l-142.924 0 59.942-59.908L470.298 0l-124.366 124.298c-26.122-65.72-90.126-112.464-165.054-112.464-98.178 0-178.008 79.832-178.008 177.972 0 81.792 55.742 150.136 131.022 170.9l0 314.352C58.542 695.786 2.8 764.274 2.8 846.062 2.8 944.17 82.63 1024 180.81 1024c98.176 0 178.006-79.83 178.006-177.938 0-76.75-49.088-141.734-117.364-166.664L241.452 356.47c47.198-17.226 85.222-53.466 104.128-99.892l126.048 126.046 76.046-76.048-63.094-63.06 144.746 0c88.934 0 161.342 72.374 161.342 161.34l0 272.02c-72.338 22.546-125.418 89.424-125.418 169.184 0 98.108 79.83 177.938 177.938 177.938 98.178 0 178.008-79.83 178.008-177.938C1021.2 767.212 969.38 700.968 898.232 677.578zM180.81 916.44c-38.868 0-70.448-31.582-70.448-70.376 0-38.864 31.58-70.446 70.448-70.446 38.864 0 70.446 31.582 70.446 70.446C251.256 884.858 219.674 916.44 180.81 916.44zM180.878 260.254c-38.864 0-70.446-31.618-70.446-70.446s31.582-70.412 70.446-70.412c38.794 0 70.376 31.584 70.376 70.412S219.674 260.254 180.878 260.254zM843.192 916.44c-38.794 0-70.376-31.582-70.376-70.376 0-38.864 31.582-70.446 70.376-70.446 38.866 0 70.448 31.582 70.448 70.446C913.638 884.858 882.056 916.44 843.192 916.44z"
+      fill="currentColor"
+      p-id="6793"
+    ></path>
+  </svg>
+);
 export const DictIcon = (props: Partial<CustomIconComponentProps>) => (
   <Icon component={DictSvg} {...props} />
 );
@@ -765,3 +797,7 @@ export const MergeIcon = (props: Partial<CustomIconComponentProps>) => (
 export const VideoIcon = (props: Partial<CustomIconComponentProps>) => (
   <Icon component={VideoSvg} {...props} />
 );
+
+export const MergeIcon2 = (props: Partial<CustomIconComponentProps>) => (
+  <Icon component={MergeSvg2} {...props} />
+);

+ 1 - 0
dashboard/src/components/admin/relation/CaseSelect.tsx

@@ -24,6 +24,7 @@ const CaseSelectWidget = ({ name = "case", width = "md" }: IWidget) => {
       value: item,
       label: intl.formatMessage({
         id: `dict.fields.type.${item}.label`,
+        defaultMessage: item,
       }),
     };
   });

+ 1 - 0
dashboard/src/components/admin/relation/GrammarSelect.tsx

@@ -67,6 +67,7 @@ const GrammarSelectWidget = ({
       value: item,
       label: intl.formatMessage({
         id: `dict.fields.type.${item}.label`,
+        defaultMessage: item,
       }),
     };
   });

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

@@ -170,6 +170,7 @@ export interface ISentenceData {
   updated_at: string;
   acceptor?: IUser;
   pr_edit_at?: string;
+  fork_at?: string;
   suggestionCount?: ISuggestionCount;
 }
 
@@ -187,6 +188,7 @@ export interface ISentenceNewRequest {
   sentences: ISentenceDiffData[];
   channel?: string;
   copy?: boolean;
+  fork_from?: string;
 }
 export interface ISentenceNewMultiResponse {
   ok: boolean;

+ 19 - 10
dashboard/src/components/channel/ChannelSentDiff.tsx

@@ -26,6 +26,7 @@ interface IWidget {
   srcChannel?: IChannel;
   destChannel?: IChannel;
   sentences?: string[];
+  important?: boolean;
   goPrev?: Function;
   onSubmit?: Function;
 }
@@ -33,6 +34,7 @@ const ChannelSentDiffWidget = ({
   srcChannel,
   destChannel,
   sentences,
+  important = false,
   goPrev,
   onSubmit,
 }: IWidget) => {
@@ -105,7 +107,11 @@ const ChannelSentDiffWidget = ({
           });
           setDiffData(diffList);
           setNewRowKeys(newRows);
-          setSelectedRowKeys(newRows);
+          if (important) {
+            setSelectedRowKeys(sentences);
+          } else {
+            setSelectedRowKeys(newRows);
+          }
           setEmptyRowKeys(emptyRows);
         }
       });
@@ -125,8 +131,9 @@ const ChannelSentDiffWidget = ({
           上一步
         </Button>
         <Select
-          defaultValue={"new"}
+          defaultValue={important ? "all" : "new"}
           style={{ width: 180 }}
+          disabled={important}
           onChange={(value: string) => {
             switch (value) {
               case "new":
@@ -182,14 +189,15 @@ const ChannelSentDiffWidget = ({
             if (typeof submitData === "undefined") {
               return;
             }
-            post<ISentenceNewRequest, ISentenceNewMultiResponse>(
-              `/v2/sentence`,
-              {
-                sentences: submitData,
-                channel: destChannel?.id,
-                copy: true,
-              }
-            )
+            const url = `/v2/sentence`;
+            const postData = {
+              sentences: submitData,
+              channel: destChannel?.id,
+              copy: true,
+              fork_from: srcChannel.id,
+            };
+            console.debug("fork post", url, postData);
+            post<ISentenceNewRequest, ISentenceNewMultiResponse>(url, postData)
               .then((json) => {
                 if (json.ok) {
                   if (typeof onSubmit !== "undefined") {
@@ -201,6 +209,7 @@ const ChannelSentDiffWidget = ({
               })
               .catch((e) => {
                 console.log(e);
+                message.error("error");
               })
               .finally(() => {
                 setLoading(false);

+ 3 - 0
dashboard/src/components/channel/CopyToModal.tsx

@@ -9,6 +9,7 @@ interface IWidget {
   channel?: IChannel;
   sentencesId?: string[];
   open?: boolean;
+  important?: boolean;
   onClose?: Function;
 }
 const CopyToModalWidget = ({
@@ -16,6 +17,7 @@ const CopyToModalWidget = ({
   channel,
   sentencesId,
   open,
+  important = false,
   onClose,
 }: IWidget) => {
   const [isModalOpen, setIsModalOpen] = useState(open);
@@ -59,6 +61,7 @@ const CopyToModalWidget = ({
           initStep={initStep}
           channel={channel}
           sentencesId={sentencesId}
+          important={important}
           onClose={() => {
             setIsModalOpen(false);
             Modal.destroyAll();

+ 3 - 0
dashboard/src/components/channel/CopyToStep.tsx

@@ -13,6 +13,7 @@ interface IWidget {
   type?: ArticleType;
   articleId?: string;
   sentencesId?: string[];
+  important?: boolean;
   stepChange?: Function;
   onClose?: Function;
 }
@@ -22,6 +23,7 @@ const CopyToStepWidget = ({
   type,
   articleId,
   sentencesId,
+  important = false,
   stepChange,
   onClose,
 }: IWidget) => {
@@ -77,6 +79,7 @@ const CopyToStepWidget = ({
           srcChannel={channel}
           destChannel={destChannel}
           sentences={sentencesId}
+          important={important}
           goPrev={() => {
             prev();
           }}

+ 30 - 1
dashboard/src/components/corpus/SentHistory.tsx

@@ -5,6 +5,9 @@ import { get } from "../../request";
 import User from "../auth/User";
 import { IUser } from "../auth/UserName";
 import TimeShow from "../general/TimeShow";
+import { IChannel } from "../channel/Channel";
+import { MergeIcon2 } from "../../assets/icon";
+import { IStudio } from "../auth/StudioName";
 
 const { Paragraph } = Typography;
 
@@ -14,6 +17,10 @@ export interface ISentHistoryData {
   content: string;
   editor: IUser;
   landmark: string;
+  fork_from?: IChannel;
+  fork_studio?: IStudio;
+  pr_from?: string | null;
+  accepter?: IUser;
   created_at: string;
 }
 
@@ -26,6 +33,9 @@ export interface ISentHistoryListResponse {
 interface ISentHistory {
   content: string;
   editor: IUser;
+  fork_from?: IChannel;
+  pr_from?: string | null;
+  accepter?: IUser;
   createdAt: string;
 }
 interface IWidget {
@@ -61,6 +71,9 @@ const SentHistoryWidget = ({ sentId }: IWidget) => {
             return {
               content: item.content,
               editor: item.editor,
+              fork_from: item.fork_from,
+              pr_from: item.pr_from,
+              accepter: item.accepter,
               createdAt: item.created_at,
             };
           });
@@ -96,12 +109,28 @@ const SentHistoryWidget = ({ sentId }: IWidget) => {
         avatar: {
           dataIndex: "image",
           editable: false,
+          render: (text, row, index, action) => {
+            return <User {...row.editor} showName={false} />;
+          },
         },
         description: {
           render: (text, row, index, action) => {
             return (
               <Space style={{ fontSize: "80%" }}>
-                <User {...row.editor} />
+                {row.accepter ? (
+                  <User {...row.accepter} showAvatar={false} />
+                ) : (
+                  <User {...row.editor} showAvatar={false} />
+                )}
+
+                {row.fork_from ? (
+                  <>
+                    <MergeIcon2 />
+                    {row.fork_from.name}
+                  </>
+                ) : (
+                  <></>
+                )}
                 <TimeShow type="secondary" createdAt={row.createdAt} />
               </Space>
             );

+ 1 - 0
dashboard/src/components/corpus/SentHistoryModal.tsx

@@ -43,6 +43,7 @@ const SentHistoryModalWidget = ({
     <>
       <span onClick={showModal}>{trigger}</span>
       <Modal
+        style={{ top: 20 }}
         width={"80%"}
         title={intl.formatMessage({
           id: "buttons.timeline",

+ 1 - 0
dashboard/src/components/dict/Community.tsx

@@ -227,6 +227,7 @@ const CommunityWidget = ({ word }: IWidget) => {
                     gid={strCase}
                     text={intl.formatMessage({
                       id: `dict.fields.type.${strCase}.label`,
+                      defaultMessage: strCase,
                     })}
                   />
                 ) : undefined;

+ 1 - 0
dashboard/src/components/dict/WordCard.tsx

@@ -67,6 +67,7 @@ const WordCardWidget = ({ data }: IWidgetWordCard) => {
                   gid={strCase}
                   text={intl.formatMessage({
                     id: `dict.fields.type.${strCase}.label`,
+                    defaultMessage: strCase,
                   })}
                 />
               );

+ 17 - 18
dashboard/src/components/general/TimeShow.tsx

@@ -30,34 +30,33 @@ const TimeShowWidget = ({
 
   let mTitle: string | undefined;
   let showTime: string | undefined;
-  if (typeof title === "undefined") {
-    if (updatedAt && createdAt) {
-      if (updatedAt === createdAt) {
-        mTitle = intl.formatMessage({
-          id: "labels.created-at",
-        });
-        showTime = createdAt;
-      } else {
-        mTitle = intl.formatMessage({
-          id: "labels.updated-at",
-        });
-        showTime = updatedAt;
-      }
-    } else if (createdAt) {
+  if (updatedAt && createdAt) {
+    if (updatedAt === createdAt) {
       mTitle = intl.formatMessage({
         id: "labels.created-at",
       });
       showTime = createdAt;
-    } else if (updatedAt) {
+    } else {
       mTitle = intl.formatMessage({
         id: "labels.updated-at",
       });
       showTime = updatedAt;
-    } else {
-      mTitle = undefined;
-      showTime = "";
     }
+  } else if (createdAt) {
+    mTitle = intl.formatMessage({
+      id: "labels.created-at",
+    });
+    showTime = createdAt;
+  } else if (updatedAt) {
+    mTitle = intl.formatMessage({
+      id: "labels.updated-at",
+    });
+    showTime = updatedAt;
   } else {
+    mTitle = undefined;
+    showTime = "";
+  }
+  if (typeof title !== "undefined") {
     mTitle = title;
   }
 

+ 1 - 0
dashboard/src/components/template/SentEdit.tsx

@@ -40,6 +40,7 @@ export interface ISentence {
   prEditAt?: string;
   channel: IChannel;
   studio?: IStudio;
+  forkAt?: string | null;
   updateAt: string;
   createdAt?: string;
   suggestionCount?: ISuggestionCount;

+ 102 - 11
dashboard/src/components/template/SentEdit/EditInfo.tsx

@@ -1,18 +1,102 @@
-import { Typography } from "antd";
+import { List, Popover, Typography, notification } from "antd";
 import { Space } from "antd";
 
-import StudioName from "../../auth/StudioName";
 import User from "../../auth/User";
 import Channel from "../../channel/Channel";
 import TimeShow from "../../general/TimeShow";
 import { ISentence } from "../SentEdit";
+import { MergeIcon2 } from "../../../assets/icon";
+import { useEffect, useState } from "react";
+import { get } from "../../../request";
+import {
+  ISentHistoryData,
+  ISentHistoryListResponse,
+} from "../../corpus/SentHistory";
+import moment from "moment";
 
 const { Text } = Typography;
 
+interface IFork {
+  sentId?: string;
+  highlight?: boolean;
+}
+const Fork = ({ sentId, highlight = false }: IFork) => {
+  const [data, setData] = useState<ISentHistoryData[]>();
+
+  useEffect(() => {
+    if (sentId) {
+      const url = `/v2/sent_history?view=sentence&id=${sentId}&fork=1`;
+      get<ISentHistoryListResponse>(url).then((json) => {
+        if (json.ok) {
+          setData(json.data.rows);
+        } else {
+          notification.error({ message: json.message });
+        }
+      });
+    }
+  }, [sentId]);
+  return (
+    <Popover
+      placement="bottom"
+      content={
+        <List
+          size="small"
+          header={highlight ? false : "已被修改"}
+          footer={false}
+          dataSource={data}
+          renderItem={(item) => (
+            <List.Item>
+              <Text>
+                {item.fork_studio?.nickName}-{item.fork_from?.name}
+              </Text>
+              <Text type="secondary" style={{ fontSize: "85%" }}>
+                <Space>
+                  <User {...item.accepter} showAvatar={false} />
+                  <TimeShow
+                    type="secondary"
+                    title="复制"
+                    createdAt={item.created_at}
+                  />
+                </Space>
+              </Text>
+            </List.Item>
+          )}
+        />
+      }
+    >
+      <span style={{ color: highlight ? "#1890ff" : "unset" }}>
+        <MergeIcon2 />
+      </span>
+    </Popover>
+  );
+};
+
+interface IMergeButton {
+  data: ISentence;
+}
+const MergeButton = ({ data }: IMergeButton) => {
+  if (data.forkAt) {
+    const fork = moment.utc(data.forkAt);
+    const fork_iso8610 = moment(data.forkAt, moment.ISO_8601);
+    const updated = moment(data.updateAt);
+    const diff = updated.diff(fork_iso8610, "seconds");
+    const diff1 = updated.diff(fork, "seconds");
+    console.debug("edit info time diff fork_iso8610 vs utc", diff, diff1);
+    if (fork.isSame(updated)) {
+      return <Fork sentId={data.id} highlight />;
+    } else {
+      return <Fork sentId={data.id} />;
+    }
+  } else {
+    return <></>;
+  }
+};
+
 interface IDetailsWidget {
   data: ISentence;
   isPr?: boolean;
 }
+
 export const Details = ({ data, isPr }: IDetailsWidget) => (
   <Space wrap>
     <Channel {...data.channel} />
@@ -30,11 +114,20 @@ export const Details = ({ data, isPr }: IDetailsWidget) => (
         createdAt={data.createdAt}
       />
     )}
-    {data.acceptor ? <User {...data.acceptor} showAvatar={false} /> : undefined}
-    {data.acceptor ? "accept at" : undefined}
-    {data.prEditAt ? (
-      <TimeShow type="secondary" updatedAt={data.updateAt} showLabel={false} />
-    ) : undefined}
+    <MergeButton data={data} />
+    <span style={{ display: "none" }}>
+      {data.acceptor ? (
+        <User {...data.acceptor} showAvatar={false} />
+      ) : undefined}
+      {data.acceptor ? "accept at" : undefined}
+      {data.prEditAt ? (
+        <TimeShow
+          type="secondary"
+          updatedAt={data.updateAt}
+          showLabel={false}
+        />
+      ) : undefined}
+    </span>
   </Space>
 );
 
@@ -44,13 +137,11 @@ interface IWidget {
   compact?: boolean;
 }
 const EditInfoWidget = ({ data, isPr = false, compact = false }: IWidget) => {
-  //console.log("data.createdAt", data.createdAt, data.updateAt);
+  console.debug("EditInfo", data);
   return (
     <div style={{ fontSize: "80%" }}>
       <Text type="secondary">
-        <Space>
-          {compact ? undefined : <Details data={data} isPr={isPr} />}
-        </Space>
+        {compact ? undefined : <Details data={data} isPr={isPr} />}
       </Text>
     </div>
   );

+ 1 - 0
dashboard/src/components/template/SentEdit/SentCanRead.tsx

@@ -66,6 +66,7 @@ const SentCanReadWidget = ({
               channel: item.channel,
               suggestionCount: item.suggestionCount,
               translationChannels: channelsId,
+              forkAt: item.fork_at,
               updateAt: item.updated_at,
             };
           });

+ 14 - 0
dashboard/src/components/template/SentEdit/SentCell.tsx

@@ -24,6 +24,7 @@ import { delete_ } from "../../../request";
 
 import "./style.css";
 import StudioName from "../../auth/StudioName";
+import CopyToModal from "../../channel/CopyToModal";
 
 interface IWidget {
   initValue?: ISentence;
@@ -58,6 +59,9 @@ const SentCellWidget = ({
   const [prOpen, setPrOpen] = useState(false);
   const discussionMessage = useAppSelector(message);
   const anchorInfo = useAppSelector(anchor);
+  const [copyOpen, setCopyOpen] = useState<boolean>(false);
+
+  const sentId = `${sentData?.book}-${sentData?.para}-${sentData?.wordStart}-${sentData?.wordEnd}`;
   const sid = `${sentData?.book}_${sentData?.para}_${sentData?.wordStart}_${sentData?.wordEnd}_${sentData?.channel.id}`;
   useEffect(() => {
     if (
@@ -137,6 +141,9 @@ const SentCellWidget = ({
         }}
         onMenuClick={(key: string) => {
           switch (key) {
+            case "copy-to":
+              setCopyOpen(true);
+              break;
             case "suggestion":
               setPrOpen(true);
               break;
@@ -353,6 +360,13 @@ const SentCellWidget = ({
         ) : undefined}
       </SentEditMenu>
       {compact ? undefined : <Divider style={{ margin: "10px 0" }} />}
+      <CopyToModal
+        important
+        sentencesId={[sentId]}
+        channel={sentData?.channel}
+        open={copyOpen}
+        onClose={() => setCopyOpen(false)}
+      />
     </div>
   );
 };

+ 7 - 2
dashboard/src/components/template/SentEdit/SentCellEditable.tsx

@@ -25,7 +25,6 @@ export const sentSave = (
 ) => {
   let url = `/v2/sentence/${data.book}_${data.para}_${data.wordStart}_${data.wordEnd}_${data.channel.id}`;
   url += "?mode=edit&html=true";
-  console.log("save url", url);
   const body = {
     book: data.book,
     para: data.para,
@@ -35,9 +34,11 @@ export const sentSave = (
     content: data.content,
     channels: data.translationChannels?.join(),
   };
+  console.log("save url", url, body);
   put<ISentenceRequest, ISentenceResponse>(url, body)
     .then((json) => {
       if (json.ok) {
+        console.debug("sent save ok", json.data);
         const newData: ISentence = {
           id: json.data.id,
           content: json.data.content,
@@ -47,7 +48,9 @@ export const sentSave = (
           wordStart: json.data.word_start,
           wordEnd: json.data.word_end,
           editor: json.data.editor,
+          studio: json.data.studio,
           channel: json.data.channel,
+          forkAt: json.data.fork_at,
           updateAt: json.data.updated_at,
         };
         ok(newData);
@@ -152,7 +155,6 @@ const SentCellEditableWidget = ({
     setSaving(true);
     let url = `/v2/sentence/${data.book}_${data.para}_${data.wordStart}_${data.wordEnd}_${data.channel.id}`;
     url += "?mode=edit&html=true";
-    console.log("save url", url);
     const body = {
       book: data.book,
       para: data.para,
@@ -162,6 +164,7 @@ const SentCellEditableWidget = ({
       content: value,
       channels: data.translationChannels?.join(),
     };
+    console.debug("save url", url, body);
     put<ISentenceRequest, ISentenceResponse>(url, body)
       .then((json) => {
         if (json.ok) {
@@ -176,7 +179,9 @@ const SentCellEditableWidget = ({
               wordStart: json.data.word_start,
               wordEnd: json.data.word_end,
               editor: json.data.editor,
+              studio: json.data.studio,
               channel: json.data.channel,
+              forkAt: json.data.fork_at,
               updateAt: json.data.updated_at,
             };
             onSave(newData);

+ 1 - 0
dashboard/src/components/template/SentEdit/SentContent.tsx

@@ -158,6 +158,7 @@ const SentContentWidget = ({
         {translation?.map((item, id) => {
           return (
             <SuggestionFocus
+              key={id}
               book={item.book}
               para={item.para}
               start={item.wordStart}

+ 9 - 0
dashboard/src/components/template/SentEdit/SentEditMenu.tsx

@@ -16,6 +16,7 @@ import {
   CommentOutlinedIcon,
   HandOutlinedIcon,
   JsonOutlinedIcon,
+  MergeIcon2,
   PasteOutLinedIcon,
 } from "../../../assets/icon";
 import { useIntl } from "react-intl";
@@ -84,6 +85,14 @@ const SentEditMenuWidget = ({
       icon: <FieldTimeOutlined />,
       disabled: isPr,
     },
+    {
+      key: "copy-to",
+      label: intl.formatMessage({
+        id: "buttons.copy.to",
+      }),
+      icon: <MergeIcon2 />,
+      disabled: isPr,
+    },
     {
       type: "divider",
     },

+ 2 - 0
dashboard/src/components/template/Wbw/WbwCase.tsx

@@ -51,6 +51,7 @@ export const caseInDict = (
       noNull.forEach((item, index, arr) => {
         arr[index] = intl.formatMessage({
           id: `dict.fields.type.${item}.short.label`,
+          defaultMessage: item,
         });
       });
       return { key: item, label: noNull.join(" ") };
@@ -150,6 +151,7 @@ const WbwCaseWidget = ({ data, display, onSplit, onChange }: IWidget) => {
               <span key={id} className="case">
                 {intl.formatMessage({
                   id: `dict.fields.type.${strCase}.short.label`,
+                  defaultMessage: strCase,
                 })}
               </span>
             );

+ 4 - 1
dashboard/src/components/template/Wbw/WbwDetailParent2.tsx

@@ -68,7 +68,10 @@ const WbwParent2Widget = ({ data, onChange }: IWidget) => {
   const options = grammar.map((item) => {
     return {
       value: `.${item}.`,
-      label: intl.formatMessage({ id: `dict.fields.type.${item}.label` }),
+      label: intl.formatMessage({
+        id: `dict.fields.type.${item}.label`,
+        defaultMessage: item,
+      }),
     };
   });
   return (

+ 1 - 0
dashboard/src/components/template/Wbw/WbwMeaningSelect.tsx

@@ -110,6 +110,7 @@ const WbwMeaningSelectWidget = ({ data, onSelect }: IWidget) => {
               name: wordType,
               local: intl.formatMessage({
                 id: `dict.fields.type.${wordType}.short.label`,
+                defaultMessage: wordType,
               }),
               meaning: [],
             });

+ 55 - 47
dashboard/src/components/template/Wbw/WbwPali.tsx

@@ -143,7 +143,7 @@ const WbwPaliWidget = ({ data, channelId, mode, display, onSave }: IWidget) => {
     setPopOpen(open);
   };
 
-  const wbwDetail = (
+  const wbwDetail = () => (
     <WbwDetail
       data={data}
       onClose={() => {
@@ -167,13 +167,15 @@ const WbwPaliWidget = ({ data, channelId, mode, display, onSave }: IWidget) => {
     />
   );
 
-  const noteIcon = data.note?.value ? (
-    data.note.value.trim() !== "" ? (
-      <Popover content={data.note?.value} placement="bottom">
-        <InfoCircleOutlined style={{ color: "blue" }} />
-      </Popover>
-    ) : undefined
-  ) : undefined;
+  const noteIcon = () =>
+    data.note?.value ? (
+      data.note.value.trim() !== "" ? (
+        <Popover content={data.note?.value} placement="bottom">
+          <InfoCircleOutlined style={{ color: "blue" }} />
+        </Popover>
+      ) : undefined
+    ) : undefined;
+
   const color = data.bookMarkColor?.value
     ? bookMarkColor[data.bookMarkColor.value]
     : "white";
@@ -182,23 +184,25 @@ const WbwPaliWidget = ({ data, channelId, mode, display, onSave }: IWidget) => {
   const videoList = data.attachments?.filter((item) =>
     item.content_type?.includes("video")
   );
-  const videoIcon = videoList ? (
-    <WbwVideoButton
-      video={videoList?.map((item) => {
-        return {
-          videoId: item.id,
-          type: item.content_type,
-          title: item.title,
-        };
-      })}
-    />
-  ) : undefined;
+  const videoIcon = () =>
+    videoList ? (
+      <WbwVideoButton
+        video={videoList?.map((item) => {
+          return {
+            videoId: item.id,
+            type: item.content_type,
+            title: item.title,
+          };
+        })}
+      />
+    ) : (
+      <></>
+    );
 
-  const relationIcon = data.relation ? (
-    <ApartmentOutlined style={{ color: "blue" }} />
-  ) : undefined;
+  const relationIcon = () =>
+    data.relation ? <ApartmentOutlined style={{ color: "blue" }} /> : undefined;
 
-  const bookMarkIcon =
+  const bookMarkIcon = () =>
     data.bookMarkText?.value && data.bookMarkText.value.trim() !== "" ? (
       <Popover
         content={<Paragraph copyable>{data.bookMarkText.value}</Paragraph>}
@@ -207,6 +211,7 @@ const WbwPaliWidget = ({ data, channelId, mode, display, onSave }: IWidget) => {
         <TagTwoTone twoToneColor={color} />
       </Popover>
     ) : undefined;
+
   let classPali: string = "pali";
   switch (data.style?.value) {
     case "note":
@@ -273,24 +278,27 @@ const WbwPaliWidget = ({ data, channelId, mode, display, onSave }: IWidget) => {
     display: "inline-block",
   };
 
-  const discussionIcon = hasComment ? (
-    <div style={commentShellStyle}>
-      <CommentBox
-        resId={data.uid}
-        resType="wbw"
-        trigger={
-          <Button icon={<CommentOutlinedIcon />} type="text" title="讨论" />
-        }
-        onCommentCountChange={(count: number) => {
-          if (count > 0) {
-            setHasComment(true);
-          } else {
-            setHasComment(false);
+  const discussionIcon = () =>
+    hasComment ? (
+      <div style={commentShellStyle}>
+        <CommentBox
+          resId={data.uid}
+          resType="wbw"
+          trigger={
+            <Button icon={<CommentOutlinedIcon />} type="text" title="讨论" />
           }
-        }}
-      />
-    </div>
-  ) : undefined;
+          onCommentCountChange={(count: number) => {
+            if (count > 0) {
+              setHasComment(true);
+            } else {
+              setHasComment(false);
+            }
+          }}
+        />
+      </div>
+    ) : (
+      <></>
+    );
 
   if (typeof data.real !== "undefined" && data.real.value !== "") {
     //非标点符号
@@ -346,11 +354,11 @@ const WbwPaliWidget = ({ data, channelId, mode, display, onSave }: IWidget) => {
           </Popover>
         </span>
         <Space>
-          {videoIcon}
-          {noteIcon}
-          {bookMarkIcon}
-          {relationIcon}
-          {discussionIcon}
+          {videoIcon()}
+          {noteIcon()}
+          {bookMarkIcon()}
+          {relationIcon()}
+          {discussionIcon()}
         </Space>
       </div>
     );
@@ -358,10 +366,10 @@ const WbwPaliWidget = ({ data, channelId, mode, display, onSave }: IWidget) => {
     //标点符号
     return (
       <div className="pali_shell" style={{ cursor: "unset" }}>
-        {data.type?.value === ":cs.para:" ? (
+        {data.bookName ? (
           <ParaLinkCtl
             title={data.word.value}
-            bookName={data.grammar?.value}
+            bookName={data.bookName}
             paragraphs={data.word?.value}
           />
         ) : (

+ 2 - 0
dashboard/src/components/template/Wbw/WbwParent2.tsx

@@ -7,6 +7,7 @@ interface IWidget {
 }
 const WbwParent2Widget = ({ data }: IWidget) => {
   const intl = useIntl();
+
   return data.grammar2?.value ? (
     data.grammar2.value.trim() !== "" ? (
       <Tooltip title={data.parent2?.value}>
@@ -16,6 +17,7 @@ const WbwParent2Widget = ({ data }: IWidget) => {
               "dict.fields.type." +
               data.grammar2.value?.replaceAll(".", "") +
               ".short.label",
+            defaultMessage: data.grammar2.value?.replaceAll(".", ""),
           })}
         </Tag>
       </Tooltip>

+ 1 - 0
dashboard/src/components/template/Wbw/WbwWord.tsx

@@ -89,6 +89,7 @@ export interface IWbw {
   attachments?: IWbwAttachment[];
   hasComment?: boolean;
   grammarId?: string;
+  bookName?: string;
 }
 export interface IWbwFields {
   real?: boolean;

+ 61 - 3
dashboard/src/components/template/WbwSent.tsx

@@ -22,6 +22,63 @@ import { settingInfo } from "../../reducers/setting";
 import { GetUserSetting } from "../auth/setting/default";
 import { getGrammar } from "../../reducers/term-vocabulary";
 
+export const paraMark = (wbwData: IWbw[]): IWbw[] => {
+  let start = false;
+  let bookCode = "";
+  let count = 0;
+  let currPara = 0;
+  let bookCodeStack: string[] = [];
+  wbwData.forEach((value: IWbw, index: number, array: IWbw[]) => {
+    if (value.word.value === "(") {
+      start = true;
+      bookCode = "";
+      bookCodeStack = [];
+      return;
+    }
+    if (start) {
+      if (!isNaN(Number(value.word.value.replaceAll("-", "")))) {
+        console.debug("para mark", "number", value.word.value);
+
+        if (bookCode === "" && bookCodeStack.length > 0) {
+          //继承之前的
+          let bookCodeList = bookCodeStack;
+          bookCode = bookCodeList[0];
+        }
+        const dot = bookCode.lastIndexOf(".");
+        let bookName = "";
+        let paraNum = "";
+        if (dot === -1) {
+          bookName = bookCode;
+          paraNum = value.word.value;
+        } else {
+          bookName = bookCode.substring(0, dot + 1);
+          paraNum = bookCode.substring(dot + 1) + value.word.value;
+        }
+        bookName = bookName.substring(0, 64).toLowerCase();
+        if (!bookCodeStack.includes(bookName)) {
+          bookCodeStack.push(bookName);
+        }
+        if (bookName !== "") {
+          array[index].bookName = bookName;
+          count++;
+        }
+      } else if (value.word.value === ";") {
+        bookCode = "";
+        return;
+      } else if (value.word.value === ")") {
+        start = false;
+        return;
+      }
+      bookCode += value.word.value;
+    }
+  });
+
+  if (count > 0) {
+    console.debug("para mark", count, wbwData);
+  }
+  return wbwData;
+};
+
 interface IMagicDictRequest {
   book: number;
   para: number;
@@ -109,7 +166,7 @@ export const WbwSentCtl = ({
   onMagicDictDone,
 }: IWidget) => {
   const intl = useIntl();
-  const [wordData, setWordData] = useState<IWbw[]>(data);
+  const [wordData, setWordData] = useState<IWbw[]>(paraMark(data));
   const [wbwMode, setWbwMode] = useState(display);
   const [fieldDisplay, setFieldDisplay] = useState(fields);
   const [displayMode, setDisplayMode] = useState<ArticleMode>();
@@ -165,14 +222,15 @@ export const WbwSentCtl = ({
 
   const update = (data: IWbw[]) => {
     console.debug("wbw update");
-    setWordData(data);
+    setWordData(paraMark(data));
     if (typeof onChange !== "undefined") {
       onChange(data);
     }
   };
+
   useEffect(() => {
     if (refreshable) {
-      setWordData(data);
+      setWordData(paraMark(data));
     }
   }, [data, refreshable]);
 

+ 1 - 1
dashboard/src/components/template/cs_para_map.ts

@@ -23,7 +23,7 @@ export const csParaMap = [
   { name: "sārattha.ṭī.2.", book: 205, para: 3 },
   { name: "sārattha.ṭī.3.", book: 206, para: 3 },
 
-  { name: "dī.ni..1.", book: 93, para: 3 },
+  { name: "dī.ni.1.", book: 93, para: 3 },
   { name: "dī.ni.2.", book: 94, para: 3 },
   { name: "dī.ni.3.", book: 95, para: 3 },
   { name: "dī.ni.aṭṭha.1.", book: 103, para: 3 },

+ 1 - 0
dashboard/src/pages/admin/nissaya-ending/list.tsx

@@ -191,6 +191,7 @@ const Widget = () => {
                 <Tag key={id}>
                   {intl.formatMessage({
                     id: `dict.fields.type.${item}.label`,
+                    defaultMessage: item,
                   })}
                 </Tag>
               ));

+ 2 - 0
dashboard/src/pages/admin/relation/list.tsx

@@ -207,6 +207,7 @@ const Widget = () => {
                   <Tag key={id}>
                     {intl.formatMessage({
                       id: `dict.fields.type.${item}.label`,
+                      defaultMessage: item,
                     })}
                   </Tag>
                 </Tooltip>
@@ -232,6 +233,7 @@ const Widget = () => {
                   <Tag key={id}>
                     {intl.formatMessage({
                       id: `dict.fields.type.${item}.label`,
+                      defaultMessage: item,
                     })}
                   </Tag>
                 </Tooltip>