diff options
Diffstat (limited to 'FrontEnd/src/app/views')
8 files changed, 220 insertions, 232 deletions
diff --git a/FrontEnd/src/app/views/admin/UserAdmin.tsx b/FrontEnd/src/app/views/admin/UserAdmin.tsx index d66abbec..fbdfd5a3 100644 --- a/FrontEnd/src/app/views/admin/UserAdmin.tsx +++ b/FrontEnd/src/app/views/admin/UserAdmin.tsx @@ -62,7 +62,7 @@ const UsernameLabel: React.FC = (props) => { const UserDeleteDialog: React.FC< DialogProps<{ username: string }, unknown> -> = ({ open, close, token, data: { username }, onSuccess }) => { +> = ({ open, close, data: { username }, onSuccess }) => { return ( <OperationDialog open={open} @@ -74,7 +74,7 @@ const UserDeleteDialog: React.FC< 0<UsernameLabel>{username}</UsernameLabel>2 </Trans> )} - onProcess={() => getHttpUserClient().delete(username, token)} + onProcess={() => getHttpUserClient().delete(username)} onSuccessAndClose={onSuccess} /> ); @@ -87,7 +87,7 @@ const UserModifyDialog: React.FC< }, HttpUser > -> = ({ open, close, token, data: { oldUser }, onSuccess }) => { +> = ({ open, close, data: { oldUser }, onSuccess }) => { return ( <OperationDialog open={open} @@ -115,15 +115,11 @@ const UserModifyDialog: React.FC< ] as const } onProcess={([username, password, nickname]) => - getHttpUserClient().patch( - oldUser.username, - { - username: username !== oldUser.username ? username : undefined, - password: password !== "" ? password : undefined, - nickname: nickname !== oldUser.nickname ? nickname : undefined, - }, - token - ) + getHttpUserClient().patch(oldUser.username, { + username: username !== oldUser.username ? username : undefined, + password: password !== "" ? password : undefined, + nickname: nickname !== oldUser.nickname ? nickname : undefined, + }) } onSuccessAndClose={onSuccess} /> @@ -138,7 +134,7 @@ const UserPermissionModifyDialog: React.FC< }, UserPermission[] > -> = ({ open, close, token, data: { username, permissions }, onSuccess }) => { +> = ({ open, close, data: { username, permissions }, onSuccess }) => { const oldPermissionBoolList: boolean[] = kUserPermissionList.map( (permission) => permissions.includes(permission) ); @@ -168,16 +164,11 @@ const UserPermissionModifyDialog: React.FC< const permission = kUserPermissionList[index]; if (oldValue === newValue) continue; if (newValue) { - await getHttpUserClient().putUserPermission( - username, - permission, - token - ); + await getHttpUserClient().putUserPermission(username, permission); } else { await getHttpUserClient().deleteUserPermission( username, - permission, - token + permission ); } } diff --git a/FrontEnd/src/app/views/home/BoardWithUser.tsx b/FrontEnd/src/app/views/home/BoardWithUser.tsx index 8afe440b..ba22916c 100644 --- a/FrontEnd/src/app/views/home/BoardWithUser.tsx +++ b/FrontEnd/src/app/views/home/BoardWithUser.tsx @@ -20,11 +20,11 @@ const BoardWithUser: React.FC<{ user: AuthUser }> = ({ user }) => { <Col xs="12" md="6"> <TimelineBoard title={t("home.bookmarkTimeline")} - load={() => getHttpBookmarkClient().list(user.token)} + load={() => getHttpBookmarkClient().list()} editHandler={{ onDelete: (timeline) => { return getHttpBookmarkClient() - .delete(timeline, user.token) + .delete(timeline) .catch((e) => { pushAlert({ message: { @@ -39,8 +39,7 @@ const BoardWithUser: React.FC<{ user: AuthUser }> = ({ user }) => { onMove: (timeline, index, offset) => { return getHttpBookmarkClient() .move( - { timeline, newPosition: index + offset + 1 }, // +1 because backend contract: index starts at 1 - user.token + { timeline, newPosition: index + offset + 1 } // +1 because backend contract: index starts at 1 ) .catch((e) => { pushAlert({ @@ -75,7 +74,7 @@ const BoardWithUser: React.FC<{ user: AuthUser }> = ({ user }) => { ? { onDelete: (timeline) => { return getHttpHighlightClient() - .delete(timeline, user.token) + .delete(timeline) .catch((e) => { pushAlert({ message: { @@ -90,8 +89,7 @@ const BoardWithUser: React.FC<{ user: AuthUser }> = ({ user }) => { onMove: (timeline, index, offset) => { return getHttpHighlightClient() .move( - { timeline, newPosition: index + offset + 1 }, // +1 because backend contract: index starts at 1 - user.token + { timeline, newPosition: index + offset + 1 } // +1 because backend contract: index starts at 1 ) .catch((e) => { pushAlert({ diff --git a/FrontEnd/src/app/views/timeline-common/TimelineCardTemplate.tsx b/FrontEnd/src/app/views/timeline-common/TimelineCardTemplate.tsx index ece1942f..e62f76fa 100644 --- a/FrontEnd/src/app/views/timeline-common/TimelineCardTemplate.tsx +++ b/FrontEnd/src/app/views/timeline-common/TimelineCardTemplate.tsx @@ -10,7 +10,11 @@ import SyncStatusBadge from "../timeline-common/SyncStatusBadge"; import CollapseButton from "../timeline-common/CollapseButton"; export interface TimelineCardTemplateProps - extends Omit<TimelineCardComponentProps<"">, "onManage" | "onMember"> { + extends Omit<TimelineCardComponentProps<"">, "operations"> { + operations: Pick< + TimelineCardComponentProps<"">["operations"], + "onHighlight" | "onBookmark" + >; infoArea: React.ReactElement; manageArea: | { type: "member"; onMember: () => void } @@ -33,13 +37,13 @@ function TimelineCardTemplate({ collapse, infoArea, manageArea, - onBookmark, - onHighlight, + operations, toggleCollapse, syncStatus, className, }: TimelineCardTemplateProps): React.ReactElement | null { const { t } = useTranslation(); + const { onBookmark, onHighlight } = operations; return ( <div className={clsx("cru-card p-2 clearfix", className)}> @@ -56,13 +60,19 @@ function TimelineCardTemplate({ <div className="text-right mt-2"> {onHighlight != null ? ( <i - className="bi-star icon-button text-yellow mr-3" + className={clsx( + timeline.isHighlight ? "bi-star-fill" : "bi-star", + "icon-button text-yellow mr-3" + )} onClick={onHighlight} /> ) : null} {onBookmark != null ? ( <i - className="bi-bookmark icon-button text-yellow mr-3" + className={clsx( + timeline.isBookmark ? "bi-bookmark-fill" : "bi-bookmark", + "icon-button text-yellow mr-3" + )} onClick={onBookmark} /> ) : null} diff --git a/FrontEnd/src/app/views/timeline-common/TimelineMember.tsx b/FrontEnd/src/app/views/timeline-common/TimelineMember.tsx index 089d11a0..efa7e971 100644 --- a/FrontEnd/src/app/views/timeline-common/TimelineMember.tsx +++ b/FrontEnd/src/app/views/timeline-common/TimelineMember.tsx @@ -3,6 +3,8 @@ import { useTranslation } from "react-i18next"; import { Container, ListGroup, Modal, Row, Col, Button } from "react-bootstrap"; import { User, useAvatar } from "@/services/user"; +import { TimelineInfo, timelineService } from "@/services/timeline"; +import { getHttpUserClient, HttpUserNotExistError } from "@/http/user"; import SearchInput from "../common/SearchInput"; import BlobImage from "../common/BlobImage"; @@ -52,15 +54,9 @@ const TimelineMemberItem: React.FC<{ ); }; -export interface TimelineMemberCallbacks { - onCheckUser: (username: string) => Promise<User | null>; - onAddUser: (user: User) => Promise<void>; - onRemoveUser: (username: string) => void; -} - export interface TimelineMemberProps { - members: User[]; - edit: TimelineMemberCallbacks | null | undefined; + timeline: TimelineInfo; + editable: boolean; } const TimelineMember: React.FC<TimelineMemberProps> = (props) => { @@ -81,7 +77,9 @@ const TimelineMember: React.FC<TimelineMemberProps> = (props) => { userSearchState.type === "user" ? userSearchState.data.username : undefined ); - const members = props.members; + const { timeline } = props; + + const members = [timeline.owner, ...timeline.members]; return ( <Container className="px-4 py-3"> @@ -91,13 +89,21 @@ const TimelineMember: React.FC<TimelineMemberProps> = (props) => { key={member.username} user={member} owner={index === 0} - onRemove={props.edit?.onRemoveUser} + onRemove={ + props.editable + ? () => { + void timelineService.removeMember( + timeline.name, + member.username + ); + } + : undefined + } /> ))} </ListGroup> {(() => { - const edit = props.edit; - if (edit != null) { + if (props.editable) { return ( <> <SearchInput @@ -115,26 +121,34 @@ const TimelineMember: React.FC<TimelineMemberProps> = (props) => { }); return; } - setUserSearchState({ type: "loading" }); - edit.onCheckUser(userSearchText).then( - (u) => { - if (u == null) { + getHttpUserClient() + .get(userSearchText) + .catch((e) => { + if (e instanceof HttpUserNotExistError) { + return null; + } else { + throw e; + } + }) + .then( + (u) => { + if (u == null) { + setUserSearchState({ + type: "error", + data: "timeline.userNotExist", + }); + } else { + setUserSearchState({ type: "user", data: u }); + } + }, + (e) => { setUserSearchState({ type: "error", - data: "timeline.userNotExist", + data: `${e as string}`, }); - } else { - setUserSearchState({ type: "user", data: u }); } - }, - (e) => { - setUserSearchState({ - type: "error", - data: `${e as string}`, - }); - } - ); + ); }} /> {(() => { @@ -166,10 +180,12 @@ const TimelineMember: React.FC<TimelineMemberProps> = (props) => { className="align-self-center" disabled={!addable} onClick={() => { - void edit.onAddUser(u).then((_) => { - setUserSearchText(""); - setUserSearchState({ type: "init" }); - }); + void timelineService + .addMember(timeline.name, u.username) + .then(() => { + setUserSearchText(""); + setUserSearchState({ type: "init" }); + }); }} > {t("timeline.member.add")} diff --git a/FrontEnd/src/app/views/timeline-common/TimelinePageTemplate.tsx b/FrontEnd/src/app/views/timeline-common/TimelinePageTemplate.tsx index 7f5c8206..f8b2b38b 100644 --- a/FrontEnd/src/app/views/timeline-common/TimelinePageTemplate.tsx +++ b/FrontEnd/src/app/views/timeline-common/TimelinePageTemplate.tsx @@ -1,16 +1,10 @@ import React from "react"; import { useTranslation } from "react-i18next"; -import { of } from "rxjs"; -import { catchError } from "rxjs/operators"; import { UiLogicError } from "@/common"; import { pushAlert } from "@/services/alert"; -import { useUser, userInfoService, UserNotExistError } from "@/services/user"; -import { - timelineService, - usePostList, - useTimelineInfo, -} from "@/services/timeline"; +import { useUser } from "@/services/user"; +import { timelineService, usePosts, useTimeline } from "@/services/timeline"; import { getHttpBookmarkClient } from "@/http/bookmark"; import { getHttpHighlightClient } from "@/http/highlight"; @@ -18,8 +12,8 @@ import { TimelineMemberDialog } from "./TimelineMember"; import TimelinePropertyChangeDialog from "./TimelinePropertyChangeDialog"; import { TimelinePageTemplateUIProps } from "./TimelinePageTemplateUI"; import { TimelinePostSendCallback } from "./TimelinePostEdit"; -import { TimelineSyncStatus } from "./SyncStatusBadge"; import { TimelinePostInfoEx } from "./Timeline"; +import { mergeDataStatus } from "@/services/DataHub2"; export interface TimelinePageTemplateProps<TManageItem> { name: string; @@ -45,8 +39,8 @@ export default function TimelinePageTemplate<TManageItem>( null ); - const timelineState = useTimelineInfo(name); - const postListState = usePostList(name); + const timelineAndStatus = useTimeline(name); + const postsAndState = usePosts(name); const onPost: TimelinePostSendCallback = React.useCallback( (req) => { @@ -68,111 +62,104 @@ export default function TimelinePageTemplate<TManageItem>( [onManageProp] ); - const childProps = ((): [ - data: TimelinePageTemplateUIProps<TManageItem>["data"], - syncStatus: TimelineSyncStatus - ] => { - if (timelineState == null) { - return [undefined, "syncing"]; + const data = ((): TimelinePageTemplateUIProps<TManageItem>["data"] => { + const { status, data: timeline } = timelineAndStatus; + if (timeline == null) { + if (status === "offline") { + return { type: "custom", value: "Network Error" }; + } else { + return undefined; + } + } else if (timeline === "notexist") { + return props.notFoundI18nKey; } else { - const { type, timeline } = timelineState; - if (timeline == null) { - if (type === "offline") { - return [{ type: "custom", value: "Network Error" }, "offline"]; - } else if (type === "synced") { - return [props.notFoundI18nKey, "synced"]; + const posts = ((): TimelinePostInfoEx[] | "forbid" | undefined => { + const { data: postsInfo } = postsAndState; + if (postsInfo === "forbid") { + return "forbid"; + } else if (postsInfo == null || postsInfo === "notexist") { + return undefined; } else { - return [undefined, "syncing"]; - } - } else { - if (postListState != null && postListState.type === "notexist") { - return [props.notFoundI18nKey, "synced"]; - } - if (postListState != null && postListState.type === "forbid") { - return ["timeline.messageCantSee", "synced"]; + return postsInfo.posts.map((post) => ({ + ...post, + onDelete: service.hasModifyPostPermission(user, timeline, post) + ? () => { + service.deletePost(name, post.id).subscribe({ + error: () => { + pushAlert({ + type: "danger", + message: t("timeline.deletePostFailed"), + }); + }, + }); + } + : undefined, + })); } - - const posts: - | TimelinePostInfoEx[] - | undefined = postListState?.posts?.map((post) => ({ - ...post, - onDelete: service.hasModifyPostPermission(user, timeline, post) + })(); + + const operations = { + onPost: service.hasPostPermission(user, timeline) ? onPost : undefined, + onManage: service.hasManagePermission(user, timeline) + ? onManage + : undefined, + onMember: () => setDialog("member"), + onBookmark: + user != null ? () => { - service.deletePost(name, post.id).subscribe({ - error: () => { + const { isBookmark } = timeline; + const client = getHttpBookmarkClient(); + const promise = isBookmark + ? client.delete(name) + : client.put(name); + promise.then( + () => { + void timelineService.syncTimeline(name); + }, + () => { pushAlert({ + message: { + type: "i18n", + key: isBookmark + ? "timeline.removeBookmarkFail" + : "timeline.addBookmarkFail", + }, type: "danger", - message: t("timeline.deletePostFailed"), }); - }, - }); + } + ); } : undefined, - })); - - const others = { - onPost: service.hasPostPermission(user, timeline) - ? onPost - : undefined, - onManage: service.hasManagePermission(user, timeline) - ? onManage - : undefined, - onMember: () => setDialog("member"), - onBookmark: - user != null - ? () => { - void getHttpBookmarkClient() - .put(name, user.token) - .then(() => { - pushAlert({ - message: { - type: "i18n", - key: "timeline.addBookmarkSuccess", - }, - type: "success", - }); - }); - } - : undefined, - onHighlight: - user != null && user.hasHighlightTimelineAdministrationPermission - ? () => { - void getHttpHighlightClient() - .put(name, user.token) - .then(() => { - pushAlert({ - message: { - type: "i18n", - key: "timeline.addHighlightSuccess", - }, - type: "success", - }); + onHighlight: + user != null && user.hasHighlightTimelineAdministrationPermission + ? () => { + const { isHighlight } = timeline; + const client = getHttpHighlightClient(); + const promise = isHighlight + ? client.delete(name) + : client.put(name); + promise.then( + () => { + void timelineService.syncTimeline(name); + }, + () => { + pushAlert({ + message: { + type: "i18n", + key: isHighlight + ? "timeline.removeHighlightFail" + : "timeline.addHighlightFail", + }, + type: "danger", }); - } - : undefined, - }; + } + ); + } + : undefined, + }; - if (type === "cache") { - return [{ timeline, posts, ...others }, "syncing"]; - } else if (type === "offline") { - return [{ timeline, posts, ...others }, "offline"]; - } else { - if (postListState == null) { - return [{ timeline, posts, ...others }, "syncing"]; - } else { - const { type: postListType } = postListState; - if (postListType === "synced") { - return [{ timeline, posts, ...others }, "synced"]; - } else if (postListType === "cache") { - return [{ timeline, posts, ...others }, "syncing"]; - } else if (postListType === "offline") { - return [{ timeline, posts, ...others }, "offline"]; - } - } - } - } + return { timeline, posts, operations }; } - throw new UiLogicError("Failed to calculate TimelinePageUITemplate props."); })(); const closeDialog = React.useCallback((): void => { @@ -181,10 +168,10 @@ export default function TimelinePageTemplate<TManageItem>( let dialogElement: React.ReactElement | undefined; - const timeline = timelineState?.timeline; + const timeline = timelineAndStatus?.data; if (dialog === "property") { - if (timeline == null) { + if (timeline == null || timeline === "notexist") { throw new UiLogicError( "Timeline is null but attempt to open change property dialog." ); @@ -205,7 +192,7 @@ export default function TimelinePageTemplate<TManageItem>( /> ); } else if (dialog === "member") { - if (timeline == null) { + if (timeline == null || timeline === "notexist") { throw new UiLogicError( "Timeline is null but attempt to open change property dialog." ); @@ -215,33 +202,8 @@ export default function TimelinePageTemplate<TManageItem>( <TimelineMemberDialog open onClose={closeDialog} - members={[timeline.owner, ...timeline.members]} - edit={ - service.hasManagePermission(user, timeline) - ? { - onCheckUser: (u) => { - return userInfoService - .getUserInfo(u) - .pipe( - catchError((e) => { - if (e instanceof UserNotExistError) { - return of(null); - } else { - throw e; - } - }) - ) - .toPromise(); - }, - onAddUser: (u) => { - return service.addMember(name, u.username).toPromise().then(); - }, - onRemoveUser: (u) => { - service.removeMember(name, u); - }, - } - : null - } + timeline={timeline} + editable={service.hasManagePermission(user, timeline)} /> ); } @@ -250,7 +212,13 @@ export default function TimelinePageTemplate<TManageItem>( return ( <> - <UiComponent data={childProps[0]} syncStatus={childProps[1]} /> + <UiComponent + data={data} + syncStatus={mergeDataStatus([ + timelineAndStatus.status, + postsAndState.status, + ])} + /> {dialogElement} </> ); diff --git a/FrontEnd/src/app/views/timeline-common/TimelinePageTemplateUI.tsx b/FrontEnd/src/app/views/timeline-common/TimelinePageTemplateUI.tsx index 20ec6e43..41246175 100644 --- a/FrontEnd/src/app/views/timeline-common/TimelinePageTemplateUI.tsx +++ b/FrontEnd/src/app/views/timeline-common/TimelinePageTemplateUI.tsx @@ -13,26 +13,30 @@ import { TimelineSyncStatus } from "./SyncStatusBadge"; export interface TimelineCardComponentProps<TManageItems> { timeline: TimelineInfo; - onManage?: (item: TManageItems | "property") => void; - onMember: () => void; - onBookmark?: () => void; - onHighlight?: () => void; - className?: string; - collapse: boolean; syncStatus: TimelineSyncStatus; + operations: { + onManage?: (item: TManageItems | "property") => void; + onMember: () => void; + onBookmark?: () => void; + onHighlight?: () => void; + }; + collapse: boolean; toggleCollapse: () => void; + className?: string; } export interface TimelinePageTemplateUIProps<TManageItems> { data?: | { timeline: TimelineInfo; - posts?: TimelinePostInfoEx[]; - onManage?: (item: TManageItems | "property") => void; - onMember: () => void; - onBookmark?: () => void; - onHighlight?: () => void; - onPost?: TimelinePostSendCallback; + posts?: TimelinePostInfoEx[] | "forbid"; + operations: { + onManage?: (item: TManageItems | "property") => void; + onMember: () => void; + onBookmark?: () => void; + onHighlight?: () => void; + onPost?: TimelinePostSendCallback; + }; } | I18nText; syncStatus: TimelineSyncStatus; @@ -155,32 +159,33 @@ export default function TimelinePageTemplateUI<TManageItems>( <CardComponent className="timeline-template-card" timeline={data.timeline} - onManage={data.onManage} - onMember={data.onMember} - onBookmark={data.onBookmark} - onHighlight={data.onHighlight} + operations={data.operations} syncStatus={syncStatus} collapse={cardCollapse} toggleCollapse={toggleCardCollapse} /> ) : null} {posts != null ? ( - <div - className="timeline-container" - style={{ minHeight: `calc(100vh - ${56 + bottomSpaceHeight}px)` }} - > - <Timeline - containerRef={timelineRef} - posts={posts} - onResize={triggerResizeEvent} - /> - </div> + posts === "forbid" ? ( + <div>{t("timeline.messageCantSee")}</div> + ) : ( + <div + className="timeline-container" + style={{ minHeight: `calc(100vh - ${56 + bottomSpaceHeight}px)` }} + > + <Timeline + containerRef={timelineRef} + posts={posts} + onResize={triggerResizeEvent} + /> + </div> + ) ) : ( <div className="full-viewport-center-child"> <Spinner variant="primary" animation="grow" /> </div> )} - {data != null && data.onPost != null ? ( + {data != null && data.operations.onPost != null ? ( <> <div style={{ height: bottomSpaceHeight }} @@ -188,7 +193,7 @@ export default function TimelinePageTemplateUI<TManageItems>( /> <TimelinePostEdit className="fixed-bottom" - onPost={data.onPost} + onPost={data.operations.onPost} onHeightChange={onPostEditHeightChange} timelineUniqueId={data.timeline.uniqueId} /> diff --git a/FrontEnd/src/app/views/timeline/TimelineInfoCard.tsx b/FrontEnd/src/app/views/timeline/TimelineInfoCard.tsx index f4dbb67d..920f504d 100644 --- a/FrontEnd/src/app/views/timeline/TimelineInfoCard.tsx +++ b/FrontEnd/src/app/views/timeline/TimelineInfoCard.tsx @@ -13,8 +13,8 @@ export type OrdinaryTimelineManageItem = "delete"; export type TimelineInfoCardProps = TimelineCardComponentProps<OrdinaryTimelineManageItem>; const TimelineInfoCard: React.FC<TimelineInfoCardProps> = (props) => { - const { onMember, onManage, ...otherProps } = props; - const { timeline } = props; + const { timeline, operations } = props; + const { onManage, onMember } = operations; const avatar = useAvatar(timeline?.owner?.username); @@ -66,7 +66,7 @@ const TimelineInfoCard: React.FC<TimelineInfoCardProps> = (props) => { }; } })()} - {...otherProps} + {...props} /> ); }; diff --git a/FrontEnd/src/app/views/user/UserInfoCard.tsx b/FrontEnd/src/app/views/user/UserInfoCard.tsx index f31a939f..01d2c096 100644 --- a/FrontEnd/src/app/views/user/UserInfoCard.tsx +++ b/FrontEnd/src/app/views/user/UserInfoCard.tsx @@ -13,8 +13,8 @@ export type PersonalTimelineManageItem = "avatar" | "nickname"; export type UserInfoCardProps = TimelineCardComponentProps<PersonalTimelineManageItem>; const UserInfoCard: React.FC<UserInfoCardProps> = (props) => { - const { onMember, onManage, ...otherProps } = props; - const { timeline } = props; + const { timeline, operations } = props; + const { onManage, onMember } = operations; const avatar = useAvatar(timeline?.owner?.username); @@ -66,7 +66,7 @@ const UserInfoCard: React.FC<UserInfoCardProps> = (props) => { }; } })()} - {...otherProps} + {...props} /> ); }; |