+ Full source and target texts appear below with no alignment markup. Select matching
+ spans in both columns, build mappings in the sidebar, then publish to save your first
+ alignment.
+
- WARNING: This action will permanently modify
- the alignment data and cannot be easily undone. Are you
- absolutely certain you want to proceed with this operation?
-
-
+ )}
{/* Content Text with Line Breaks Applied */}
{instance.content && (
- {/* Loading State for Annotation */}
- {segmentationAnnotationId && isLoadingAnnotation && (
+ {/* Loading State for Annotation (skip when showing a saved segmentation from the list) */}
+ {!pickedSegmentationId && segmentationAnnotationId && isLoadingAnnotation && (
+ )}
+
+ {/* Single Full-Page Message when text exists in cataloger (not when adding edition to URL text) */}
+ {selectedText && !isCreatingNewText && !isEmbeddedEdition ? (
@@ -870,7 +972,7 @@ const TextCreation = () => {
{/* Search Input - Only show when no text is selected and not creating new */}
- {!selectedText && !isCreatingNewText && (
+ {!embedded?.forceNewText && !selectedText && !isCreatingNewText && (
{
- {/* Show instance form only when creating new text with content */}
- {isCreatingNewText && editedContent && editedContent.trim() !== "" && (
+ {/* Show instance form when new text + content, or embedded edition on existing text + content */}
+ {(isCreatingNewText || (isEmbeddedEdition && selectedText)) &&
+ editedContent &&
+ editedContent.trim() !== "" && (
void}) {
+ return (
+
+
+
+ Publish
+
+
+
+
+ Webuddhist
+
+
+
+
+
+
+ );
+ }
+
+ export default PublishOptions;
\ No newline at end of file
diff --git a/frontend/src/features/cataloger/components/RelatedEditionItem.tsx b/frontend/src/features/cataloger/components/RelatedEditionItem.tsx
new file mode 100644
index 00000000..ae00da1a
--- /dev/null
+++ b/frontend/src/features/cataloger/components/RelatedEditionItem.tsx
@@ -0,0 +1,32 @@
+import TextCard from "@/components/TextCard";
+import { useText } from "@/hooks/useTexts";
+import { useTranslation } from "react-i18next";
+import { Link } from "react-router-dom";
+
+function RelatedInstanceItem({ sourceInstanceId,relatedInstance }: { sourceInstanceId:string,relatedInstance: RelatedInstance }) {
+ const textId = relatedInstance.text_id;
+ const instanceId =relatedInstance.id;
+ const {t} = useTranslation();
+ const {data:textDetails} =useText(textId);
+ const title = textDetails?.title?.bo|| textDetails?.title?.tib||textDetails?.title?.tibphono || textDetails?.title?.en || textDetails?.title?.sa || textDetails?.title?.pi || t('textInstances.untitled');
+ const type=textDetails?.translation_of ? 'translation' : textDetails?.commentary_of ? 'commentary' : 'source';
+ function getEditionLink(){
+ return `/texts/${textId}/editions`;
+ }
+ return
+
+
+
+ }
+
+ export default RelatedInstanceItem
\ No newline at end of file
diff --git a/frontend/src/features/cataloger/components/UpdateTextForm.tsx b/frontend/src/features/cataloger/components/UpdateTextForm.tsx
new file mode 100644
index 00000000..aadcd4ff
--- /dev/null
+++ b/frontend/src/features/cataloger/components/UpdateTextForm.tsx
@@ -0,0 +1,254 @@
+import { useText, useUpdateText } from "@/hooks/useTexts";
+import { useEffect, useState } from "react";
+import { useTranslation } from "react-i18next";
+import { useParams } from "react-router-dom";
+import { coerceLicense, type LicenseType, type Title as TitleType, type UpdateTextPayload } from "@/types/text";
+import Title from "@/components/formComponent/Title";
+import AlternativeTitle from "@/components/formComponent/AlternativeTitle";
+import Copyright from "@/components/formComponent/Copyright";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import { AlertCircle, X } from "lucide-react";
+import { Button } from "@/components/ui/button";
+import { MultilevelCategorySelector } from "@/components/MultilevelCategorySelector";
+
+
+export const UpdateTextForm = () => {
+ const { t } = useTranslation();
+ const { text_id } = useParams<{ text_id: string }>();
+ const updateTextMutation = useUpdateText();
+ const { data: text } = useText(text_id ?? "");
+ const [titles, setTitles] = useState([]);
+ const [altTitles, setAltTitles] = useState([]);
+ const [bdrc, setBdrc] = useState("");
+ const [wiki, setWiki] = useState("");
+ const [copyright, setCopyright] = useState("Unknown");
+ const [license, setLicense] = useState("public");
+ const [categoryId, setCategoryId] = useState("");
+ const [isSubmitting, setIsSubmitting] = useState(false);
+ const [error, setError] = useState(null);
+ const [success, setSuccess] = useState(false);
+
+ const cn = (...classes: Array) => {
+ return classes.filter(Boolean).join(" ");
+ };
+
+ // Initialize form with existing text data
+ useEffect(() => {
+ if (text) {
+ // Initialize titles
+ if (text.title) {
+ const titleArray: TitleType[] = Object.entries(text.title).map(([lang, value]) => ({
+ language: lang,
+ value: value,
+ }));
+ setTitles(titleArray);
+ }
+
+ // Initialize alt_titles
+ if (text.alt_titles && Array.isArray(text.alt_titles) && text.alt_titles.length > 0) {
+ const altTitlesArray: TitleType[][] = text.alt_titles.map((altTitle) =>
+ Object.entries(altTitle).map(([lang, value]) => ({
+ language: lang,
+ value: value,
+ }))
+ );
+ setAltTitles(altTitlesArray);
+ }
+
+ // Initialize other fields
+ if (text.bdrc) setBdrc(text.bdrc);
+ if (text.wiki) setWiki(text.wiki);
+ if (text.category_id) setCategoryId(text.category_id);
+ setLicense(coerceLicense(text.license));
+ }
+ }, [text]);
+
+ const handleSubmit = async () => {
+ setError(null);
+ setIsSubmitting(true);
+
+ try {
+ if (!text_id) {
+ throw new Error("Missing text ID");
+ }
+
+ // Build title object from titles array
+ const title: Record = {};
+ titles.forEach((titleEntry) => {
+ if (titleEntry.language && titleEntry.value.trim()) {
+ title[titleEntry.language] = titleEntry.value.trim();
+ }
+ });
+
+
+
+ // Build update payload (only include fields that have values)
+ const updatePayload: UpdateTextPayload = {};
+
+ if (Object.keys(title).length > 0) {
+ updatePayload.title = title;
+ }
+ if (bdrc.trim()) {
+ updatePayload.bdrc = bdrc.trim();
+ }
+ if (wiki.trim()) {
+ updatePayload.wiki = wiki.trim();
+ }
+ if (copyright && copyright !== "Unknown") {
+ updatePayload.copyright = copyright.trim();
+ }
+ updatePayload.license = license;
+ if (categoryId) {
+ updatePayload.category_id = categoryId;
+ }
+ // Note: alt_title in UpdateText is Dict[str, List[str]], but we're using alt_titles format
+ // We'll convert it if needed, but for now skip it as it's a different format
+
+ await updateTextMutation.mutateAsync({
+ textId: text_id,
+ textData: updatePayload,
+ });
+
+ setSuccess(true);
+ setTimeout(() => {
+ setSuccess(false);
+ }, 3000);
+ } catch (err: unknown) {
+ setError(
+ err instanceof Error ? err.message : t("messages.updateError")
+ );
+ } finally {
+ setIsSubmitting(false);
+ }
+ };
+
+ return (
+
+
+ {/* Header */}
+
+
{t("common.updateText") || "Update Text"}
+
{t("common.updateTextDescription") || "Update text detail"}
@@ -179,3 +156,29 @@ const TextsPage = () => {
export default TextsPage;
+
+
+function TextCreateButton() {
+ const { t } = useTranslation();
+
+ const navigate = useNavigate()
+ const [, setEditedContent] = useLocalStorage("editedContent", "")
+ const { data: permission, isFetching: isFetchingPermission } = usePermission()
+ const isAdmin = permission?.role === "admin"
+ return (
+ {
+ setEditedContent("")
+ navigate("/create")
+ }}
+ disabled={!isAdmin}
+ className="shrink-0 bg-[var(--color-primary)] hover:bg-[var(--color-primary)]/90 text-white cursor-pointer"
+ >
+ }
+ text={t("common.create")}
+ />
+
+ )}
\ No newline at end of file
diff --git a/frontend/src/pages/TextInstances.tsx b/frontend/src/pages/TextEditions.tsx
similarity index 64%
rename from frontend/src/pages/TextInstances.tsx
rename to frontend/src/pages/TextEditions.tsx
index d79f8110..c7b55f8e 100644
--- a/frontend/src/pages/TextInstances.tsx
+++ b/frontend/src/pages/TextEditions.tsx
@@ -7,8 +7,8 @@ import { useParams, Link } from "react-router-dom";
import TextCard from "@/components/TextCard";
import TextInstanceCard from "@/components/TextInstanceCard";
import { BreadCrumb } from '@app';
-import type { OpenPechaTextInstanceListItem, RelatedInstance } from "@/types/text";
-import { useMemo, useState } from "react";
+import type { Contribution, OpenPechaTextInstanceListItem, RelatedInstance } from "@/types/text";
+import { useMemo } from "react";
import { useTranslation } from "react-i18next";
import {
Table,
@@ -18,11 +18,14 @@ import {
TableRow,
} from "@/components/ui/table";
import { Button } from "@/components/ui/button";
+import PublishOptions from "@/features/cataloger/components/PublishOptions";
+import RelatedInstanceItem from "@/features/cataloger/components/RelatedEditionItem";
+import { Plus, User } from "lucide-react";
+import { Badge } from "@/components/ui/badge";
function TextInstances() {
const { t } = useTranslation();
const { text_id } = useParams();
- const [filter, setFilter] = useState<"translation" | "commentary" | "all">("all");
const {
data: instances = [],
isLoading: isLoadingInstances,
@@ -144,20 +147,8 @@ function TextInstances() {
const title = text?.title?.bo || text?.title?.en || text?.title?.sa || text?.title?.pi|| t('textInstances.untitled');
- const textWithoutAlignmentExists = relatedInstances.some((relatedInstance: RelatedInstance) => !relatedInstance.annotation);
- // Helper function to get title - always get the first value from dictionary
- const getTitle = (titleObj: RelatedInstance["metadata"]["title"]) => {
- if (!titleObj || Object.keys(titleObj).length === 0) {
- return t('textInstances.untitled');
- }
- return Object.values(titleObj)[0] || t('textInstances.untitled');
- };
- const filteredInstances = relatedInstances.filter((instance: RelatedInstance) => {
- if (filter === "all") {
- return true;
- }
- return instance.relationship === filter;
- });
+
+
const handlePublishToWebuddhist = () => {
const webuddhist_cataloger=import.meta.env.VITE_WEBUDDHIST_CATALOGER_URL
const url= `${webuddhist_cataloger}/text/${text_id}`;
@@ -171,30 +162,26 @@ function TextInstances() {
-
{title}
- {textWithoutAlignmentExists && (
-
-
-
-
- Warning: Some texts do not have alignment.
-
-
-
-
-
- )}
-
+
+
+ {title}
+
+
+
-
-
+
+
+
+
+
+ Create New Edition
+
+
+
{/* Original Instances Layout */}
@@ -208,13 +195,7 @@ function TextInstances() {
{/* Related Instances Section - Only show if critical instance exists */}
{criticalInstance && (
-
-
{t('textInstances.relatedTexts')}
-
- Filter by:
-
-
-
+
{/* Loading state for related instances */}
{isLoadingRelated && (
@@ -256,7 +237,6 @@ function TextInstances() {
)}
-
{/* Display related instances */}
{!isLoadingRelated && !relatedError && (
<>
@@ -283,6 +263,7 @@ function TextInstances() {