diff --git a/api/src/common/utility.ts b/api/src/common/utility.ts index 99d04316..daf4ea6c 100644 --- a/api/src/common/utility.ts +++ b/api/src/common/utility.ts @@ -60,6 +60,7 @@ export function calc_vote_weight(upvotes: number, downvotes: number): number { export const putLangCodesToFileName = ( file_name: string, langCodes: LanguageInput, + customSuffix?: string, ): string => { if (!langCodes.language_code) { throw new Error(`language_code insn't provided!`); @@ -76,6 +77,9 @@ export const putLangCodesToFileName = ( if (langCodes.geo_code) { fname += `-${langCodes.geo_code}`; } + if (customSuffix?.length && customSuffix?.length > 0) { + fname += `.${customSuffix}`; + } fname += '.' + suffixes.join('.'); return fname; }; diff --git a/api/src/components/documents/document-translation.service.ts b/api/src/components/documents/document-translation.service.ts new file mode 100644 index 00000000..1a7ff115 --- /dev/null +++ b/api/src/components/documents/document-translation.service.ts @@ -0,0 +1,94 @@ +import { Injectable } from '@nestjs/common'; +import { LanguageInput } from '../common/types'; +import { PericopeTrService } from '../pericope-translations/pericope-tr.service'; +import { + PericopiesService, + WORDS_JOINER, +} from '../pericopies/pericopies.service'; +import { FileService } from '../file/file.service'; +import { FileUrlOutput } from './types'; +import { Readable } from 'stream'; +import { DocumentsService } from './documents.service'; +import { putLangCodesToFileName } from '../../common/utility'; +import { ErrorType } from '../../common/types'; + +const DEFAULT_TEXTY_FILE_MIME_TYPE = 'text/plain'; +const TEXT_IF_NO_TRANSLATION_NO_ORIGINAL = '_error_'; + +@Injectable() +export class DocumentTranslateService { + constructor( + private readonly documentsService: DocumentsService, + private readonly pericopeTrService: PericopeTrService, + private readonly pericopiesService: PericopiesService, + private readonly fileService: FileService, + ) {} + + async translateByPericopies( + documentId: string, + targetLang: LanguageInput, + ): Promise { + const document = await this.documentsService.getDocument( + Number(documentId), + ); + if (document.error !== ErrorType.NoError || !document.document?.file_name) { + return { + error: ErrorType.DocumentNotFound, + fileUrl: null, + fileName: null, + }; + } + const start_word = await this.pericopiesService.getFirstWordOfDocument( + documentId, + ); + const allWords = + start_word === null + ? [] + : await this.pericopiesService.getWordsTillEndOfDocument( + documentId, + start_word, + ); + const pericopeIds: string[] = []; + allWords.forEach((word) => { + if (word.pericope_id) { + pericopeIds.push(word.pericope_id); + } + }); + const pericopeTranslationsPromises = pericopeIds.map((pId) => + this.pericopeTrService.getRecomendedPericopeTranslation(pId, targetLang), + ); + const pericopiesOriginalsPromises = pericopeIds.map((pid) => + this.pericopiesService.getPericopeTextWithDescription(pid), + ); + + const pericopiesTranslations = await Promise.all( + pericopeTranslationsPromises, + ); + const pericopiesOriginals = await Promise.all(pericopiesOriginalsPromises); + const finalStrings = pericopeIds.map((pId) => { + const tr = pericopiesTranslations.find( + (pt) => pt?.pericope_id === pId, + )?.translation; + return ( + tr || + pericopiesOriginals.find((po) => po.pericope_id === pId) + ?.pericope_text || + TEXT_IF_NO_TRANSLATION_NO_ORIGINAL + ); + }); + + const fileContentStream = Readable.from([finalStrings.join(WORDS_JOINER)]); + const newFileName = putLangCodesToFileName( + document.document.file_name, + targetLang, + 'by_pericopies', + ); + + const fileUrl = await this.fileService.uploadTemporaryFile( + fileContentStream, + newFileName, + DEFAULT_TEXTY_FILE_MIME_TYPE, + ); + return { error: ErrorType.NoError, fileUrl, fileName: newFileName }; + } +} diff --git a/api/src/components/documents/documents.module.ts b/api/src/components/documents/documents.module.ts index 898ca01f..da986230 100644 --- a/api/src/components/documents/documents.module.ts +++ b/api/src/components/documents/documents.module.ts @@ -11,6 +11,9 @@ import { DocumentWordEntriesService } from './document-word-entries.service'; import { WordRangesService } from './word-ranges.service'; import { AuthorizationModule } from '../authorization/authorization.module'; import { AuthorizationService } from '../authorization/authorization.service'; +import { DocumentTranslateService } from './document-translation.service'; +import { PericopeTrModule } from '../pericope-translations/pericope-tr.module'; +import { PericopiesModule } from '../pericopies/pericopies.module'; @Module({ imports: [ @@ -18,6 +21,9 @@ import { AuthorizationService } from '../authorization/authorization.service'; forwardRef(() => AuthenticationModule), forwardRef(() => WordsModule), forwardRef(() => AuthorizationModule), + forwardRef(() => PericopeTrModule), + forwardRef(() => PericopiesModule), + forwardRef(() => DocumentsModule), FileModule, ], providers: [ @@ -26,6 +32,7 @@ import { AuthorizationService } from '../authorization/authorization.service'; WordRangesService, DocumentsResolver, AuthorizationService, + DocumentTranslateService, ], exports: [DocumentsService, DocumentWordEntriesService, WordRangesService], }) diff --git a/api/src/components/documents/documents.resolver.ts b/api/src/components/documents/documents.resolver.ts index d3525e23..511724a3 100644 --- a/api/src/components/documents/documents.resolver.ts +++ b/api/src/components/documents/documents.resolver.ts @@ -32,8 +32,11 @@ import { WordRangesOutput, WordRangeInput, WordRangesListConnection, + TranslateDocumentByPericopiesInput, + FileUrlOutput, } from './types'; import { LanguageInput } from '../common/types'; +import { DocumentTranslateService } from './document-translation.service'; @Injectable() @Resolver() @@ -44,6 +47,7 @@ export class DocumentsResolver { private documentsSevice: DocumentsService, private documentWordEntriesService: DocumentWordEntriesService, private wordRangesService: WordRangesService, + private documentTranslateService: DocumentTranslateService, ) {} @Mutation(() => DocumentUploadOutput) @@ -186,11 +190,29 @@ export class DocumentsResolver { input: WordRangeInput[], @Context() req: any, ): Promise { - Logger.log('upsertWordRanges: ', JSON.stringify(input, null, 2)); + Logger.log( + 'DocumentsResolver#upsertWordRanges: ', + JSON.stringify(input, null, 2), + ); return this.wordRangesService.upserts(input, getBearer(req) || '', null); } + @Mutation(() => FileUrlOutput) + async documentByPericopiesTranslate( + @Args('input', { type: () => TranslateDocumentByPericopiesInput }) + input: TranslateDocumentByPericopiesInput, + ): Promise { + Logger.log( + 'DocumentsResolver#documentByPericopiesTranslate: ', + JSON.stringify(input, null, 2), + ); + return this.documentTranslateService.translateByPericopies( + input.documentId, + input.targetLang, + ); + } + @Subscription(() => DocumentUploadOutput, { name: SubscriptionToken.documentAdded, }) diff --git a/api/src/components/documents/types.ts b/api/src/components/documents/types.ts index 20a87491..0754bb8d 100644 --- a/api/src/components/documents/types.ts +++ b/api/src/components/documents/types.ts @@ -1,6 +1,6 @@ import { Field, ID, Int, InputType, ObjectType } from '@nestjs/graphql'; import { GenericOutput } from '../../common/types'; -import { PageInfo } from '../common/types'; +import { LanguageInput, PageInfo } from '../common/types'; import { WordlikeString } from '../words/types'; @@ -131,3 +131,15 @@ export class TextFromRange { export class TextFromRangesOutput extends GenericOutput { @Field(() => [TextFromRange]) list: TextFromRange[]; } + +@InputType() +export class TranslateDocumentByPericopiesInput { + @Field(() => String) documentId: string; + @Field(() => LanguageInput) targetLang: LanguageInput; +} + +@ObjectType() +export class FileUrlOutput extends GenericOutput { + @Field(() => String, { nullable: true }) fileUrl: string | null; + @Field(() => String, { nullable: true }) fileName: string | null; +} diff --git a/api/src/components/pericopies/pericopies.service.ts b/api/src/components/pericopies/pericopies.service.ts index 737bf02a..237e5f82 100644 --- a/api/src/components/pericopies/pericopies.service.ts +++ b/api/src/components/pericopies/pericopies.service.ts @@ -331,6 +331,9 @@ export class PericopiesService { ); } + /** + * @returns WordsTillEndOfDocumentSqlR[] - words are already ordered by level of joining, i.e. by the order of appearence in the text + */ async getWordsTillEndOfDocument( documentId: string, start_word_id: string, diff --git a/api/src/schema.gql b/api/src/schema.gql index f4c1f989..258bea07 100644 --- a/api/src/schema.gql +++ b/api/src/schema.gql @@ -288,6 +288,12 @@ type FileUploadUrlResponse { url: String! } +type FileUrlOutput { + error: ErrorType! + fileName: String + fileUrl: String +} + type Flag { created_at: String! created_by: ID! @@ -675,6 +681,7 @@ type Mutation { createQuestionOnWordRange(input: CreateQuestionOnWordRangeUpsertInput!): QuestionOnWordRangesOutput! createTaggingOnWordRange(begin_document_word_entry_id: ID!, end_document_word_entry_id: ID!, tag_names: [String!]!): WordRangeTagWithVotesOutput! deletePericopie(pericope_id: ID!): PericopeDeleteOutput! + documentByPericopiesTranslate(input: TranslateDocumentByPericopiesInput!): FileUrlOutput! documentUpload(input: DocumentUploadInput!): DocumentUploadOutput! emailResponseResolver(input: EmailResponseInput!): EmailResponseOutput! forceMarkAndRetranslateOriginalMapsIds(originalMapsIds: [String!]!): GenericOutput! @@ -1667,6 +1674,11 @@ type TranslateAllWordsAndPhrasesByBotResult { translatedWordCount: Int! } +input TranslateDocumentByPericopiesInput { + documentId: String! + targetLang: LanguageInput! +} + input TranslatedLanguageInfoInput { fromLanguageCode: ID! toLanguageCode: ID diff --git a/frontend/.gitignore b/frontend/.gitignore index 071a04d7..68eba805 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -32,4 +32,3 @@ dist/ /test-results/ /playwright-report/ /playwright/.cache/ -/public/meta.json diff --git a/frontend/graphql.schema.json b/frontend/graphql.schema.json index 79c6cca6..2f05db92 100644 --- a/frontend/graphql.schema.json +++ b/frontend/graphql.schema.json @@ -2234,6 +2234,57 @@ "enumValues": null, "possibleTypes": null }, + { + "kind": "OBJECT", + "name": "FileUrlOutput", + "description": null, + "fields": [ + { + "name": "error", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "ErrorType", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "fileName", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "fileUrl", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, { "kind": "OBJECT", "name": "Flag", @@ -6124,6 +6175,39 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "documentByPericopiesTranslate", + "description": null, + "args": [ + { + "name": "input", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "TranslateDocumentByPericopiesInput", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "FileUrlOutput", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "documentUpload", "description": null, @@ -21560,6 +21644,49 @@ "enumValues": null, "possibleTypes": null }, + { + "kind": "INPUT_OBJECT", + "name": "TranslateDocumentByPericopiesInput", + "description": null, + "fields": null, + "inputFields": [ + { + "name": "documentId", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "targetLang", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "LanguageInput", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, { "kind": "INPUT_OBJECT", "name": "TranslatedLanguageInfoInput", diff --git a/frontend/public/meta.json b/frontend/public/meta.json new file mode 100644 index 00000000..6a3252ea --- /dev/null +++ b/frontend/public/meta.json @@ -0,0 +1,3 @@ +{ + "version": "0.1.0" +} diff --git a/frontend/src/components/common/VoteButtonsHorizontal/VoteButtonsHorizontal.tsx b/frontend/src/components/common/VoteButtonsHorizontal/VoteButtonsHorizontal.tsx index 8cf93439..474bde8c 100644 --- a/frontend/src/components/common/VoteButtonsHorizontal/VoteButtonsHorizontal.tsx +++ b/frontend/src/components/common/VoteButtonsHorizontal/VoteButtonsHorizontal.tsx @@ -9,6 +9,7 @@ export type VoteButtonsHerizontalProps = { onVoteDownClick: () => void; upVotes: number; downVotes: number; + flex?: string; }; export function VoteButtonsHorizontal({ @@ -16,6 +17,7 @@ export function VoteButtonsHorizontal({ onVoteDownClick, upVotes, downVotes, + flex, }: VoteButtonsHerizontalProps) { const [selected, setSelected] = useState(''); @@ -30,7 +32,7 @@ export function VoteButtonsHorizontal({ }; return ( - + { + openModal2( + , + ); + }; + const dropDownList = [ { key: 'download_button', @@ -77,6 +88,20 @@ export function DocumentItem({ document, onClickItem }: DocumentItemProps) { ), }, + { + key: 'pericopiesTranslateButton', + component: ( + + ), + }, ].filter((item) => item.component !== null); return ( diff --git a/frontend/src/components/documents/DocumentViewer/DocumentViewerV2.tsx b/frontend/src/components/documents/DocumentViewer/DocumentViewerV2.tsx new file mode 100644 index 00000000..948dd020 --- /dev/null +++ b/frontend/src/components/documents/DocumentViewer/DocumentViewerV2.tsx @@ -0,0 +1,765 @@ +import { + useRef, + useMemo, + useEffect, + useCallback, + useState, + ReactNode, + MouseEvent, +} from 'react'; +import { Stack } from '@mui/material'; +import { Virtuoso, ListRange, VirtuosoHandle } from 'react-virtuoso'; + +import { Dot, Word } from './styled'; +import { SkeletonRow } from './SkeletonRow'; + +import { useGetDocumentWordEntriesByDocumentIdLazyQuery } from '../../../generated/graphql'; +import { DeleteCircle } from '../../common/icons/DeleteCircle'; + +const DOCUMENT_PAGE_REMEMBER = 'DOCUMENT_PAGE_REMEMBER'; + +export type TempPage = { + id: string; + first: number; + after: string | null; +}; + +export type RangeItem = { + entryId: string; + order: number; +}; + +export type Range = { + begin: RangeItem; + end: RangeItem; +}; + +export type WordlikeString = { + id: string; + wordlike_string: string; +}; + +export type WordEntry = { + id: string; + wordlike_string: WordlikeString; + parent_id?: string; + page: number; +}; + +export type DocumentViewerHandle = { + getTextFromRange(start: string, end: string): string; +}; + +export type DocumentViewerV2Props = { + range?: Range; + drawRanges: { + begin: string; + end: string; + }[]; + dots: { + entryId: string; + component?: ReactNode; + }[]; + onClickWord( + entryId: string, + order: number, + e: MouseEvent, + ): void; + documentId: string; + onChangeRange(range: Range): void; + onChangeRangeText(sentence: string): void; + onLoadPage?(tempPage: TempPage): void; + customScrollParent?: HTMLElement; +}; + +export function DocumentViewerV2({ + documentId, + range, + drawRanges, + dots, + onClickWord, + onChangeRange, + onChangeRangeText, + onLoadPage, + customScrollParent, +}: DocumentViewerV2Props) { + const [getDocumentWordEntriesByDocumentId] = + useGetDocumentWordEntriesByDocumentIdLazyQuery(); + + const [entriesData, setEntriesData] = useState<(TempPage | WordEntry[])[]>( + [], + ); + const [rowWidth, setRowWidth] = useState(0); + const [requiredPage, setRequiredPage] = useState(null); + const [rememberedPage, setRememberedPage] = useState<{ + after: string | null; + wordId: string | null; + } | null>(null); + const [viewInitialIndex, setViewInitialIndex] = useState(null); + + const virtuosoRef = useRef(null); + const timerRef = useRef(); + + const calcRowWidth = useCallback(() => { + const bodyWidth = document.body.offsetWidth; + + setRowWidth(Math.min(bodyWidth - 32, 777 - 32)); + }, []); + + useEffect(() => { + window.addEventListener('resize', calcRowWidth); + calcRowWidth(); + }, [calcRowWidth]); + + useEffect(() => { + if (rememberedPage && viewInitialIndex !== null && virtuosoRef.current) { + virtuosoRef.current.scrollToIndex({ + index: viewInitialIndex, + align: 'start', + behavior: 'auto', + }); + setViewInitialIndex(null); + setRememberedPage(null); + } + }, [rememberedPage, viewInitialIndex]); + + useEffect(() => { + if (!customScrollParent) { + return; + } + + const previousData = localStorage.getItem(DOCUMENT_PAGE_REMEMBER); + + const after = + previousData && JSON.parse(previousData)[`document_id:${documentId}`] + ? JSON.parse(previousData)[`document_id:${documentId}`].after + : null; + + const wordId = + previousData && JSON.parse(previousData)[`document_id:${documentId}`] + ? JSON.parse(previousData)[`document_id:${documentId}`].wordId + : null; + + if (wordId) { + setRememberedPage({ after, wordId }); + setTimeout( + () => + setRequiredPage({ + id: `page_${JSON.parse(after).page + 1}`, + after: after, + first: 1, + }), + 1000, + ); + } + }, [customScrollParent, documentId]); + + useEffect(() => { + (async () => { + const firstPage = await getDocumentWordEntriesByDocumentId({ + variables: { + document_id: documentId, + first: 1, + after: null, + }, + }); + + const totalPages = + firstPage.data?.getDocumentWordEntriesByDocumentId.pageInfo + .totalEdges || 0; + + const pageEntriesData: TempPage[] = []; + + for (let i = 0; i < totalPages; i++) { + pageEntriesData.push({ + id: `page_${i + 1}`, + after: JSON.stringify({ document_id: +documentId, page: i }), + first: 1, + }); + } + + setEntriesData(pageEntriesData); + })(); + }, [documentId, getDocumentWordEntriesByDocumentId]); + + const handleWordClick = useCallback( + (entryId: string, index: number, e: MouseEvent) => { + if (range) { + // ...A... ... ...B... + if (range.begin.entryId === entryId) { + // ...A(X)... ... ...B... + onChangeRange({ + begin: range.begin, + end: range.begin, + }); + } else if (range.end.entryId === entryId) { + // ...A... ... ...B(X)... + onChangeRange({ + begin: range.end, + end: range.end, + }); + } else { + if (range.begin.order >= index) { + // ...X ... A... ... ...B... + onChangeRange({ + begin: { + entryId, + order: index, + }, + end: range.end, + }); + } else if (range.end.order <= index) { + // ... A... ... ...B... X ... + onChangeRange({ + begin: range.begin, + end: { + entryId, + order: index, + }, + }); + } else if (index - range.begin.order <= range.end.order - index) { + // ... A... X ... ... ...B... + onChangeRange({ + begin: { + entryId, + order: index, + }, + end: range.end, + }); + } else { + // ... A... ... X ...B... + onChangeRange({ + begin: range.begin, + end: { + entryId, + order: index, + }, + }); + } + } + } + + onClickWord(entryId, index, e); + }, + [onChangeRange, onClickWord, range], + ); + + const fetchMore = useCallback( + async (page: TempPage) => { + const { data } = await getDocumentWordEntriesByDocumentId({ + variables: { + document_id: documentId, + first: page.first, + after: page.after, + }, + }); + + if (!data) { + return; + } + + const word_entries: WordEntry[] = []; + data.getDocumentWordEntriesByDocumentId.edges.forEach((edge) => { + edge.node.forEach((item) => + word_entries.push({ + id: item.document_word_entry_id, + wordlike_string: { + id: item.wordlike_string.wordlike_string_id, + wordlike_string: item.wordlike_string.wordlike_string, + }, + parent_id: item.parent_document_word_entry_id || undefined, + page: item.page, + }), + ); + }); + + setEntriesData((data) => { + const refactoredData: (WordEntry[] | TempPage)[] = []; + + data + .map((item) => { + if ( + !Array.isArray(item) && + item.after === page.after && + item.first === page.first + ) { + return word_entries; + } else { + return item; + } + }) + .forEach((item) => { + if (refactoredData.length === 0) { + refactoredData.push(item); + return; + } + + const lastItem = refactoredData[refactoredData.length - 1]; + + if (!Array.isArray(item) || !Array.isArray(lastItem)) { + refactoredData.push(item); + return; + } + + lastItem.push(...item); + + return; + }, []); + + return refactoredData; + }); + }, + [documentId, getDocumentWordEntriesByDocumentId], + ); + + useEffect(() => { + let sentence: string = ''; + let start = false; + + if (range && entriesData.length > 0) { + for (const data of entriesData) { + if (!Array.isArray(data)) { + if (start) { + sentence = `${sentence} ... `; + } + continue; + } + + for (let i = 0; i < data.length; i++) { + if (data[i].id === range.begin.entryId) { + start = true; + } + + if (start) { + sentence = `${sentence} ${data[i].wordlike_string.wordlike_string}`; + } + + if (data[i].id === range.end.entryId) { + start = false; + onChangeRangeText(sentence); + return; + } + } + } + } + }, [entriesData, onChangeRangeText, range]); + + useEffect(() => { + if (!requiredPage) { + return; + } + + const timer = setTimeout(() => { + fetchMore(requiredPage); + onLoadPage && onLoadPage(requiredPage); + }, 500); + + return () => { + if (timer) { + clearTimeout(timer); + } + }; + }, [fetchMore, requiredPage, onLoadPage]); + + const handleLoading = useCallback((tempPage: TempPage) => { + setRequiredPage(tempPage); + }, []); + + const startTimer = useCallback( + (entryId: string, order: number) => { + timerRef.current = setTimeout(() => { + onChangeRange({ + begin: { + entryId: entryId, + order: order, + }, + end: { + entryId: entryId, + order: order, + }, + }); + }, 2000); + }, + [onChangeRange], + ); + + const cancelTimer = useCallback(() => { + clearTimeout(timerRef.current); + }, []); + + const rowData = useMemo(() => { + const rowData: { + row: JSX.Element; + after: string | null; + wordId: string | null; + }[] = []; + const tempRow: { + cols: { + wordEntry: WordEntry; + order: number; + }[]; + width: number; + } = { + cols: [], + width: 0, + }; + + const dotsMap = new Map< + string, + { + entryId: string; + component?: ReactNode; + } + >(); + + dots.forEach((dot) => dotsMap.set(dot.entryId, dot)); + + const canvas = document.createElement('canvas'); + const context = canvas.getContext('2d'); + + if (!context || rowWidth === 0) { + return rowData; + } + + const fontSize = 14; + const fontWeight = 400; + const fontFamily = 'Poppins'; + const padding = 6; + + context.font = `${fontWeight} ${fontSize}px ${fontFamily}`; + + const drawRangeBeginMap = new Map< + string, + { begin: string; end: string }[] + >(); + const drawRangeEndMap = new Map(); + + let begins: { begin: string; end: string }[] = []; + + drawRanges.forEach((item) => { + const beginArr = drawRangeBeginMap.get(item.begin); + if (beginArr) { + beginArr.push(item); + } else { + drawRangeBeginMap.set(item.begin, [item]); + } + + const endArr = drawRangeEndMap.get(item.end); + if (endArr) { + endArr.push(item); + } else { + drawRangeEndMap.set(item.end, [item]); + } + }); + + if (range) { + const beginArr = drawRangeBeginMap.get(range.begin.entryId); + if (beginArr) { + beginArr.push({ + begin: range.begin.entryId, + end: range.end.entryId, + }); + } else { + drawRangeBeginMap.set(range.begin.entryId, [ + { + begin: range.begin.entryId, + end: range.end.entryId, + }, + ]); + } + + const endArr = drawRangeEndMap.get(range.end.entryId); + if (endArr) { + endArr.push({ + begin: range.begin.entryId, + end: range.end.entryId, + }); + } else { + drawRangeEndMap.set(range.end.entryId, [ + { + begin: range.begin.entryId, + end: range.end.entryId, + }, + ]); + } + } + + let wordCounter = 0; + + const getWordProps = (entry: WordEntry, order: number, padding: string) => { + const beginArr = drawRangeBeginMap.get(entry.id); + + if (beginArr) { + begins.push(...beginArr); + } + + const dot = dotsMap.get(entry.id) || null; + const isDot = dot ? true : false; + const dotCom = dot ? dot.component : null; + + let classStr = `edit `; + classStr += begins.length > 0 ? 'selected' : ''; + classStr += ` ${ + entry.id === range?.begin.entryId ? 'left-boundary' : '' + }`; + classStr += ` ${entry.id === range?.end.entryId ? 'right-boundary' : ''}`; + + const cursor = isDot ? 'pointer' : 'default'; + + const endArr = drawRangeEndMap.get(entry.id); + + if (endArr) { + begins = begins.filter((item) => { + const exists = endArr.find((endItem) => { + if (endItem.begin === item.begin && endItem.end === item.end) { + return true; + } else { + return false; + } + }); + + if (exists) { + return false; + } else { + return true; + } + }); + } + + const handleClick = (e: MouseEvent) => { + handleWordClick(entry.id, order, e); + }; + + const wordlikeString = entry.wordlike_string.wordlike_string; + + return { + sx: { + cursor, + padding, + }, + classStr, + handleClick, + wordlikeString, + dotCom, + isDot, + }; + }; + + for (const data of entriesData) { + if (!Array.isArray(data)) { + for (let i = 0; i < 60; i++) { + const skeletonCom = ( + + ); + + rowData.push({ + row: skeletonCom, + after: data.after, + wordId: null, + }); + } + continue; + } + + const entries: WordEntry[] = data; + + for (let i = 0; i < entries.length; i++) { + const entry = entries[i]; + + if (rememberedPage && entry.id === rememberedPage.wordId) { + setViewInitialIndex(rowData.length); + } + + const wordlikeString = entry.wordlike_string.wordlike_string; + + const wordWidth = context.measureText(wordlikeString).width + padding; + + if (tempRow.width + wordWidth < rowWidth - 35) { + tempRow.cols.push({ + wordEntry: entry, + order: wordCounter, + }); + tempRow.width = tempRow.width + wordWidth; + } else { + const rowCom = ( + ({ + color: theme.palette.text.gray, + })} + > + {tempRow.cols.map((col) => { + const { + sx, + classStr, + handleClick, + wordlikeString, + dotCom, + isDot, + } = getWordProps( + col.wordEntry, + col.order, + `0 ${ + 3 + + (rowWidth - tempRow.width - 35) / tempRow.cols.length / 2 + }px`, + ); + + return ( + startTimer(col.wordEntry.id, col.order)} + onMouseUp={cancelTimer} + onMouseMove={cancelTimer} + onTouchStart={() => startTimer(col.wordEntry.id, col.order)} + onTouchMove={cancelTimer} + onTouchEnd={cancelTimer} + > + {wordlikeString} + {isDot ? dotCom || : null} + {range?.begin.entryId === col.wordEntry.id ? ( + + ) : null} + + ); + })} + + ); + + rowData.push({ + row: rowCom, + after: JSON.stringify({ + document_id: +documentId, + page: tempRow.cols[0].wordEntry.page - 1, + }), + wordId: tempRow.cols[0].wordEntry.id, + }); + tempRow.cols = [{ wordEntry: entry, order: wordCounter }]; + tempRow.width = wordWidth; + } + + wordCounter++; + } + } + + if (tempRow.cols.length) { + const rowCom = ( + ({ + color: theme.palette.text.gray, + })} + > + {tempRow.cols.map((col) => { + const { sx, classStr, handleClick, wordlikeString, dotCom, isDot } = + getWordProps(col.wordEntry, col.order, '0 3px'); + + return ( + startTimer(col.wordEntry.id, col.order)} + onMouseUp={cancelTimer} + onMouseMove={cancelTimer} + onTouchStart={() => startTimer(col.wordEntry.id, col.order)} + onTouchMove={cancelTimer} + onTouchEnd={cancelTimer} + > + {wordlikeString} + {isDot ? dotCom || : null} + {range?.begin.entryId === col.wordEntry.id ? ( + + ) : null} + + ); + })} + + ); + + rowData.push({ + row: rowCom, + after: JSON.stringify({ + document_id: +documentId, + page: tempRow.cols[0].wordEntry.page - 1, + }), + wordId: tempRow.cols[0].wordEntry.id, + }); + } + + return rowData; + }, [ + cancelTimer, + documentId, + dots, + drawRanges, + entriesData, + handleLoading, + handleWordClick, + range, + rememberedPage, + rowWidth, + startTimer, + ]); + + const handleRangeChanged = useCallback( + (range: ListRange) => { + if (!rowData[range.startIndex].wordId || !customScrollParent) { + return; + } + + const previousData = localStorage.getItem(DOCUMENT_PAGE_REMEMBER); + + if (!previousData) { + localStorage.setItem( + DOCUMENT_PAGE_REMEMBER, + JSON.stringify({ + [`document_id:${documentId}`]: { + after: rowData[range.startIndex].after, + wordId: rowData[range.startIndex].wordId, + }, + }), + ); + } else { + const data = JSON.parse(previousData); + + localStorage.setItem( + DOCUMENT_PAGE_REMEMBER, + JSON.stringify({ + ...data, + [`document_id:${documentId}`]: { + after: rowData[range.startIndex].after, + wordId: rowData[range.startIndex].wordId, + }, + }), + ); + } + }, + [customScrollParent, documentId, rowData], + ); + + return ( + <> + data.row} + /> +
+ +
+ + ); +} diff --git a/frontend/src/components/documents/DocumentViewer/index.ts b/frontend/src/components/documents/DocumentViewer/index.ts index 69294d73..b884701e 100644 --- a/frontend/src/components/documents/DocumentViewer/index.ts +++ b/frontend/src/components/documents/DocumentViewer/index.ts @@ -1,2 +1,3 @@ export { DocumentViewer } from './DocumentViewer'; +export { DocumentViewerV2 } from './DocumentViewerV2'; export type { DocumentViewerHandle } from './DocumentViewer'; diff --git a/frontend/src/components/documents/DocumentViewer/styled.tsx b/frontend/src/components/documents/DocumentViewer/styled.tsx index 24b49c87..e09559df 100644 --- a/frontend/src/components/documents/DocumentViewer/styled.tsx +++ b/frontend/src/components/documents/DocumentViewer/styled.tsx @@ -21,10 +21,10 @@ export const Word = styled('div')(({ theme }) => ({ letterSpacing: '-0.5px', }, '&.left-boundary': { - borderRadius: '5px', + borderLeft: `1px solid ${theme.palette.background.blue}`, }, '&.right-boundary': { - borderRadius: '0 5px 5px 0', + borderRight: `1px solid ${theme.palette.background.blue}`, }, })); diff --git a/frontend/src/components/documents/DocumentsPage/DocumentPericopiesTranslateModal.tsx b/frontend/src/components/documents/DocumentsPage/DocumentPericopiesTranslateModal.tsx new file mode 100644 index 00000000..b1bd84f2 --- /dev/null +++ b/frontend/src/components/documents/DocumentsPage/DocumentPericopiesTranslateModal.tsx @@ -0,0 +1,176 @@ +import { + Stack, + Typography, + Divider, + Button, + LinearProgress, +} from '@mui/material'; + +import { useTr } from '../../../hooks/useTr'; +import { Check } from '../../common/icons/Check'; + +import { + TextyDocument, + useDocumentByPericopiesTranslateMutation, +} from '../../../generated/graphql'; + +import { LangSelector } from '../../common/LangSelector/LangSelector'; +import { useCallback } from 'react'; +import { langInfo2langInput } from '../../../../../utils'; +import { NavArrowRight } from '../../common/icons/NavArrowRight'; +import { useIonToast } from '@ionic/react'; +import { useAppContext } from '../../../hooks/useAppContext'; + +type DocumentPericopiesTranslateModalProps = { + onClose(): void; + document: TextyDocument; + doShowLangSelector?: boolean; +}; + +export function DocumentPericopiesTranslateModal({ + onClose, + document, + doShowLangSelector = true, +}: DocumentPericopiesTranslateModalProps) { + const { tr } = useTr(); + const { + states: { + global: { + langauges: { + documentPage: { target: targetLang }, + }, + }, + }, + actions: { changeDocumentTargetLanguage }, + } = useAppContext(); + const [present] = useIonToast(); + + const [documentByPericopiesTranslate, { loading, data }] = + useDocumentByPericopiesTranslateMutation(); + + const translateFn = useCallback(() => { + if (!targetLang) { + present({ + message: `${tr('Please choose language!')}`, + duration: 1500, + position: 'top', + color: 'danger', + }); + return; + } + documentByPericopiesTranslate({ + variables: { + documentId: document.document_id, + targetLang: langInfo2langInput(targetLang), + }, + }); + }, [ + document.document_id, + documentByPericopiesTranslate, + present, + targetLang, + tr, + ]); + + let title = tr('Translate using pericopies translations'); + let content = tr( + 'Click the button below to compose file from translated pericopies.', + ); + let bottomCom = ( + + {doShowLangSelector && ( + { + changeDocumentTargetLanguage(lang); + }} + onClearClick={() => changeDocumentTargetLanguage(null)} + /> + )} + {targetLang && ( + + )} + + + ); + + if (loading) { + title = tr('Composing document'); + content = tr('Started document composition ... '); + bottomCom = ( + + + + {document.file_name} + + + ); + } + + if (data && data.documentByPericopiesTranslate.fileUrl) { + title = tr('Great news, translated file composed!'); + content = tr('Click the link below to download translated file.'); + bottomCom = ( + + + {data.documentByPericopiesTranslate.fileName} + + + + ); + } + + if (data && !data.documentByPericopiesTranslate.fileUrl) { + title = tr('Something went wrong'); + content = tr( + 'We apologize for the inconvenience, there seems to be an issue with translating document at the moment. Please, try again later.', + ); + bottomCom = ( + + + + ); + } + + return ( + + + + {title} + + + + {content} + + + {bottomCom} + + ); +} diff --git a/frontend/src/components/documents/gql.graphql b/frontend/src/components/documents/gql.graphql index 07618efc..a1fd6f5a 100644 --- a/frontend/src/components/documents/gql.graphql +++ b/frontend/src/components/documents/gql.graphql @@ -182,3 +182,15 @@ subscription SubscribeToDocumentAdded { } } } + +mutation DocumentByPericopiesTranslate( + $documentId: String! + $targetLang: LanguageInput! +) { + documentByPericopiesTranslate( + input: { documentId: $documentId, targetLang: $targetLang } + ) { + fileUrl + fileName + } +} diff --git a/frontend/src/components/pericope-translations/PericopeTrPage.tsx b/frontend/src/components/pericope-translations/PericopeTrPage.tsx index 5777a5e9..afb1e094 100644 --- a/frontend/src/components/pericope-translations/PericopeTrPage.tsx +++ b/frontend/src/components/pericope-translations/PericopeTrPage.tsx @@ -21,10 +21,12 @@ export const PericopeTrPage: React.FC = ({ const { states: { global: { - langauges: { sourceLang, targetLang }, + langauges: { + documentPage: { source: sourceLang, target: targetLang }, + }, }, }, - actions: { setSourceLanguage, setTargetLanguage }, + actions: { changeDocumentSourceLanguage, changeDocumentTargetLanguage }, } = useAppContext(); const handleShowDocuments = () => { @@ -46,17 +48,17 @@ export const PericopeTrPage: React.FC = ({ title={tr('Select your language')} selected={sourceLang} onChange={(_langTag, langInfo) => { - setSourceLanguage(langInfo); + changeDocumentSourceLanguage(langInfo); }} - onClearClick={() => setSourceLanguage(null)} + onClearClick={() => changeDocumentSourceLanguage(null)} /> { - setTargetLanguage(langInfo); + changeDocumentTargetLanguage(langInfo); }} - onClearClick={() => setTargetLanguage(null)} + onClearClick={() => changeDocumentTargetLanguage(null)} /> + ); +} diff --git a/frontend/src/components/pericopies/PericopeDocumentViewer/PericopeDocumentViewerV2.tsx b/frontend/src/components/pericopies/PericopeDocumentViewer/PericopeDocumentViewerV2.tsx new file mode 100644 index 00000000..d6918e80 --- /dev/null +++ b/frontend/src/components/pericopies/PericopeDocumentViewer/PericopeDocumentViewerV2.tsx @@ -0,0 +1,176 @@ +import { useMemo, useState, useCallback, ReactNode, useEffect } from 'react'; +import { Box } from '@mui/material'; + +import { Dot } from '../../documents/DocumentViewer/styled'; +import { DocumentViewerV2 } from '../../documents/DocumentViewer'; +import { PericopeReactionV2 } from './PericopeReactionV2'; +import { PericopeAddButtonV2 } from './PericopeAddButtonV2'; + +import { TempPage } from '../../documents/DocumentViewer/DocumentViewer'; + +import { + PericopeWithVote, + useGetPericopiesByDocumentIdQuery, +} from '../../../generated/graphql'; +import { useSubscribeToPericopiesAddedSubscription } from '../../../hooks/useUpsertPericopeMutation'; +import { useSubscribeToPericopieDeletedSubscription } from '../../../hooks/useDeletePericopeMutation'; +import { useSubscribeToPericopeVoteStatusToggledSubscription } from '../../../hooks/useTogglePericopeVoteStatusMutation'; + +type PericopeDocumentViewerV2Props = { + documentId: string; + customScrollParent?: HTMLElement; +}; + +export function PericopeDocumentViewerV2({ + documentId, + customScrollParent, +}: PericopeDocumentViewerV2Props) { + const { data, fetchMore } = useGetPericopiesByDocumentIdQuery({ + variables: { + document_id: documentId, + first: 1, + after: null, + }, + }); + useSubscribeToPericopiesAddedSubscription(); + useSubscribeToPericopieDeletedSubscription(); + useSubscribeToPericopeVoteStatusToggledSubscription(); + + const [selectedWordEntryId, setSelectedWordEntryId] = useState( + null, + ); + const [requiredPage, setRequiredPage] = useState(null); + + useEffect(() => { + if (!requiredPage) { + return; + } + + const timer = setTimeout(() => { + fetchMore({ + variables: { + document_id: documentId, + first: requiredPage.first, + after: requiredPage.after + '', + }, + }); + }, 1000); + + return () => { + if (timer) { + clearTimeout(timer); + } + }; + }, [requiredPage, fetchMore, documentId]); + + const { dots, pericopeMap } = useMemo(() => { + const pericopeMap = new Map(); + + if (data) { + data.getPericopiesByDocumentId.edges.forEach((edge) => { + edge.node.forEach((pericopeWithVote) => { + pericopeMap.set(pericopeWithVote.start_word, pericopeWithVote); + }); + }); + } + + const dots: { + entryId: string; + component?: ReactNode; + }[] = []; + + for (const pericopeWithVote of pericopeMap.values()) { + let dotColor = 'blue'; + + if (pericopeWithVote.upvotes > pericopeWithVote.downvotes) { + dotColor = 'green'; + } else if (pericopeWithVote.upvotes < pericopeWithVote.downvotes) { + dotColor = 'red'; + } + + dots.push({ + entryId: pericopeWithVote.start_word, + component: ( + + theme.palette.background[ + dotColor as keyof typeof theme.palette.background + ], + }} + /> + ), + }); + } + + return { dots, pericopeMap }; + }, [data]); + + const handleWordClick = useCallback((entryId: string) => { + setSelectedWordEntryId(entryId); + }, []); + + const handleLoadPage = useCallback((page: TempPage) => { + setRequiredPage(page); + }, []); + + const handleClose = () => { + setSelectedWordEntryId(null); + }; + + const selectedPericope = selectedWordEntryId + ? pericopeMap.get(selectedWordEntryId) || null + : null; + + let popoverCom: ReactNode = null; + + if (selectedPericope) { + popoverCom = ( + + ); + } else if (selectedWordEntryId) { + popoverCom = ( + + ); + } + + const drawRanges = selectedWordEntryId + ? [{ begin: selectedWordEntryId, end: selectedWordEntryId }] + : []; + + return ( + <> + {}} + onChangeRange={() => {}} + onClickWord={handleWordClick} + onLoadPage={handleLoadPage} + customScrollParent={customScrollParent} + /> + + {selectedWordEntryId ? ( + theme.palette.background.white, + boxShadow: '0px -5px 14px 0px rgba(128, 136, 163, 0.20)', + }} + > + {popoverCom} + + ) : null} + + ); +} diff --git a/frontend/src/components/pericopies/PericopeDocumentViewer/PericopeReactionV2.tsx b/frontend/src/components/pericopies/PericopeDocumentViewer/PericopeReactionV2.tsx new file mode 100644 index 00000000..7adb5c13 --- /dev/null +++ b/frontend/src/components/pericopies/PericopeDocumentViewer/PericopeReactionV2.tsx @@ -0,0 +1,88 @@ +import { Stack, Button } from '@mui/material'; + +import { VoteButtonsHorizontal } from '../../common/VoteButtonsHorizontal'; +import { DiscussionIconButton } from '../../Discussion/DiscussionButton/DiscussionIconButton'; + +import { TableNameType, PericopeWithVote } from '../../../generated/graphql'; +import { useTogglePericopeVoteStatusMutation } from '../../../hooks/useTogglePericopeVoteStatusMutation'; +import { useDeletePericopeMutation } from '../../../hooks/useDeletePericopeMutation'; + +import { useTr } from '../../../hooks/useTr'; +import { DeleteCircle } from '../../common/icons/DeleteCircle'; + +type PericopeReactionV2Props = { + pericope: PericopeWithVote; + onClose(): void; +}; + +export function PericopeReactionV2({ + pericope, + onClose, +}: PericopeReactionV2Props) { + const { tr } = useTr(); + const [deletePericope] = useDeletePericopeMutation(); + + const [togglePericopeVoteStatus] = useTogglePericopeVoteStatusMutation(); + + const handleUpClick = () => { + togglePericopeVoteStatus({ + variables: { + pericope_id: pericope.pericope_id, + vote: true, + }, + }); + }; + + const handleDeletePericope = () => { + deletePericope({ + variables: { + pericope_id: pericope.pericope_id, + }, + }); + onClose(); + }; + + const handleDownClick = () => { + togglePericopeVoteStatus({ + variables: { + pericope_id: pericope.pericope_id, + vote: false, + }, + }); + }; + + return ( + + + + + + ); +} diff --git a/frontend/src/components/pericopies/PericopeDocumentViewer/index.ts b/frontend/src/components/pericopies/PericopeDocumentViewer/index.ts index 3f09cbd5..eef9c6a8 100644 --- a/frontend/src/components/pericopies/PericopeDocumentViewer/index.ts +++ b/frontend/src/components/pericopies/PericopeDocumentViewer/index.ts @@ -1 +1,2 @@ export { PericopeDocumentViewer } from './PericopeDocumentViewer'; +export { PericopeDocumentViewerV2 } from './PericopeDocumentViewerV2'; diff --git a/frontend/src/components/super-tool/SuperDocumentViewer/SuperDocumentViewer.tsx b/frontend/src/components/super-tool/SuperDocumentViewer/SuperDocumentViewer.tsx index b062a2cf..576f1d17 100644 --- a/frontend/src/components/super-tool/SuperDocumentViewer/SuperDocumentViewer.tsx +++ b/frontend/src/components/super-tool/SuperDocumentViewer/SuperDocumentViewer.tsx @@ -1,7 +1,7 @@ import { ViewMode } from '../../documents/DocumentViewer/DocumentViewer'; import { SuperToolKind } from '../SuperDocumentViewerPage/ToolBox'; -import { PericopeDocumentViewer } from '../../pericopies/PericopeDocumentViewer'; +import { PericopeDocumentViewerV2 } from '../../pericopies/PericopeDocumentViewer'; import { TaggingDocumentViewer } from '../../tagging/TaggingDocumentViewer'; import { QADocumentViewer } from '../../qa/QADocumentViewer'; @@ -20,9 +20,8 @@ export function SuperDocumentViewer({ }: SuperDocumentViewerProps) { if (tool === SuperToolKind.Pericope) { return ( - ); diff --git a/frontend/src/components/super-tool/SuperDocumentViewerPage/SuperDocumentViewerPage.tsx b/frontend/src/components/super-tool/SuperDocumentViewerPage/SuperDocumentViewerPage.tsx index 2b9532dd..9ef6c02d 100644 --- a/frontend/src/components/super-tool/SuperDocumentViewerPage/SuperDocumentViewerPage.tsx +++ b/frontend/src/components/super-tool/SuperDocumentViewerPage/SuperDocumentViewerPage.tsx @@ -23,8 +23,7 @@ import { NavArrowDown } from '../../common/icons/NavArrowDown'; import { NavArrowUp } from '../../common/icons/NavArrowUp'; import { SuperDocumentViewer } from '../SuperDocumentViewer/SuperDocumentViewer'; import { SuperPericopiesTranslator } from '../SuperPericopiesTranslator'; -import { useSubscribeToRecomendedPericopiesChangedSubscription } from '../../../hooks/useSubscribeToRecomendedPericopiesChanged'; -import { useBestPericopeTrChangedSubscription } from '../../../hooks/useSubscribeToRecomendedPericopies'; +import { useBestPericopeTrChangedSubscription } from '../../../hooks/useBestPericopeTrChangedSubscription'; export function SuperDocumentViewerPage() { const { tr } = useTr(); @@ -65,7 +64,6 @@ export function SuperDocumentViewerPage() { setPageStatus('hidden'); }); - useSubscribeToRecomendedPericopiesChangedSubscription(); useBestPericopeTrChangedSubscription(); const handleToggleMode = useCallback(() => { @@ -128,7 +126,7 @@ export function SuperDocumentViewerPage() { zIndex: 10, width: matches ? 'calc(777px)' : 'calc(100%)', background: (theme) => theme.palette.background.white, - marginLeft: '-15px', + marginLeft: '-30px', padding: '15px', borderRadius: '0 0 20px 20px', boxShadow: '0px 5px 14px 0px rgba(128, 136, 163, 0.20)', diff --git a/frontend/src/components/super-tool/SuperDocumentViewerPage/ToolBox.tsx b/frontend/src/components/super-tool/SuperDocumentViewerPage/ToolBox.tsx index 83c72a41..ff25f375 100644 --- a/frontend/src/components/super-tool/SuperDocumentViewerPage/ToolBox.tsx +++ b/frontend/src/components/super-tool/SuperDocumentViewerPage/ToolBox.tsx @@ -19,6 +19,9 @@ import { TextyDocument } from '../../../generated/graphql'; import { useTr } from '../../../hooks/useTr'; import { useAppContext } from '../../../hooks/useAppContext'; +import { IonIcon } from '@ionic/react'; +import { languageOutline } from 'ionicons/icons'; +import { DocumentPericopiesTranslateModal } from '../../documents/DocumentsPage/DocumentPericopiesTranslateModal'; export enum TabKind { Document = 'document', @@ -75,9 +78,12 @@ export function ToolBox({ }, }, }, + actions: { createModal }, } = useAppContext(); const [showStringSearch, setShowStringSearch] = useState(false); + const { openModal, closeModal } = createModal(); + const tabs = useMemo( () => [ { @@ -154,6 +160,15 @@ export function ToolBox({ downloadFromUrl(document.file_name, document.file_url); }; + const handlePericopiesTranslate = () => { + openModal( + , + ); + }; + const handleToggleSearchInput = () => { setShowStringSearch((_value) => !_value); }; @@ -173,6 +188,20 @@ export function ToolBox({ ), }, + { + key: 'pericopiesTranslateButton', + component: ( + + ), + }, ].filter((item) => item.component !== null); const filterCom = @@ -291,7 +320,16 @@ export function ToolBox({ > {tr('Source')}: - +
diff --git a/frontend/src/generated/graphql.tsx b/frontend/src/generated/graphql.tsx index 708a459a..28518541 100644 --- a/frontend/src/generated/graphql.tsx +++ b/frontend/src/generated/graphql.tsx @@ -318,6 +318,13 @@ export type FileUploadUrlResponse = { url: Scalars['String']['output']; }; +export type FileUrlOutput = { + __typename?: 'FileUrlOutput'; + error: ErrorType; + fileName?: Maybe; + fileUrl?: Maybe; +}; + export type Flag = { __typename?: 'Flag'; created_at: Scalars['String']['output']; @@ -748,6 +755,7 @@ export type Mutation = { createQuestionOnWordRange: QuestionOnWordRangesOutput; createTaggingOnWordRange: WordRangeTagWithVotesOutput; deletePericopie: PericopeDeleteOutput; + documentByPericopiesTranslate: FileUrlOutput; documentUpload: DocumentUploadOutput; emailResponseResolver: EmailResponseOutput; forceMarkAndRetranslateOriginalMapsIds: GenericOutput; @@ -872,6 +880,11 @@ export type MutationDeletePericopieArgs = { }; +export type MutationDocumentByPericopiesTranslateArgs = { + input: TranslateDocumentByPericopiesInput; +}; + + export type MutationDocumentUploadArgs = { input: DocumentUploadInput; }; @@ -2823,6 +2836,11 @@ export type TranslateAllWordsAndPhrasesByBotResult = { translatedWordCount: Scalars['Int']['output']; }; +export type TranslateDocumentByPericopiesInput = { + documentId: Scalars['String']['input']; + targetLang: LanguageInput; +}; + export type TranslatedLanguageInfoInput = { fromLanguageCode: Scalars['ID']['input']; toLanguageCode?: InputMaybe; @@ -3562,6 +3580,14 @@ export type SubscribeToDocumentAddedSubscriptionVariables = Exact<{ [key: string export type SubscribeToDocumentAddedSubscription = { __typename?: 'Subscription', documentAdded: { __typename?: 'DocumentUploadOutput', error: ErrorType, document?: { __typename?: 'TextyDocument', document_id: string, file_id: string, file_name: string, file_url: string, language_code: string, dialect_code?: string | null, geo_code?: string | null, created_by: string } | null } }; +export type DocumentByPericopiesTranslateMutationVariables = Exact<{ + documentId: Scalars['String']['input']; + targetLang: LanguageInput; +}>; + + +export type DocumentByPericopiesTranslateMutation = { __typename?: 'Mutation', documentByPericopiesTranslate: { __typename?: 'FileUrlOutput', fileUrl?: string | null, fileName?: string | null } }; + export type EmailResponseMutationVariables = Exact<{ token: Scalars['String']['input']; }>; @@ -6784,6 +6810,43 @@ export function useSubscribeToDocumentAddedSubscription(baseOptions?: Apollo.Sub } export type SubscribeToDocumentAddedSubscriptionHookResult = ReturnType; export type SubscribeToDocumentAddedSubscriptionResult = Apollo.SubscriptionResult; +export const DocumentByPericopiesTranslateDocument = gql` + mutation DocumentByPericopiesTranslate($documentId: String!, $targetLang: LanguageInput!) { + documentByPericopiesTranslate( + input: {documentId: $documentId, targetLang: $targetLang} + ) { + fileUrl + fileName + } +} + `; +export type DocumentByPericopiesTranslateMutationFn = Apollo.MutationFunction; + +/** + * __useDocumentByPericopiesTranslateMutation__ + * + * To run a mutation, you first call `useDocumentByPericopiesTranslateMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useDocumentByPericopiesTranslateMutation` returns a tuple that includes: + * - A mutate function that you can call at any time to execute the mutation + * - An object with fields that represent the current status of the mutation's execution + * + * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; + * + * @example + * const [documentByPericopiesTranslateMutation, { data, loading, error }] = useDocumentByPericopiesTranslateMutation({ + * variables: { + * documentId: // value for 'documentId' + * targetLang: // value for 'targetLang' + * }, + * }); + */ +export function useDocumentByPericopiesTranslateMutation(baseOptions?: Apollo.MutationHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useMutation(DocumentByPericopiesTranslateDocument, options); + } +export type DocumentByPericopiesTranslateMutationHookResult = ReturnType; +export type DocumentByPericopiesTranslateMutationResult = Apollo.MutationResult; +export type DocumentByPericopiesTranslateMutationOptions = Apollo.BaseMutationOptions; export const EmailResponseDocument = gql` mutation EmailResponse($token: String!) { emailResponseResolver(input: {token: $token}) { @@ -11992,6 +12055,7 @@ export const namedOperations = { DocumentUpload: 'DocumentUpload', UpsertWordRange: 'UpsertWordRange', BotTranslateDocument: 'BotTranslateDocument', + DocumentByPericopiesTranslate: 'DocumentByPericopiesTranslate', EmailResponse: 'EmailResponse', UploadFile: 'UploadFile', ToggleFlagWithRef: 'ToggleFlagWithRef', diff --git a/frontend/src/hooks/useSubscribeToRecomendedPericopies.ts b/frontend/src/hooks/useBestPericopeTrChangedSubscription.ts similarity index 94% rename from frontend/src/hooks/useSubscribeToRecomendedPericopies.ts rename to frontend/src/hooks/useBestPericopeTrChangedSubscription.ts index 84e156e7..7e9c6be4 100644 --- a/frontend/src/hooks/useSubscribeToRecomendedPericopies.ts +++ b/frontend/src/hooks/useBestPericopeTrChangedSubscription.ts @@ -14,8 +14,6 @@ export function useBestPericopeTrChangedSubscription() { data.data.bestPericopeTrChanged.newVoteStatus, data.data.bestPericopeTrChanged.newPericopeTr, ); - client.cache; - console.log(data); }, }); } diff --git a/frontend/src/hooks/useCacheBuster.ts b/frontend/src/hooks/useCacheBuster.ts index b843b3f7..a116f629 100644 --- a/frontend/src/hooks/useCacheBuster.ts +++ b/frontend/src/hooks/useCacheBuster.ts @@ -49,7 +49,14 @@ export const useCacheBuster = () => { useEffect(() => { fetch('/meta.json') - .then((response) => response.json()) + .then((response) => { + try { + return response.json(); + } catch (error) { + console.error(error); + return { version: '' }; + } + }) .then((meta) => { const latestVersion = meta.version; const currentVersion = appVersion; diff --git a/frontend/src/hooks/useSubscribeToRecomendedPericopiesChanged.ts b/frontend/src/hooks/useSubscribeToRecomendedPericopiesChanged.ts index e35d4ee5..e0741c29 100644 --- a/frontend/src/hooks/useSubscribeToRecomendedPericopiesChanged.ts +++ b/frontend/src/hooks/useSubscribeToRecomendedPericopiesChanged.ts @@ -5,11 +5,10 @@ import { export function useSubscribeToRecomendedPericopiesChangedSubscription() { return useGenSubscribeToRecomendedPericopiesChangedSubscription({ - onData: ({ data, client }) => { + onData: ({ client }) => { client.refetchQueries({ include: [GetPericopiesTrDocument], }); - console.log(data); }, }); } diff --git a/package-lock.json b/package-lock.json index d55d25cb..d92c5115 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,7 +7,7 @@ "devDependencies": { "@playwright/test": "^1.40.0" }, - "version": "0.1.0" + "version": "0.3.0" }, "node_modules/@playwright/test": { "version": "1.40.0", @@ -69,5 +69,5 @@ } } }, - "version": "0.1.0" + "version": "0.3.0" } diff --git a/package.json b/package.json index 8eceb916..57cdf18d 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "version": "0.1.0", + "version": "0.3.0", "devDependencies": { "@playwright/test": "^1.40.0" }