diff --git a/packages/client/src/api.tsx b/packages/client/src/api.tsx
index 150e72a1f..364af95d7 100644
--- a/packages/client/src/api.tsx
+++ b/packages/client/src/api.tsx
@@ -21,7 +21,10 @@ import {
IUser,
Relation,
RequestPermissionUpdate,
+ IRequestStats,
+ IAudit,
} from "@shared/types";
+import { ISetting, ISettingGroup } from "@shared/types/settings";
import * as errors from "@shared/types/errors";
import { NetworkError } from "@shared/types/errors";
import { IRequestSearch } from "@shared/types/request-search";
@@ -674,7 +677,8 @@ class Api {
entityIds: string[],
options?: IApiOptions
): Promise<(EntitiesDeleteSuccessResponse | EntitiesDeleteErrorResponse)[]> {
- const out: (EntitiesDeleteSuccessResponse | EntitiesDeleteErrorResponse)[] = [];
+ const out: (EntitiesDeleteSuccessResponse | EntitiesDeleteErrorResponse)[] =
+ [];
try {
const response = await this.connection.delete(`/entities/`, {
data: {
@@ -682,13 +686,21 @@ class Api {
},
...options,
});
- const data = (response.data as IResponseGeneric>).data;
+ const data = (
+ response.data as IResponseGeneric<
+ Record
+ >
+ ).data;
if (data) {
for (const errorEntityId of Object.keys(data)) {
if (data[errorEntityId] === true) {
out.push({ entityId: errorEntityId, details: data[errorEntityId] });
} else {
- out.push({ entityId: errorEntityId, error: true, details: data[errorEntityId] });
+ out.push({
+ entityId: errorEntityId,
+ error: true,
+ details: data[errorEntityId],
+ });
}
}
}
@@ -851,6 +863,21 @@ class Api {
}
}
+ /**
+ * Stats
+ */
+ async statsGet(
+ data: IRequestStats,
+ options?: IApiOptions
+ ): Promise> {
+ try {
+ const response = await this.connection.post(`/stats`, data, options);
+ return response;
+ } catch (err) {
+ throw this.handleError(err);
+ }
+ }
+
/**
* Audit
*/
@@ -869,6 +896,20 @@ class Api {
}
}
+ async auditGetFirst(
+ options?: IApiOptions
+ ): Promise>> {
+ try {
+ const response = await this.connection.get(
+ `/audits?skip=0&take=1&from=1970`,
+ options
+ );
+ return response;
+ } catch (err) {
+ throw this.handleError(err);
+ }
+ }
+
/**
* Statement
* Editor container
@@ -1218,6 +1259,40 @@ class Api {
}
}
+ async documentRemoveAnchors(
+ documentId: string,
+ entityId: string,
+ options?: IApiOptions
+ ): Promise> {
+ try {
+ const response = await this.connection.patch(
+ `/documents/${documentId}/removeAnchors?entityId=${entityId}`,
+ document,
+ options
+ );
+ return response;
+ } catch (err) {
+ throw this.handleError(err);
+ }
+ }
+
+ async documentRemoveAnchor(
+ documentId: string,
+ entityId: string,
+ anchorText: string,
+ anchorIndex: number,
+ options?: IApiOptions
+ ): Promise> {
+ try {
+ // todo add api endpoint
+
+ // @ts-ignore
+ return null;
+ } catch (err) {
+ throw this.handleError(err);
+ }
+ }
+
/**
* Document update
*/
@@ -1237,6 +1312,113 @@ class Api {
throw this.handleError(err);
}
}
+
+ /**
+ * Setting get
+ * @param settingId
+ * @param options
+ * @returns
+ */
+ async settingGet(
+ settingId: string,
+ options?: IApiOptions
+ ): Promise>> {
+ try {
+ const response = await this.connection.get(
+ `/settings/${settingId}`,
+ options
+ );
+ return response;
+ } catch (err) {
+ throw this.handleError(err);
+ }
+ }
+
+ /**
+ * Setting group get
+ * @param settingId
+ * @param options
+ * @returns
+ */
+ async settingGroupGet(
+ settingGroupId: string,
+ options?: IApiOptions
+ ): Promise>> {
+ try {
+ const response = await this.connection.get(
+ `/settings/group/${settingGroupId}`,
+ options
+ );
+ return response;
+ } catch (err) {
+ throw this.handleError(err);
+ }
+ }
+
+ /**
+ * Setting group get
+ * @param settingId
+ * @param data
+ * @param options
+ * @returns
+ */
+ async settingGroupUpdate(
+ settingGroupId: string,
+ data: { id: string; value: unknown }[],
+ options?: IApiOptions
+ ): Promise>> {
+ try {
+ const response = await this.connection.put(
+ `/settings/group/${settingGroupId}`,
+ data,
+ options
+ );
+ return response;
+ } catch (err) {
+ throw this.handleError(err);
+ }
+ }
+
+ /**
+ * Setting update
+ * @param settingId
+ * @param data
+ * @param options
+ * @returns
+ */
+ async settingUpdate(
+ settingId: string,
+ data: { value: unknown },
+ options?: IApiOptions
+ ): Promise> {
+ try {
+ const response = await this.connection.put(
+ `/settings/${settingId}`,
+ data,
+ options
+ );
+ return response;
+ } catch (err) {
+ throw this.handleError(err);
+ }
+ }
+
+ /**
+ * Get owner's info
+ * @param settingId
+ * @param options
+ * @returns
+ */
+ async usersGetOwner(
+ options?: IApiOptions
+ ): Promise>> {
+ try {
+ const response = await this.connection.get(`/users/owner`, options);
+ return response;
+ } catch (err) {
+ throw this.handleError(err);
+ }
+ }
}
const apiSingleton = new Api();
diff --git a/packages/client/src/components/advanced/Annotator/Annotator.tsx b/packages/client/src/components/advanced/Annotator/Annotator.tsx
index ade26754a..15706a007 100644
--- a/packages/client/src/components/advanced/Annotator/Annotator.tsx
+++ b/packages/client/src/components/advanced/Annotator/Annotator.tsx
@@ -539,7 +539,7 @@ export const TextAnnotator = ({
{annotator && (
-
+
}
diff --git a/packages/client/src/components/advanced/DocumentModal/DocumentModalEdit.tsx b/packages/client/src/components/advanced/DocumentModal/DocumentModalEdit.tsx
index b4cc3dd5f..7c010c861 100644
--- a/packages/client/src/components/advanced/DocumentModal/DocumentModalEdit.tsx
+++ b/packages/client/src/components/advanced/DocumentModal/DocumentModalEdit.tsx
@@ -1,25 +1,31 @@
-import React, { useEffect, useRef, useState } from "react";
+import React, { useEffect, useState } from "react";
-import { Modal, ModalContent, ModalFooter, ModalHeader } from "components";
-import { IResponseDocument } from "@shared/types";
+import { IDocument, IDocumentMeta, IResponseDocument } from "@shared/types";
+import { Modal, ModalContent, ModalHeader } from "components";
import { useWindowSize } from "hooks/useWindowSize";
import { getShortLabelByLetterCount } from "utils/utils";
import TextAnnotator from "../Annotator/Annotator";
import AnnotatorProvider from "../Annotator/AnnotatorProvider";
+import { Annotator } from "@inkvisitor/annotator/src/lib";
-interface DocumentModal {
- document: IResponseDocument | undefined;
+interface DocumentModalEdit {
+ document: IResponseDocument | IDocumentMeta | IDocument | undefined;
onClose: () => void;
+ anchor?: { entityId: string; occurence?: number };
}
-const DocumentModalEdit: React.FC = ({ onClose, document }) => {
+const DocumentModalEdit: React.FC = ({
+ onClose,
+ document,
+ anchor,
+}) => {
const [show, setShow] = useState(false);
-
useEffect(() => {
setShow(true);
}, []);
-
const [windowWidth, windowHeight] = useWindowSize();
+ // const [annotatorInitialized, setAnnotatorInitialized] = useState(false);
+
return (
= ({ onClose, document }) => {
width={965}
height={windowHeight - 180}
displayLineNumbers={true}
+ hlEntities={[]}
+ storedAnnotatorScroll={0}
+ forwardAnnotator={(newAnnotator) => {
+ // if (!annotatorInitialized && newAnnotator && anchor?.entityId) {
+ anchor?.entityId &&
+ newAnnotator?.scrollToAnchor(anchor?.entityId);
+ // setAnnotatorInitialized(true);
+ // }
+ }}
/>
) : (
Document not found
)}
- {/*
-
- */}
);
};
diff --git a/packages/client/src/components/advanced/EntityTag/EntityTag.tsx b/packages/client/src/components/advanced/EntityTag/EntityTag.tsx
index dbcc7b4ff..21786d9c3 100644
--- a/packages/client/src/components/advanced/EntityTag/EntityTag.tsx
+++ b/packages/client/src/components/advanced/EntityTag/EntityTag.tsx
@@ -72,6 +72,9 @@ export const EntityTag: React.FC = ({
const draggedEntity: DraggedEntityReduxItem = useAppSelector(
(state) => state.draggedEntity
);
+ if (entity === undefined) {
+ return null;
+ }
if (!entity) {
return null;
diff --git a/packages/client/src/components/advanced/GlobalValidationsModal/GlobalValidationsModal.tsx b/packages/client/src/components/advanced/GlobalValidationsModal/GlobalValidationsModal.tsx
new file mode 100644
index 000000000..6d0aaeca7
--- /dev/null
+++ b/packages/client/src/components/advanced/GlobalValidationsModal/GlobalValidationsModal.tsx
@@ -0,0 +1,329 @@
+import {
+ EProtocolTieType,
+ ITerritoryValidation,
+} from "@shared/types/territory";
+import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
+import api from "api";
+import {
+ Button,
+ ButtonGroup,
+ Loader,
+ Modal,
+ ModalContent,
+ ModalFooter,
+ ModalHeader,
+ Submit,
+} from "components";
+import { ValidationRule } from "components/advanced";
+import React, { useEffect, useMemo, useState } from "react";
+import { FaPlus, FaToggleOff, FaToggleOn } from "react-icons/fa";
+import { rootTerritoryId } from "Theme/constants";
+import {
+ StyledBlockSeparator,
+ StyledGridForm,
+ StyledGridFormLabel,
+ StyledGridSectionHeading,
+ StyledSectionHeader,
+ StyledToggleWrap,
+ StyledValidationCount,
+ StyledValidationList,
+} from "./GlobalValidationsModalStyles";
+import { deepCopy } from "utils/utils";
+import { IEntity } from "@shared/types";
+import { useSearchParams } from "hooks";
+import {
+ entityKeys,
+ territoryKeys,
+ valencyKeys,
+ globalValidationsDict,
+ ValidationKey,
+} from "@shared/enums/warning";
+import { GlobalValidationsSettingsRow } from "./GlobalValidationsSettingsRow";
+import { ISetting } from "@shared/types/settings";
+import { toast } from "react-toastify";
+
+const initialRulesState: Record = Object.keys(
+ globalValidationsDict
+).reduce((acc, key) => {
+ acc[key as ValidationKey] = true;
+ return acc;
+}, {} as Record);
+
+const initValidation: ITerritoryValidation = {
+ detail: "",
+ entityClasses: [],
+ classifications: [],
+ allowedEntities: [],
+ allowedClasses: [],
+ propType: [],
+ tieType: EProtocolTieType.Property,
+};
+
+interface GlobalValidationsModal {
+ setShowGlobalValidations: React.Dispatch>;
+}
+export const GlobalValidationsModal: React.FC = ({
+ setShowGlobalValidations,
+}) => {
+ const [showModal, setShowModal] = useState(false);
+ useEffect(() => {
+ setShowModal(true);
+ }, []);
+
+ const {
+ status,
+ data: rootTerritory,
+ error: entityError,
+ isFetching,
+ } = useQuery({
+ queryKey: ["entity", rootTerritoryId],
+ queryFn: async () => {
+ const res = await api.detailGet(rootTerritoryId);
+ return res.data;
+ },
+ enabled: api.isLoggedIn(),
+ });
+
+ const {
+ status: settingsStatus,
+ data: settings,
+ error: settingsError,
+ isFetching: settingsIsFetching,
+ } = useQuery({
+ queryKey: ["settings"],
+ queryFn: async () => {
+ const res = await api.settingGroupGet("validations");
+ return res.data.data?.settings;
+ },
+ enabled: api.isLoggedIn(),
+ });
+
+ const validations = rootTerritory?.data.validations || [];
+
+ const queryClient = useQueryClient();
+
+ const updateEntityMutation = useMutation({
+ mutationFn: async (changes: Partial) =>
+ await api.entityUpdate(rootTerritoryId, changes),
+
+ onSuccess: (data, variables) => {
+ queryClient.invalidateQueries({ queryKey: ["entity"] });
+ },
+ });
+
+ const updateSettingsMutation = useMutation({
+ mutationFn: async (newSettings: Omit[]) =>
+ await api.settingGroupUpdate("validations", newSettings),
+
+ onSuccess: (data, variables) => {
+ queryClient.invalidateQueries({ queryKey: ["settings"] });
+ toast.success("settings updated");
+ },
+ });
+
+ const [tempIndexToRemove, setTempIndexToRemove] = useState(
+ false
+ );
+
+ const initValidationRule = () => {
+ updateEntityMutation.mutate({
+ data: {
+ validations: validations
+ ? [...validations, initValidation]
+ : [initValidation],
+ },
+ });
+ };
+
+ const removeValidationRule = (indexToRemove: number) => {
+ updateEntityMutation.mutate({
+ data: {
+ validations: (validations as ITerritoryValidation[])?.filter(
+ (_, index) => index !== indexToRemove
+ ),
+ },
+ });
+ setTempIndexToRemove(false);
+ };
+
+ const handleUpdateValidation = (
+ key: number,
+ changes: Partial
+ ) => {
+ const validationsCopy = deepCopy(validations as ITerritoryValidation[]);
+ const updatedObject: ITerritoryValidation = {
+ ...validationsCopy[key],
+ ...changes,
+ };
+ const newValidation = [
+ ...validationsCopy.slice(0, key),
+ updatedObject,
+ ...validationsCopy.slice(key + 1),
+ ];
+ updateEntityMutation.mutate({
+ data: {
+ validations: newValidation,
+ },
+ });
+ };
+
+ const [rules, setRules] =
+ useState>(initialRulesState);
+
+ const settingsKeyValue = useMemo(
+ () =>
+ settings?.reduce((acc, setting) => {
+ acc[setting.id as ValidationKey] = setting.value as boolean;
+ return acc;
+ }, {} as Record),
+ [settings]
+ );
+
+ useEffect(() => {
+ if (settingsKeyValue) {
+ setRules(settingsKeyValue);
+ }
+ }, [JSON.stringify(settingsKeyValue)]);
+
+ const toggleRule = (key: ValidationKey) => {
+ setRules((prev) => ({ ...prev, [key]: !prev[key] }));
+ };
+
+ return (
+ <>
+ setShowGlobalValidations(false)}
+ width={650}
+ >
+ setShowGlobalValidations(false)}
+ />
+
+
+
+ Valency validations
+
+
+ {valencyKeys.map((val, key) => (
+ toggleRule(val)}
+ />
+ ))}
+
+
+ Entity validations
+
+
+ {entityKeys.map((val, key) => (
+ toggleRule(val)}
+ />
+ ))}
+
+
+ Territory validations
+
+
+ {territoryKeys.map((val, key) => (
+ toggleRule(val)}
+ />
+ ))}
+
+
+ {rootTerritory && (
+ <>
+
+ Root T validation
+ {`${validations?.length} Root T validations`}
+
+ }
+ label="new validation rule"
+ color="primary"
+ onClick={initValidationRule}
+ />
+
+
+
+ {(validations as ITerritoryValidation[])?.map(
+ (validation, key) => {
+ return (
+
+
+ ) => {
+ handleUpdateValidation(key, changes);
+ }}
+ removeValidationRule={() => {
+ setTempIndexToRemove(key);
+ }}
+ isInsideTemplate={false}
+ userCanEdit
+ />
+ {key !== validations.length - 1 && (
+
+ )}
+
+ );
+ }
+ )}
+
+ >
+ )}
+
+
+
+
+
+
+
+
+
+ {
+ tempIndexToRemove !== false &&
+ removeValidationRule(tempIndexToRemove);
+ }}
+ onCancel={() => setTempIndexToRemove(false)}
+ />
+ >
+ );
+};
diff --git a/packages/client/src/components/advanced/GlobalValidationsModal/GlobalValidationsModalStyles.tsx b/packages/client/src/components/advanced/GlobalValidationsModal/GlobalValidationsModalStyles.tsx
new file mode 100644
index 000000000..ff4b471d7
--- /dev/null
+++ b/packages/client/src/components/advanced/GlobalValidationsModal/GlobalValidationsModalStyles.tsx
@@ -0,0 +1,68 @@
+import styled from "styled-components";
+
+export const StyledGridForm = styled.div`
+ display: grid;
+ grid-template-columns: auto 1fr;
+ gap: 1rem;
+ align-items: center;
+ margin-left: 0.8rem;
+ margin-bottom: 2.5rem;
+`;
+export const StyledGridSectionHeading = styled.div`
+ display: grid;
+ justify-content: end;
+ align-items: center;
+ font-weight: ${({ theme }) => theme.fontWeight["bold"]};
+ margin-top: 1rem;
+`;
+interface StyledGridFormLabel {
+ $disabled: boolean;
+ ref: React.Dispatch>;
+}
+export const StyledGridFormLabel = styled.div`
+ display: grid;
+ justify-content: end;
+ align-items: center;
+ color: ${({ theme, $disabled }) => ($disabled ? theme.color["greyer"] : "")};
+`;
+export const StyledValidationList = styled.div`
+ display: flex;
+ flex-direction: column;
+ row-gap: 1.5rem;
+`;
+export const StyledBlockSeparator = styled.div`
+ width: 100%;
+ grid-column: span 2;
+ border-top: 1px dashed grey;
+`;
+interface StyledToggleWrap {
+ $active: boolean;
+ $disabled: boolean;
+}
+export const StyledToggleWrap = styled.div`
+ display: inline-flex;
+ align-items: center;
+ gap: 0.5rem;
+ color: ${({ theme, $active, $disabled }) =>
+ $disabled
+ ? theme.color["greyer"]
+ : $active
+ ? theme.color["info"]
+ : theme.color["danger"]};
+ cursor: ${({ $disabled }) => ($disabled ? "not-allowed" : "pointer")};
+`;
+
+export const StyledSectionHeader = styled.div`
+ display: flex;
+ align-items: center;
+ gap: 1rem;
+ font-weight: ${({ theme }) => theme.fontWeight.regular};
+ font-size: ${({ theme }) => theme.fontSize.lg};
+ margin-bottom: ${({ theme }) => theme.space[4]};
+ color: ${({ theme }) => theme.color["primary"]};
+`;
+
+export const StyledValidationCount = styled.p`
+ color: ${({ theme }) => theme.color["info"]};
+ font-size: ${({ theme }) => theme.fontSize["sm"]};
+`;
diff --git a/packages/client/src/components/advanced/GlobalValidationsModal/GlobalValidationsSettingsRow.tsx b/packages/client/src/components/advanced/GlobalValidationsModal/GlobalValidationsSettingsRow.tsx
new file mode 100644
index 000000000..4f75f7921
--- /dev/null
+++ b/packages/client/src/components/advanced/GlobalValidationsModal/GlobalValidationsSettingsRow.tsx
@@ -0,0 +1,69 @@
+import {
+ globalValidationsDict,
+ ValidationKey,
+ WarningKey,
+ WarningTypeEnums,
+} from "@shared/enums/warning";
+import React, { useState } from "react";
+import {
+ StyledGridFormLabel,
+ StyledToggleWrap,
+} from "./GlobalValidationsModalStyles";
+import { FaToggleOn, FaToggleOff } from "react-icons/fa";
+import { Tooltip } from "components";
+
+interface GlobalValidationsSettingsRow {
+ validation: ValidationKey;
+ active: boolean;
+ toggleRule: () => void;
+}
+export const GlobalValidationsSettingsRow: React.FC<
+ GlobalValidationsSettingsRow
+> = ({ validation, active, toggleRule }) => {
+ const [referenceElement, setReferenceElement] =
+ useState(null);
+ const [showTooltip, setShowTooltip] = useState(false);
+ const tooltipLabel = globalValidationsDict[validation].label;
+ const tooltipContent = globalValidationsDict[validation].description;
+
+ const isDisabled = !globalValidationsDict[validation].editAllowed;
+ return (
+ <>
+ setShowTooltip(true)}
+ onMouseLeave={() => setShowTooltip(false)}
+ $disabled={isDisabled}
+ >
+ {globalValidationsDict[validation].label}
+
+
+ !isDisabled && toggleRule()}
+ >
+ {active ? (
+ <>
+ active
+ >
+ ) : (
+ <>
+ inactive
+ >
+ )}
+
+
+
+ {tooltipContent && (
+ {tooltipContent}
}
+ visible={showTooltip}
+ referenceElement={referenceElement}
+ // position={tooltipPosition}
+ />
+ )}
+ >
+ );
+};
diff --git a/packages/client/src/components/advanced/Menu/Menu.tsx b/packages/client/src/components/advanced/Menu/Menu.tsx
index ac714cb86..bf920f0e0 100644
--- a/packages/client/src/components/advanced/Menu/Menu.tsx
+++ b/packages/client/src/components/advanced/Menu/Menu.tsx
@@ -92,7 +92,12 @@ export const Menu: React.FC