feat: batch actions (#431)
This commit is contained in:
parent
f5dcdfe2e5
commit
a49778bb16
|
|
@ -41,6 +41,8 @@
|
|||
},
|
||||
"Convert to Page": "ページに変換",
|
||||
"Convert to Post": "記事に変換",
|
||||
"Select All": "すべて選択",
|
||||
"Deselect All": "選択解除",
|
||||
"Delete": "削除",
|
||||
"Heading": "見出し",
|
||||
"Bold": "太字",
|
||||
|
|
|
|||
|
|
@ -45,6 +45,8 @@
|
|||
"Edit": "編輯 ",
|
||||
"Convert to Page": "轉換為頁面",
|
||||
"Convert to Post": "轉換為文章",
|
||||
"Select All": "全部選擇",
|
||||
"Deselect All": "取消選擇",
|
||||
"Delete": "刪除",
|
||||
"Heading": "標題",
|
||||
"Bold": "粗體",
|
||||
|
|
|
|||
|
|
@ -45,6 +45,8 @@
|
|||
"Edit": "编辑",
|
||||
"Convert to Page": "转换为页面",
|
||||
"Convert to Post": "转换为文章",
|
||||
"Select All": "全部选择",
|
||||
"Deselect All": "取消选择",
|
||||
"Delete": "删除",
|
||||
"Heading": "标题",
|
||||
"Bold": "粗体",
|
||||
|
|
|
|||
|
|
@ -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 }) => (
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
Loading…
Reference in New Issue