Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
160 changes: 121 additions & 39 deletions src/components/OperationCard.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,49 @@
import { Button, Card, Elevation, H4, H5, Icon, Tag } from '@blueprintjs/core'
import { Button, ButtonGroup, Card, Elevation, H4, H5, Icon, Tag } from '@blueprintjs/core'
import { Tooltip2 } from '@blueprintjs/popover2'

import clsx from 'clsx'
import { copyShortCode, handleDownloadJSON } from 'services/operation'
import { copyShortCode, handleDownloadJSON, handleRating } from 'services/operation'

import { ReLink } from 'components/ReLink'
import { RelativeTime } from 'components/RelativeTime'
import { AddToOperationSetButton } from 'components/operation-set/AddToOperationSet'
import { OperationRating } from 'components/viewer/OperationRating'
import { OpDifficulty, Operation } from 'models/operation'
import { OpDifficulty, Operation, OpRatingType } from 'models/operation'

import { useLevels } from '../apis/level'
import { createCustomLevel, findLevelByStageName } from '../models/level'
import { Paragraphs } from './Paragraphs'
import { EDifficulty } from './entity/EDifficulty'
import { EDifficultyLevel, NeoELevel } from './entity/ELevel'
import { useEffect } from 'react'
import { useOperation } from 'apis/operation'
import { AppToaster } from './Toaster'
import { formatError } from 'utils/error'

export const NeoOperationCard = ({ operation }: { operation: Operation }) => {
export const NeoOperationCard = ({operationId}:{operationId: Operation['id']}) => {
const { data: levels } = useLevels()
const {
data: operation,
error,
} = useOperation({
id: operationId,
suspense: true,
})

// make eslint happy: we got Suspense out there
if (!operation) throw new Error('unreachable')

useEffect(() => {
}, [operation])

useEffect(() => {
if (error) {
AppToaster.show({
intent: 'danger',
message: `刷新作业失败:${formatError(error)}`,
})
}
}, [error])

return (
<Card interactive={true} elevation={Elevation.TWO} className="relative">
Expand Down Expand Up @@ -111,9 +137,32 @@ export const NeoOperationCard = ({ operation }: { operation: Operation }) => {
)
}

export const OperationCard = ({ operation }: { operation: Operation }) => {
export const OperationCard = ({operationId}:{operationId: Operation['id']}) => {
const { data: levels } = useLevels()

const {
data: operation,
error,
} = useOperation({
id: operationId,
suspense: true,
})

// make eslint happy: we got Suspense out there
if (!operation) throw new Error('unreachable')

useEffect(() => {
}, [operation])

useEffect(() => {
if (error) {
AppToaster.show({
intent: 'danger',
message: `刷新作业失败:${formatError(error)}`,
})
}
}, [error])

return (
<Card
interactive={true}
Expand Down Expand Up @@ -240,39 +289,72 @@ const CardActions = ({
operation: Operation
}) => {
return (
<div className={clsx('flex gap-1', className)}>
<Tooltip2
placement="bottom"
content={
<div className="max-w-sm dark:text-slate-900">下载原 JSON</div>
}
>
<Button
small
icon="download"
onClick={() => handleDownloadJSON(operation.parsedContent)}
/>
</Tooltip2>
<Tooltip2
placement="bottom"
content={
<div className="max-w-sm dark:text-slate-900">复制神秘代码</div>
}
>
<Button
small
icon="clipboard"
onClick={() => copyShortCode(operation)}
/>
</Tooltip2>
<Tooltip2
placement="bottom"
content={
<div className="max-w-sm dark:text-slate-900">添加到作业集</div>
}
>
<AddToOperationSetButton small icon="plus" operationId={operation.id} />
</Tooltip2>
</div>
<>
<div className={clsx('flex gap-1', className)}>
<Tooltip2
placement="bottom"
content={
<div className="max-w-sm dark:text-slate-900">下载原 JSON</div>
}
>
<Button
small
icon="download"
onClick={() => handleDownloadJSON(operation.parsedContent)}
/>
</Tooltip2>
<Tooltip2
placement="bottom"
content={
<div className="max-w-sm dark:text-slate-900">复制神秘代码</div>
}
>
<Button
small
icon="clipboard"
onClick={() => copyShortCode(operation)}
/>
</Tooltip2>
<Tooltip2
placement="bottom"
content={
<div className="max-w-sm dark:text-slate-900">添加到作业集</div>
}
>
<AddToOperationSetButton small icon="plus" operationId={operation.id} />
</Tooltip2>
</div>
<div className={clsx('mt-8 flex', className)}>
<ButtonGroup className="flex items-center">
<Tooltip2 content="o(*≧▽≦)ツ" placement="bottom">
<Button
small
icon="thumbs-up"
intent={
operation.ratingType === OpRatingType.Like
? 'success'
: 'none'
}
className="mr-2"
active={operation.ratingType === OpRatingType.Like}
onClick={() => handleRating(operation.ratingType === OpRatingType.Like ? OpRatingType.None : OpRatingType.Like, operation.id)}
/>
</Tooltip2>
<Tooltip2 content=" ヽ(。>д<)p" placement="bottom">
<Button
small
icon="thumbs-down"
intent={
operation.ratingType === OpRatingType.Dislike
? 'danger'
: 'none'
}
active={operation.ratingType === OpRatingType.Dislike}
onClick={() => handleRating(operation.ratingType === OpRatingType.Dislike ? OpRatingType.None : OpRatingType.Dislike, operation.id)}
/>
</Tooltip2>
</ButtonGroup>
</div>
</>
)
}
4 changes: 2 additions & 2 deletions src/components/OperationList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,12 @@ export const OperationList: ComponentType<UseOperationsParams> =
}}
>
{operations.map((operation) => (
<NeoOperationCard operation={operation} key={operation.id} />
<NeoOperationCard operationId = {operation.id} key={operation.id} />
))}
</div>
) : (
operations.map((operation) => (
<OperationCard operation={operation} key={operation.id} />
<OperationCard operationId={operation.id} key={operation.id} />
))
)

Expand Down
36 changes: 10 additions & 26 deletions src/components/viewer/OperationViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,13 @@ import { ErrorBoundary } from '@sentry/react'

import {
deleteOperation,
rateOperation,
useOperation,
useRefreshOperations,
} from 'apis/operation'
import { useAtom } from 'jotai'
import { ComponentType, FC, useEffect, useState } from 'react'
import { Link } from 'react-router-dom'
import { copyShortCode, handleDownloadJSON } from 'services/operation'
import { copyShortCode, handleDownloadJSON, handleRating } from 'services/operation'

import { FactItem } from 'components/FactItem'
import { Paragraphs } from 'components/Paragraphs'
Expand Down Expand Up @@ -123,14 +122,14 @@ export const OperationViewer: ComponentType<{
const {
data: operation,
error,
mutate,
} = useOperation({
id: operationId,
suspense: true,
})

useEffect(() => {
// on finished loading, scroll to #fragment if any
console.log("update operation")
if (operation) {
const fragment = window.location.hash
if (fragment) {
Expand Down Expand Up @@ -158,24 +157,6 @@ export const OperationViewer: ComponentType<{
}
}, [error])

const handleRating = async (decision: OpRatingType) => {
// cancel rating if already rated by the same type
if (decision === operation.ratingType) {
decision = OpRatingType.None
}

wrapErrorMessage(
(e) => `提交评分失败:${formatError(e)}`,
mutate(async (val) => {
await rateOperation({
id: operationId,
rating: decision,
})
return val
}),
).catch(console.warn)
}

return (
<DrawerLayout
title={
Expand Down Expand Up @@ -233,7 +214,6 @@ export const OperationViewer: ComponentType<{
<OperationViewerInner
levels={levels}
operation={operation}
handleRating={handleRating}
/>
</ErrorBoundary>
</DrawerLayout>
Expand Down Expand Up @@ -276,11 +256,9 @@ const EmptyOperator: FC<{
function OperationViewerInner({
levels,
operation,
handleRating,
}: {
levels: Level[]
operation: Operation
handleRating: (decision: OpRatingType) => Promise<void>
}) {
return (
<div className="h-full overflow-auto py-4 px-8 pt-8">
Expand Down Expand Up @@ -318,7 +296,10 @@ function OperationViewerInner({
}
className="mr-2"
active={operation.ratingType === OpRatingType.Like}
onClick={() => handleRating(OpRatingType.Like)}
onClick={() => {
var type = operation.ratingType === OpRatingType.Like ? OpRatingType.None : OpRatingType.Like;
handleRating(type, operation.id)
}}
/>
</Tooltip2>
<Tooltip2 content=" ヽ(。>д<)p" placement="bottom">
Expand All @@ -330,7 +311,10 @@ function OperationViewerInner({
: 'none'
}
active={operation.ratingType === OpRatingType.Dislike}
onClick={() => handleRating(OpRatingType.Dislike)}
onClick={() => {
var type = operation.ratingType === OpRatingType.Dislike ? OpRatingType.None : OpRatingType.Dislike;
handleRating(type, operation.id)
}}
/>
</Tooltip2>
</ButtonGroup>
Expand Down
34 changes: 34 additions & 0 deletions src/services/operation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ import { CopilotDocV1 } from '../models/copilot.schema'
import { ShortCodeContent, toShortCode } from '../models/shortCode'
import { formatError } from '../utils/error'
import { snakeCaseKeysUnicode } from '../utils/object'
import { wrapErrorMessage } from 'utils/wrapErrorMessage'
import { OpRatingType } from 'models/operation'
import { rateOperation } from 'apis/operation'
import { mutate } from 'swr'

export const handleDownloadJSON = (operationDoc: CopilotDocV1.Operation) => {
// pretty print the JSON
Expand Down Expand Up @@ -52,3 +56,33 @@ export const copyShortCode = async (target: { id: number }) => {
})
}
}

export const handleRating = async (decision: OpRatingType, operationId: number) => {
const getMessage = (decision: OpRatingType): string => {
switch (decision) {
case OpRatingType.None:
return '已取消评价~';
case OpRatingType.Like:
return '已点赞~';
case OpRatingType.Dislike:
return '已点踩~';
default:
return '未知评价';
}
};
const message = getMessage(decision);
AppToaster.show({
message: message,
intent: 'success',
})
wrapErrorMessage(
(e) => `提交评分失败:${formatError(e)}`,
mutate(async (val) => {
Copy link
Contributor

@martinwang2002 martinwang2002 Feb 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wrapErrorMessage(
    (e) => `提交评分失败:${formatError(e)}`,
    mutate(
      `rateOperation-${operationId}-${decision}`,
      async (val) => {
        await rateOperation({
          id: operationId,
          rating: decision,
        })
        return val
      }),
  ).catch(console.warn)

for mutex of http requests.

@guansss , previously, multple like and dislike post operations were sent to the server.

In other words, I would doubt the validity of the data if they were sent from the browser.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@martinwang2002 mutate() 的 key 不是用来做 mutex 的吧,是用来与 useSWR() 里同样的 key 对应起来,以更新缓存并触发 revalidate

短时间发送多个请求是没问题的,反正最后都会 revalidate,以服务器返回的数据为准

Copy link
Contributor

@guansss guansss Feb 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

我试了一下,这请求确实太多了点……

edit: 原因是这个 mutate() 的第一个参数是 key,这里传了个发请求的函数进去作为 key,所以实际上是整个页面里有多少个 useSWR() 就发了多少个 rating 请求,并且刷新了所有的 useSWR()……

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

我试了一下,这请求确实太多了点……

edit: 原因是这个 mutate() 的第一个参数是 key,这里传了个发请求的函数进去作为 key,所以实际上是整个页面里有多少个 useSWR() 就发了多少个 rating 请求,并且刷新了所有的 useSWR()……

一个函数作为key传入后,被当成了filter,这就导致了“整个页面里有多少个 useSWR() 就发了多少个 rating 请求”。

ref: https://swr.vercel.app/docs/mutation#parameters

await rateOperation({
id: operationId,
rating: decision,
})
return val
}),
).catch(console.warn)
}