Files
Trilium/apps/client/src/widgets/dialogs/bulk_actions.tsx
2025-08-19 21:41:05 +03:00

127 lines
4.8 KiB
TypeScript

import { useEffect, useState, useCallback } from "preact/hooks";
import { t } from "../../services/i18n";
import Modal from "../react/Modal";
import ReactBasicWidget from "../react/ReactBasicWidget";
import "./bulk_actions.css";
import { BulkActionAffectedNotes } from "@triliumnext/commons";
import server from "../../services/server";
import FormCheckbox from "../react/FormCheckbox";
import Button from "../react/Button";
import bulk_action from "../../services/bulk_action";
import toast from "../../services/toast";
import RenameNoteBulkAction from "../bulk_actions/note/rename_note";
import FNote from "../../entities/fnote";
import froca from "../../services/froca";
import useTriliumEvent from "../react/hooks";
function BulkActionComponent() {
const [ selectedOrActiveNoteIds, setSelectedOrActiveNoteIds ] = useState<string[]>();
const [ bulkActionNote, setBulkActionNote ] = useState<FNote | null>();
const [ includeDescendants, setIncludeDescendants ] = useState(false);
const [ affectedNoteCount, setAffectedNoteCount ] = useState(0);
const [ existingActions, setExistingActions ] = useState<RenameNoteBulkAction[]>([]);
const [ shown, setShown ] = useState(false);
useTriliumEvent("openBulkActionsDialog", async ({ selectedOrActiveNoteIds }) => {
setSelectedOrActiveNoteIds(selectedOrActiveNoteIds);
setBulkActionNote(await froca.getNote("_bulkAction"));
setShown(true);
});
useEffect(() => {
if (!selectedOrActiveNoteIds || !bulkActionNote) return;
server.post<BulkActionAffectedNotes>("bulk-action/affected-notes", {
noteIds: selectedOrActiveNoteIds,
includeDescendants
}).then(({ affectedNoteCount }) => setAffectedNoteCount(affectedNoteCount));
}, [ selectedOrActiveNoteIds, includeDescendants, bulkActionNote ]);
const refreshExistingActions = useCallback(() => {
if (!bulkActionNote) return;
setExistingActions(bulk_action.parseActions(bulkActionNote));
}, [bulkActionNote]);
useEffect(() => {
refreshExistingActions();
}, [refreshExistingActions]);
useTriliumEvent("entitiesReloaded", ({ loadResults }) => {
if (loadResults.getAttributeRows().find((row) =>
row.type === "label" && row.name === "action" && row.noteId === "_bulkAction")) {
refreshExistingActions();
}
}, shown);
return (
<Modal
className="bulk-actions-dialog"
size="xl"
title={t("bulk_actions.bulk_actions")}
footer={<Button text={t("bulk_actions.execute_bulk_actions")} primary />}
show={shown}
onSubmit={async () => {
await server.post("bulk-action/execute", {
noteIds: selectedOrActiveNoteIds,
includeDescendants
});
toast.showMessage(t("bulk_actions.bulk_actions_executed"), 3000);
setShown(false);
}}
onHidden={() => setShown(false)}
>
<h4>{t("bulk_actions.affected_notes")}: <span>{affectedNoteCount}</span></h4>
<FormCheckbox
name="include-descendants" label={t("bulk_actions.include_descendants")}
currentValue={includeDescendants} onChange={setIncludeDescendants}
/>
<h4>{t("bulk_actions.available_actions")}</h4>
<AvailableActionsList />
<h4>{t("bulk_actions.chosen_actions")}</h4>
<ExistingActionsList existingActions={existingActions} />
</Modal>
)
}
function AvailableActionsList() {
return <table class="bulk-available-action-list">
{bulk_action.ACTION_GROUPS.map((actionGroup) => {
return (
<tr>
<td>{ actionGroup.title }:</td>
{actionGroup.actions.map(({ actionName, actionTitle }) =>
<Button
size="small"
text={actionTitle}
onClick={() => bulk_action.addAction("_bulkAction", actionName)}
/>
)}
</tr>
);
})}
</table>;
}
function ExistingActionsList({ existingActions }: { existingActions?: RenameNoteBulkAction[] }) {
return (
<table class="bulk-existing-action-list">
{ existingActions
? existingActions
.map(action => action.doRender())
.filter(renderedAction => renderedAction !== null)
: <p>{t("bulk_actions.none_yet")}</p>
}
</table>
);
}
export default class BulkActionsDialog extends ReactBasicWidget {
get component() {
return <BulkActionComponent />
}
}