From 10c73d937df6666dc299d3bea234abaa81866a21 Mon Sep 17 00:00:00 2001 From: vmonakhov Date: Sun, 22 Dec 2024 20:16:03 +0300 Subject: [PATCH] Kalmyk markup groups -- https://github.com/ispras/lingvodoc-react/issues/1152 (#1177) * add modal Join markups * part table 2 * part table 1 * add sticky and radio buttons (part) * kalmyk markup: part * kalmyk markups: part * kalmyk markup: part selected relations * kalmyl markup: buttons m and g * Init * queries * mutations * model * fixes * fixes * selection * event listener optimization * reset markup action * minor * highlights * daily work * fixes * merging * kalmyk marups: fix modal * kalmyk markup: fix texts * fixes * next fixes * kalmyk markup: fix styles and text * kalmyk markups: fix translations * next work * fixes * next work * fixes * daily work * next work * next work * fixes * onCleverClose * minor * improved JoinMarkupModal * fixes and beauty * minor * fixed dnd metadata * refetching only changed lexical entries * cleanup * kalmyk markups: styles for markers * kalmyk markups: fix markers * kalmyk markup: fix markers * cleanup * important fix * breakdown with markups * kalmyk markup: delete unnecessary * kalmyk markup: fix styles * kalmyk markup: add getTranslation * edit with markups * kalmyk markup: fix modal * kalmyk markup: fixes modal * kalmyk markup: add translation * kalmyk markup: fix styles * kalmyk markup: fixes * beauty * save xlsx * using real dictionary name * using select * fix --------- Co-authored-by: nataliauvarova <90451645+nataliauvarova@users.noreply.github.com> Co-authored-by: nataliauvarova --- src/api/i18n.js | 21 + src/components/CorporaView/index.js | 391 ++++++++------- src/components/JoinMarkupsModal/index.js | 468 ++++++++++++++++++ src/components/JoinMarkupsModal/styles.scss | 110 +++++ src/components/LexicalEntryCorp/Text.js | 505 ++++++++++++++++---- src/components/LexicalEntryCorp/index.js | 216 +++++++-- src/pages/Perspective/style.scss | 19 +- src/styles/main.scss | 190 ++++---- src/utils/patienceDiff.js | 471 ++++++++++++++++++ 9 files changed, 2019 insertions(+), 372 deletions(-) create mode 100644 src/components/JoinMarkupsModal/index.js create mode 100644 src/components/JoinMarkupsModal/styles.scss create mode 100644 src/utils/patienceDiff.js diff --git a/src/api/i18n.js b/src/api/i18n.js index 8d06d4bb..68c77cc7 100644 --- a/src/api/i18n.js +++ b/src/api/i18n.js @@ -96,10 +96,12 @@ export const stringsToTranslate = [ "Ask questions about the LingvoDoc program at", "assertive", "assumptive", + "Attach", "Attach new user", "Attached to another group.", "attributive", "Audio", + "Author", "Authors", "automatic creation of dictionaries from text corpora", "automatic segmentation of native speaker surveys, uploaded into the Telegram channel “LingvoDoc Support”, into separate words", @@ -221,6 +223,7 @@ export const stringsToTranslate = [ "Create dictionary", "Create field", "Create language", + "Create markup", "Create new", "Create new perspective", "Create one or more perspectives", @@ -250,7 +253,12 @@ export const stringsToTranslate = [ "Definitely endangered", "Delete", "Delete image file", + "Delete groups", + "Delete markup", "Delete markup file", + "Delete markup group", + "Delete markup(s)", + "Delete markup(s) with related groups", "Delete organization", "Delete sound file", "Deleting", @@ -452,6 +460,7 @@ export const stringsToTranslate = [ "iterative", "Ivannikov Institute for System Programming of the Russian Academy of Sciences", "Join", + "Join markups", "Keep", "Keep skipped vowel interval characters", "Language", @@ -465,6 +474,7 @@ export const stringsToTranslate = [ "Languages databases", "Last modified at", "left", + "Left text", "Legend", "Levenshtein distance limit for entity content matching", "Lexical entries", @@ -482,6 +492,7 @@ export const stringsToTranslate = [ "Link to search results:", "Linked organizations:", "Linking", + "Literal translation", "Loading", "Loading additional filter data...", "Loading adjacent character data", @@ -493,6 +504,7 @@ export const stringsToTranslate = [ "Loading linked perspective data", "Loading locale data", "Loading markup data", + "Loading markups and groups data", "Loading metadata", "Loading perspective data", "Loading perspective selection", @@ -747,6 +759,7 @@ export const stringsToTranslate = [ "results", "results on", "Return to tree", + "Right text", "Role", "Role data loading error, please contact adiministrators.", "Role in the sentence", @@ -827,6 +840,7 @@ export const stringsToTranslate = [ "singular", "Skipping text output, too long.", "software for morphological analysis of glossed corpora, in particular, automatic identification of government models", + "Some of the selected markups take part in bundles. Are you sure you want to delete the markups and related groups?", "Sort by acceptance", "Sort by cases", "Sort by verbs", @@ -855,10 +869,12 @@ export const stringsToTranslate = [ "subjunctive", "Submit", "Subscribe all the existing dictionaries and corpora related to this language and its sublanguages", + "Success", "Successfully added", "Successfully created perspective.", "Successfully deleted organization", "Successfully removed", + "Such group already exists.", "Suggested cognates", "Suggested cognate groups", "suggestions", @@ -872,6 +888,8 @@ export const stringsToTranslate = [ "Tense-Aspect", "text entities analysed", "Text markup", + "The group was successfully added.", + "The group was successfully deleted.", "The entity is currently published. Click to unpublish.", "The entity is NOT currently published. Click to publish.", "The pros of the LingvoDoc platform", @@ -905,6 +923,7 @@ export const stringsToTranslate = [ "Translation loading error", "Translations", "Translator", + "Translit", "transpnition marker", "Try closing the dialog and opening it again; if the error persists, please contact administrators.", "Try reloading the page; if the error persists, please contact administrators.", @@ -930,6 +949,7 @@ export const stringsToTranslate = [ "Updating valency data...", "Upload", "Upload successful", + "Uploading", "uploading audio files of any size, (audio)corpora in ELAN format, texts in Word .odt format", "URL with results of saving data should appear soon after clicking save button in the tasks", "Use linked data", @@ -962,6 +982,7 @@ export const stringsToTranslate = [ "Voice", "Vowel formant count threshold", "Vulnerable", + "Warning", "web", "With field selection", "Word file", diff --git a/src/components/CorporaView/index.js b/src/components/CorporaView/index.js index 5b4e410e..d32e528e 100644 --- a/src/components/CorporaView/index.js +++ b/src/components/CorporaView/index.js @@ -5,7 +5,7 @@ import { connect } from "react-redux"; import { Button, Dimmer, Header, Icon, Table } from "semantic-ui-react"; import { gql } from "@apollo/client"; import { graphql } from "@apollo/client/react/hoc"; -import update from 'immutability-helper'; +import update from "immutability-helper"; import { drop, flow, isEqual, reverse, sortBy, take } from "lodash"; import PropTypes from "prop-types"; import { branch, compose, renderComponent } from "recompose"; @@ -13,9 +13,12 @@ import { bindActionCreators } from "redux"; import styled from "styled-components"; import ApproveModal from "components/ApproveModal"; +/* new!!!!!! */ +import JoinMarkupsModal from "components/JoinMarkupsModal"; +/* /new!!!!!! */ import Pagination from "components/Pagination"; import Placeholder from "components/Placeholder"; -import { openModal } from "ducks/modals"; +import { openModal, closeModal } from "ducks/modals"; import { addLexicalEntry, resetEntriesSelection, @@ -89,7 +92,8 @@ export const queryLexicalEntries = gql` published accepted additional_metadata { - link_perspective_id + link_perspective_id, + markups } is_subject_for_parsing } @@ -99,22 +103,16 @@ export const queryLexicalEntries = gql` `; const updateLexgraphMutation = gql` - mutation updateLexgraph($id: LingvodocID!, - $lexgraph_before: String!, - $lexgraph_after: String!) { - update_entity_content(id: $id, - lexgraph_before: $lexgraph_before, - lexgraph_after: $lexgraph_after) { + mutation updateLexgraph($id: LingvodocID!, $lexgraph_before: String!, $lexgraph_after: String!) { + update_entity_content(id: $id, lexgraph_before: $lexgraph_before, lexgraph_after: $lexgraph_after) { triumph } } `; const updateEntityParentMutation = gql` - mutation updateEntityParent($id: LingvodocID!, - $new_parent_id: LingvodocID!) { - update_entity(id: $id, - new_parent_id: $new_parent_id) { + mutation updateEntityParent($id: LingvodocID!, $new_parent_id: LingvodocID!) { + update_entity(id: $id, new_parent_id: $new_parent_id) { entity triumph } @@ -151,16 +149,8 @@ const createLexicalEntryMutation = gql` `; const createEntityMutation = gql` - mutation createEntity( - $parent_id: LingvodocID! - $field_id: LingvodocID! - $lexgraph_after: String - ) { - create_entity( - parent_id: $parent_id - field_id: $field_id - lexgraph_after: $lexgraph_after - ) { + mutation createEntity($parent_id: LingvodocID!, $field_id: LingvodocID!, $lexgraph_after: String) { + create_entity(parent_id: $parent_id, field_id: $field_id, lexgraph_after: $lexgraph_after) { entity { id parent_id @@ -224,7 +214,6 @@ const TableComponent = ({ /* eslint-enable react/prop-types */ actions }) => { - return (
@@ -243,7 +232,7 @@ const TableComponent = ({ selectDisabledIndeterminate={selectDisabledIndeterminate} disabled={disabledHeader} actions={actions} - mode={mode} + mode={mode} /> { const lexgraph_entity = get_lexgraph_entity(lexentry_id_source); - return lexgraph_entity ? lexgraph_entity.content || '' : ''; + return lexgraph_entity ? lexgraph_entity.content || "" : ""; }; const setSort = (field, order) => { setSortByField(field, order); - this.setState( - {dnd_enabled: false}, - () => console.log("dnd_enabled: ", this.state.dnd_enabled)); + this.setState({ dnd_enabled: false }, () => console.log("dnd_enabled: ", this.state.dnd_enabled)); }; const resetSort = () => { resetSortByField(); - this.setState( - {dnd_enabled: true}, - () => console.log("dnd_enabled: ", this.state.dnd_enabled)); + this.setState({ dnd_enabled: true }, () => console.log("dnd_enabled: ", this.state.dnd_enabled)); }; - const addEntry = (lexgraph_min) => { - + const addEntry = lexgraph_min => { /* Will need a valid ordering field and a valid minimal ordering marker. */ - if (!lexgraph_field_id) - { + if (!lexgraph_field_id) { window.logger.err(`Invalid ordering field id ${lexgraph_field_id}.`); return; } - if (!lexgraph_min && lexgraph_min !== "") - { + if (!lexgraph_min && lexgraph_min !== "") { window.logger.err(`Invalid minimal ordering marker ${lexgraph_min}.`); return; } @@ -452,19 +435,27 @@ class P extends React.Component { id, entitiesMode }, - update: (cache, { data: { create_lexicalentry: { lexicalentry }}}) => { - cache.updateQuery({ + update: ( + cache, + { + data: { + create_lexicalentry: { lexicalentry } + } + } + ) => { + cache.updateQuery( + { query: queryLexicalEntries, - variables: {id, entitiesMode} + variables: { id, entitiesMode } }, - (data) => ({ + data => ({ perspective: { ...data.perspective, lexical_entries: [lexicalentry, ...data.perspective.lexical_entries] } }) ); - }, + } }).then(({ data: d }) => { if (!d.loading && !d.error) { const { @@ -478,14 +469,24 @@ class P extends React.Component { field_id: lexgraph_field_id, lexgraph_after: lexgraph_min }, - update: (cache, { data: { create_entity: { entity }}}) => { - cache.updateQuery({ + update: ( + cache, + { + data: { + create_entity: { entity } + } + } + ) => { + cache.updateQuery( + { query: queryLexicalEntries, - variables: {id, entitiesMode} + variables: { id, entitiesMode } }, - (data) => { - const lexical_entries = data.perspective.lexical_entries.filter(le => !isEqual(le.id, lexicalentry.id)); - const lexicalentry_updated = {...lexicalentry, entities: [...lexicalentry.entities, entity]}; + data => { + const lexical_entries = data.perspective.lexical_entries.filter( + le => !isEqual(le.id, lexicalentry.id) + ); + const lexicalentry_updated = { ...lexicalentry, entities: [...lexicalentry.entities, entity] }; return { perspective: { ...data.perspective, @@ -494,7 +495,7 @@ class P extends React.Component { }; } ); - }, + } }); } }); @@ -526,21 +527,27 @@ class P extends React.Component { ids: selectedEntries }, update: (cache, { data }) => { - if (data.loading || data.error) {return;} - const { bulk_delete_lexicalentry: { deleted_entries }} = data; - cache.updateQuery({ + if (data.loading || data.error) { + return; + } + const { + bulk_delete_lexicalentry: { deleted_entries } + } = data; + cache.updateQuery( + { query: queryLexicalEntries, - variables: {id, entitiesMode} + variables: { id, entitiesMode } }, - (data) => ({ - perspective: - { ...data.perspective, - lexical_entries: data.perspective.lexical_entries.filter( - le => !deleted_entries.some(de => isEqual(le.id, de.id))) - } + data => ({ + perspective: { + ...data.perspective, + lexical_entries: data.perspective.lexical_entries.filter( + le => !deleted_entries.some(de => isEqual(le.id, de.id)) + ) + } }) ); - }, + } }).then(() => { resetSelection(); }); @@ -550,6 +557,12 @@ class P extends React.Component { openNewModal(ApproveModal, { perspectiveId: id, mode }); }; + /* new!!!! */ + const onJoinMarkups = () => { + openNewModal(JoinMarkupsModal, { perspectiveId: id }); + }; + /* /new!!!! */ + /* Basic case-insensitive, case-sensitive compare. */ const ci_cs_compare = (str_a, str_b) => { const result = str_a.toLowerCase().localeCompare(str_b.toLowerCase(), undefined, { numeric: true }); @@ -557,8 +570,9 @@ class P extends React.Component { }; const orderEntries = es => { - if (!lexgraph_field_id) - {return es;} + if (!lexgraph_field_id) { + return es; + } const sortedEntries = sortBy(es, e => { const entities = e.entities.filter(entity => isEqual(entity.field_id, lexgraph_field_id)); @@ -587,16 +601,18 @@ class P extends React.Component { // apply sorting es => { // init - let [ field, order ] = [null, "a"]; + let [field, order] = [null, "a"]; // sort by 'Order' column or no sorting required if (!sortByField) { - if (lexgraph_field_id) - {[ field, order ] = [ lexgraph_field_id, "a" ];} - else - {return es;} + if (lexgraph_field_id) { + [field, order] = [lexgraph_field_id, "a"]; + } else { + return es; + } + } else { + ({ field, order } = sortByField); } - else {({ field, order } = sortByField);} if (!field) { field = lexgraph_field_id ? lexgraph_field_id : [66, 10]; @@ -645,25 +661,25 @@ class P extends React.Component { const entries = processEntries(lexicalEntries.slice()); const lexgraph_min = () => { - if (!lexgraph_field_id) - {return null;} + if (!lexgraph_field_id) { + return null; + } - let min_res = ''; - for (let i=0; i { - /* Need a valid source lexical entry and at least one of preceeding/succeeding entries. */ - - if (!lexentry_id_source || (!lexentry_id_before && !lexentry_id_after)) - { + + if (!lexentry_id_source || (!lexentry_id_before && !lexentry_id_after)) { this.setState({ cards: [] }); @@ -673,8 +689,7 @@ class P extends React.Component { /* Will need a valid ordering field. */ - if (!lexgraph_field_id) - { + if (!lexgraph_field_id) { window.logger.err(`Invalid ordering field id ${lexgraph_field_id}.`); this.setState({ @@ -693,8 +708,7 @@ class P extends React.Component { const current_lexgraph_min = lexgraph_min(); - if (!current_lexgraph_min && current_lexgraph_min !== '') - { + if (!current_lexgraph_min && current_lexgraph_min !== "") { window.logger.err(`Invalid minimal ordering marker "${current_lexgraph_min}".`); this.setState({ @@ -704,27 +718,33 @@ class P extends React.Component { return; } - if (lexgraph_after < current_lexgraph_min) - lexgraph_after = current_lexgraph_min; + if (lexgraph_after < current_lexgraph_min) lexgraph_after = current_lexgraph_min; /* If for some reason the entry being moved does not have an ordering marker, we create one. */ - if (!entity) - { + if (!entity) { createEntity({ variables: { parent_id: lexentry_id_source, field_id: lexgraph_field_id, lexgraph_after }, - update: (cache, { data: { create_entity: { entity }}}) => { - cache.updateQuery({ + update: ( + cache, + { + data: { + create_entity: { entity } + } + } + ) => { + cache.updateQuery( + { query: queryLexicalEntries, - variables: {id, entitiesMode} + variables: { id, entitiesMode } }, - (data) => { + data => { const lexical_entries = data.perspective.lexical_entries.filter(le => !isEqual(le.id, lexicalentry.id)); - const lexicalentry_updated = {...lexicalentry, entities: [...lexicalentry.entities, entity]}; + const lexicalentry_updated = { ...lexicalentry, entities: [...lexicalentry.entities, entity] }; return { perspective: { ...data.perspective, @@ -733,7 +753,7 @@ class P extends React.Component { }; } ); - }, + } }); this.setState({ @@ -753,7 +773,7 @@ class P extends React.Component { }, refetchQueries: [ { - query: queryLexicalEntries, + query: queryLexicalEntries, variables: { id, entitiesMode @@ -762,12 +782,12 @@ class P extends React.Component { ], awaitRefetchQueries: true }).then( - (data) => { + data => { this.setState({ cards: [] }); }, - (error) => { + error => { this.setState({ cards: [] }); @@ -823,7 +843,7 @@ class P extends React.Component { const selectedRows = []; const selectedColumns = []; - const items = this.state.move && pageEntries || e; + const items = (this.state.move && pageEntries) || e; const checkedRow = this.state.checkedRow; const checkedColumn = this.state.checkedColumn; @@ -912,18 +932,16 @@ class P extends React.Component { /* /isTableLanguagesPublish */ const moveListItem = (dragIndex, hoverIndex, prevCards) => { - this.setState({ cards: update(prevCards, { $splice: [ [dragIndex, 1], - [hoverIndex, 0, prevCards[dragIndex]], - ], + [hoverIndex, 0, prevCards[dragIndex]] + ] }) }); - this.setState({move: true}); - + this.setState({ move: true }); }; function* allEntriesGenerator() { @@ -936,11 +954,22 @@ class P extends React.Component { style={{ overflowY: "auto" }} className={(mode === "edit" && "lingvo-scrolling-tab lingvo-scrolling-tab_edit") || "lingvo-scrolling-tab"} > - - {((mode === "edit") || (mode === "publish" && isAuthenticated) || (mode === "contributions" && isAuthenticated)) && ( + {(mode === "edit" || + (mode === "publish" && isAuthenticated) || + (mode === "contributions" && isAuthenticated)) && (
+ {/* new!!!!! */} + {mode === "edit" && ( +
- setSort(fieldId, order)} - onSortModeReset={() => resetSort()} - selectEntries={mode === "edit"} - entries={this.state.cards.length && this.state.cards || items} - checkEntries={isTableLanguagesPublish} - selectedRows={selectedRows} - selectedColumns={selectedColumns} - onCheckColumn={this.onCheckColumn} - onCheckAll={this.onCheckAll} - mode={mode} - dnd_enabled={this.state.dnd_enabled} - /> - -
- - } + {activeDndProvider && ( + + + setSort(fieldId, order)} + onSortModeReset={() => resetSort()} + selectEntries={mode === "edit"} + entries={(this.state.cards.length && this.state.cards) || items} + checkEntries={isTableLanguagesPublish} + selectedRows={selectedRows} + selectedColumns={selectedColumns} + onCheckColumn={this.onCheckColumn} + onCheckAll={this.onCheckAll} + mode={mode} + dnd_enabled={this.state.dnd_enabled} + /> + +
+
+ )}
- - {!!_ROWS_PER_PAGE && - { - const scrollContainer = document.querySelector(".lingvo-scrolling-tab__table"); - smoothScroll(0, 0, null, scrollContainer); - if (isTableLanguagesPublish) { - this.resetCheckedColumn(); - this.resetCheckedAll(); - } - }} - className="lingvo-pagination-block_perspective" - />} + {!!_ROWS_PER_PAGE && ( + { + const scrollContainer = document.querySelector(".lingvo-scrolling-tab__table"); + smoothScroll(0, 0, null, scrollContainer); + if (isTableLanguagesPublish) { + this.resetCheckedColumn(); + this.resetCheckedAll(); + } + }} + className="lingvo-pagination-block_perspective" + /> + )} ); } @@ -1076,10 +1105,11 @@ P.propTypes = { createLexicalEntry: PropTypes.func.isRequired, mergeLexicalEntries: PropTypes.func.isRequired, removeLexicalEntries: PropTypes.func.isRequired, - updateLexgraph: PropTypes.func.isRequired, + updateLexgraph: PropTypes.func.isRequired, selectLexicalEntry: PropTypes.func.isRequired, resetEntriesSelection: PropTypes.func.isRequired, openModal: PropTypes.func.isRequired, + closeModal: PropTypes.func.isRequired, createdEntries: PropTypes.array.isRequired, selectedEntries: PropTypes.array.isRequired, user: PropTypes.object.isRequired, @@ -1108,12 +1138,13 @@ const PerspectiveView = compose( resetSortByField: resetOrderedSortByField, selectLexicalEntry, resetEntriesSelection, - openModal + openModal, + closeModal }, dispatch ) ), - graphql(createEntityMutation, {name: "createEntity"}), + graphql(createEntityMutation, { name: "createEntity" }), graphql(createLexicalEntryMutation, { name: "createLexicalEntry" }), graphql(mergeLexicalEntriesMutation, { name: "mergeLexicalEntries" }), graphql(removeLexicalEntriesMutation, { name: "removeLexicalEntries" }), @@ -1362,7 +1393,17 @@ export const LexicalEntryByIds = compose( }) )(LexicalEntryViewBaseByIds); -const PerspectiveViewWrapper = ({ id, className, mode, entitiesMode, page, data, filter, sortByField, activeDndProvider }) => { +const PerspectiveViewWrapper = ({ + id, + className, + mode, + entitiesMode, + page, + data, + filter, + sortByField, + activeDndProvider +}) => { if (data.error) { return null; } @@ -1415,7 +1456,7 @@ PerspectiveViewWrapper.propTypes = { filter: PropTypes.string, data: PropTypes.object.isRequired, sortByField: PropTypes.object, - activeDndProvider: PropTypes.bool, + activeDndProvider: PropTypes.bool }; PerspectiveViewWrapper.defaultProps = { diff --git a/src/components/JoinMarkupsModal/index.js b/src/components/JoinMarkupsModal/index.js new file mode 100644 index 00000000..a5172e0f --- /dev/null +++ b/src/components/JoinMarkupsModal/index.js @@ -0,0 +1,468 @@ +import React, { useCallback, useContext, useState } from "react"; +import { Button, Checkbox, Select, Modal, Table, Message, Icon, Confirm } from "semantic-ui-react"; +import { isEqual } from "lodash"; +import PropTypes from "prop-types"; +import { useMutation } from "hooks"; +import { gql, useQuery, useApolloClient } from "@apollo/client"; +import { lexicalEntryQuery } from "components/LexicalEntryCorp"; + +import TranslationContext from "Layout/TranslationContext"; + +import "./styles.scss"; + +// Using this query we get data for single markups and for existent groups +// We have to control broken groups and clean markups of them +const getMarkupTreeQuery = gql` + query getMarkupTree($perspectiveId: LingvodocID!, $groupType: String, $author: Int) { + markups(perspective_id: $perspectiveId) { + id + text + offset + field_translation + field_position + markup_groups(group_type: $groupType, author: $author) { + client_id + object_id + type + author_id + author_name + created_at + } + } + } +`; + +// Entities' additional metadata should be updated as well +// 'markups' has the following format: [[ entity_client_id, entity_object_id, markup_start_offset ], ... ] +const createMarkupGroupMutation = gql` + mutation createMarkupGroup($groupType: String!, $markups: [[Int]], $perspectiveId: LingvodocID!) { + create_markup_group(group_type: $groupType, markups: $markups, perspective_id: $perspectiveId) { + entry_ids + triumph + } + } +`; + +// 'markups' has the following format: [[ ], ... ] +export const deleteMarkupGroupMutation = gql` + mutation deleteMarkupGroup($groupIds: [[Int]]!, $markups: [[Int]], $perspectiveId: LingvodocID) { + delete_markup_group(group_ids: $groupIds, markups: $markups, perspective_id: $perspectiveId) { + entry_ids + triumph + } + } +`; + +const saveMarkupGroupsMutation = gql` + mutation saveMarkupGroups($perspectiveId: LingvodocID!, $fieldList: [ObjectVal]!, $groupList: [ObjectVal]!) { + save_markup_groups(perspective_id: $perspectiveId, field_list: $fieldList, group_list: $groupList) { + xlsx_url + message + triumph + } + } +`; + + +export const refetchLexicalEntries = (entry_ids, client) => + entry_ids.forEach(le_id => + client.query({ + query: lexicalEntryQuery, + variables: { id: le_id, entitiesMode: "all" }, + notifyOnNetworkStatusChange: true, + fetchPolicy: "network-only" + }) + ); + +const JoinMarkupsModal = ({ perspectiveId, onClose }) => { + const getTranslation = useContext(TranslationContext); + + const [firstTextRelation, setFirstTextRelation] = useState(null); + const [secondTextRelation, setSecondTextRelation] = useState(null); + const [typeRelation, setTypeRelation] = useState(null); + const [selectedRelations, setSelectedRelations] = useState([]); + + const [markupDict, setMarkupDict] = useState({}); + const [groupDict, setGroupDict] = useState({}); + const [groupTotal, setGroupTotal] = useState(0); + const [selectedTotal, setSelectedTotal] = useState(0); + + const joinActive = firstTextRelation && secondTextRelation && typeRelation; + const deleteActive = !!selectedTotal; + + const [warnMessage, setWarnMessage] = useState(null); + const [errorMessage, setErrorMessage] = useState(null); + const [successMessage, setSuccessMessage] = useState(null); + const [confirmation, setConfirmation] = useState(null); + + const client = useApolloClient(); + + const [createMarkupGroup] = useMutation(createMarkupGroupMutation, { + onCompleted: data => refetchLexicalEntries(data.create_markup_group.entry_ids, client) + }); + + const [deleteMarkupGroup] = useMutation(deleteMarkupGroupMutation, { + onCompleted: data => refetchLexicalEntries(data.delete_markup_group.entry_ids, client) + }); + + const [saveMarkupGroups] = useMutation(saveMarkupGroupsMutation, { + onCompleted: ({save_markup_groups: result}) => { + if (result.triumph) { + setSuccessMessage( + `${getTranslation("Markup groups were saved into xlsx file.")} + ${getTranslation("Follow")} ${getTranslation("result url")}` + ); + } else { + setWarnMessage(getTranslation(result.message)); + } + } + }); + + const resetMessages = () => { + setWarnMessage(null); + setErrorMessage(null); + setSuccessMessage(null); + }; + + const setRelationDict = markups => { + const markupDict = {}; + const groupDict = {}; + let total = 0; + + for (const markup of markups) { + const { field_position: f_pos, field_translation: f_name, markup_groups: groups, ...markup_data } = markup; + + const f_id = `${f_pos}_${f_name}`; + + if (!(f_id in markupDict)) { + markupDict[f_id] = []; + } + markupDict[f_id].push(markup_data); + + for (const group of groups) { + const { client_id, object_id, ...group_data } = group; + + const g_id = `${client_id}_${object_id}`; + + if (!(g_id in groupDict)) { + groupDict[g_id] = { ...group_data, markups: [] }; + } + groupDict[g_id].markups.push(markup_data); + + if (groupDict[g_id].markups.length === 2) { + total++; + } + } + } + + if (Object.keys(markupDict).length < 2) { + onClose(); + window.logger.warn(getTranslation("Please set markups in both fields of the table")); + } + + setMarkupDict(markupDict); + setGroupDict(groupDict); + setGroupTotal(total); + }; + + const { data, error, loading, refetch } = useQuery(getMarkupTreeQuery, { + variables: { perspectiveId }, + fetchPolicy: "network-only", + onCompleted: data => setRelationDict(data.markups) + }); + + const onAddRelation = useCallback(() => { + resetMessages(); + + if (!firstTextRelation || !secondTextRelation || !typeRelation) { + throw new Error("No either two markups or relation type is selected."); + } + + for (const group of Object.values(groupDict)) { + const ids = group.markups.map(markup => markup.id); + if (ids.includes(firstTextRelation) && ids.includes(secondTextRelation) && group.type === typeRelation) { + window.logger.warn(getTranslation("Such group already exists.")); + return; + } + } + + createMarkupGroup({ + variables: { + groupType: typeRelation, + markups: [firstTextRelation.split("_"), secondTextRelation.split("_")], + perspectiveId + } + }).then(refetch); + + setFirstTextRelation(null); + setSecondTextRelation(null); + setTypeRelation(null); + + window.logger.suc(getTranslation("The group was successfully added.")); + }, [firstTextRelation, secondTextRelation, typeRelation, groupDict]); + + const onDeleteRelation = useCallback(() => { + resetMessages(); + + const groupIds = selectedRelations.map(id => id.split("_")); + + const markups = []; + selectedRelations.forEach(id => { + const group_markups = groupDict[id].markups.map(m => m.id.split("_")); + markups.push(...group_markups); + }); + + deleteMarkupGroup({ + variables: { groupIds, markups } + }).then(refetch); + + setSelectedRelations([]); + setSelectedTotal(0); + + window.logger.suc(getTranslation("The group was successfully deleted.")); + }, [groupDict, selectedRelations]); + + const onRelationSelect = (relation_id, checked) => { + const selectedIds = selectedRelations; + + const position = selectedIds.indexOf(relation_id); + + if (position === -1 && checked) { + selectedIds.push(relation_id); + } else { + selectedIds.splice(position, 1); + } + + const selectedTotal = selectedIds.length; + setSelectedRelations(selectedIds); + setSelectedTotal(selectedTotal); + }; + + const onSaveXlsx = useCallback(() => { + const groupList = []; + const fieldList = + Object.keys(markupDict).map(id => id.split("_")[1]).concat([getTranslation('Type'), getTranslation('Author')]); + + for (const group of Object.values(groupDict)) { + groupList.push({ + text: group.markups.map(m => m.text), + type: getTranslation(group.type), + author: group.author_name, + }); + } + + saveMarkupGroups({variables: {perspectiveId, fieldList, groupList}}); + }, [markupDict, groupDict]); + + if (Object.keys(markupDict) < 2) { + return; + } + + const firstField = Object.keys(markupDict)[0]; + const secondField = Object.keys(markupDict)[1]; + + const firstText = markupDict[firstField].map(m => (m.id === firstTextRelation ? m.text : "")); + const secondText = markupDict[secondField].map(m => (m.id === secondTextRelation ? m.text : "")); + + const group_type_list = [ + "Transliteration", + "Transcription", + "Calque", + "Transposition", + "Descriptive translation", + "Similatory translation", + "Neologism", + "Semi-calque", + "Lexico-grammatical replacement", + "Antonymous", + "Compensation" + ].sort().map((t, k) => ({ + key: k, + value: t, + text: getTranslation(t) + })); + + return ( + + {getTranslation("Join markups")} + + {error || loading ? ( + + {`${getTranslation("Loading markups and groups data")}...`} + + ) : ( +
+
+ {/* Table Markups */} +
+
+ + + + +
+ {firstField.split("_")[1]}: {firstText} +
+
+
+
+ + {markupDict[firstField].map(markup => { + return ( + + { + setFirstTextRelation(markup.id); + resetMessages(); + }} + className={(markup.id === firstTextRelation && "selected-text-relation") || ""} + > + {markup.text} + + + ); + })} + +
+
+ +
+ + + + +
+ {secondField.split("_")[1]}: {secondText} +
+
+
+
+ + {markupDict[secondField].map(markup => { + return ( + + { + setSecondTextRelation(markup.id); + resetMessages(); + }} + className={(markup.id === secondTextRelation && "selected-text-relation") || ""} + > + {markup.text} + + + ); + })} + +
+
+
+