diff --git a/packages/client/src/components/basic/Message/Message.tsx b/packages/client/src/components/basic/Message/Message.tsx index 75c4febcf..f8da78e3e 100644 --- a/packages/client/src/components/basic/Message/Message.tsx +++ b/packages/client/src/components/basic/Message/Message.tsx @@ -1,9 +1,16 @@ import { WarningTypeEnums } from "@shared/enums"; import { IEntity, IWarning } from "@shared/types"; +import api from "api"; +import { EntityTag } from "components/advanced"; import React, { useEffect, useState } from "react"; import { TiWarningOutline } from "react-icons/ti"; -import { StyledMessage } from "./MessateStyles"; +import { EntityColors } from "types"; import { getShortLabelByLetterCount } from "utils/utils"; +import { + StyledMessage, + StyledMessageTValidationContent, +} from "./MessageStyles"; +import theme from "Theme/theme"; interface Message { warning: IWarning; @@ -19,18 +26,126 @@ export const Message: React.FC = ({ warning, entities }) => { pa: "Pseudo-Actant", }; - const [entity, setEntity] = useState(false); + const [extendedEntities, setExtendedEntities] = useState< + Record + >(entities ?? {}); + + useEffect((): void => { + const entitiesOut = []; + const newEntityIds: string[] = []; + + async function getEntities(eids: string[]) { + const extractedEntities: Record = { ...entities }; + for (const eid of eids) { + const entityRes = await api.entitiesGet(eid); + if (entityRes?.data && !entities?.[eid]) { + extractedEntities[eid] = entityRes.data; + } + } + setExtendedEntities(extractedEntities); + } + + const isInEntities = (eid: string) => { + return entities && entities[eid] ? true : false; + }; + + warning?.validation?.propType?.forEach((eid) => { + if (eid && !isInEntities(eid)) { + newEntityIds.push(eid); + } + }); + + warning?.validation?.allowedEntities?.forEach((eid) => { + if (eid && !isInEntities(eid)) { + newEntityIds.push(eid); + } + }); + + if (newEntityIds.length > 0) { + getEntities(newEntityIds); + } + }, [warning, entities]); + + const [entity, setEntity] = useState(undefined); useEffect(() => { if (warning.position?.entityId && entities) { const entity = entities[warning.position.entityId]; if (entity) { setEntity(entity); + } else { + setEntity(undefined); } } }, [warning, entities]); - function getWarningMessage({ type, position }: IWarning): JSX.Element { + function renderEntityTags(entityIds: (string | undefined)[]): JSX.Element { + return ( + <> + {entityIds.map((eid) => { + if (eid) { + const entity = extendedEntities?.[eid]; + if (entity) { + return ( +
+ +
+ ); + } + } + return <>{eid}; + })} + + ); + } + function renderValidationLabel(warning: IWarning): JSX.Element { + if (warning.validation?.detail) { + return ( + + {" "} + [{warning.validation?.detail}] + + ); + } else { + return <>; + } + } + + function renderEntityClasses( + entityClasses: string[] | undefined + ): JSX.Element { + if (entityClasses) { + return ( + <> + {entityClasses.map((entityClass, index) => { + const classItem = EntityColors[entityClass]; + const colorName = classItem?.color ?? "transparent"; + const color = theme.color[colorName] as string; + + return ( + + + {classItem.entityClass} + + {index < entityClasses.length - 1 ? ", " : ""} + + ); + })} + + ); + } else { + return <>; + } + } + + function getWarningMessage(): JSX.Element { + const { type, position } = warning; const positionName = position?.subSection ? ` - ${positionObject[position.subSection]}` : ""; @@ -132,10 +247,84 @@ export const Message: React.FC = ({ warning, entities }) => { ); case WarningTypeEnums.MAEE: return Missing Action/event equivalent; + case WarningTypeEnums.LM: return Missing label language attribute; + case WarningTypeEnums.PSM: return Missing part of speech attribute; + + // T-based validations + case WarningTypeEnums.TVEP: + return ( + + {renderEntityTags([warning?.position?.entityId])} should have a + property {renderValidationLabel(warning)} + + ); + case WarningTypeEnums.TVEPT: + return ( + + T-based validations: + {renderEntityTags([warning?.position?.entityId])} is missing a + required property with type{" "} + {renderEntityTags(warning.validation?.propType ?? [])} + {renderValidationLabel(warning)} + + ); + case WarningTypeEnums.TVEPV: + const classAllowed = + warning.validation?.allowedClasses && + warning.validation?.allowedClasses.length > 0; + return ( + + {renderEntityTags([warning?.position?.entityId])} has a wrong + property type {renderEntityTags(warning.validation?.propType ?? [])} + {classAllowed && ( + <> + - should be of type{" "} + {renderEntityClasses(warning.validation?.allowedClasses)} + + )} + {!classAllowed && ( + <> + - should be of values{" "} + {renderEntityTags(warning.validation?.allowedEntities ?? [])} + + )} + {renderValidationLabel(warning)} + + ); + case WarningTypeEnums.TVEC: + return ( + + {renderEntityTags([warning?.position?.entityId])} should have a + classification {renderValidationLabel(warning)} + + ); + case WarningTypeEnums.TVECE: + return ( + + {renderEntityTags([warning?.position?.entityId])} is not classified + with valid entity{" "} + {renderEntityClasses(warning.validation?.allowedClasses)}{" "} + {renderValidationLabel(warning)} + + ); + case WarningTypeEnums.TVER: + return ( + + {renderEntityTags([warning?.position?.entityId])} should have a + reference {renderValidationLabel(warning)} + + ); + case WarningTypeEnums.TVERE: + return ( + + {renderEntityTags([warning?.position?.entityId])} is not referenced + to a valid entity {renderValidationLabel(warning)} + + ); default: return <>; } @@ -146,7 +335,7 @@ export const Message: React.FC = ({ warning, entities }) => {
- {getWarningMessage(warning)} + {getWarningMessage()} ); }; diff --git a/packages/client/src/components/basic/Message/MessateStyles.tsx b/packages/client/src/components/basic/Message/MessageStyles.tsx similarity index 86% rename from packages/client/src/components/basic/Message/MessateStyles.tsx rename to packages/client/src/components/basic/Message/MessageStyles.tsx index ca509dfb2..47986179f 100644 --- a/packages/client/src/components/basic/Message/MessateStyles.tsx +++ b/packages/client/src/components/basic/Message/MessageStyles.tsx @@ -14,3 +14,8 @@ export const StyledMessage = styled.div` font-size: ${({ theme }) => theme.fontSize["xs"]}; border: 1.5px solid ${({ theme }) => theme.color["warningBorder"]}; `; + +export const StyledMessageTValidationContent = styled.div` + display: inline; + items-align: center; +`; diff --git a/packages/client/src/pages/Main/containers/EntityDetailBox/EntityDetail/EntityDetailProtocol/EntityDetailProtocol.tsx b/packages/client/src/pages/Main/containers/EntityDetailBox/EntityDetail/EntityDetailProtocol/EntityDetailProtocol.tsx index fc47f246e..09223cf81 100644 --- a/packages/client/src/pages/Main/containers/EntityDetailBox/EntityDetail/EntityDetailProtocol/EntityDetailProtocol.tsx +++ b/packages/client/src/pages/Main/containers/EntityDetailBox/EntityDetail/EntityDetailProtocol/EntityDetailProtocol.tsx @@ -99,7 +99,7 @@ export const EntityDetailProtocol: React.FC = ({ Guidelines resource - {guidelinesResource.length > 0 ? ( + {guidelinesResource && guidelinesResource.length > 0 ? ( theme.space["1"]}; grid-auto-flow: row; + margin-top: ${({ theme }) => theme.space[4]}; `; export const StyledDetailContentRowValueID = styled.div` diff --git a/packages/client/src/pages/Main/containers/EntityDetailBox/EntityDetail/EntityDetailValidationSection/EntityDetailValidationRule/EntityDetailValidationRule.tsx b/packages/client/src/pages/Main/containers/EntityDetailBox/EntityDetail/EntityDetailValidationSection/EntityDetailValidationRule/EntityDetailValidationRule.tsx index 1777640a3..05f575e50 100644 --- a/packages/client/src/pages/Main/containers/EntityDetailBox/EntityDetail/EntityDetailValidationSection/EntityDetailValidationRule/EntityDetailValidationRule.tsx +++ b/packages/client/src/pages/Main/containers/EntityDetailBox/EntityDetail/EntityDetailValidationSection/EntityDetailValidationRule/EntityDetailValidationRule.tsx @@ -56,7 +56,15 @@ export const EntityDetailValidationRule: React.FC< return allowedEntities !== undefined && allowedEntities.length > 0; }, [allowedEntities]); - const onlyResourceAllowed = tieType === EProtocolTieType.Reference; + const allowedEntitiesClasses = useMemo(() => { + if (tieType === EProtocolTieType.Classification) { + return [EntityEnums.Class.Concept]; + } + if (tieType === EProtocolTieType.Reference) { + return [EntityEnums.Class.Resource]; + } + return classesAll; + }, [tieType]); return ( @@ -138,6 +146,8 @@ export const EntityDetailValidationRule: React.FC< updateValidationRule({ tieType: EProtocolTieType.Classification, propType: [], + allowedClasses: [], + allowedEntities: [], }), selected: tieType === EProtocolTieType.Classification, }, @@ -195,7 +205,7 @@ export const EntityDetailValidationRule: React.FC< )} {/* Allowed classes */} - {!onlyResourceAllowed && ( + {tieType === EProtocolTieType.Property && ( <> Allowed E types - {!onlyResourceAllowed ? "Allowed E values" : "Allowed Resources"} + {tieType === EProtocolTieType.Classification && "Allowed Concepts"} + {tieType === EProtocolTieType.Reference && "Allowed Resources"} + {tieType === EProtocolTieType.Property && "Allowed E values"} {allowedEntities?.map((entityId, key) => ( @@ -234,9 +246,7 @@ export const EntityDetailValidationRule: React.FC< ))} {!(!userCanEdit && allowedEntities && allowedEntities.length > 0) && ( { updateValidationRule({ @@ -250,6 +260,7 @@ export const EntityDetailValidationRule: React.FC< /> )} + {/* Detail */} Detail / Notes { if (classList.includes(EntityEnums.Extension.Any)) { - return Any; + return ; } if ( classList.includes(EntityEnums.Extension.Empty) || diff --git a/packages/server/src/models/entity/entity.test.ts b/packages/server/src/models/entity/entity.test.ts index c2fea7e63..ee70a19b3 100644 --- a/packages/server/src/models/entity/entity.test.ts +++ b/packages/server/src/models/entity/entity.test.ts @@ -3,6 +3,7 @@ import { Db } from "@service/rethink"; import Entity from "./entity"; import Statement, { StatementActant, + StatementAction, StatementTerritory, } from "@models/statement/statement"; import { clean } from "@modules/common.test"; @@ -101,7 +102,7 @@ describe("test Entity.beforeSave", () => { props: [ new Prop({ type: new PropSpec({ - entityId: notTpl.id + entityId: notTpl.id, }), }), ], @@ -128,7 +129,7 @@ describe("test Entity.beforeSave", () => { props: [ new Prop({ type: new PropSpec({ - entityId: tpl.id + entityId: tpl.id, }), }), ], diff --git a/packages/server/src/models/statement/response.test.ts b/packages/server/src/models/statement/response.test.ts index 4bc786e27..9cf79c0a1 100644 --- a/packages/server/src/models/statement/response.test.ts +++ b/packages/server/src/models/statement/response.test.ts @@ -2,13 +2,16 @@ import Action from "@models/action/action"; import Group from "@models/group/group"; import Location from "@models/location/location"; import Person from "@models/person/person"; +import { newMockRequest } from "@modules/common.test"; +import { Db } from "@service/rethink"; import { EntityEnums, StatementEnums, WarningTypeEnums } from "@shared/enums"; -import { IAction, IEntity } from "@shared/types"; +import { IEntity } from "@shared/types"; import { InternalServerError } from "@shared/types/errors"; import "ts-jest"; import { ResponseStatement } from "./response"; -import Statement, { StatementActant, StatementAction } from "./statement"; +import Statement, { StatementActant } from "./statement"; import { prepareStatement } from "./statement.test"; +import Territory from "@models/territory/territory"; class MockResponse extends ResponseStatement { static new(): MockResponse { @@ -27,7 +30,7 @@ class MockResponse extends ResponseStatement { action.data.entities[pos] = map[key]; } - this.data.actions.push(new StatementAction({ actionId: action.id })); + // this.data.actions.push(new StatementAction({ actionId: action.id })); this.entities[action.id] = action; } @@ -95,16 +98,21 @@ describe("models/statement/response", function () { }); describe("test ResponseStatement.getWarnings", function () { - test("not prepared entity should thrown an error", () => { + const db = new Db(); + const request = newMockRequest(db); + test("not prepared entity should thrown an error", async () => { const [, statement] = prepareStatement(); const response = new ResponseStatement(statement); - expect(() => response.getWarnings()).toThrowError(InternalServerError); + + const warning = await response.getWarnings(request); + + expect(() => warning).toThrowError(InternalServerError); }); - test("no action", () => { + test("no action", async () => { const response = MockResponse.new(); - const warnings = response.getWarnings(); + const warnings = await response.getWarnings(request); expect(warnings.find((w) => w.type === WarningTypeEnums.NA)).toBeTruthy(); }); @@ -736,4 +744,43 @@ describe("models/statement/response", function () { }); }); }); + + // describe("test T-based validations", function () { + // const db = new Db(); + // const request = newMockRequest(db); + // test("test TVEC", async () => { + // const territory1 = new Territory({ + // id: "T1", + // data: { + // validations: [], + // parent: { + // territoryId: "T2", + // order: 1, + // }, + // }, + // }); + // const territory2 = new Territory({ + // id: "T2", + // data: { + // validations: [], + // parent: false, + // }, + // }); + + // const statement = new Statement({ + // id: "statement", + // data: { + // text: "", + // actions: [], + // actants: [], + // tags: [], + // territory: { territoryId: "T1", order: 1 }, + // }, + // }); + + // const sResponse = new ResponseStatement(statement); + + // await sResponse.prepare(request); + // }); + // }); }); diff --git a/packages/server/src/models/statement/response.ts b/packages/server/src/models/statement/response.ts index 58c451bca..c10e10d18 100644 --- a/packages/server/src/models/statement/response.ts +++ b/packages/server/src/models/statement/response.ts @@ -5,11 +5,17 @@ import { IProp, IResponseStatement, IStatement, + ITerritory, } from "@shared/types"; import { OrderType } from "@shared/types/response-statement"; -import { IWarning, IWarningPosition, IWarningPositionSection } from "@shared/types/warning"; +import { + IWarning, + IWarningPosition, + IWarningPositionSection, +} from "@shared/types/warning"; import { ActionEntity } from "@models/action/action"; +import treeCache from "@service/treeCache"; import { WarningTypeEnums } from "@shared/enums"; import { InternalServerError } from "@shared/types/errors"; import { Connection } from "rethinkdb-ts"; @@ -17,6 +23,13 @@ import { IRequest } from "src/custom_typings/request"; import Entity from "../entity/entity"; import { PositionRules } from "./PositionRules"; import Statement from "./statement"; +import { + EProtocolTieType, + ITerritoryValidation, +} from "@shared/types/territory"; +import { ResponseEntityDetail } from "@models/entity/response"; +import { getEntityClass } from "@models/factory"; +import { findEntityById } from "@service/shorthands"; export class ResponseStatement extends Statement implements IResponseStatement { entities: { [key: string]: IEntity }; @@ -35,7 +48,7 @@ export class ResponseStatement extends Statement implements IResponseStatement { this.right = this.getUserRoleMode(req.getUserOrFail()); await this.prepareEntities(req.db.connection); this.elementsOrders = this.prepareElementsOrders(); - this.warnings = this.getWarnings(); + this.warnings = await this.getWarnings(req); } /** @@ -74,11 +87,13 @@ export class ResponseStatement extends Statement implements IResponseStatement { */ newStatementWarning( warningType: WarningTypeEnums, - position: IWarningPosition + position: IWarningPosition, + validation?: ITerritoryValidation ): IWarning { return { type: warningType, origin: this.id, + validation, position, }; } @@ -90,6 +105,7 @@ export class ResponseStatement extends Statement implements IResponseStatement { */ getEntity(id: string): IEntity { const entity = this.entities[id]; + if (!entity) { throw new InternalServerError(`Entity ${id} not preloaded`); } @@ -140,6 +156,210 @@ export class ResponseStatement extends Statement implements IResponseStatement { return warnings; } + /** + * check all avalidation warnings for single entity + */ + async getTValidationWarnings(req: IRequest): Promise { + let warnings: IWarning[] = []; + + console.log(""); + console.log("!!! VALIDATION !!!", this.id); + + const parentTId = this.data.territory?.territoryId as string; + + const entitiesFull: ResponseEntityDetail[] = []; + + const allEntities = [ + ...this.data.actants.map((a) => a.entityId), + ...this.data.actions.map((a) => a.actionId), + ]; + + for (const ai in allEntities) { + const entityId = allEntities[ai]; + const entityData = this.getEntity(entityId); + const entityModel = getEntityClass({ ...entityData }); + const entity = new ResponseEntityDetail(entityModel); + + await entity.prepare(req); + + entitiesFull.push(entity); + } + + if (parentTId) { + const lineageTIds = [parentTId, ...treeCache.tree.idMap[parentTId].path]; + + for (const tId of lineageTIds) { + const tEntity = this.getEntity(tId) as ITerritory; + const tValidations = tEntity.data.validations; + + for (const tValidation of tValidations ?? []) { + const { + detail, + entityClasses, + classifications, + tieType, + propType, + allowedClasses, + allowedEntities, + } = tValidation; + + const addNewValidationWarning = ( + entityId: string, + code: WarningTypeEnums + ) => { + warnings.push( + this.newStatementWarning( + code, + { + section: IWarningPositionSection.Statement, + subSection: `statement`, + entityId: entityId, + }, + tValidation + ) + ); + }; + + const entitiesToCheck = entitiesFull + .filter((entity) => { + // falls under entity Classes + if (!entityClasses || !entityClasses.length) { + // no entity class condition + return true; + } + + return entityClasses.includes(entity.class); + }) + .filter((entity) => { + // falls under classification condition + if (!classifications || !classifications.length) { + // there is no classification condition + return true; + } + const claEntities = entity.relations.CLA?.connections?.map( + (c) => entity.entities[c.entityIds[1]] + ); + + // at least one required classifications is fullfilled + return classifications.some((classCondition) => + claEntities?.map((c) => c.id).includes(classCondition) + ); + }); + + for (const ei in entitiesToCheck) { + const entity = entitiesToCheck[ei]; + // CLASSIFICATION TIE + if (tieType === EProtocolTieType.Classification) { + if (!allowedEntities || !allowedEntities.length) { + // no condition set, so we need at least one classification + if (!entity.relations.CLA?.connections?.length) { + addNewValidationWarning(entity.id, WarningTypeEnums.TVEC); + } + } else { + // classifications of the entity + const claEntities = entity.relations.CLA?.connections?.map( + (c) => entity.entities[c.entityIds[1]] + ); + if ( + !allowedEntities.some((classCondition) => + claEntities?.map((c) => c.id).includes(classCondition) + ) + ) { + addNewValidationWarning(entity.id, WarningTypeEnums.TVECE); + } + } + } + + // REFERENCE TIE + else if (tieType === EProtocolTieType.Reference) { + const eReferences = entity.references; + // at least one reference (any) needs to be assigned to the E + if (!allowedEntities || !allowedEntities.length) { + if (eReferences.length === 0) { + addNewValidationWarning(entity.id, WarningTypeEnums.TVER); + } + } else { + // at least one reference needs to be of the allowed entity + if ( + !eReferences.some((r) => + allowedEntities?.includes(r.resource) + ) + ) { + addNewValidationWarning(entity.id, WarningTypeEnums.TVERE); + } + } + } + + // PROPERTY TIE + else if (tieType === EProtocolTieType.Property) { + // at least one property needs to be assigned to the E + if ( + (!allowedClasses || !allowedClasses.length) && + (!allowedEntities || !allowedEntities.length) && + (!propType || !propType.length) + ) { + if (entity.props.length === 0) { + addNewValidationWarning(entity.id, WarningTypeEnums.TVEP); + } + } + + // type is defined but value is empty + else if ( + propType?.length && + !allowedEntities?.length && + !allowedClasses?.length + ) { + if ( + !entity.props.some((p) => propType?.includes(p.type.entityId)) + ) { + addNewValidationWarning(entity.id, WarningTypeEnums.TVEPT); + } + } + + // type is defined, and value classes are defined + else if (propType?.length && allowedClasses?.length) { + let passed = true; + for (const pi in entity.props) { + const p = entity.props[pi]; + const propValueEntityId = p.value.entityId; + const propValueEntity = await findEntityById( + req.db, + propValueEntityId + ); + console.log(allowedClasses, propValueEntity); + if ( + propType?.includes(p.type.entityId) && + !allowedClasses?.includes(propValueEntity.class) + ) { + passed = false; + } + } + if (!passed) { + addNewValidationWarning(entity.id, WarningTypeEnums.TVEPV); + } + } + // type is defined, and value entities are defined + else if (propType?.length && allowedEntities?.length) { + console.log(entity.props, propType, allowedEntities); + if ( + !entity.props.some( + (p) => + propType?.includes(p.type.entityId) && + allowedEntities.includes(p.value.entityId) + ) + ) { + addNewValidationWarning(entity.id, WarningTypeEnums.TVEPV); + } + } + } + } + } + } + } + + return warnings; + } + /** * checks actions -> actants relations for single position and generates appropriate IWarning entries * @param position @@ -235,9 +455,12 @@ export class ResponseStatement extends Statement implements IResponseStatement { * get a list of all warnings for actions -> actants relations * @returns list of warnings */ - getWarnings(): IWarning[] { + async getWarnings(req: IRequest): Promise { let warnings: IWarning[] = []; + const tbasedWarnings = await this.getTValidationWarnings(req); + warnings = warnings.concat(tbasedWarnings); + if (!this.data.actions.length) { warnings.push(this.newStatementWarning(WarningTypeEnums.NA, {})); return warnings; @@ -365,3 +588,6 @@ export class ResponseStatement extends Statement implements IResponseStatement { }); } } +function findEntity(propValueEntityId: string) { + throw new Error("Function not implemented."); +} diff --git a/packages/server/src/models/statement/statement.ts b/packages/server/src/models/statement/statement.ts index 101a030bd..1fde94df8 100644 --- a/packages/server/src/models/statement/statement.ts +++ b/packages/server/src/models/statement/statement.ts @@ -500,12 +500,22 @@ class Statement extends Entity implements IStatement { }); }); - if (this.data.territory) { - entitiesIds[this.data.territory.territoryId] = null; + // append territory lineage to the root T + const parentT = this.data.territory?.territoryId; + if (parentT) { + const treeCacheInstance = treeCache.tree.idMap[parentT]; + const lineageTIds = [ + parentT, + ...(treeCacheInstance ? treeCacheInstance.path : []), + ]; + if (lineageTIds) { + lineageTIds.forEach((tid) => { + entitiesIds[tid] = null; + }); + } } this.data.tags.forEach((t) => (entitiesIds[t] = null)); - return Object.keys(entitiesIds).filter((id) => !!id); } diff --git a/packages/shared/enums/warning.ts b/packages/shared/enums/warning.ts index 381b43c2b..ef60f9ed6 100644 --- a/packages/shared/enums/warning.ts +++ b/packages/shared/enums/warning.ts @@ -20,4 +20,13 @@ export enum WarningTypeEnums { PSM = "PSM", // Part of speech is empty LM = "LM", // Language is missing VETM = "VETM", // Empty valency for Action + + // T-based validations + TVEP = "TVEP", // Property missing + TVEPT = "TVEPT", // Property wrong type + TVEPV = "TVEPV", // Property wrong value + TVEC = "TVEC", // Classification missing + TVECE = "TVECE", // Classification wrong entity + TVER = "TVER", // Reference missing + TVERE = "TVERE", // Reference wrong entity } diff --git a/packages/shared/types/warning.ts b/packages/shared/types/warning.ts index e29cdd0e6..7b9e156b1 100644 --- a/packages/shared/types/warning.ts +++ b/packages/shared/types/warning.ts @@ -1,9 +1,11 @@ import { WarningTypeEnums } from "../enums"; +import { ITerritoryValidation } from "./territory"; export interface IWarning { type: WarningTypeEnums; position?: IWarningPosition; origin: string; + validation?: ITerritoryValidation; } export interface IWarningPosition {