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" && (
+
}
+ content={this.context("Join markups")}
+ onClick={onJoinMarkups}
+ className="lingvo-button-green lingvo-perspective-button"
+ />
+ )}
+ {/* /new!!!!! */}
{mode === "edit" && (
-
}
content={this.context("Add lexical entry")}
onClick={() => addEntry(lexgraph_min())}
@@ -987,71 +1016,71 @@ class P extends React.Component {
)}
- {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}
- />
-
-
-
- }
+ {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}
+
+
+ );
+ })}
+
+
+
+
+
+
+ {/* /Table Markups */}
+
+
+ {warnMessage && (
+
+ {getTranslation("Warning")}
+ {warnMessage}
+
+ )}
+ {successMessage && (
+
+ {getTranslation("Success")}
+
+
+ )}
+
+
+ {/* Table Relations */}
+
+
+
+
+ {firstField.split("_")[1]}
+ {secondField.split("_")[1]}
+ {getTranslation("Type")}
+ {getTranslation("Author")}
+
+
+
+ {Object.keys(groupDict).map(
+ group_id =>
+ groupDict[group_id].markups.length > 1 && (
+
+
+ isEqual(e, relation.id))}
+ onChange={(e, { checked }) => onRelationSelect(group_id, checked)}
+ />
+
+ {groupDict[group_id].markups[0].text}
+ {groupDict[group_id].markups[1].text}
+ {getTranslation(groupDict[group_id].type)}
+ {groupDict[group_id].author_name}
+
+ )
+ )}
+
+
+ {/* /Table Relations */}
+
+
+ )}
+
+
+
+
+
+
+ setConfirmation(null)}
+ className="lingvo-confirm"
+ />
+
+ );
+};
+
+JoinMarkupsModal.propTypes = {
+ perspectiveId: PropTypes.arrayOf(PropTypes.number).isRequired,
+ onClose: PropTypes.func.isRequired
+};
+
+export default JoinMarkupsModal;
diff --git a/src/components/JoinMarkupsModal/styles.scss b/src/components/JoinMarkupsModal/styles.scss
new file mode 100644
index 00000000..79f8bc2d
--- /dev/null
+++ b/src/components/JoinMarkupsModal/styles.scss
@@ -0,0 +1,110 @@
+.join-markups-content {
+ display: flex;
+ flex-direction: column;
+ height: 65vh;
+ min-height: 450px;
+
+ &__markups {
+ height: 50%;
+ padding-bottom: 20px;
+ }
+
+ &__relations {
+ height: 50%;
+ overflow-y: auto;
+
+ & .ui.table thead th {
+ position: relative;
+ position: sticky;
+ top: 0;
+ z-index: 1;
+
+ @media only screen and (max-device-width: 767px), only screen and (max-width: 767px) {
+ position: static;
+ }
+
+ &.th-checkbox {
+ width: 50px;
+ }
+
+ &.th-markup {
+ width: 30%;
+ }
+ }
+ }
+}
+
+.block-add-relation {
+ display: flex;
+ height: 100%;
+
+ &__column {
+ flex: 1 1 33%;
+ //width: 33%;
+ overflow-y: auto;
+ margin: 0 0 0 10px;
+
+ & .ui.table thead th {
+ position: relative;
+ position: sticky;
+ top: 0;
+ z-index: 1;
+
+ & .selected-markup {
+ display: -webkit-box;
+ -webkit-box-orient: vertical;
+ -webkit-line-clamp: 1;
+ overflow: hidden;
+ overflow-wrap: break-word;
+ }
+
+ & .selected-markup__text {
+ color: #4e46b4;
+ }
+ }
+
+ & .ui.table tr td {
+ border-radius: 8px !important;
+ }
+
+ & .ui.table .selected-text-relation {
+ background: #4e46b4 !important;
+ color: #fff !important;
+ }
+
+ &:first-of-type {
+ margin: 0 10px 0 0;
+ }
+
+ @media only screen and (max-device-width: 767px), only screen and (max-width: 767px) {
+ margin-left: 5px;
+
+ & .ui.table thead th {
+ position: static;
+ }
+
+ &:first-of-type {
+ margin-right: 5px;
+ margin-left: 0;
+ }
+ }
+ }
+
+ &__actions {
+ flex: 0 1 auto;
+ min-width: 197px;
+ display: flex;
+ flex-direction: column;
+ padding: 10px;
+ padding-right: 0;
+
+ & .ui.button {
+ margin-top: 10px !important;
+ }
+
+ @media only screen and (max-device-width: 767px), only screen and (max-width: 767px) {
+ min-width: 130px;
+ max-width: 130px;
+ }
+ }
+}
diff --git a/src/components/LexicalEntryCorp/Text.js b/src/components/LexicalEntryCorp/Text.js
index b61bed6c..e659fa34 100644
--- a/src/components/LexicalEntryCorp/Text.js
+++ b/src/components/LexicalEntryCorp/Text.js
@@ -1,11 +1,15 @@
-import React, { useCallback, useState } from "react";
+import React, { useCallback, useState, useContext, useEffect } from "react";
+import TranslationContext from "Layout/TranslationContext";
import { useDrag } from "react-dnd";
-import { RegExpMarker } from "react-mark.js";
+import { RangesMarker } from "react-mark.js";
import TextareaAutosize from "react-textarea-autosize";
-import { Button, Checkbox } from "semantic-ui-react";
+import { Button, Checkbox, Confirm } from "semantic-ui-react";
import { find, isEqual } from "lodash";
import PropTypes from "prop-types";
import { onlyUpdateForKeys } from "recompose";
+import { deleteMarkupGroupMutation, refetchLexicalEntries } from "components/JoinMarkupsModal";
+import { useMutation } from "hooks";
+import { gql, useApolloClient } from "@apollo/client";
import Entities from "./index";
@@ -32,8 +36,7 @@ const TextEntityContent = ({
update,
id
}) => {
-
- const is_order_column = (number && column.english_translation === "Order");
+ const is_order_column = number && column.english_translation === "Order";
const [edit, setEdit] = useState(false);
const [content, setContent] = useState(entity.content);
@@ -42,6 +45,194 @@ const TextEntityContent = ({
const [dropped, setDropped] = useState(null);
+ const [marking, setMarking] = useState({});
+ const [browserSelection, setBrowserSelection] = useState(null);
+ const [confirmation, setConfirmation] = useState(null);
+
+ const client = useApolloClient();
+ const [deleteMarkupGroup] = useMutation(deleteMarkupGroupMutation, {
+ onCompleted: data => {
+ refetchLexicalEntries(data.delete_markup_group.entry_ids, client);
+ window.logger.suc(getTranslation("Markup(s) with related groups were removed"));
+ }
+ });
+
+ const getTranslation = useContext(TranslationContext);
+
+ const text = is_number ? number : entity.content;
+ const markups = entity.additional_metadata?.markups || [];
+
+ const getCurrentArea = useCallback(() => document.getElementById(id), [id]);
+
+ // Pre-checking if current selection can be handled
+ // as markup and choosing base text container
+ const getCurrentSelection = (checkSelectedText = true) => {
+ if (!document.getSelection().rangeCount) {
+ return null;
+ }
+
+ const range = document.getSelection().getRangeAt(0);
+ const selectedText = range.toString();
+
+ if (checkSelectedText && !selectedText.length) {
+ return null;
+ }
+
+ let startContainer = range.startContainer;
+
+ // Going up to target element if we are inside e.g. tag
+ while (startContainer.parentElement.tagName !== "DIV") {
+ startContainer = startContainer.parentElement;
+ if (!startContainer || !startContainer.parentElement) {
+ return null;
+ }
+ }
+
+ // if not "edit" mode. We can not simply check for mode value because of useEffect and EventListener specific
+ if (startContainer.parentElement.parentElement.parentElement?.classList[0] !== "lingvo-input-buttons-group__name") {
+ return null;
+ }
+
+ if (getCurrentArea().contains(startContainer)) {
+ return {
+ range,
+ selectedText,
+ startContainer
+ };
+ }
+ return null;
+ };
+
+ // Calculating summary selection length
+ const onBrowserSelection = () => {
+ const currentSelection = getCurrentSelection();
+
+ if (!currentSelection) {
+ return;
+ }
+
+ const { range, selectedText, startContainer } = currentSelection;
+
+ let startOffset = range.startOffset;
+ let node = startContainer.previousSibling;
+
+ // Calculate real start offset through the all previous siblings
+ while (!!node) {
+ startOffset += node.textContent.length;
+ node = node.previousSibling;
+ }
+
+ const endOffset = startOffset + selectedText.length;
+
+ console.log(id + " : " + startOffset + " : " + endOffset + " : " + selectedText);
+
+ setBrowserSelection({
+ startOffset,
+ endOffset,
+ selectedText
+ });
+ };
+
+ // Reset browser selection on some events
+ const resetMarkupAction = event => {
+ if (!!getCurrentSelection(event.type !== "mousedown")) {
+ setBrowserSelection(null);
+ console.log("Reset markup action : " + id + " : " + Date.now());
+ }
+ };
+
+ // Setting current markup action: create_markup/delete_markup/delete_with_group
+ // according to browser selection
+ useEffect(() => {
+ if (!browserSelection) {
+ setMarking({
+ action: null,
+ result: markups,
+ groupsToDelete: null
+ });
+ return;
+ }
+
+ const startSelection = browserSelection.startOffset;
+ const endSelection = browserSelection.endOffset;
+ const selectedText = browserSelection.selectedText;
+
+ var selected_action = null;
+ const selected_markups = [[]];
+ const selected_groups = [];
+
+ // 'markups' variable has the following format:
+ // [[[start_offset, end_offset], [group1_cid, group1_oid], ..., [groupN_cid, groupN_oid]]]
+ for (const markup of markups) {
+ const [indexes, ...groups] = markup;
+ if (!indexes || indexes.length !== 2) {
+ continue;
+ }
+ const [startMarkup, endMarkup] = indexes;
+
+ if (
+ (startMarkup <= startSelection && startSelection < endMarkup) ||
+ (startMarkup < endSelection && endSelection <= endMarkup) ||
+ (startSelection < startMarkup && endMarkup < endSelection)
+ ) {
+ selected_action = "delete_markup";
+
+ if (groups.length > 0) {
+ selected_groups.push(...groups);
+ }
+ } else {
+ selected_markups.push(markup);
+ }
+ }
+
+ if (selected_groups.length) {
+ selected_action = "delete_with_group";
+ }
+
+ if (
+ !selected_action &&
+ selectedText === selectedText.trim() &&
+ (startSelection === 0 || /\W/.test(text[startSelection - 1])) &&
+ (endSelection === text.length || /\W/.test(text[endSelection]))
+ ) {
+ selected_action = "create_markup";
+ selected_markups.push([[startSelection, endSelection]]);
+ }
+
+ console.log(selected_action + "; groups_to_delete: " + selected_groups);
+
+ setMarking({
+ action: selected_action,
+ result: selected_markups,
+ groupsToDelete: selected_groups
+ });
+ }, [browserSelection]);
+
+ // Perform markup action
+ const onMarkupAction = () => {
+ const { result, action, groupsToDelete } = marking;
+
+ if (action === "delete_with_group") {
+ setConfirmation({
+ content: getTranslation("Delete selected markups and related groups? Are you sure?"),
+ func: () => {
+ setConfirmation(null);
+ deleteMarkupGroup({ variables: { groupIds: groupsToDelete, perspectiveId: entry.parent_id } });
+ update(entity, undefined, result);
+ }
+ });
+ } else {
+ update(entity, undefined, result);
+
+ if (action === "create_markup") {
+ window.logger.suc(getTranslation("Markup was created"));
+ } else if (action === "delete_markup") {
+ window.logger.suc(getTranslation("Markup was deleted"));
+ }
+ }
+ setBrowserSelection(null);
+ };
+
const onEdit = useCallback(() => {
if (!edit) {
setEdit(true);
@@ -51,21 +242,26 @@ const TextEntityContent = ({
}
}, [edit, content]);
- const onKeyDown = useCallback((event) => {
-
- breakdown(event, parentEntity, entity);
+ const onKeyDown = useCallback(
+ event => {
+ breakdown(event, parentEntity, entity);
- if (event.code === "Enter" && !event.ctrlKey) {
- onEdit();
- }
+ if (event.code === "Enter" && !event.ctrlKey) {
+ onEdit();
+ }
- }, [edit, content]);
+ if (event.keyCode === 27) {
+ setEdit(false);
+ }
+ },
+ [edit, content]
+ );
// useDrag - the list item is draggable
- const [{ isDragging}, dragRef, preview] = useDrag({
- type: 'entity',
- item: { id, content },
- collect: (monitor) => ({
+ const [{ isDragging }, dragRef, preview] = useDrag({
+ type: "entity",
+ item: { id, content, metadata: entity.additional_metadata },
+ collect: monitor => ({
isDragging: monitor.isDragging()
}),
end: (item, monitor) => {
@@ -76,7 +272,13 @@ const TextEntityContent = ({
}
});
- const text = is_number ? number : entity.content;
+ useEffect(() => {
+ const element = getCurrentArea();
+ element?.addEventListener("mouseenter", onBrowserSelection);
+ element?.addEventListener("mouseleave", resetMarkupAction);
+ element?.children[0]?.addEventListener("mouseup", onBrowserSelection);
+ element?.children[0]?.addEventListener("mousedown", resetMarkupAction);
+ }, [preview]);
if (checkEntries) {
if (checkedAll) {
@@ -125,53 +327,143 @@ const TextEntityContent = ({
const ln = /\(\d+\)/;
const snt = /\u2260/;
const missed = /[/]missed text[/]/;
- const metatext = new RegExp(
- `${pg_ln.source }|${
- pg.source }|${
- ln.source }|${
- snt.source }|${
- missed.source}`
- );
+ const metatext = new RegExp([pg_ln, pg, ln, snt, missed].map(regex => regex.source).join("|"), "g");
+
+ const highlights = [];
+ const highlightsMarkup = [];
+
+ for (const [indexes, ..._] of markups) {
+ if (!indexes || indexes.length !== 2) {
+ continue;
+ }
+ const [startMarkup, endMarkup] = indexes;
+
+ highlightsMarkup.push({
+ start: startMarkup,
+ length: endMarkup - startMarkup
+ });
+ }
+
+ let segment;
+
+ while ((segment = metatext.exec(text)) !== null) {
+ highlights.push({
+ start: segment.index,
+ length: segment[0].length
+ });
+ }
switch (mode) {
case "edit":
return !dropped ? (
-
+
{!(is_being_updated || edit) && (
-
{text}
+
+
+
+ {text}
+
+
+
)}
{(is_being_updated || edit) && (
-
setContent(event.target.value)}
+ onChange={event => setContent(event.target.value)}
onKeyDown={onKeyDown}
- className="lingvo-input-action lingvo-input-action_textarea"
+ className="lingvo-input-action lingvo-input-action_textarea"
/>
)}
- { read_only || (
+ {read_only || (
} />
- : edit ? : }
+ {/* Markups */}
+ {marking.action === "create_markup" && (
+
+ )}
+ {marking.action === "delete_markup" && (
+
+ )}
+ {marking.action === "delete_with_group" && (
+
+ )}
+ {/* /Markups */}
+
+ ) : edit ? (
+
+ ) : (
+
+ )
+ }
onClick={onEdit}
disabled={is_being_updated || !text}
className={is_being_updated ? "lingvo-button-spinner" : ""}
/>
{is_being_removed ? (
- } disabled className="lingvo-button-spinner" />
+ }
+ disabled
+ className="lingvo-button-spinner"
+ />
) : (
} onClick={() => remove(entity)} />
)}
)}
+ setConfirmation(null)}
+ className="lingvo-confirm"
+ />
) : null;
case "publish":
return (
{column.english_translation &&
- column.english_translation === "Number of the languages" &&
+ column.english_translation === "Number of the languages" &&
entity.id &&
entity.parent_id ? (
@@ -183,10 +475,26 @@ const TextEntityContent = ({
) : (
-
{text}
+
+
+
+ {text}
+
+
+
)}
{
publish(entity, checked);
@@ -209,24 +517,52 @@ const TextEntityContent = ({
case "view":
return (
- {text}
+
+
+
+ {text}
+
+
+
);
case "contributions":
return entity.accepted ? (
- {text}
+
+
+
+ {text}
+
+
+
) : (
- }
- onClick={() => accept(entity, true)}
- />
+ } onClick={() => accept(entity, true)} />
);
default:
return null;
}
-
};
const Text = onlyUpdateForKeys([
@@ -239,7 +575,7 @@ const Text = onlyUpdateForKeys([
"checkedColumn",
"checkedAll",
"number",
- "id",
+ "id"
])(props => {
const {
perspectiveId,
@@ -268,7 +604,7 @@ const Text = onlyUpdateForKeys([
is_being_removed,
is_being_updated,
number,
- id,
+ id
} = props;
const subColumn = find(columns, c => isEqual(c.self_id, column.column_id));
@@ -276,7 +612,7 @@ const Text = onlyUpdateForKeys([
return (
{subColumn && (
{
-
+const Edit = ({ onSave, onCancel, is_being_created, parentEntity, breakdown }) => {
const [content, setContent] = useState("");
- const onChange = useCallback((event) => {
-
- setContent(event.target.value);
-
- }, [content]);
+ const onChange = useCallback(
+ event => {
+ setContent(event.target.value);
+ },
+ [content]
+ );
- const onKeyDown = useCallback((event) => {
+ const onKeyDown = useCallback(
+ event => {
+ breakdown(event, parentEntity);
- breakdown(event, parentEntity);
+ if (event.code === "Enter" && !event.ctrlKey) {
+ if (content) {
+ onSave(content);
+ }
+ }
- if (event.code === "Enter" && !event.ctrlKey) {
+ if (event.keyCode === 27) {
+ onCancel();
+ }
+ },
+ [content]
+ );
+ const onHandlerSave = useCallback(
+ event => {
if (content) {
onSave(content);
}
- }
-
- if (event.keyCode === 27) {
- onCancel();
- }
- }, [content]);
-
- const onHandlerSave = useCallback((event) => {
-
- if (content) {
- onSave(content);
- }
+ },
+ [content]
+ );
- }, [content]);
-
return (
-
- : }
+
+ ) : (
+
+ )
+ }
onClick={onHandlerSave}
disabled={is_being_created || !content}
className={is_being_created ? "lingvo-button-spinner" : ""}
@@ -407,7 +745,6 @@ const Edit = ({
);
-
};
Edit.propTypes = {
diff --git a/src/components/LexicalEntryCorp/index.js b/src/components/LexicalEntryCorp/index.js
index 1443560c..bd829cea 100644
--- a/src/components/LexicalEntryCorp/index.js
+++ b/src/components/LexicalEntryCorp/index.js
@@ -1,7 +1,8 @@
import React, {useCallback, useContext, useState} from "react";
import { useDrop } from "react-dnd";
-import { Button } from "semantic-ui-react";
+import { Button, Confirm } from "semantic-ui-react";
import { gql } from "@apollo/client";
+import { useMutation } from "hooks";
import { graphql, withApollo } from "@apollo/client/react/hoc";
import { flow, isEqual } from "lodash";
import PropTypes from "prop-types";
@@ -9,7 +10,9 @@ import { compose } from "recompose";
import { queryCounter } from "backend";
import { queryLexicalEntries } from "components/CorporaView";
+import { deleteMarkupGroupMutation, refetchLexicalEntries } from "components/JoinMarkupsModal";
import TranslationContext from "Layout/TranslationContext";
+import { patienceDiff } from "utils/patienceDiff";
import { compositeIdToString, compositeIdToString as id2str } from "utils/compositeId";
import GroupingTag from "./GroupingTag";
@@ -28,6 +31,7 @@ const createEntityMutation = gql`
$self_id: LingvodocID
$content: String
$file_content: Upload
+ $metadata: ObjectVal
) {
create_entity(
parent_id: $parent_id
@@ -35,6 +39,7 @@ const createEntityMutation = gql`
self_id: $self_id
content: $content
file_content: $file_content
+ additional_metadata: $metadata
) {
triumph
}
@@ -66,14 +71,14 @@ const removeEntityMutation = gql`
`;
const updateEntityMutation = gql`
- mutation updateEntity($id: LingvodocID!, $content: String!) {
- update_entity_content(id: $id, content: $content) {
+ mutation updateEntity($id: LingvodocID!, $content: String!, $markups: [ObjectVal]) {
+ update_entity_content(id: $id, content: $content, markups: $markups) {
triumph
}
}
`;
-const lexicalEntryQuery = gql`
+export const lexicalEntryQuery = gql`
query LexicalEntryQuery($id: LingvodocID!, $entitiesMode: String!) {
lexicalentry(id: $id) {
id
@@ -92,7 +97,8 @@ const lexicalEntryQuery = gql`
published
accepted
additional_metadata {
- link_perspective_id
+ link_perspective_id,
+ markups
}
is_subject_for_parsing
}
@@ -172,13 +178,21 @@ const Entities = ({
const [is_being_created, setIsBeingCreated] = useState(false);
const [remove_set, setRemoveSet] = useState({});
const [update_set, setUpdateSet] = useState({});
+ const [confirmation, setConfirmation] = useState(null);
+
+ const [deleteMarkupGroup] = useMutation(deleteMarkupGroupMutation, {
+ onCompleted: data => {
+ refetchLexicalEntries(data.delete_markup_group.entry_ids, client);
+ window.logger.warn(getTranslation("Markup(s) with related groups were removed"));
+ }
+ });
const getTranslation = useContext(TranslationContext);
const [{ isOver }, dropRef] = useDrop({
accept: 'entity',
drop: (item) => {
- create(item.content, parentEntity == null ? null : parentEntity.id);
+ create(item.content, parentEntity == null ? null : parentEntity.id, item.metadata);
},
collect: (monitor) => ({
isOver: monitor.isOver()
@@ -258,11 +272,11 @@ const Entities = ({
}, [edit]);
- const create = useCallback((content, self_id) => {
+ const create = useCallback((content, self_id, metadata=null) => {
setIsBeingCreated(true);
- const variables = { parent_id: entry.id, field_id: column.id };
+ const variables = { parent_id: entry.id, field_id: column.id, metadata };
if (content instanceof File) {
variables.content = null;
variables.file_content = content;
@@ -362,6 +376,24 @@ const Entities = ({
remove_set2[entity_id_str] = null;
setRemoveSet(remove_set2);
+ const groupsToDelete = [];
+
+ for (const markup of (entity.additional_metadata?.markups ?? [])) {
+ if (!markup.length) {
+ continue;
+ }
+
+ const [_, ...groupIds] = markup;
+
+ if (groupIds.length > 0) {
+ groupsToDelete.push(...groupIds);
+ }
+ }
+
+ if (groupsToDelete.length > 0) {
+ deleteMarkupGroup({ variables: { groupIds: groupsToDelete, perspectiveId }});
+ }
+
removeEntity({
variables: { id: entity.id },
refetchQueries: [
@@ -391,7 +423,7 @@ const Entities = ({
});
}, [remove_set]);
- const update = useCallback((entity, content) => {
+ const update = useCallback((entity, content = entity.content, ready_markups = null) => {
const entity_id_str = id2str(entity.id);
@@ -399,8 +431,57 @@ const Entities = ({
update_set2[entity_id_str] = null;
setUpdateSet(update_set2);
+ const markups = ready_markups || [[]];
+
+ if (!ready_markups &&
+ content !== entity.content &&
+ entity.additional_metadata?.markups &&
+ entity.additional_metadata.markups.length > 1) {
+
+ const diff = patienceDiff(entity.content, content).lines;
+
+ for (const markup of entity.additional_metadata.markups) {
+ if (!markup.length || markup[0].length !== 2) {
+ continue;
+ }
+
+ let [[startOffset, endOffset], ...groupIds] = markup;
+ const startIndex = diff.map(ch => ch.aIndex).indexOf(startOffset);
+ const endIndex = diff.map(ch => ch.aIndex).indexOf(endOffset);
+ const markupDiff = diff.slice(0, endIndex);
+
+ for (const [i, {aIndex, bIndex}] of markupDiff.entries()) {
+ if (aIndex === -1) {
+ if (i < startIndex) {
+ startOffset++;
+ }
+ endOffset++;
+ }
+ if (bIndex === -1) {
+ if (i < startIndex) {
+ startOffset--;
+ }
+ endOffset--;
+ }
+ }
+
+ // Markup must be not empty and have at least one letter char
+ if (startOffset < endOffset &&
+ /\w/.test(content.slice(startOffset, endOffset))) {
+ markups.push([[startOffset, endOffset], ...groupIds]);
+ window.logger.suc(getTranslation("Markup was moved"));
+
+ } else if (groupIds.length > 0) {
+ deleteMarkupGroup({ variables: { groupIds, perspectiveId } });
+
+ } else {
+ window.logger.warn(getTranslation("Markup was deleted"));
+ }
+ }
+ }
+
updateEntity({
- variables: { id: entity.id, content },
+ variables: { id: entity.id, content, markups },
refetchQueries: [
{
query: lexicalEntryQuery,
@@ -428,35 +509,104 @@ const Entities = ({
});
}, [update_set]);
+ const splitEntity = ({
+ entity,
+ parentEntity,
+ beforeCaret,
+ afterCaret,
+ metadata,
+ firstMarkups,
+ secondMarkups
+ }) => {
+
+ if (entity) {
+ remove(entity);
+ }
+
+ create(
+ beforeCaret,
+ parentEntity === null ? null : parentEntity.id,
+ metadata={...metadata, 'markups': firstMarkups}
+ );
+
+ create(
+ afterCaret,
+ parentEntity === null ? null : parentEntity.id,
+ metadata={...metadata, 'markups': secondMarkups}
+ );
+ };
+
/* Shortcut "ctrl+Enter" */
const breakdown = useCallback((event, parentEntity, entity) => {
if (event.ctrlKey && event.code === "Enter") {
- event.preventDefault();
+ event.preventDefault();
- const eventTarget = event.target;
- const targetValue = eventTarget.value;
+ const eventTarget = event.target;
+ const targetValue = eventTarget.value;
- const selectionStart = getSelectionStart(eventTarget);
- const selectionEnd = getSelectionEnd(eventTarget);
+ const selectionStart = getSelectionStart(eventTarget);
+ const selectionEnd = getSelectionEnd(eventTarget);
- if (selectionStart === 0 && selectionEnd === 0) {
- return;
- }
+ if (selectionStart === 0 && selectionEnd === 0) {
+ return;
+ }
- if (selectionStart === targetValue.length && selectionEnd === targetValue.length) {
- return;
+ if (selectionStart === targetValue.length && selectionEnd === targetValue.length) {
+ return;
+ }
+
+ const beforeCaret = targetValue.substring(0, selectionStart).replace(/ /g, '\x20') || '\x20';
+ const afterCaret = targetValue.substring(selectionStart).replace(/ /g, '\x20') || '\x20';
+ const metadata = entity.additional_metadata || {};
+ const markups = metadata.markups || [];
+ const brokenGroups = [];
+ let brokenMarkup = false;
+ const firstMarkups = [[]];
+ const secondMarkups = [[]];
+
+ for (const markup of markups) {
+ if (!markup.length || markup[0].length !== 2) {
+ continue;
}
- const beforeCaret = targetValue.substring(0, selectionStart).replace(/ /g, '\x20') || '\x20';
- const afterCaret = targetValue.substring(selectionStart).replace(/ /g, '\x20') || '\x20';
-
- if (entity) {
- remove(entity);
+ const [[startOffset, endOffset], ...groupIds] = markup;
+
+ if (startOffset < selectionStart && endOffset <= selectionStart) {
+ firstMarkups.push(markup);
+ } else if (startOffset >= selectionStart && endOffset > selectionStart) {
+ secondMarkups.push([[startOffset - selectionStart, endOffset - selectionStart], ...groupIds]);
+ } else {
+ brokenMarkup = true;
+ brokenGroups.push(...groupIds);
}
- create(beforeCaret, parentEntity === null ? null : parentEntity.id);
- create(afterCaret, parentEntity === null ? null : parentEntity.id);
- }
+ }
+
+ const variables = {
+ entity,
+ parentEntity,
+ beforeCaret,
+ afterCaret,
+ metadata,
+ firstMarkups,
+ secondMarkups
+ };
+
+ if (brokenMarkup) {
+ setConfirmation({
+ content: getTranslation("You are going to delete a markup and related groups? Are you sure?"),
+ func: () => {
+ setConfirmation(null);
+ splitEntity({...variables});
+ if (brokenGroups.length > 0) {
+ deleteMarkupGroup({ variables: { groupIds: brokenGroups, perspectiveId } });
+ }
+ }
+ });
+ } else {
+ splitEntity({...variables});
+ }
+ }
}, []);
const props = {
@@ -519,7 +669,7 @@ const Entities = ({
is_being_removed={remove_set.hasOwnProperty(id2str(entity.id))}
is_being_updated={update_set.hasOwnProperty(id2str(entity.id))}
number={number}
- id={entity.id}
+ id={entity.id}
/>
))}
{mode === "edit" && !is_order_column && (
@@ -547,6 +697,14 @@ const Entities = ({
)}
)}
+ setConfirmation(null)}
+ className="lingvo-confirm"
+ />
);
};
diff --git a/src/pages/Perspective/style.scss b/src/pages/Perspective/style.scss
index cbe9b621..4fe4c87e 100644
--- a/src/pages/Perspective/style.scss
+++ b/src/pages/Perspective/style.scss
@@ -435,8 +435,11 @@
border-radius: 0 8px 8px 0 !important;
}
- @media only screen and (max-device-width: 767px), only screen and (max-width: 767px) {
+ &:only-child {
+ border-radius: 8px !important;
+ }
+ @media only screen and (max-device-width: 767px), only screen and (max-width: 767px) {
&:first-child {
border-radius: 8px 8px 0 0 !important;
}
@@ -449,6 +452,10 @@
border-radius: 0 0 8px 8px !important;
}
+ &:only-child {
+ border-radius: 8px !important;
+ }
+
&:not(:last-child) {
&::after {
content: none;
@@ -482,6 +489,10 @@
border-radius: 0 8px 8px 0 !important;
}
+ & td:only-child {
+ border-radius: 8px !important;
+ }
+
@media only screen and (max-device-width: 767px), only screen and (max-width: 767px) {
& td:first-child {
border-radius: 8px 8px 0 0 !important;
@@ -494,6 +505,10 @@
& td:last-child {
border-radius: 0 0 8px 8px !important;
}
+
+ & td:only-child {
+ border-radius: 8px !important;
+ }
}
}
@@ -586,7 +601,6 @@
}
&.ui.table thead tr {
-
& th.lingvo-dnd-headercell {
&_hidden {
display: none !important;
@@ -690,7 +704,6 @@
color: #372f9d;
}
}
-
}
/* /lingvo perspective component text */
diff --git a/src/styles/main.scss b/src/styles/main.scss
index 531658fb..b0107015 100644
--- a/src/styles/main.scss
+++ b/src/styles/main.scss
@@ -924,8 +924,10 @@ body {
background-color: #fff !important;
border: 1px solid #fff !important;
box-shadow: none !important;
- -webkit-transition: opacity .1s ease,background-color .1s ease,color .1s ease,background .1s ease,border-color .1s ease;
- transition: opacity .1s ease,background-color .1s ease,color .1s ease,background .1s ease,border-color .1s ease !important;
+ -webkit-transition: opacity 0.1s ease, background-color 0.1s ease, color 0.1s ease, background 0.1s ease,
+ border-color 0.1s ease;
+ transition: opacity 0.1s ease, background-color 0.1s ease, color 0.1s ease, background 0.1s ease,
+ border-color 0.1s ease !important;
& .lingvo-icon {
background-color: #57b894;
@@ -955,7 +957,6 @@ body {
background-color: #999ca0;
}
}
-
}
.lingvo-button-green2 {
@@ -970,8 +971,10 @@ body {
background-color: transparent !important;
border: 1px solid transparent !important;
box-shadow: none !important;
- -webkit-transition: opacity .1s ease,background-color .1s ease,color .1s ease,background .1s ease,border-color .1s ease;
- transition: opacity .1s ease,background-color .1s ease,color .1s ease,background .1s ease,border-color .1s ease !important;
+ -webkit-transition: opacity 0.1s ease, background-color 0.1s ease, color 0.1s ease, background 0.1s ease,
+ border-color 0.1s ease;
+ transition: opacity 0.1s ease, background-color 0.1s ease, color 0.1s ease, background 0.1s ease,
+ border-color 0.1s ease !important;
& .lingvo-icon {
background-color: #57b894;
@@ -1001,7 +1004,6 @@ body {
background-color: #999ca0;
}
}
-
}
.lingvo-button-red {
@@ -1016,8 +1018,10 @@ body {
background-color: #fff !important;
border: 1px solid #fff !important;
box-shadow: none !important;
- -webkit-transition: opacity .1s ease,background-color .1s ease,color .1s ease,background .1s ease,border-color .1s ease;
- transition: opacity .1s ease,background-color .1s ease,color .1s ease,background .1s ease,border-color .1s ease !important;
+ -webkit-transition: opacity 0.1s ease, background-color 0.1s ease, color 0.1s ease, background 0.1s ease,
+ border-color 0.1s ease;
+ transition: opacity 0.1s ease, background-color 0.1s ease, color 0.1s ease, background 0.1s ease,
+ border-color 0.1s ease !important;
& .lingvo-icon {
background-color: #db4e4e;
@@ -1025,7 +1029,7 @@ body {
margin-right: 4px;
vertical-align: top;
}
-
+
&:hover {
background-color: #db4e4e !important;
color: #fff !important;
@@ -1047,7 +1051,6 @@ body {
background-color: #999ca0;
}
}
-
}
.lingvo-button-greenest {
@@ -1062,8 +1065,10 @@ body {
color: #fff !important;
border: 1px solid #57b894 !important;
box-shadow: none !important;
- -webkit-transition: opacity .1s ease,background-color .1s ease,color .1s ease,background .1s ease,border-color .1s ease;
- transition: opacity .1s ease,background-color .1s ease,color .1s ease,background .1s ease,border-color .1s ease !important;
+ -webkit-transition: opacity 0.1s ease, background-color 0.1s ease, color 0.1s ease, background 0.1s ease,
+ border-color 0.1s ease;
+ transition: opacity 0.1s ease, background-color 0.1s ease, color 0.1s ease, background 0.1s ease,
+ border-color 0.1s ease !important;
&:hover {
background: #333 !important;
@@ -1073,11 +1078,10 @@ body {
&.ui.button.disabled,
&.ui.button:disabled {
background-color: #fff !important;
- border: 1px solid #999CA0 !important;
+ border: 1px solid #999ca0 !important;
color: #999ca0 !important;
opacity: 1 !important;
}
-
}
.lingvo-button-redder {
@@ -1092,8 +1096,10 @@ body {
color: #fff !important;
border: 1px solid #db4e4e !important;
box-shadow: none !important;
- -webkit-transition: opacity .1s ease,background-color .1s ease,color .1s ease,background .1s ease,border-color .1s ease;
- transition: opacity .1s ease,background-color .1s ease,color .1s ease,background .1s ease,border-color .1s ease !important;
+ -webkit-transition: opacity 0.1s ease, background-color 0.1s ease, color 0.1s ease, background 0.1s ease,
+ border-color 0.1s ease;
+ transition: opacity 0.1s ease, background-color 0.1s ease, color 0.1s ease, background 0.1s ease,
+ border-color 0.1s ease !important;
&:hover {
background: #333 !important;
@@ -1103,11 +1109,10 @@ body {
&.ui.button.disabled,
&.ui.button:disabled {
background-color: #fff !important;
- border: 1px solid #999CA0 !important;
+ border: 1px solid #999ca0 !important;
color: #999ca0 !important;
opacity: 1 !important;
}
-
}
.lingvo-button-black {
@@ -1122,8 +1127,10 @@ body {
border: 1px solid #333 !important;
color: #fff !important;
box-shadow: none !important;
- -webkit-transition: opacity .1s ease,background-color .1s ease,color .1s ease,background .1s ease,border-color .1s ease;
- transition: opacity .1s ease,background-color .1s ease,color .1s ease,background .1s ease,border-color .1s ease !important;
+ -webkit-transition: opacity 0.1s ease, background-color 0.1s ease, color 0.1s ease, background 0.1s ease,
+ border-color 0.1s ease;
+ transition: opacity 0.1s ease, background-color 0.1s ease, color 0.1s ease, background 0.1s ease,
+ border-color 0.1s ease !important;
&:hover {
background-color: #fff !important;
@@ -1134,11 +1141,10 @@ body {
&.ui.button.disabled,
&.ui.button:disabled {
background-color: #fff !important;
- border: 1px solid #999CA0 !important;
+ border: 1px solid #999ca0 !important;
color: #999ca0 !important;
opacity: 1 !important;
}
-
}
/* confirm */
@@ -2104,12 +2110,10 @@ body {
}
&_down {
-
&:hover {
background-color: #5d54cd;
}
}
-
}
&_sort_up {
@@ -2196,7 +2200,7 @@ body {
mask-image: url("../images/icon_published.svg");
width: 13px;
height: 12px;
- background-color: #57B894;
+ background-color: #57b894;
cursor: pointer;
}
@@ -2324,7 +2328,6 @@ body {
/* popups */
.lingvo-popup {
-
&.popup {
font-family: Rubik, Arial, sans-serif !important;
border-radius: 4px !important;
@@ -2362,7 +2365,7 @@ body {
&_published {
&.top.left.popup {
margin-left: -26px !important;
-
+
&::before {
left: 24px !important;
}
@@ -2370,13 +2373,12 @@ body {
&.top.right.popup {
margin-right: -26px !important;
-
+
&::before {
right: 24px !important;
}
}
}
-
}
.lingvo-popup-inverted {
@@ -2577,7 +2579,7 @@ body {
font-family: Rubik, Arial, sans-serif !important;
}
- &.ui.fitted.checkbox .box,
+ &.ui.fitted.checkbox .box,
&.ui.fitted.checkbox label {
padding-left: 0 !important;
}
@@ -2779,14 +2781,14 @@ body {
font-size: 10px !important;
}
- &.ui.checkbox input:not([type=radio]):indeterminate ~ .box:before,
- &.ui.checkbox input:not([type=radio]):indeterminate ~ label:before {
+ &.ui.checkbox input:not([type="radio"]):indeterminate ~ .box:before,
+ &.ui.checkbox input:not([type="radio"]):indeterminate ~ label:before {
background: #4e46b4 !important;
border: 1px solid #4e46b4 !important;
}
- &.ui.checkbox input:not([type=radio]):indeterminate ~ .box:after,
- &.ui.checkbox input:not([type=radio]):indeterminate ~ label:after {
+ &.ui.checkbox input:not([type="radio"]):indeterminate ~ .box:after,
+ &.ui.checkbox input:not([type="radio"]):indeterminate ~ label:after {
color: #fff !important;
top: 1px !important;
}
@@ -2822,19 +2824,19 @@ body {
background-color: #efeefa !important;
}
- & .ui.radio.checkbox .box:after,
+ & .ui.radio.checkbox .box:after,
& .ui.radio.checkbox label:after {
- background-color: #5D54CD !important;
+ background-color: #5d54cd !important;
top: 2px !important;
}
- & .ui.radio.checkbox input:checked ~ .box:after,
+ & .ui.radio.checkbox input:checked ~ .box:after,
& .ui.radio.checkbox input:checked ~ label:after {
- background-color: #5D54CD !important;
+ background-color: #5d54cd !important;
}
-
+
& .ui.radio.checkbox input:checked ~ label {
- color: #5D54CD !important;
+ color: #5d54cd !important;
font-weight: 500 !important;
}
@@ -3021,7 +3023,7 @@ body {
.lingvo-merge-group {
padding: 6px 0 14px 0;
margin: 0 0 14px 0;
- border-bottom: 1px solid #DCDCDC;
+ border-bottom: 1px solid #dcdcdc;
&:last-child {
border-bottom: none;
@@ -3092,20 +3094,18 @@ body {
flex-direction: column;
& .ui.button {
- margin: 0 0 10px 0;
-
+ margin: 0 0 10px 0;
+
&:last-child {
margin: 0;
}
}
}
-
}
/* /lingvo merge buttons */
/* lingvo labeled input */
.lingvo-labeled-input {
-
&.ui.labeled.input > .label {
font-family: Rubik, Arial, sans-serif !important;
font-size: 16px !important;
@@ -3241,7 +3241,6 @@ body {
}
&.active {
-
&.ui.selection {
border-radius: 10px !important;
border-color: #5d54cd !important;
@@ -3294,7 +3293,6 @@ body {
color: #372f9d !important;
}
}
-
}
&.upward > .menu {
@@ -3445,7 +3443,6 @@ body {
/* lingvo dropdown inline */
.lingvo-dropdown-inline {
-
&.dropdown {
display: inline-block !important;
position: relative !important;
@@ -3493,16 +3490,15 @@ body {
vertical-align: top;
background-color: #5d54cd;
margin-right: 8px;
-
+
&_save,
- &_table {
+ &_table {
background-color: #57b894;
}
&_create {
background-color: #f3aa18;
}
-
}
&:hover {
@@ -3517,7 +3513,6 @@ body {
height: 14px !important;
border: none !important;
}
-
}
&.upward > .menu {
@@ -3527,7 +3522,6 @@ body {
&_perspective {
&.ui.dropdown {
-
& .menu {
min-width: 374px !important;
@@ -3540,7 +3534,6 @@ body {
&_perspectives {
&.ui.dropdown {
-
& .menu {
min-width: 374px !important;
@@ -3550,13 +3543,11 @@ body {
}
}
}
-
}
/* /lingvo dropdown inline */
/* lingvo dropdown item */
.lingvo-dropdown-item {
-
.ui.menu &.ui.dropdown {
position: relative !important;
@@ -3607,7 +3598,6 @@ body {
color: #372f9d !important;
}
}
-
}
&.ui.active.upward > .menu {
@@ -3617,7 +3607,6 @@ body {
&_tools {
.ui.menu &.ui.dropdown {
-
& .menu {
min-width: 290px !important;
@@ -3627,7 +3616,6 @@ body {
}
}
}
-
}
/* /lingvo dropdown item */
@@ -3933,9 +3921,7 @@ body {
}
}
}
-
}
-
}
&.ui.basic.buttons &__drag:first-child .button {
@@ -3996,7 +3982,7 @@ body {
&.ui.basic.buttons {
border-top-left-radius: 0 !important;
border-bottom-left-radius: 0 !important;
-
+
& .button {
&:first-child {
border-top-left-radius: 0 !important;
@@ -4044,10 +4030,8 @@ body {
/*width: 118px !important;*/
min-width: 118px !important;
}
-
}
/* /textarea */
-
}
/* /lingvo input action */
@@ -4065,11 +4049,10 @@ body {
align-items: center;
&__name {
- padding: 0 6px;
+ padding: 0 6px;
}
& .lingvo-input-action.ui.input {
-
& > input {
border-top-right-radius: 0 !important;
border-bottom-right-radius: 0 !important;
@@ -4089,13 +4072,11 @@ body {
&_drag {
opacity: 0.3 !important;
}
-
}
/* /lingvo input buttons group */
/* lingvo labeled button */
.lingvo-labeled-button {
-
&.ui.button {
font-size: 16px !important;
line-height: 20px !important;
@@ -4120,7 +4101,7 @@ body {
position: relative !important;
&::before {
- content: '';
+ content: "";
position: absolute;
top: 0;
left: 0;
@@ -4145,14 +4126,12 @@ body {
}
}
}
-
}
/* /lingvo labeled button */
/* lingvo button spinner */
.lingvo-button-spinner {
-
- &.ui.button:disabled,
+ &.ui.button:disabled,
.ui.buttons &.disabled.button {
opacity: 1 !important;
}
@@ -4163,7 +4142,7 @@ body {
.lingvo-rst-tree {
& .rst__lineHalfHorizontalRight::before,
& .rst__lineFullVertical::after,
- & .rst__lineHalfVerticalTop::after,
+ & .rst__lineHalfVerticalTop::after,
& .rst__lineHalfVerticalBottom::after {
background-color: #dcdcdc !important;
}
@@ -4194,7 +4173,7 @@ body {
& .rst__rowLabel {
padding-right: 0 !important;
-
+
& + .rst__rowToolbar:not(:empty) {
padding-left: 20px !important;
}
@@ -4261,10 +4240,14 @@ body {
font-family: Mulish, Arial, sans-serif;
line-height: 22px;
padding: 10px 16px 11px 16px;
- -webkit-transition: background-color .1s ease, color .1s ease, -webkit-box-shadow .1s ease, border-color .1s ease, font-weight .1s ease;
- transition: background-color .1s ease, color .1s ease, -webkit-box-shadow .1s ease, border-color .1s ease, font-weight .1s ease;
- transition: background-color .1s ease, box-shadow .1s ease, color .1s ease, border-color .1s ease, font-weight .1s ease;
- transition: background-color .1s ease, box-shadow .1s ease, color .1s ease, border-color .1s ease, font-weight .1s ease;
+ -webkit-transition: background-color 0.1s ease, color 0.1s ease, -webkit-box-shadow 0.1s ease,
+ border-color 0.1s ease, font-weight 0.1s ease;
+ transition: background-color 0.1s ease, color 0.1s ease, -webkit-box-shadow 0.1s ease, border-color 0.1s ease,
+ font-weight 0.1s ease;
+ transition: background-color 0.1s ease, box-shadow 0.1s ease, color 0.1s ease, border-color 0.1s ease,
+ font-weight 0.1s ease;
+ transition: background-color 0.1s ease, box-shadow 0.1s ease, color 0.1s ease, border-color 0.1s ease,
+ font-weight 0.1s ease;
&.active {
color: #fff;
@@ -4320,7 +4303,6 @@ body {
padding: 10px 10px 10px 0 !important;
}
}
-
}
/* /lingvo dictionaries tabs */
@@ -4406,7 +4388,6 @@ body {
width: 100% !important;
}
}
-
}
/* /lingvo search input */
@@ -4414,4 +4395,51 @@ body {
/*
.leaflet .leaflet-attribution-flag {
display: none !important;
-}*/
\ No newline at end of file
+}*/
+
+/* Markup button */
+.lingvo-button-markup {
+ font-weight: 900 !important;
+ width: 40px !important;
+ font-size: 14px !important;
+ color: #999ca0 !important;
+
+ .ui.basic.buttons &_create.button {
+ color: #57b894 !important;
+
+ &:focus {
+ color: #57b894 !important;
+ }
+
+ &:hover {
+ background-color: #57b894 !important;
+ color: #fff !important;
+ }
+ }
+
+ .ui.basic.buttons &_delete.button {
+ color: #db4e4e !important;
+
+ &:focus {
+ color: #db4e4e !important;
+ }
+
+ &:hover {
+ background-color: #db4e4e !important;
+ color: #fff !important;
+ }
+ }
+}
+/* /Markup button */
+
+/* Markup markers */
+.lingvo-marker-markup {
+ //background-color: #ff0;
+ background-color: #90ee90;
+}
+
+.lingvo-marker {
+ //background-color: #f3aa18 !important;
+ background-color: #ffd700 !important;
+}
+/* /Markup markers */
diff --git a/src/utils/patienceDiff.js b/src/utils/patienceDiff.js
new file mode 100644
index 00000000..d4f5933c
--- /dev/null
+++ b/src/utils/patienceDiff.js
@@ -0,0 +1,471 @@
+/**
+ * program: "patienceDiff" algorithm implemented in javascript.
+ * author: Jonathan Trent
+ * version: 3.0
+ *
+ * use: patienceDiff( aLines[], bLines[], diffPlusFlag )
+ * patienceDiff( aString, bString, diffPlusFlag )
+ *
+ * where:
+ * aLines[] contains the original text lines.
+ * bLines[] contains the new text lines.
+ *
+ * aString contains the original string.
+ * bString contains the new string.
+ *
+ * diffPlusFlag if true, returns additional arrays with the subset of lines that were
+ * either deleted or inserted. These additional arrays are used by patienceDiffPlus.
+ *
+ * Note that if strings are passed, the patience diff algorithm has been enhanced to
+ * iteratively seek common unique string chunks between aString and bString, to accommodate
+ * the fact that the larger a string, the less likely that unique individual characters
+ * will be present, thereby shortcircuiting the step of finding the longest common
+ * subsequence...
+ *
+ * returns an object with the following properties:
+ * lines[] with properties of:
+ * line containing the line of text from aLines or bLines.
+ * aIndex referencing the index in aLines[].
+ * bIndex referencing the index in bLines[].
+ * (Note: The line is text from either aLines or bLines, with aIndex and bIndex
+ * referencing the original index. If aIndex === -1 then the line is new from bLines,
+ * and if bIndex === -1 then the line is old from aLines.)
+ * lineCountDeleted is the number of lines from aLines[] not appearing in bLines[].
+ * lineCountInserted is the number of lines from bLines[] not appearing in aLines[].
+ * lineCountMoved is 0. (Only set when using patienceDiffPlus.)
+ *
+ */
+
+export function patienceDiff( aLines, bLines, diffPlusFlag ) {
+
+ //
+ // findUnique finds all unique values in arr[lo..hi], inclusive. This
+ // function is used in preparation for determining the longest common
+ // subsequence. Specifically, it first reduces the array range in question
+ // to unique values.
+ //
+ // Returns an ordered Map, with the arr[i] value as the Map key and the
+ // array index i as the Map value.
+ //
+
+ function findUnique( arr, lo, hi, unit ) {
+
+ const lineMap = new Map();
+
+ for ( let i = lo; i <= hi + 1 - unit; i ++ ) {
+
+ let line = arr.slice( i, i + unit );
+
+ if ( lineMap.has( line ) ) {
+
+ lineMap.get( line ).count ++;
+
+ } else {
+
+ lineMap.set( line, {
+ count: 1,
+ index: i
+ } );
+
+ }
+
+ }
+
+ lineMap.forEach( ( val, key, map ) => {
+
+ if ( val.count !== 1 ) {
+
+ map.delete( key );
+
+ } else {
+
+ map.set( key, val.index );
+
+ }
+
+ } );
+
+ return lineMap;
+
+ }
+
+ //
+ // uniqueCommon finds all the unique common entries between aArray[aLo..aHi]
+ // and bArray[bLo..bHi], inclusive. This function uses findUnique to pare
+ // down the aArray and bArray ranges first, before then walking the comparison
+ // between the two arrays.
+ //
+ // Returns an ordered Map, with the Map key as the common line between aArray
+ // and bArray, with the Map value as an object containing the array indexes of
+ // the matching unique lines.
+ //
+
+ function uniqueCommon( aArray, aLo, aHi, bArray, bLo, bHi ) {
+
+ let chunkSize = 0, ma, mb;
+
+ do {
+
+ chunkSize ++;
+
+ ma = findUnique( aArray, aLo, aHi, chunkSize );
+ mb = findUnique( bArray, bLo, bHi, chunkSize );
+
+ ma.forEach( ( val, key, map ) => {
+
+ if ( mb.has( key ) ) {
+
+ map.set( key, {
+ indexA: val,
+ indexB: mb.get( key )
+ } );
+
+ } else {
+
+ map.delete( key );
+
+ }
+
+ } );
+
+ } while ( ma.size === 0 && chunkSize <= aHi - aLo + 1 && chunkSize < bHi - bLo + 1 && typeof aArray === 'string' );
+
+ return ma;
+
+ }
+
+ //
+ // longestCommonSubsequence takes an ordered Map from the function uniqueCommon
+ // and determines the Longest Common Subsequence (LCS).
+ //
+ // Returns an ordered array of objects containing the array indexes of the
+ // matching lines for a LCS.
+ //
+
+ function longestCommonSubsequence( abMap ) {
+
+ const ja = [];
+
+ // First, walk the list creating the jagged array.
+
+ abMap.forEach( ( val, key, map ) => {
+
+ let i = 0;
+
+ while ( ja[ i ] && ja[ i ][ ja[ i ].length - 1 ].indexB < val.indexB ) {
+
+ i ++;
+
+ }
+
+ if ( ! ja[ i ] ) {
+
+ ja[ i ] = [];
+
+ }
+
+ if ( 0 < i ) {
+
+ val.prev = ja[ i - 1 ][ ja[ i - 1 ].length - 1 ];
+
+ }
+
+ ja[ i ].push( val );
+
+ } );
+
+ // Now, pull out the longest common subsequence.
+
+ let lcs = [];
+
+ if ( 0 < ja.length ) {
+
+ let n = ja.length - 1;
+ lcs = [ ja[ n ][ ja[ n ].length - 1 ] ];
+
+ while ( lcs[ lcs.length - 1 ].prev ) {
+
+ lcs.push( lcs[ lcs.length - 1 ].prev );
+
+ }
+
+ }
+
+ return lcs.reverse();
+
+ }
+
+ // "result" is the array used to accumulate the aLines that are deleted, the
+ // lines that are shared between aLines and bLines, and the bLines that were
+ // inserted.
+
+ const result = [];
+ let deleted = 0;
+ let inserted = 0;
+
+ // aMove and bMove will contain the lines that don't match, and will be returned
+ // for possible searching of lines that moved.
+
+ const aMove = [];
+ const aMoveIndex = [];
+ const bMove = [];
+ const bMoveIndex = [];
+
+ //
+ // addToResult simply pushes the latest value onto the "result" array. This
+ // array captures the diff of the line, aIndex, and bIndex from the aLines
+ // and bLines array.
+ //
+
+ function addToResult( aIndex, bIndex ) {
+
+ if ( bIndex < 0 ) {
+
+ aMove.push( aLines[ aIndex ] );
+ aMoveIndex.push( result.length );
+ deleted ++;
+
+ } else if ( aIndex < 0 ) {
+
+ bMove.push( bLines[ bIndex ] );
+ bMoveIndex.push( result.length );
+ inserted ++;
+
+ }
+
+ result.push( {
+ line: 0 <= aIndex ? aLines[ aIndex ] : bLines[ bIndex ],
+ aIndex: aIndex,
+ bIndex: bIndex,
+ } );
+
+ }
+
+ //
+ // addSubMatch handles the lines between a pair of entries in the LCS. Thus,
+ // this function might recursively call recurseLCS to further match the lines
+ // between aLines and bLines.
+ //
+
+ function addSubMatch( aLo, aHi, bLo, bHi ) {
+
+ // Match any lines at the beginning of aLines and bLines.
+
+ while ( aLo <= aHi && bLo <= bHi && aLines[ aLo ] === bLines[ bLo ] ) {
+
+ addToResult( aLo ++, bLo ++ );
+
+ }
+
+ // Match any lines at the end of aLines and bLines, but don't place them
+ // in the "result" array just yet, as the lines between these matches at
+ // the beginning and the end need to be analyzed first.
+
+ let aHiTemp = aHi;
+
+ while ( aLo <= aHi && bLo <= bHi && aLines[ aHi ] === bLines[ bHi ] ) {
+
+ aHi --;
+ bHi --;
+
+ }
+
+ // Now, check to determine with the remaining lines in the subsequence
+ // whether there are any unique common lines between aLines and bLines.
+ //
+ // If not, add the subsequence to the result (all aLines having been
+ // deleted, and all bLines having been inserted).
+ //
+ // If there are unique common lines between aLines and bLines, then let's
+ // recursively perform the patience diff on the subsequence.
+
+ let uniqueCommonMap = uniqueCommon( aLines, aLo, aHi, bLines, bLo, bHi );
+ //let unit = 2;
+ //while ( typeof aLines === 'string' && uniqueCommonMap.size === 0 && unit < aLines.length && unit < bLines.length ) {
+ // uniqueCommonMap = uniqueCommon( aLines, aLo, aHi, bLines, bLo, bHi, unit++ );
+ //}
+
+ if ( uniqueCommonMap.size === 0 ) {
+
+ while ( aLo <= aHi ) {
+
+ addToResult( aLo ++, - 1 );
+
+ }
+
+ while ( bLo <= bHi ) {
+
+ addToResult( - 1, bLo ++ );
+
+ }
+
+ } else {
+
+ recurseLCS( aLo, aHi, bLo, bHi, uniqueCommonMap );
+
+ }
+
+ // Finally, let's add the matches at the end to the result.
+
+ while ( aHi < aHiTemp ) {
+
+ addToResult( ++ aHi, ++ bHi );
+
+ }
+
+ }
+
+ //
+ // recurseLCS finds the longest common subsequence (LCS) between the arrays
+ // aLines[aLo..aHi] and bLines[bLo..bHi] inclusive. Then for each subsequence
+ // recursively performs another LCS search (via addSubMatch), until there are
+ // none found, at which point the subsequence is dumped to the result.
+ //
+
+ function recurseLCS( aLo, aHi, bLo, bHi, uniqueCommonMap ) {
+
+ const x = longestCommonSubsequence( uniqueCommonMap || uniqueCommon( aLines, aLo, aHi, bLines, bLo, bHi ) );
+
+ if ( x.length === 0 ) {
+
+ addSubMatch( aLo, aHi, bLo, bHi );
+
+ } else {
+
+ if ( aLo < x[ 0 ].indexA || bLo < x[ 0 ].indexB ) {
+
+ addSubMatch( aLo, x[ 0 ].indexA - 1, bLo, x[ 0 ].indexB - 1 );
+
+ }
+
+ let i;
+ for ( i = 0; i < x.length - 1; i ++ ) {
+
+ addSubMatch( x[ i ].indexA, x[ i + 1 ].indexA - 1, x[ i ].indexB, x[ i + 1 ].indexB - 1 );
+
+ }
+
+ if ( x[ i ].indexA <= aHi || x[ i ].indexB <= bHi ) {
+
+ addSubMatch( x[ i ].indexA, aHi, x[ i ].indexB, bHi );
+
+ }
+
+ }
+
+ }
+
+ recurseLCS( 0, aLines.length - 1, 0, bLines.length - 1 );
+
+ if ( diffPlusFlag ) {
+
+ return {
+ lines: result,
+ lineCountDeleted: deleted,
+ lineCountInserted: inserted,
+ lineCountMoved: 0,
+ aMove: aMove,
+ aMoveIndex: aMoveIndex,
+ bMove: bMove,
+ bMoveIndex: bMoveIndex,
+ };
+
+ }
+
+ return {
+ lines: result,
+ lineCountDeleted: deleted,
+ lineCountInserted: inserted,
+ lineCountMoved: 0,
+ };
+
+}
+
+/**
+ * program: "patienceDiffPlus" algorithm implemented in javascript.
+ * author: Jonathan Trent
+ * version: 2.0
+ *
+ * use: patienceDiffPlus( aLines[], bLines[] )
+ *
+ * where:
+ * aLines[] contains the original text lines.
+ * bLines[] contains the new text lines.
+ *
+ * returns an object with the following properties:
+ * lines[] with properties of:
+ * line containing the line of text from aLines or bLines.
+ * aIndex referencing the index in aLine[].
+ * bIndex referencing the index in bLines[].
+ * (Note: The line is text from either aLines or bLines, with aIndex and bIndex
+ * referencing the original index. If aIndex === -1 then the line is new from bLines,
+ * and if bIndex === -1 then the line is old from aLines.)
+ * moved is true if the line was moved from elsewhere in aLines[] or bLines[].
+ * lineCountDeleted is the number of lines from aLines[] not appearing in bLines[].
+ * lineCountInserted is the number of lines from bLines[] not appearing in aLines[].
+ * lineCountMoved is the number of lines that moved.
+ *
+ */
+
+function patienceDiffPlus( aLines, bLines ) {
+
+ const difference = patienceDiff( aLines, bLines, true );
+
+ let aMoveNext = difference.aMove;
+ let aMoveIndexNext = difference.aMoveIndex;
+ let bMoveNext = difference.bMove;
+ let bMoveIndexNext = difference.bMoveIndex;
+
+ delete difference.aMove;
+ delete difference.aMoveIndex;
+ delete difference.bMove;
+ delete difference.bMoveIndex;
+
+ let lastLineCountMoved;
+
+ do {
+
+ let aMove = aMoveNext;
+ let aMoveIndex = aMoveIndexNext;
+ let bMove = bMoveNext;
+ let bMoveIndex = bMoveIndexNext;
+
+ aMoveNext = [];
+ aMoveIndexNext = [];
+ bMoveNext = [];
+ bMoveIndexNext = [];
+
+ let subDiff = patienceDiff( aMove, bMove );
+
+ lastLineCountMoved = difference.lineCountMoved;
+
+ subDiff.lines.forEach( ( v, i ) => {
+
+ if ( 0 <= v.aIndex && 0 <= v.bIndex ) {
+
+ difference.lines[ aMoveIndex[ v.aIndex ] ].moved = true;
+ difference.lines[ bMoveIndex[ v.bIndex ] ].aIndex = aMoveIndex[ v.aIndex ];
+ difference.lines[ bMoveIndex[ v.bIndex ] ].moved = true;
+ difference.lineCountInserted --;
+ difference.lineCountDeleted --;
+ difference.lineCountMoved ++;
+
+ } else if ( v.bIndex < 0 ) {
+
+ aMoveNext.push( aMove[ v.aIndex ] );
+ aMoveIndexNext.push( aMoveIndex[ v.aIndex ] );
+
+ } else {
+
+ bMoveNext.push( bMove[ v.bIndex ] );
+ bMoveIndexNext.push( bMoveIndex[ v.bIndex ] );
+
+ }
+
+ } );
+
+ } while ( 0 < difference.lineCountMoved - lastLineCountMoved );
+
+ return difference;
+
+}