diff options
Diffstat (limited to 'FrontEnd/src/app/views/timeline-common')
4 files changed, 306 insertions, 384 deletions
diff --git a/FrontEnd/src/app/views/timeline-common/TimelineCardTemplate.tsx b/FrontEnd/src/app/views/timeline-common/TimelineCardTemplate.tsx deleted file mode 100644 index c29e628d..00000000 --- a/FrontEnd/src/app/views/timeline-common/TimelineCardTemplate.tsx +++ /dev/null @@ -1,137 +0,0 @@ -import React from "react"; -import clsx from "clsx"; -import { useTranslation } from "react-i18next"; -import { Dropdown, Button } from "react-bootstrap"; - -import { getHttpHighlightClient } from "@/http/highlight"; -import { getHttpBookmarkClient } from "@/http/bookmark"; - -import { useUser } from "@/services/user"; -import { pushAlert } from "@/services/alert"; -import { timelineVisibilityTooltipTranslationMap } from "@/services/timeline"; - -import { TimelineCardComponentProps } from "../timeline-common/TimelinePageTemplateUI"; -import CollapseButton from "../timeline-common/CollapseButton"; - -export interface TimelineCardTemplateProps - extends Omit<TimelineCardComponentProps<"">, "operations"> { - infoArea: React.ReactElement; - manageArea: - | { type: "member"; onMember: () => void } - | { - type: "manage"; - items: ( - | { - type: "button"; - text: string; - color?: string; - onClick: () => void; - } - | { type: "divider" } - )[]; - }; -} - -function TimelineCardTemplate({ - timeline, - collapse, - infoArea, - manageArea, - toggleCollapse, - className, -}: TimelineCardTemplateProps): React.ReactElement | null { - const { t } = useTranslation(); - - const user = useUser(); - - return ( - <div className={clsx("cru-card p-2 clearfix", className)}> - <div className="float-right d-flex align-items-center"> - <CollapseButton collapse={collapse} onClick={toggleCollapse} /> - </div> - <div style={{ display: collapse ? "none" : "block" }}> - {infoArea} - <p className="mb-0">{timeline.description}</p> - <small className="mt-1 d-block"> - {t(timelineVisibilityTooltipTranslationMap[timeline.visibility])} - </small> - <div className="text-right mt-2"> - <i - className={clsx( - timeline.isHighlight ? "bi-star-fill" : "bi-star", - "icon-button text-yellow mr-3" - )} - onClick={ - user != null && user.hasHighlightTimelineAdministrationPermission - ? () => { - getHttpHighlightClient() - [timeline.isHighlight ? "delete" : "put"](timeline.name) - .catch(() => { - pushAlert({ - message: timeline.isHighlight - ? "timeline.removeHighlightFail" - : "timeline.addHighlightFail", - type: "danger", - }); - }); - } - : undefined - } - /> - {user != null ? ( - <i - className={clsx( - timeline.isBookmark ? "bi-bookmark-fill" : "bi-bookmark", - "icon-button text-yellow mr-3" - )} - onClick={() => { - getHttpBookmarkClient() - [timeline.isBookmark ? "delete" : "put"](timeline.name) - .catch(() => { - pushAlert({ - message: timeline.isBookmark - ? "timeline.removeBookmarkFail" - : "timeline.addBookmarkFail", - type: "danger", - }); - }); - }} - /> - ) : null} - {manageArea.type === "manage" ? ( - <Dropdown className="d-inline-block"> - <Dropdown.Toggle variant="outline-primary"> - {t("timeline.manage")} - </Dropdown.Toggle> - <Dropdown.Menu> - {manageArea.items.map((item, index) => { - if (item.type === "divider") { - return <Dropdown.Divider key={index} />; - } else { - return ( - <Dropdown.Item - key={index} - onClick={item.onClick} - className={ - item.color != null ? "text-" + item.color : undefined - } - > - {t(item.text)} - </Dropdown.Item> - ); - } - })} - </Dropdown.Menu> - </Dropdown> - ) : ( - <Button variant="outline-primary" onClick={manageArea.onMember}> - {t("timeline.memberButton")} - </Button> - )} - </div> - </div> - </div> - ); -} - -export default TimelineCardTemplate; diff --git a/FrontEnd/src/app/views/timeline-common/TimelinePageCardTemplate.tsx b/FrontEnd/src/app/views/timeline-common/TimelinePageCardTemplate.tsx new file mode 100644 index 00000000..921f1390 --- /dev/null +++ b/FrontEnd/src/app/views/timeline-common/TimelinePageCardTemplate.tsx @@ -0,0 +1,173 @@ +import React from "react"; +import clsx from "clsx"; +import { useTranslation } from "react-i18next"; +import { Dropdown, Button } from "react-bootstrap"; + +import { getHttpHighlightClient } from "@/http/highlight"; +import { getHttpBookmarkClient } from "@/http/bookmark"; + +import { useUser } from "@/services/user"; +import { pushAlert } from "@/services/alert"; +import { timelineVisibilityTooltipTranslationMap } from "@/services/timeline"; + +import { TimelinePageCardProps } from "./TimelinePageTemplate"; + +import CollapseButton from "./CollapseButton"; +import { TimelineMemberDialog } from "./TimelineMember"; +import TimelinePropertyChangeDialog from "./TimelinePropertyChangeDialog"; + +export interface TimelineCardTemplateProps extends TimelinePageCardProps { + infoArea: React.ReactElement; + manageArea: + | { type: "member" } + | { + type: "manage"; + items: ( + | { + type: "button"; + text: string; + color?: string; + onClick: () => void; + } + | { type: "divider" } + )[]; + }; + dialog: string | "property" | "member" | null; + setDialog: (dialog: "property" | "member" | null) => void; +} + +const TimelinePageCardTemplate: React.FC<TimelineCardTemplateProps> = ({ + timeline, + collapse, + toggleCollapse, + infoArea, + manageArea, + onReload, + className, + dialog, + setDialog, +}) => { + const { t } = useTranslation(); + + const user = useUser(); + + return ( + <> + <div className={clsx("cru-card p-2 clearfix", className)}> + <div className="float-right d-flex align-items-center"> + <CollapseButton collapse={collapse} onClick={toggleCollapse} /> + </div> + <div style={{ display: collapse ? "none" : "block" }}> + {infoArea} + <p className="mb-0">{timeline.description}</p> + <small className="mt-1 d-block"> + {t(timelineVisibilityTooltipTranslationMap[timeline.visibility])} + </small> + <div className="text-right mt-2"> + <i + className={clsx( + timeline.isHighlight ? "bi-star-fill" : "bi-star", + "icon-button text-yellow mr-3" + )} + onClick={ + user != null && + user.hasHighlightTimelineAdministrationPermission + ? () => { + getHttpHighlightClient() + [timeline.isHighlight ? "delete" : "put"](timeline.name) + .catch(() => { + pushAlert({ + message: timeline.isHighlight + ? "timeline.removeHighlightFail" + : "timeline.addHighlightFail", + type: "danger", + }); + }); + } + : undefined + } + /> + {user != null ? ( + <i + className={clsx( + timeline.isBookmark ? "bi-bookmark-fill" : "bi-bookmark", + "icon-button text-yellow mr-3" + )} + onClick={() => { + getHttpBookmarkClient() + [timeline.isBookmark ? "delete" : "put"](timeline.name) + .catch(() => { + pushAlert({ + message: timeline.isBookmark + ? "timeline.removeBookmarkFail" + : "timeline.addBookmarkFail", + type: "danger", + }); + }); + }} + /> + ) : null} + {manageArea.type === "manage" ? ( + <Dropdown className="d-inline-block"> + <Dropdown.Toggle variant="outline-primary"> + {t("timeline.manage")} + </Dropdown.Toggle> + <Dropdown.Menu> + {manageArea.items.map((item, index) => { + if (item.type === "divider") { + return <Dropdown.Divider key={index} />; + } else { + return ( + <Dropdown.Item + key={index} + onClick={item.onClick} + className={ + item.color != null + ? "text-" + item.color + : undefined + } + > + {t(item.text)} + </Dropdown.Item> + ); + } + })} + </Dropdown.Menu> + </Dropdown> + ) : ( + <Button + variant="outline-primary" + onClick={() => setDialog("member")} + > + {t("timeline.memberButton")} + </Button> + )} + </div> + </div> + </div> + {(() => { + if (dialog === "member") { + return ( + <TimelineMemberDialog + timeline={timeline} + onClose={() => setDialog(null)} + open + onChange={onReload} + /> + ); + } else if (dialog === "property") { + return ( + <TimelinePropertyChangeDialog + timeline={timeline} + close={() => setDialog(null)} + open + onChange={onReload} + /> + ); + } + })()} + </> + ); +}; + +export default TimelinePageCardTemplate; diff --git a/FrontEnd/src/app/views/timeline-common/TimelinePageTemplate.tsx b/FrontEnd/src/app/views/timeline-common/TimelinePageTemplate.tsx index 92eb0887..3087c20e 100644 --- a/FrontEnd/src/app/views/timeline-common/TimelinePageTemplate.tsx +++ b/FrontEnd/src/app/views/timeline-common/TimelinePageTemplate.tsx @@ -1,33 +1,39 @@ import React from "react"; - -import { UiLogicError } from "@/common"; +import { useTranslation } from "react-i18next"; +import { Spinner } from "react-bootstrap"; import { HttpNetworkError, HttpNotFoundError } from "@/http/common"; -import { getHttpTimelineClient, HttpTimelineInfo } from "@/http/timeline"; - -import { TimelineMemberDialog } from "./TimelineMember"; -import TimelinePropertyChangeDialog from "./TimelinePropertyChangeDialog"; -import { TimelinePageTemplateUIProps } from "./TimelinePageTemplateUI"; - -export interface TimelinePageTemplateProps<TManageItem> { - name: string; - onManage: (item: TManageItem) => void; - UiComponent: React.ComponentType< - Omit<TimelinePageTemplateUIProps<TManageItem>, "CardComponent"> - >; +import { + getHttpTimelineClient, + HttpTimelineInfo, + HttpTimelinePostInfo, +} from "@/http/timeline"; + +import { getAlertHost } from "@/services/alert"; + +import Timeline from "./Timeline"; +import TimelinePostEdit from "./TimelinePostEdit"; + +export interface TimelinePageCardProps { + timeline: HttpTimelineInfo; + collapse: boolean; + toggleCollapse: () => void; + className?: string; + onReload: () => void; +} + +export interface TimelinePageTemplateProps { + timelineName: string; notFoundI18nKey: string; reloadKey: number; onReload: () => void; + CardComponent: React.ComponentType<TimelinePageCardProps>; } -export default function TimelinePageTemplate<TManageItem>( - props: TimelinePageTemplateProps<TManageItem> -): React.ReactElement | null { - const { name, reloadKey, onReload } = props; +const TimelinePageTemplate: React.FC<TimelinePageTemplateProps> = (props) => { + const { timelineName, reloadKey, onReload, CardComponent } = props; - const [dialog, setDialog] = React.useState<null | "property" | "member">( - null - ); + const { t } = useTranslation(); const [timeline, setTimeline] = React.useState< HttpTimelineInfo | "loading" | "offline" | "notexist" | "error" @@ -38,7 +44,7 @@ export default function TimelinePageTemplate<TManageItem>( let subscribe = true; void getHttpTimelineClient() - .getTimeline(name) + .getTimeline(timelineName) .then( (data) => { if (subscribe) { @@ -61,70 +67,117 @@ export default function TimelinePageTemplate<TManageItem>( return () => { subscribe = false; }; - }, [name, reloadKey]); + }, [timelineName, reloadKey]); - let dialogElement: React.ReactElement | undefined; - const closeDialog = (): void => setDialog(null); + const scrollToBottom = React.useCallback(() => { + window.scrollTo(0, document.body.scrollHeight); + }, []); - if (dialog === "property") { - if (typeof timeline !== "object") { - throw new UiLogicError( - "Timeline is null but attempt to open change property dialog." - ); + const [bottomSpaceHeight, setBottomSpaceHeight] = React.useState<number>(0); + + const [timelineReloadKey, setTimelineReloadKey] = React.useState<number>(0); + + const [newPosts, setNewPosts] = React.useState<HttpTimelinePostInfo[]>([]); + + const reloadTimeline = (): void => { + setTimelineReloadKey((old) => old + 1); + setNewPosts([]); + }; + + const onPostEditHeightChange = React.useCallback((height: number): void => { + setBottomSpaceHeight(height); + if (height === 0) { + const alertHost = getAlertHost(); + if (alertHost != null) { + alertHost.style.removeProperty("margin-bottom"); + } + } else { + const alertHost = getAlertHost(); + if (alertHost != null) { + alertHost.style.marginBottom = `${height}px`; + } } + }, []); + + const cardCollapseLocalStorageKey = `timeline.${timelineName}.cardCollapse`; - dialogElement = ( - <TimelinePropertyChangeDialog - open - close={closeDialog} - timeline={timeline} - onChange={onReload} - /> + const [cardCollapse, setCardCollapse] = React.useState<boolean>(true); + React.useEffect(() => { + const savedCollapse = + window.localStorage.getItem(cardCollapseLocalStorageKey) === "true"; + setCardCollapse(savedCollapse); + }, [cardCollapseLocalStorageKey]); + + const toggleCardCollapse = (): void => { + const newState = !cardCollapse; + setCardCollapse(newState); + window.localStorage.setItem( + cardCollapseLocalStorageKey, + newState.toString() ); - } else if (dialog === "member") { - if (typeof timeline !== "object") { - throw new UiLogicError( - "Timeline is null but attempt to open change property dialog." - ); - } + }; + + let body: React.ReactElement; - dialogElement = ( - <TimelineMemberDialog - open - onClose={closeDialog} - timeline={timeline} - onChange={onReload} - /> + if (timeline == "loading") { + body = ( + <div className="full-viewport-center-child"> + <Spinner variant="primary" animation="grow" /> + </div> + ); + } else if (timeline === "offline") { + // TODO: i18n + body = <p className="text-danger">Offline!</p>; + } else if (timeline === "notexist") { + body = <p className="text-danger">{t(props.notFoundI18nKey)}</p>; + } else if (timeline === "error") { + // TODO: i18n + body = <p className="text-danger">Error!</p>; + } else { + body = ( + <> + <CardComponent + className="timeline-template-card" + timeline={timeline} + collapse={cardCollapse} + toggleCollapse={toggleCardCollapse} + onReload={onReload} + /> + <div + className="timeline-container" + style={{ + minHeight: `calc(100vh - ${56 + bottomSpaceHeight}px)`, + }} + > + <Timeline + top={40} + timelineName={timeline.name} + reloadKey={timelineReloadKey} + onReload={reloadTimeline} + additionalPosts={newPosts} + onLoad={scrollToBottom} + /> + </div> + {timeline.postable ? ( + <> + <div + style={{ height: bottomSpaceHeight }} + className="flex-fix-length" + /> + <TimelinePostEdit + className="fixed-bottom" + timeline={timeline} + onHeightChange={onPostEditHeightChange} + onPosted={(newPost) => { + setNewPosts((old) => [...old, newPost]); + }} + /> + </> + ) : null} + </> ); } + return body; +}; - const { UiComponent } = props; - - return ( - <> - <UiComponent - timeline={ - typeof timeline === "object" - ? { - ...timeline, - operations: { - onManage: timeline.manageable - ? (item) => { - if (item === "property") { - setDialog(item); - } else { - props.onManage(item); - } - } - : undefined, - onMember: () => setDialog("member"), - }, - } - : timeline - } - notExistMessageI18nKey={props.notFoundI18nKey} - /> - {dialogElement} - </> - ); -} +export default TimelinePageTemplate; diff --git a/FrontEnd/src/app/views/timeline-common/TimelinePageTemplateUI.tsx b/FrontEnd/src/app/views/timeline-common/TimelinePageTemplateUI.tsx deleted file mode 100644 index 7319d84d..00000000 --- a/FrontEnd/src/app/views/timeline-common/TimelinePageTemplateUI.tsx +++ /dev/null @@ -1,167 +0,0 @@ -import React from "react"; -import { useTranslation } from "react-i18next"; -import { Spinner } from "react-bootstrap"; - -import { getAlertHost } from "@/services/alert"; - -import { HttpTimelineInfo, HttpTimelinePostInfo } from "@/http/timeline"; - -import Timeline from "./Timeline"; -import TimelinePostEdit from "./TimelinePostEdit"; - -export interface TimelineCardComponentProps<TManageItems> { - timeline: HttpTimelineInfo; - operations: { - onManage?: (item: TManageItems | "property") => void; - onMember: () => void; - }; - collapse: boolean; - toggleCollapse: () => void; - className?: string; -} - -export interface TimelinePageTemplateUIOperations<TManageItems> { - onManage?: (item: TManageItems | "property") => void; - onMember: () => void; - onBookmark?: () => void; - onHighlight?: () => void; -} - -export interface TimelinePageTemplateUIProps<TManageItems> { - timeline: - | (HttpTimelineInfo & { - operations: TimelinePageTemplateUIOperations<TManageItems>; - }) - | "notexist" - | "offline" - | "loading" - | "error"; - notExistMessageI18nKey: string; - CardComponent: React.ComponentType<TimelineCardComponentProps<TManageItems>>; -} - -export default function TimelinePageTemplateUI<TManageItems>( - props: TimelinePageTemplateUIProps<TManageItems> -): React.ReactElement | null { - const { timeline, CardComponent } = props; - - const { t } = useTranslation(); - - const scrollToBottom = React.useCallback(() => { - window.scrollTo(0, document.body.scrollHeight); - }, []); - - const [bottomSpaceHeight, setBottomSpaceHeight] = React.useState<number>(0); - - const [timelineReloadKey, setTimelineReloadKey] = React.useState<number>(0); - - const [newPosts, setNewPosts] = React.useState<HttpTimelinePostInfo[]>([]); - - const reloadTimeline = (): void => { - setTimelineReloadKey((old) => old + 1); - setNewPosts([]); - }; - - const onPostEditHeightChange = React.useCallback((height: number): void => { - setBottomSpaceHeight(height); - if (height === 0) { - const alertHost = getAlertHost(); - if (alertHost != null) { - alertHost.style.removeProperty("margin-bottom"); - } - } else { - const alertHost = getAlertHost(); - if (alertHost != null) { - alertHost.style.marginBottom = `${height}px`; - } - } - }, []); - - const timelineName = typeof timeline === "object" ? timeline.name : null; - - const cardCollapseLocalStorageKey = - timelineName != null ? `timeline.${timelineName}.cardCollapse` : null; - - const [cardCollapse, setCardCollapse] = React.useState<boolean>(true); - React.useEffect(() => { - if (cardCollapseLocalStorageKey != null) { - const savedCollapse = - window.localStorage.getItem(cardCollapseLocalStorageKey) === "true"; - setCardCollapse(savedCollapse); - } - }, [cardCollapseLocalStorageKey]); - - const toggleCardCollapse = (): void => { - const newState = !cardCollapse; - setCardCollapse(newState); - if (cardCollapseLocalStorageKey != null) { - window.localStorage.setItem( - cardCollapseLocalStorageKey, - newState.toString() - ); - } - }; - - let body: React.ReactElement; - - if (timeline == "loading") { - body = ( - <div className="full-viewport-center-child"> - <Spinner variant="primary" animation="grow" /> - </div> - ); - } else if (timeline === "offline") { - // TODO: i18n - body = <p className="text-danger">Offline!</p>; - } else if (timeline === "notexist") { - body = <p className="text-danger">{t(props.notExistMessageI18nKey)}</p>; - } else if (timeline === "error") { - // TODO: i18n - body = <p className="text-danger">Error!</p>; - } else { - const { operations } = timeline; - body = ( - <> - <CardComponent - className="timeline-template-card" - timeline={timeline} - operations={operations} - collapse={cardCollapse} - toggleCollapse={toggleCardCollapse} - /> - <div - className="timeline-container" - style={{ - minHeight: `calc(100vh - ${56 + bottomSpaceHeight}px)`, - }} - > - <Timeline - top={40} - timelineName={timeline.name} - reloadKey={timelineReloadKey} - onReload={reloadTimeline} - additionalPosts={newPosts} - onLoad={scrollToBottom} - /> - </div> - {timeline.postable ? ( - <> - <div - style={{ height: bottomSpaceHeight }} - className="flex-fix-length" - /> - <TimelinePostEdit - className="fixed-bottom" - timeline={timeline} - onHeightChange={onPostEditHeightChange} - onPosted={(newPost) => { - setNewPosts((old) => [...old, newPost]); - }} - /> - </> - ) : null} - </> - ); - } - return body; -} |