feat: batch actions (#431)

This commit is contained in:
Nya Candy 2023-04-27 05:33:35 +08:00 committed by GitHub
parent f5dcdfe2e5
commit a49778bb16
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 257 additions and 4 deletions

View File

@ -41,6 +41,8 @@
},
"Convert to Page": "ページに変換",
"Convert to Post": "記事に変換",
"Select All": "すべて選択",
"Deselect All": "選択解除",
"Delete": "削除",
"Heading": "見出し",
"Bold": "太字",

View File

@ -45,6 +45,8 @@
"Edit": "編輯 ",
"Convert to Page": "轉換為頁面",
"Convert to Post": "轉換為文章",
"Select All": "全部選擇",
"Deselect All": "取消選擇",
"Delete": "刪除",
"Heading": "標題",
"Bold": "粗體",

View File

@ -45,6 +45,8 @@
"Edit": "编辑",
"Convert to Page": "转换为页面",
"Convert to Post": "转换为文章",
"Select All": "全部选择",
"Deselect All": "取消选择",
"Delete": "删除",
"Heading": "标题",
"Bold": "粗体",

View File

@ -2,7 +2,7 @@ import { nanoid } from "nanoid"
import { Trans, useTranslation } from "next-i18next"
import Link from "next/link"
import { useRouter } from "next/router"
import { Fragment, useMemo } from "react"
import { Fragment, useMemo, useState } from "react"
import { Menu } from "@headlessui/react"
import { useQueryClient } from "@tanstack/react-query"
@ -21,6 +21,7 @@ import { TabItem, Tabs } from "../ui/Tabs"
import { Tooltip } from "../ui/Tooltip"
import { UniLink } from "../ui/UniLink"
import { DashboardMain } from "./DashboardMain"
import { PagesManagerBatchSelectActionTab } from "./PagesManagerBatchSelectActionTab"
import { PagesManagerMenu } from "./PagesManagerMenu"
export const PagesManager: React.FC<{
@ -47,6 +48,9 @@ export const PagesManager: React.FC<{
visibility,
})
// Batch selections
const [batchSelected, setBatchSelected] = useState<string[]>([])
const tabItems: TabItem[] = [
{
value: PageVisibilityEnum.All,
@ -208,7 +212,21 @@ export const PagesManager: React.FC<{
{description}
</div>
</header>
<Tabs items={tabItems} />
{batchSelected.length > 0 ? (
<PagesManagerBatchSelectActionTab
isPost={isPost}
isNotxLogContent={
!!tabItems.find((item) => item.text === "Others on Crossbell") // Not sure if there are better ways
?.active
}
pages={pages}
batchSelected={batchSelected}
setBatchSelected={setBatchSelected}
/>
) : (
<Tabs items={tabItems} />
)}
<div className="-mt-3">
{!pages.data?.pages?.[0].total && (
@ -222,8 +240,41 @@ export const PagesManager: React.FC<{
<Link
key={page.id}
href={getPageEditLink(page)}
className="group relative hover:bg-zinc-100 rounded-lg py-3 px-3 transition-colors -mx-3 flex justify-between"
className="group relative hover:bg-zinc-100 rounded-lg py-3 px-3 transition-colors -mx-3 flex"
>
<div className="w-10 flex-shrink-0 flex self-center">
<button
className={cn(
`text-gray-400 relative z-10 w-8 h-8 rounded inline-flex group-hover:visible justify-center items-center`,
batchSelected.includes(page.id)
? "bg-gray-200"
: `hover:bg-gray-200`,
)}
onClick={(e) => {
e.preventDefault()
// Toggle selection
if (batchSelected.includes(page.id)) {
// Deselect
setBatchSelected(
batchSelected.filter((pageId) => pageId !== page.id),
)
} else {
// Do select
setBatchSelected([...batchSelected, page.id])
}
}}
>
<i
className={`${
batchSelected.includes(page.id)
? "i-mingcute-check-line"
: isPost
? "i-mingcute-news-line"
: "i-mingcute-file-line"
} text-2xl`}
/>
</button>
</div>
<div className="min-w-0">
{page.title ? (
<div className="flex items-center">
@ -246,7 +297,7 @@ export const PagesManager: React.FC<{
</span>
</div>
</div>
<div className="w-10 flex-shrink-0">
<div className="w-10 flex-shrink-0 flex self-center ml-auto">
<Menu>
{({ open, close }) => (
<>

View File

@ -0,0 +1,196 @@
import { useTranslation } from "next-i18next"
import { useRouter } from "next/router"
import React from "react"
import toast from "react-hot-toast"
import type { Note, Notes } from "unidata.js"
import type { UseInfiniteQueryResult } from "@tanstack/react-query"
import { useQueryClient } from "@tanstack/react-query"
import { type TabItem, Tabs } from "~/components/ui/Tabs"
import { APP_NAME } from "~/lib/env"
import { delStorage, getStorage, setStorage } from "~/lib/storage"
import { useCreateOrUpdatePage, useDeletePage } from "~/queries/page"
export const PagesManagerBatchSelectActionTab: React.FC<{
isPost: boolean
isNotxLogContent: boolean
pages: UseInfiniteQueryResult<Notes, unknown>
batchSelected: string[]
setBatchSelected: (selected: string[]) => void
}> = ({ isPost, isNotxLogContent, pages, batchSelected, setBatchSelected }) => {
const { t } = useTranslation(["dashboard", "site"])
const router = useRouter()
const subdomain = router.query.subdomain as string
const queryClient = useQueryClient()
// Convert or Delete page
const createOrUpdatePage = useCreateOrUpdatePage()
const deletePage = useDeletePage()
const tabItems: TabItem[] = [
{
text: "Select All",
onClick: () => {
// Get all page IDs
const allIDs: string[] = []
pages.data?.pages.map((page) =>
page.list?.map((page) => {
allIDs.push(page.id)
}),
)
setBatchSelected(allIDs)
},
},
{
text: "Deselect All",
onClick: () => {
setBatchSelected([])
},
},
{
text:
"Convert to " +
(isNotxLogContent
? `${APP_NAME} ${isPost ? "Post" : "Page"}`
: isPost
? "Page"
: "Post"),
onClick: async () => {
// Start message
const toastId = toast.loading("Converting...")
// Find all selected
const selectedPages: Note[] = []
pages.data?.pages.map((page) =>
page.list?.map((page) => {
if (batchSelected.includes(page.id)) {
selectedPages.push(page)
}
}),
)
// Convert all // TODO: use multicall to optimize
try {
await Promise.all(
selectedPages.map((page) => {
// Check again to ensure it's not
const isNotxLogContent = !page.applications?.includes("xlog")
const targetNoteBase = {
published: true,
pageId: page.id,
siteId: subdomain,
tags: page.tags
?.filter((tag) => tag !== "post" && tag !== "page")
?.join(", "),
applications: page.applications,
}
if (isNotxLogContent) {
return createOrUpdatePage.mutateAsync({
...targetNoteBase,
isPost: isPost, // Convert to xLog content
})
} else {
if (!page.metadata) {
// Is draft
const data = getStorage(`draft-${subdomain}-${page.id}`)
data.isPost = !isPost
setStorage(`draft-${subdomain}-${page.id}`, data)
} else {
// IsNote
return createOrUpdatePage.mutateAsync({
...targetNoteBase,
isPost: !isPost, // Change type
})
}
}
}),
)
toast.success(t("Converted!"), {
id: toastId,
})
} catch (e) {
// Some failed
toast.error(t("Failed to convert."), {
id: toastId,
})
}
// Invalidate site data refresh
await Promise.all([
queryClient.invalidateQueries(["getPagesBySite", subdomain]),
...selectedPages.map((page) =>
queryClient.invalidateQueries(["getPage", page.id]),
),
])
// Unselect all
setBatchSelected([])
},
},
{
text: "Delete",
onClick: async () => {
// Start message
const toastId = toast.loading("Deleting...")
// Find all selected
const selectedPages: Note[] = []
pages.data?.pages.map((page) =>
page.list?.map((page) => {
if (batchSelected.includes(page.id)) {
selectedPages.push(page)
}
}),
)
// Delete all // TODO: use multicall to optimize
try {
await Promise.all(
selectedPages.map((page) => {
if (!page.metadata) {
// Is draft
delStorage(`draft-${subdomain}-${page.id}`)
} else {
// Is Note
return deletePage.mutateAsync({
site: subdomain,
id: page.id,
})
}
}),
)
toast.success(t("Deleted!"), {
id: toastId,
})
} catch (e) {
toast.error(t("Fail to Deleted."), {
// It should be "Failed to delete.", but the translation key is that so :shrug:
id: toastId,
})
}
// Refresh site data
await Promise.all([
queryClient.refetchQueries(["getPagesBySite", subdomain]),
...selectedPages.map((page) =>
queryClient.refetchQueries(["getPage", page.id]),
),
])
// Unselect all
setBatchSelected([])
},
},
]
return <Tabs items={tabItems} />
}
export default PagesManagerBatchSelectActionTab