refactor: rename category ui

Signed-off-by: Innei <i@innei.in>
This commit is contained in:
Innei 2024-08-20 19:42:04 +08:00
parent 918d85a591
commit 5f30bfa7c4
No known key found for this signature in database
GPG Key ID: 0F62D33977F021F7
5 changed files with 162 additions and 35 deletions

View File

@ -3,7 +3,11 @@ import {
getStableRouterNavigate,
} from "@renderer/atoms/route"
import { setSidebarActiveView } from "@renderer/atoms/sidebar"
import { ROUTE_ENTRY_PENDING, ROUTE_FEED_PENDING } from "@renderer/constants"
import {
ROUTE_ENTRY_PENDING,
ROUTE_FEED_IN_FOLDER,
ROUTE_FEED_PENDING,
} from "@renderer/constants"
import { FeedViewType } from "@renderer/lib/enum"
import { isUndefined } from "lodash-es"
@ -11,6 +15,7 @@ type NavigateEntryOptions = Partial<{
feedId: string | null
entryId: string | null
view: FeedViewType
folderName: string
}>
/**
* @description a hook to navigate to `feedId`, `entryId`, add search for `view`, `level`
@ -19,7 +24,7 @@ type NavigateEntryOptions = Partial<{
export const useNavigateEntry = () => navigateEntry
export const navigateEntry = (options: NavigateEntryOptions) => {
const { entryId, feedId, view } = options || {}
const { entryId, feedId, view, folderName } = options || {}
const { params, searchParams } = getReadonlyRoute()
let finalFeedId = feedId || params.feedId || ROUTE_FEED_PENDING
@ -27,6 +32,10 @@ export const navigateEntry = (options: NavigateEntryOptions) => {
finalFeedId = ROUTE_FEED_PENDING
}
if (folderName) {
finalFeedId = `${ROUTE_FEED_IN_FOLDER}${folderName}`
}
finalFeedId = encodeURIComponent(finalFeedId)
const nextSearchParams = new URLSearchParams(searchParams)

View File

@ -5,6 +5,7 @@ import {
import {
FEED_COLLECTION_LIST,
ROUTE_ENTRY_PENDING,
ROUTE_FEED_IN_FOLDER,
ROUTE_FEED_PENDING,
} from "@renderer/constants"
import { FeedViewType } from "@renderer/lib/enum"
@ -45,9 +46,13 @@ export interface BizRouteParams {
isAllFeeds: boolean
isPendingEntry: boolean
folderName?: string
}
const parseRouteParams = (params: Params<any>, search: URLSearchParams): BizRouteParams => {
const parseRouteParams = (
params: Params<any>,
search: URLSearchParams,
): BizRouteParams => {
const _view = search.get("view")
const view =
@ -63,6 +68,9 @@ const parseRouteParams = (params: Params<any>, search: URLSearchParams): BizRout
isCollection: params.feedId === FEED_COLLECTION_LIST,
isAllFeeds: params.feedId === ROUTE_FEED_PENDING,
isPendingEntry: params.entryId === ROUTE_ENTRY_PENDING,
folderName: params.feedId?.startsWith(ROUTE_FEED_IN_FOLDER) ?
params.feedId.slice(ROUTE_FEED_IN_FOLDER.length) :
undefined,
}
}

View File

@ -1,3 +1,4 @@
import { MotionButtonBase } from "@renderer/components/ui/button"
import { LoadingCircle } from "@renderer/components/ui/loading"
import { ROUTE_FEED_IN_FOLDER, views } from "@renderer/constants"
import { useNavigateEntry } from "@renderer/hooks/biz/useNavigateEntry"
@ -16,11 +17,10 @@ import {
import { useFeedUnreadStore } from "@renderer/store/unread"
import { useMutation } from "@tanstack/react-query"
import { AnimatePresence, m } from "framer-motion"
import { memo, useEffect, useRef, useState } from "react"
import { Fragment, memo, useEffect, useRef, useState } from "react"
import { useModalStack } from "../../components/ui/modal/stacked/hooks"
import { CategoryRemoveDialogContent } from "./category-remove-dialog"
import { CategoryRenameContent } from "./category-rename-dialog"
import { FeedItem } from "./item"
import { UnreadNumber } from "./unread-number"
@ -83,7 +83,7 @@ function FeedCategoryImpl({
navigate({
entryId: null,
// TODO joint feedId is too long, need to be optimized
feedId: `${ROUTE_FEED_IN_FOLDER}${folderName}`,
folderName,
view,
})
}
@ -113,6 +113,31 @@ function FeedCategoryImpl({
},
})
const [isCategoryEditing, setIsCategoryEditing] = useState(false)
const renameMutation = useMutation({
mutationFn: async ({
lastCategory,
newCategory,
}: {
lastCategory: string
newCategory: string
}) => subscriptionActions.renameCategory(lastCategory, newCategory),
onMutate({ lastCategory, newCategory }) {
const routeParams = getRouteParams()
if (routeParams.folderName === lastCategory) {
navigate({
folderName: newCategory,
})
}
setIsCategoryEditing(false)
},
})
const isCategoryIsWaiting = renameMutation.isPending || isChangePending
return (
<div tabIndex={-1} onClick={stopPropagation}>
{!!showCollapse && (
@ -123,7 +148,9 @@ function FeedCategoryImpl({
)}
onClick={(e) => {
e.stopPropagation()
setCategoryActive()
if (!isCategoryEditing) {
setCategoryActive()
}
}}
onContextMenu={(e) => {
showNativeMenu(
@ -150,18 +177,7 @@ function FeedCategoryImpl({
type: "text",
label: "Rename category",
click: () => {
present({
title: "Rename Category",
clickOutsideToDismiss: true,
content: ({ dismiss }) => (
<CategoryRenameContent
feedIdList={ids}
category={folderName || ""}
view={view}
onSuccess={dismiss}
/>
),
})
setIsCategoryEditing(true)
},
},
{
@ -186,6 +202,7 @@ function FeedCategoryImpl({
<button
type="button"
onClick={(e) => {
if (isCategoryEditing) return
e.stopPropagation()
setOpen(!open)
}}
@ -195,25 +212,66 @@ function FeedCategoryImpl({
)}
tabIndex={-1}
>
{isChangePending ? (
{isCategoryIsWaiting ? (
<LoadingCircle size="small" className="mr-2 size-[16px]" />
) : (
<div className="mr-2 size-[16px]">
<i className="i-mgc-right-cute-fi transition-transform" />
</div>
)}
) : isCategoryEditing ?
(
<MotionButtonBase
onClick={() => {
setIsCategoryEditing(false)
}}
className="center flex"
>
<i className="i-mgc-close-cute-re text-red-500" />
</MotionButtonBase>
) :
(
<div className="mr-2 size-[16px]">
<i className="i-mgc-right-cute-fi transition-transform" />
</div>
)}
</button>
<span
className={cn(
"truncate",
!showUnreadCount &&
(unread ? "font-bold" : "font-medium opacity-70"),
)}
>
{folderName}
</span>
{isCategoryEditing ? (
<form
className="ml-3 flex w-full items-center"
onSubmit={(e) => {
e.preventDefault()
return renameMutation.mutateAsync({
lastCategory: folderName!,
newCategory: e.currentTarget.category.value,
})
}}
>
<input
name="category"
autoFocus
defaultValue={folderName}
className="w-full appearance-none bg-transparent caret-accent"
/>
<MotionButtonBase
type="submit"
className="center -mr-1 flex size-5 shrink-0 rounded-lg text-green-600 hover:bg-theme-button-hover dark:text-green-400"
>
<i className="i-mgc-check-filled size-3" />
</MotionButtonBase>
</form>
) : (
<Fragment>
<span
className={cn(
"grow truncate",
!showUnreadCount &&
(unread ? "font-bold" : "font-medium opacity-70"),
)}
>
{folderName}
</span>
<UnreadNumber unread={unread} className="ml-2" />
</Fragment>
)}
</div>
<UnreadNumber unread={unread} className="ml-2" />
</div>
)}
<AnimatePresence>

View File

@ -61,6 +61,14 @@ class SubscriptionServiceStatic extends BaseService<SubscriptionModelWithId> {
return this.table.where("userId").equals(userId).delete()
}
}
async renameCategory(userId: string, feedIdList: string[], category: string) {
return this.table
.where("userId")
.equals(userId)
.and((item) => feedIdList.includes(item.feedId))
.modify({ category })
}
}
export const SubscriptionService = new SubscriptionServiceStatic()

View File

@ -304,6 +304,50 @@ class SubscriptionActions {
),
)
}
async renameCategory(lastCategory: string, newCategory: string) {
const subscriptionIds = [] as string[]
const state = get()
for (const feedId in state.data) {
const subscription = state.data[feedId]
if (
subscription.category === lastCategory ||
subscription.defaultCategory === lastCategory
) {
subscriptionIds.push(feedId)
}
}
set((state) =>
produce(state, (state) => {
for (const feedId of subscriptionIds) {
const subscription = state.data[feedId]
if (subscription) {
subscription.category = newCategory
subscription.defaultCategory = undefined
}
}
}),
)
return doMutationAndTransaction(
() =>
apiClient.categories.$patch({
json: {
feedIdList: subscriptionIds,
category: newCategory,
},
}),
async () =>
// Db
SubscriptionService.renameCategory(
whoami()!.id,
subscriptionIds,
newCategory,
),
)
}
}
export const subscriptionActions = new SubscriptionActions()