From 8fd9d352be06b508c8830b882a96ab6cb98decae Mon Sep 17 00:00:00 2001 From: "Jashanpreet Singh (json singh)" <20891087+jashanbhullar@users.noreply.github.com> Date: Fri, 29 Sep 2023 14:00:27 +0530 Subject: [PATCH] chore(web): MD Block (#712) Co-authored-by: KaWaite <34051327+KaWaite@users.noreply.github.com> Co-authored-by: pyshx --- server/e2e/gql_storytelling_test.go | 6 +- server/pkg/builtin/manifest.yml | 14 +-- server/pkg/builtin/manifest_ja.yml | 91 +++++++++++++++++++ web/src/beta/components/ListItem/index.tsx | 6 +- .../components/Markdown/index.stories.tsx | 45 +++++++-- web/src/beta/components/Markdown/index.tsx | 1 + .../components/PopoverMenuContent/index.tsx | 4 +- .../tabs/map/LeftPanel/Layers/LayerItem.tsx | 3 +- .../beta/features/Editor/useStorytelling.ts | 11 ++- .../Block/builtin/Markdown/Editor.tsx | 69 ++++++++++++++ .../Block/builtin/Markdown/index.tsx | 47 ++++++++++ .../core/StoryPanel/Block/builtin/index.ts | 6 +- .../lib/core/StoryPanel/Page/BlockAddBar.tsx | 2 +- .../core/StoryPanel/PanelContent/index.tsx | 6 ++ .../services/api/storytellingApi/blocks.ts | 2 + web/src/services/i18n/translations/en.yml | 1 + web/src/services/i18n/translations/ja.yml | 1 + 17 files changed, 282 insertions(+), 33 deletions(-) create mode 100644 web/src/beta/lib/core/StoryPanel/Block/builtin/Markdown/Editor.tsx create mode 100644 web/src/beta/lib/core/StoryPanel/Block/builtin/Markdown/index.tsx diff --git a/server/e2e/gql_storytelling_test.go b/server/e2e/gql_storytelling_test.go index 56bc5f88ef..b3da31fbcd 100644 --- a/server/e2e/gql_storytelling_test.go +++ b/server/e2e/gql_storytelling_test.go @@ -934,8 +934,8 @@ func TestStoryPageBlocksCRUD(t *testing.T) { res.Object(). Path("$.data.node.stories[0].pages[0].blocks").Equal([]any{}) - _, _, blockID1 := createBlock(e, sID, storyID, pageID, "reearth", "storyBlock", nil) - _, _, blockID2 := createBlock(e, sID, storyID, pageID, "reearth", "storyBlock", nil) + _, _, blockID1 := createBlock(e, sID, storyID, pageID, "reearth", "textStoryBlock", nil) + _, _, blockID2 := createBlock(e, sID, storyID, pageID, "reearth", "textStoryBlock", nil) _, res = fetchSceneForStories(e, sID) res.Object(). @@ -947,7 +947,7 @@ func TestStoryPageBlocksCRUD(t *testing.T) { res.Object(). Path("$.data.node.stories[0].pages[0].blocks[:].id").Equal([]string{blockID2, blockID1}) - _, _, blockID3 := createBlock(e, sID, storyID, pageID, "reearth", "storyBlock", lo.ToPtr(1)) + _, _, blockID3 := createBlock(e, sID, storyID, pageID, "reearth", "textStoryBlock", lo.ToPtr(1)) _, res = fetchSceneForStories(e, sID) res.Object(). diff --git a/server/pkg/builtin/manifest.yml b/server/pkg/builtin/manifest.yml index 88e0e7aacf..1295b3da1b 100644 --- a/server/pkg/builtin/manifest.yml +++ b/server/pkg/builtin/manifest.yml @@ -2070,18 +2070,6 @@ extensions: type: string ui: datetime title: Time - - id: storyBlock - name: Block - type: storyBlock - description: Storytelling Block - schema: - groups: - - id: default - title: Basic - fields: - - id: title - type: string - title: Title - id: textStoryBlock name: Text Block type: storyBlock @@ -2105,7 +2093,7 @@ extensions: title: Content ui: multiline - id: mdTextStoryBlock - name: MD Test Block + name: MD Text Block type: storyBlock description: Storytelling MD Text Block schema: diff --git a/server/pkg/builtin/manifest_ja.yml b/server/pkg/builtin/manifest_ja.yml index 5f0414597a..004105ad02 100644 --- a/server/pkg/builtin/manifest_ja.yml +++ b/server/pkg/builtin/manifest_ja.yml @@ -1001,3 +1001,94 @@ extensions: always: 常に desktop: デスクトップのみ mobile: モバイルのみ + story: + name: ストーリー + description: ストーリーテリングのストーリー + storyPage: + name: ストーリーページ + description: ストーリーテリングのページ + propertySchema: + panel: + title: パネル設定 + fields: + padding: + title: 余白 + gap: + title: 空隙 + title: + title: タイトル設定 + fields: + title: + title: タイトル + color: + title: 色 + cameraAnimation: + title: カメラアニメーション + fields: + cameraPosition: + title: カメラ位置 + cameraDuration: + title: カメラ移動時間 + cameraDelay: + title: カメラ待機時間 + timePoint: + title: タイムポイント + fields: + timePoint: + title: 時間 + textStoryBlock: + name: テキスト + description: ストーリーテリングのテキストブロック + propertySchema: + panel: + title: パネル設定 + fields: + padding: + title: 余白 + default: + title: テキスト + fields: + text: + title: コンテンツ + mdTextStoryBlock: + name: マークダウン + description: ストーリーテリングのマークダウンブロック + propertySchema: + panel: + title: パネル設定 + fields: + padding: + title: 余白 + default: + title: マークダウン + fields: + text: + title: コンテンツ + imageStoryBlock: + name: 画像 + description: ストーリーテリングの画像ブロック + propertySchema: + panel: + title: パネル設定 + fields: + padding: + title: 余白 + default: + title: 画像 + fields: + text: + title: コンテンツ + videoStoryBlock: + name: 動画 + description: ストーリーテリングの動画ブロック + propertySchema: + panel: + title: パネル設定 + fields: + padding: + title: 余白 + default: + title: 動画 + fields: + text: + title: コンテンツ \ No newline at end of file diff --git a/web/src/beta/components/ListItem/index.tsx b/web/src/beta/components/ListItem/index.tsx index 38edc4a448..1c480ad1d7 100644 --- a/web/src/beta/components/ListItem/index.tsx +++ b/web/src/beta/components/ListItem/index.tsx @@ -36,7 +36,11 @@ const ListItem: FC = ({ return ( - {children} + {typeof children === "string" ? ( + {children} + ) : ( + children + )} {actionContent && ( A block quote with ~strikethrough~ and a URL: https://reactjs.org. @@ -9,20 +11,47 @@ const markdown = ` * [ ] todo * [x] done + +[Link](https://reactjs.org) + +### Image +![Alt text](https://images.pexels.com/photos/5656637/pexels-photo-5656637.jpeg?auto=compress&cs=tinysrgb&w=200) + A table: | a | b | | - | - | `; -export default { - component: Component, - parameters: { actions: { argTypesRegex: "^on.*" } }, -} as Meta; +const meta: Meta = { + component: Markdown, +}; + +export default meta; + +type Story = StoryObj; -export const Default: Story = args => ; +export const Default: Story = (args: Props) => { + return ( + +
+ +
+
+ ); +}; + +const Wrapper = styled.div` + display: flex; + flex-direction: column; + gap: 10%; + margin-left: 2rem; + margin-top: 2rem; + height: 300px; +`; Default.args = { children: markdown, - backgroundColor: "#fff", + onClick: () => console.log("clicked"), + onDoubleClick: () => console.log("double clicked clicked"), }; diff --git a/web/src/beta/components/Markdown/index.tsx b/web/src/beta/components/Markdown/index.tsx index 43d1b554cd..9f3b43a07c 100644 --- a/web/src/beta/components/Markdown/index.tsx +++ b/web/src/beta/components/Markdown/index.tsx @@ -58,6 +58,7 @@ const Wrapper = styled.div<{ styles?: Typography; dark: boolean }>` border-bottom: none; } + /* TODO: Fix hardcoded colors here */ code { background-color: ${({ dark }) => dark ? "rgba(240, 246, 252, 0.15)" : "rgba(27, 31, 35, 0.05)"}; diff --git a/web/src/beta/components/PopoverMenuContent/index.tsx b/web/src/beta/components/PopoverMenuContent/index.tsx index ce9506e7c8..f77c28c6d0 100644 --- a/web/src/beta/components/PopoverMenuContent/index.tsx +++ b/web/src/beta/components/PopoverMenuContent/index.tsx @@ -1,10 +1,10 @@ -import Icon, { Icons } from "@reearth/beta/components/Icon"; +import Icon from "@reearth/beta/components/Icon"; import { css, styled } from "@reearth/services/theme"; export type MenuItem = { name: string; isSelected?: boolean; - icon?: Icons; + icon?: string; disabled?: boolean; onClick?: () => void; }; diff --git a/web/src/beta/features/Editor/tabs/map/LeftPanel/Layers/LayerItem.tsx b/web/src/beta/features/Editor/tabs/map/LeftPanel/Layers/LayerItem.tsx index 4e20671868..00ad0de078 100644 --- a/web/src/beta/features/Editor/tabs/map/LeftPanel/Layers/LayerItem.tsx +++ b/web/src/beta/features/Editor/tabs/map/LeftPanel/Layers/LayerItem.tsx @@ -3,7 +3,6 @@ import { MouseEvent, useCallback, useState } from "react"; import TextInput from "@reearth/beta/components/fields/common/TextInput"; import ListItem from "@reearth/beta/components/ListItem"; import PopoverMenuContent from "@reearth/beta/components/PopoverMenuContent"; -import Text from "@reearth/beta/components/Text"; import type { LayerNameUpdateProps } from "@reearth/beta/features/Editor/useLayers"; type LayerItemProps = { @@ -90,7 +89,7 @@ const LayerItem = ({ onBlur={handleEditExit} /> ) : ( - {layerTitle} + layerTitle )} ); diff --git a/web/src/beta/features/Editor/useStorytelling.ts b/web/src/beta/features/Editor/useStorytelling.ts index 5b203639fb..eb8b49846b 100644 --- a/web/src/beta/features/Editor/useStorytelling.ts +++ b/web/src/beta/features/Editor/useStorytelling.ts @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useMemo, useState } from "react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import type { FlyTo } from "@reearth/beta/lib/core/types"; import type { Camera } from "@reearth/beta/utils/value"; @@ -18,6 +18,8 @@ const getPage = (id?: string, pages?: Page[]) => { export default function ({ sceneId, onFlyTo }: Props) { const t = useT(); + const timeoutRef = useRef(); + const { useStoriesQuery, useCreateStoryPage, @@ -66,7 +68,12 @@ export default function ({ sceneId, onFlyTo }: Props) { if (!destination) return; const duration = camera.fields.find(f => f.id === "cameraDuration")?.value as number; - onFlyTo({ ...destination }, { duration }); + const delay = (camera.fields.find(f => f.id === "cameraDelay")?.value ?? 0) as number; + + if (timeoutRef.current) clearTimeout(timeoutRef.current); + timeoutRef.current = setTimeout(() => { + onFlyTo({ ...destination }, { duration }); + }, delay * 1000); } }, [selectedStory?.pages, onFlyTo], diff --git a/web/src/beta/lib/core/StoryPanel/Block/builtin/Markdown/Editor.tsx b/web/src/beta/lib/core/StoryPanel/Block/builtin/Markdown/Editor.tsx new file mode 100644 index 0000000000..c8f42e383b --- /dev/null +++ b/web/src/beta/lib/core/StoryPanel/Block/builtin/Markdown/Editor.tsx @@ -0,0 +1,69 @@ +import { debounce } from "lodash-es"; +import { useContext, useCallback, useLayoutEffect, useRef, useMemo, useState } from "react"; + +import Markdown from "@reearth/beta/components/Markdown"; +import { useT } from "@reearth/services/i18n"; +import { styled } from "@reearth/services/theme"; + +import { BlockContext } from "../common/Wrapper"; + +export type Props = { + text: string; + onUpdate: (text: string) => void; +}; + +const MdBlockEditor: React.FC = ({ text, onUpdate }) => { + const textareaRef = useRef(null); + const t = useT(); + const context = useContext(BlockContext); + + const [value, setValue] = useState(text); + + const debouncedHandleTextUpdate = useMemo(() => debounce(onUpdate, 1000), [onUpdate]); + + const onChange = useCallback( + (e: React.ChangeEvent) => { + setValue(e.currentTarget.value); + debouncedHandleTextUpdate(e.currentTarget.value); + }, + [debouncedHandleTextUpdate], + ); + + useLayoutEffect(() => { + if (!textareaRef?.current) return; + // Reset height - important to shrink on delete + textareaRef.current.style.height = "inherit"; + // Set height + textareaRef.current.style.height = `${textareaRef.current.scrollHeight}px`; + }, [value, context?.editMode]); + + return context?.editMode ? ( + + ) : ( + {value || t("Add markdown text here")} + ); +}; + +const StyledTextArea = styled.textarea` + width: 100%; + resize: none; + overflow: hidden; + ${({ value }) => !value && "min-height: 115px;"} + border: none; + font-size: 14px; + padding: 0px; + outline: none; +`; + +const StyledMarkdown = styled(Markdown)<{ empty: boolean }>` + ${({ empty }) => empty && "min-height: 115px;"} + font-size: 14px; + opacity: ${({ empty }) => (!empty ? 1 : 0.6)}; +`; + +export default MdBlockEditor; diff --git a/web/src/beta/lib/core/StoryPanel/Block/builtin/Markdown/index.tsx b/web/src/beta/lib/core/StoryPanel/Block/builtin/Markdown/index.tsx new file mode 100644 index 0000000000..1e74b0953b --- /dev/null +++ b/web/src/beta/lib/core/StoryPanel/Block/builtin/Markdown/index.tsx @@ -0,0 +1,47 @@ +import { useCallback, useMemo } from "react"; + +import { ValueTypes } from "@reearth/beta/utils/value"; + +import { getFieldValue } from "../../../utils"; +import { CommonProps as BlockProps } from "../../types"; +import usePropertyValueUpdate from "../common/usePropertyValueUpdate"; +import BlockWrapper from "../common/Wrapper"; + +import MdEditor from "./Editor"; + +export type Props = BlockProps; + +const MdBlock: React.FC = ({ block, isSelected, ...props }) => { + const text = useMemo( + () => getFieldValue(block?.property?.items ?? [], "text") as ValueTypes["string"], + [block?.property?.items], + ); + + const { handlePropertyValueUpdate } = usePropertyValueUpdate(); + + const handleTextUpdate = useCallback( + (text: string) => { + const schemaGroup = block?.property?.items?.find( + i => i.schemaGroup === "default", + )?.schemaGroup; + if (!block?.property?.id || !schemaGroup) return; + handlePropertyValueUpdate(schemaGroup, block?.property?.id, "text", "string")(text); + }, + [block?.property?.id, block?.property?.items, handlePropertyValueUpdate], + ); + + return ( + + + + ); +}; + +export default MdBlock; diff --git a/web/src/beta/lib/core/StoryPanel/Block/builtin/index.ts b/web/src/beta/lib/core/StoryPanel/Block/builtin/index.ts index b0738098c5..78959a9174 100644 --- a/web/src/beta/lib/core/StoryPanel/Block/builtin/index.ts +++ b/web/src/beta/lib/core/StoryPanel/Block/builtin/index.ts @@ -2,6 +2,7 @@ import { merge } from "lodash-es"; import { IMAGE_BUILTIN_STORY_BLOCK_ID, + MD_BUILTIN_STORY_BLOCK_ID, TEXT_BUILTIN_STORY_BLOCK_ID, TITLE_BUILTIN_STORY_BLOCK_ID, VIDEO_BUILTIN_STORY_BLOCK_ID, @@ -10,6 +11,7 @@ import { import { Component } from ".."; import ImageBlock from "./Image"; +import MdBlock from "./Markdown"; import TextBlock from "./Text"; import TitleBlock from "./Title"; import VideoBlock from "./Video"; @@ -18,7 +20,8 @@ export type ReEarthBuiltinStoryBlocks = Record< | typeof TITLE_BUILTIN_STORY_BLOCK_ID | typeof IMAGE_BUILTIN_STORY_BLOCK_ID | typeof TEXT_BUILTIN_STORY_BLOCK_ID - | typeof VIDEO_BUILTIN_STORY_BLOCK_ID, + | typeof VIDEO_BUILTIN_STORY_BLOCK_ID + | typeof MD_BUILTIN_STORY_BLOCK_ID, T >; @@ -30,6 +33,7 @@ const reearthBuiltin: BuiltinStoryBlocks = { [IMAGE_BUILTIN_STORY_BLOCK_ID]: ImageBlock, [TEXT_BUILTIN_STORY_BLOCK_ID]: TextBlock, [VIDEO_BUILTIN_STORY_BLOCK_ID]: VideoBlock, + [MD_BUILTIN_STORY_BLOCK_ID]: MdBlock, }; const builtin = merge({}, reearthBuiltin); diff --git a/web/src/beta/lib/core/StoryPanel/Page/BlockAddBar.tsx b/web/src/beta/lib/core/StoryPanel/Page/BlockAddBar.tsx index d94db69c43..04d9a25d37 100644 --- a/web/src/beta/lib/core/StoryPanel/Page/BlockAddBar.tsx +++ b/web/src/beta/lib/core/StoryPanel/Page/BlockAddBar.tsx @@ -26,7 +26,7 @@ const BlockAddBar: React.FC = ({ installableStoryBlocks?.map?.(sb => { return { name: sb.name, - icon: "plugin", + icon: sb.extensionId ?? "plugin", onClick: () => { onBlockAdd?.(sb.extensionId, sb.pluginId); onBlockOpen(); diff --git a/web/src/beta/lib/core/StoryPanel/PanelContent/index.tsx b/web/src/beta/lib/core/StoryPanel/PanelContent/index.tsx index 043d564266..a6ee15f1eb 100644 --- a/web/src/beta/lib/core/StoryPanel/PanelContent/index.tsx +++ b/web/src/beta/lib/core/StoryPanel/PanelContent/index.tsx @@ -101,6 +101,12 @@ const PagesWrapper = styled.div<{ showingIndicator?: boolean; isEditable?: boole height: ${({ showingIndicator }) => (showingIndicator ? "calc(100% - 8px)" : "100%")}; overflow-y: auto; cursor: ${({ isEditable }) => (isEditable ? "pointer" : "default")}; + + ::-webkit-scrollbar { + display: none; + } + scrollbar-width: none; + -ms-overflow-style: none; `; const PageGap = styled.div<{ height?: number }>` diff --git a/web/src/services/api/storytellingApi/blocks.ts b/web/src/services/api/storytellingApi/blocks.ts index 5a4cfdd639..e82c47a316 100644 --- a/web/src/services/api/storytellingApi/blocks.ts +++ b/web/src/services/api/storytellingApi/blocks.ts @@ -32,11 +32,13 @@ export const TITLE_BUILTIN_STORY_BLOCK_ID = "reearth/titleStoryBlock"; // pseudo export const IMAGE_BUILTIN_STORY_BLOCK_ID = "reearth/imageStoryBlock"; export const TEXT_BUILTIN_STORY_BLOCK_ID = "reearth/textStoryBlock"; export const VIDEO_BUILTIN_STORY_BLOCK_ID = "reearth/videoStoryBlock"; +export const MD_BUILTIN_STORY_BLOCK_ID = "reearth/mdTextStoryBlock"; export const AVAILABLE_STORY_BLOCK_IDS = [ IMAGE_BUILTIN_STORY_BLOCK_ID, TEXT_BUILTIN_STORY_BLOCK_ID, VIDEO_BUILTIN_STORY_BLOCK_ID, + MD_BUILTIN_STORY_BLOCK_ID, ]; export type StoryBlockQueryProps = SceneQueryProps; diff --git a/web/src/services/i18n/translations/en.yml b/web/src/services/i18n/translations/en.yml index b5f5d6450b..a3eaf1755b 100644 --- a/web/src/services/i18n/translations/en.yml +++ b/web/src/services/i18n/translations/en.yml @@ -199,6 +199,7 @@ Play timeline: Play timeline ellipse: ellipse Open timeline: Open timeline Padding settings: Padding settings +Add markdown text here: '' Drop here: Drop here Write your story :): Write your story :) Normal: Normal diff --git a/web/src/services/i18n/translations/ja.yml b/web/src/services/i18n/translations/ja.yml index 28a2d74353..84fe57e2e2 100644 --- a/web/src/services/i18n/translations/ja.yml +++ b/web/src/services/i18n/translations/ja.yml @@ -180,6 +180,7 @@ Play timeline: タイムラインを再生する ellipse: 円錐 Open timeline: タイムラインを開く Padding settings: 余白設定 +Add markdown text here: '' Drop here: ここにドロップ Write your story :): '' Normal: ''