diff --git a/public/locales/ja/dashboard.json b/public/locales/ja/dashboard.json index 76ecccfb..86124889 100644 --- a/public/locales/ja/dashboard.json +++ b/public/locales/ja/dashboard.json @@ -41,6 +41,8 @@ }, "Convert to Page": "ページに変換", "Convert to Post": "記事に変換", + "Select All": "すべて選択", + "Deselect All": "選択解除", "Delete": "削除", "Heading": "見出し", "Bold": "太字", diff --git a/public/locales/zh-TW/dashboard.json b/public/locales/zh-TW/dashboard.json index 6822c03a..a18f85e2 100644 --- a/public/locales/zh-TW/dashboard.json +++ b/public/locales/zh-TW/dashboard.json @@ -45,6 +45,8 @@ "Edit": "編輯 ", "Convert to Page": "轉換為頁面", "Convert to Post": "轉換為文章", + "Select All": "全部選擇", + "Deselect All": "取消選擇", "Delete": "刪除", "Heading": "標題", "Bold": "粗體", diff --git a/public/locales/zh/dashboard.json b/public/locales/zh/dashboard.json index 051b2dfa..c9c73591 100644 --- a/public/locales/zh/dashboard.json +++ b/public/locales/zh/dashboard.json @@ -45,6 +45,8 @@ "Edit": "编辑", "Convert to Page": "转换为页面", "Convert to Post": "转换为文章", + "Select All": "全部选择", + "Deselect All": "取消选择", "Delete": "删除", "Heading": "标题", "Bold": "粗体", diff --git a/src/components/dashboard/PagesManager.tsx b/src/components/dashboard/PagesManager.tsx index 1f7b8f28..01bf29a4 100644 --- a/src/components/dashboard/PagesManager.tsx +++ b/src/components/dashboard/PagesManager.tsx @@ -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([]) + const tabItems: TabItem[] = [ { value: PageVisibilityEnum.All, @@ -208,7 +212,21 @@ export const PagesManager: React.FC<{ {description} - + + {batchSelected.length > 0 ? ( + item.text === "Others on Crossbell") // Not sure if there are better ways + ?.active + } + pages={pages} + batchSelected={batchSelected} + setBatchSelected={setBatchSelected} + /> + ) : ( + + )}
{!pages.data?.pages?.[0].total && ( @@ -222,8 +240,41 @@ export const PagesManager: React.FC<{ +
+ +
{page.title ? (
@@ -246,7 +297,7 @@ export const PagesManager: React.FC<{
-
+
{({ open, close }) => ( <> diff --git a/src/components/dashboard/PagesManagerBatchSelectActionTab.tsx b/src/components/dashboard/PagesManagerBatchSelectActionTab.tsx new file mode 100644 index 00000000..d9a844b4 --- /dev/null +++ b/src/components/dashboard/PagesManagerBatchSelectActionTab.tsx @@ -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 + 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 +} + +export default PagesManagerBatchSelectActionTab