diff --git a/core/app/datasource/src/main/java/io/openk9/datasource/events/DatasourceEventBus.java b/core/app/datasource/src/main/java/io/openk9/datasource/events/DatasourceEventBus.java index af2fccb84..241ebddaf 100644 --- a/core/app/datasource/src/main/java/io/openk9/datasource/events/DatasourceEventBus.java +++ b/core/app/datasource/src/main/java/io/openk9/datasource/events/DatasourceEventBus.java @@ -1,8 +1,11 @@ package io.openk9.datasource.events; import io.quarkus.runtime.Startup; +import io.smallrye.reactive.messaging.rabbitmq.OutgoingRabbitMQMetadata; import org.eclipse.microprofile.reactive.messaging.Channel; import org.eclipse.microprofile.reactive.messaging.Emitter; +import org.eclipse.microprofile.reactive.messaging.Message; +import org.eclipse.microprofile.reactive.messaging.Metadata; import javax.enterprise.context.ApplicationScoped; import javax.inject.Inject; @@ -12,7 +15,16 @@ public class DatasourceEventBus { public void sendEvent(DatasourceMessage datasourceMessage) { - quoteRequestEmitter.send(datasourceMessage); + quoteRequestEmitter.send( + Message.of( + datasourceMessage, + Metadata.of(OutgoingRabbitMQMetadata + .builder() + .withDeliveryMode(2) + .build() + ) + ) + ); } @Inject diff --git a/core/app/datasource/src/main/java/io/openk9/datasource/graphql/DocTypeGraphqlResource.java b/core/app/datasource/src/main/java/io/openk9/datasource/graphql/DocTypeGraphqlResource.java index e1a379c25..84307d32c 100644 --- a/core/app/datasource/src/main/java/io/openk9/datasource/graphql/DocTypeGraphqlResource.java +++ b/core/app/datasource/src/main/java/io/openk9/datasource/graphql/DocTypeGraphqlResource.java @@ -92,6 +92,20 @@ public Uni> getDocTypeFieldsFromDocType( docTypeId, after, before, first, last, searchText, sortByList, notEqual); } + @Query + public Uni> getDocTypeFieldsFromDocTypeByParent( + @Id long docTypeId, + @Description("id of the parent docTypeField (0 if root )") long parentId, + @Description("fetching only nodes after this node (exclusive)") String after, + @Description("fetching only nodes before this node (exclusive)") String before, + @Description("fetching only the first certain number of nodes") Integer first, + @Description("fetching only the last certain number of nodes") Integer last, + String searchText, Set sortByList, + @Description("if notEqual is true, it returns unbound entities") @DefaultValue("false") boolean notEqual) { + return docTypeService.getDocTypeFieldsConnectionByParent( + docTypeId, parentId, after, before, first, last, searchText, sortByList, notEqual); + } + @Query public Uni getDocType(@Id long id) { return docTypeService.findById(id); diff --git a/core/app/datasource/src/main/java/io/openk9/datasource/index/mappings/MappingsKey.java b/core/app/datasource/src/main/java/io/openk9/datasource/index/mappings/MappingsKey.java index a4a17ef89..5290fc40a 100644 --- a/core/app/datasource/src/main/java/io/openk9/datasource/index/mappings/MappingsKey.java +++ b/core/app/datasource/src/main/java/io/openk9/datasource/index/mappings/MappingsKey.java @@ -4,6 +4,14 @@ public class MappingsKey { private final String key; private final String hashKey; + public static MappingsKey of(String key) { + return new MappingsKey(key); + } + + public static MappingsKey of(String key, String hashKey) { + return new MappingsKey(key, hashKey); + } + public MappingsKey(String key) { this(key, key); } diff --git a/core/app/datasource/src/main/java/io/openk9/datasource/index/mappings/MappingsUtil.java b/core/app/datasource/src/main/java/io/openk9/datasource/index/mappings/MappingsUtil.java index fa5adb5f1..3e79e9af7 100644 --- a/core/app/datasource/src/main/java/io/openk9/datasource/index/mappings/MappingsUtil.java +++ b/core/app/datasource/src/main/java/io/openk9/datasource/index/mappings/MappingsUtil.java @@ -214,84 +214,79 @@ public static Map docTypeFieldsToMappings( .filter(docTypeField -> docTypeField.getParentDocTypeField() == null) .collect(Collectors.toList()), new LinkedHashMap<>(), - "", - new MappingsKey("properties") + MappingsKey.of("properties") ); } private static Map createMappings_( Collection docTypeFields, - Map acc, String accPath, MappingsKey nextKey) { + Map acc, + MappingsKey nextKey) { for (DocTypeField docTypeField : docTypeFields) { - String fieldName = docTypeField - .getFieldName() - .replace(accPath.isEmpty() ? "" : accPath + ".", ""); - - FieldType fieldType = docTypeField.getFieldType(); - - boolean isObject = fieldType == FieldType.OBJECT || fieldType == FieldType.I18N; - - String[] fieldNamesArray = fieldName.split("\\."); - Map current = acc; - for (int i = 0; i < fieldNamesArray.length; i++) { - - String currentFieldName = fieldNamesArray[i]; + current = visit(nextKey, current); - boolean isLast = i == fieldNamesArray.length - 1; + if (docTypeField.getParentDocTypeField() == null) { + DocType docType = docTypeField.getDocType(); + String docTypeName = docType.getName(); + if (!docTypeName.equals("default")) { + current = visit(MappingsKey.of(docTypeName), current); - current = (Map) current.computeIfAbsent( - nextKey, k -> new LinkedHashMap<>()); + current = visit(MappingsKey.of("properties"), current); + } + } + String fieldName = docTypeField.getFieldName(); - current = (Map) current.computeIfAbsent( - new MappingsKey(currentFieldName), k -> new LinkedHashMap<>()); + FieldType fieldType = docTypeField.getFieldType(); - if (isLast) { + boolean isObject = fieldType == FieldType.OBJECT || fieldType == FieldType.I18N; - if (!isObject) { - current.put(new MappingsKey("type"), fieldType.getType()); + current = visit(MappingsKey.of(fieldName), current); - Analyzer analyzer = docTypeField.getAnalyzer(); + if (!isObject) { + current.put(MappingsKey.of("type"), fieldType.getType()); - if (analyzer != null) { - current.put(new MappingsKey("analyzer"), analyzer.getName()); - } + Analyzer analyzer = docTypeField.getAnalyzer(); - String fieldConfig = docTypeField.getJsonConfig(); + if (analyzer != null) { + current.put(MappingsKey.of("analyzer"), analyzer.getName()); + } - if (fieldConfig != null) { - JsonObject fieldConfigJson = new JsonObject(fieldConfig); - for (Map.Entry entry : fieldConfigJson) { - current.putIfAbsent(new MappingsKey(entry.getKey()), entry.getValue()); - } - } - } + String fieldConfig = docTypeField.getJsonConfig(); - Set subDocTypeFields = docTypeField.getSubDocTypeFields(); - - if (subDocTypeFields != null) { - createMappings_( - subDocTypeFields, - current, - accPath.isEmpty() - ? fieldName - : String.join(".", accPath, fieldName), - isObject - ? new MappingsKey("properties") - : new MappingsKey("fields")); + if (fieldConfig != null) { + JsonObject fieldConfigJson = new JsonObject(fieldConfig); + for (Map.Entry entry : fieldConfigJson) { + current.putIfAbsent(new MappingsKey(entry.getKey()), entry.getValue()); } } + } + Set subDocTypeFields = docTypeField.getSubDocTypeFields(); + + if (subDocTypeFields != null) { + createMappings_( + subDocTypeFields, + current, + isObject + ? MappingsKey.of("properties") + : MappingsKey.of("fields")); } + } return acc; } + private static Map visit(MappingsKey nextKey, Map current) { + return (Map) current.computeIfAbsent( + nextKey, k -> new LinkedHashMap<>()); + } + } diff --git a/core/app/datasource/src/main/java/io/openk9/datasource/model/DocType.java b/core/app/datasource/src/main/java/io/openk9/datasource/model/DocType.java index 64547587a..f782908d9 100644 --- a/core/app/datasource/src/main/java/io/openk9/datasource/model/DocType.java +++ b/core/app/datasource/src/main/java/io/openk9/datasource/model/DocType.java @@ -46,6 +46,8 @@ @Cacheable public class DocType extends K9Entity { + public static final String DEFAULT_NAME = "default"; + @Column(name = "name", nullable = false, unique = true) private String name; @@ -55,7 +57,8 @@ public class DocType extends K9Entity { @OneToMany( mappedBy = "docType", cascade = javax.persistence.CascadeType.ALL, - fetch = FetchType.LAZY + fetch = FetchType.LAZY, + orphanRemoval = true ) @ToString.Exclude @JsonIgnore diff --git a/core/app/datasource/src/main/java/io/openk9/datasource/model/DocTypeField.java b/core/app/datasource/src/main/java/io/openk9/datasource/model/DocTypeField.java index 835e08812..1fdc85c13 100644 --- a/core/app/datasource/src/main/java/io/openk9/datasource/model/DocTypeField.java +++ b/core/app/datasource/src/main/java/io/openk9/datasource/model/DocTypeField.java @@ -18,6 +18,8 @@ package io.openk9.datasource.model; import com.fasterxml.jackson.annotation.JsonIgnore; +import io.openk9.datasource.model.util.DocTypeFieldUtils; +import lombok.AccessLevel; import lombok.Getter; import lombok.RequiredArgsConstructor; import lombok.Setter; @@ -30,11 +32,16 @@ import javax.persistence.Entity; import javax.persistence.EnumType; import javax.persistence.Enumerated; +import javax.persistence.FetchType; import javax.persistence.JoinColumn; import javax.persistence.Lob; import javax.persistence.ManyToOne; import javax.persistence.OneToMany; +import javax.persistence.PostLoad; +import javax.persistence.PostPersist; +import javax.persistence.PostUpdate; import javax.persistence.Table; +import javax.persistence.Transient; import javax.persistence.UniqueConstraint; import java.util.LinkedHashSet; import java.util.Objects; @@ -62,7 +69,7 @@ public class DocTypeField extends BaseDocTypeField { private String fieldName; @ToString.Exclude - @ManyToOne(fetch = javax.persistence.FetchType.LAZY, cascade = javax.persistence.CascadeType.ALL) + @ManyToOne @JoinColumn(name = "doc_type_id") @JsonIgnore private DocType docType; @@ -111,6 +118,11 @@ public class DocTypeField extends BaseDocTypeField { @JsonIgnore private Set aclMappings = new LinkedHashSet<>(); + @Transient + @Getter(AccessLevel.NONE) + @Setter(AccessLevel.NONE) + private String path; + public Set getChildren() { return subDocTypeFields; } @@ -124,6 +136,14 @@ public Set getDocTypeFieldAndChildren() { return docTypeFields; } + public String getPath() { + if (path == null) { + refreshPath(); + } + + return path; + } + @Override public boolean equals(Object o) { if (this == o) { @@ -141,4 +161,12 @@ public boolean equals(Object o) { public int hashCode() { return getClass().hashCode(); } + + @PostLoad + @PostPersist + @PostUpdate + protected void refreshPath() { + this.path = DocTypeFieldUtils.fieldPath(this); + } + } \ No newline at end of file diff --git a/core/app/datasource/src/main/java/io/openk9/datasource/model/UserField.java b/core/app/datasource/src/main/java/io/openk9/datasource/model/UserField.java index c5b8c1430..0210b5b0f 100644 --- a/core/app/datasource/src/main/java/io/openk9/datasource/model/UserField.java +++ b/core/app/datasource/src/main/java/io/openk9/datasource/model/UserField.java @@ -93,7 +93,7 @@ public static void apply( if (value != null && !value.isBlank()) { boolQueryBuilder.should( - QueryBuilders.termQuery(docTypeField.getFieldName(), value)); + QueryBuilders.termQuery(docTypeField.getPath(), value)); } } @@ -105,7 +105,7 @@ public static void apply( if (values != null && !values.isEmpty()) { boolQueryBuilder.should( - QueryBuilders.termsQuery(docTypeField.getFieldName(), values)); + QueryBuilders.termsQuery(docTypeField.getPath(), values)); } } diff --git a/core/app/datasource/src/main/java/io/openk9/datasource/model/dto/DocTypeFieldDTO.java b/core/app/datasource/src/main/java/io/openk9/datasource/model/dto/DocTypeFieldDTO.java index 0103eea0e..e5a4e1f2d 100644 --- a/core/app/datasource/src/main/java/io/openk9/datasource/model/dto/DocTypeFieldDTO.java +++ b/core/app/datasource/src/main/java/io/openk9/datasource/model/dto/DocTypeFieldDTO.java @@ -19,6 +19,7 @@ import io.openk9.datasource.model.FieldType; import io.openk9.datasource.model.dto.util.K9EntityDTO; +import io.openk9.datasource.validation.Alnum; import io.openk9.datasource.validation.json.Json; import lombok.EqualsAndHashCode; import lombok.Getter; @@ -55,6 +56,7 @@ public class DocTypeFieldDTO extends K9EntityDTO { private Boolean exclude; @NotNull + @Alnum private String fieldName; @Json diff --git a/core/app/datasource/src/main/java/io/openk9/datasource/model/util/DocTypeFieldUtils.java b/core/app/datasource/src/main/java/io/openk9/datasource/model/util/DocTypeFieldUtils.java new file mode 100644 index 000000000..fc09b80d0 --- /dev/null +++ b/core/app/datasource/src/main/java/io/openk9/datasource/model/util/DocTypeFieldUtils.java @@ -0,0 +1,24 @@ +package io.openk9.datasource.model.util; + +import io.openk9.datasource.model.DocType; +import io.openk9.datasource.model.DocTypeField; + +public class DocTypeFieldUtils { + + public static String fieldPath(DocTypeField docTypeField) { + DocType docType = docTypeField.getDocType(); + return fieldPath(docType != null ? docType.getName() : null, docTypeField); + } + + public static String fieldPath(String docTypeName, DocTypeField docTypeField) { + + String rootPath = + docTypeName != null && !docTypeName.equals("default") ? docTypeName + "." : ""; + + DocTypeField parent = docTypeField.getParentDocTypeField(); + + String fieldName = docTypeField.getFieldName(); + + return parent != null ? fieldPath(parent) + "." + fieldName : rootPath + fieldName; + } +} diff --git a/core/app/datasource/src/main/java/io/openk9/datasource/processor/indexwriter/IndexerEvents.java b/core/app/datasource/src/main/java/io/openk9/datasource/processor/indexwriter/IndexerEvents.java index ca8fbf2c3..a077b0379 100644 --- a/core/app/datasource/src/main/java/io/openk9/datasource/processor/indexwriter/IndexerEvents.java +++ b/core/app/datasource/src/main/java/io/openk9/datasource/processor/indexwriter/IndexerEvents.java @@ -5,6 +5,7 @@ import io.openk9.datasource.model.DocType; import io.openk9.datasource.model.DocTypeField; import io.openk9.datasource.model.FieldType; +import io.openk9.datasource.model.util.DocTypeFieldUtils; import io.openk9.datasource.processor.util.Field; import io.openk9.datasource.service.DocTypeService; import io.openk9.datasource.sql.TransactionInvoker; @@ -33,7 +34,6 @@ import java.util.LinkedHashSet; import java.util.List; import java.util.Map; -import java.util.Optional; import java.util.Set; import java.util.function.Function; import java.util.stream.Collectors; @@ -58,20 +58,21 @@ public Uni generateDocTypeFields(DataIndex dataIndex) { } return indexService.getMappings(dataIndex.getName()) - .map(IndexerEvents::_toFlatFields) - .map(IndexerEvents::_toDocTypeFields) + .map(IndexerEvents::toDocTypeFields) .plug(docTypeFields -> Uni .combine() .all() - .unis(docTypeFields, _getDocumentTypes(dataIndex.getName())).asTuple()) - .map(_toDocTypeFieldMap()) + .unis(docTypeFields, _getDocumentTypes(dataIndex.getName())) + .asTuple() + ) + .map(IndexerEvents::toDocTypeAndFieldsGroup) .call(_persistDocType(dataIndex)) .replaceWithVoid(); } @ConsumeEvent("createOrUpdateDataIndex") @ActivateRequestContext - Uni createOrUpdateDataIndex(JsonObject jsonObject) { + public Uni createOrUpdateDataIndex(JsonObject jsonObject) { return Uni.createFrom().deferred(() -> { @@ -82,99 +83,127 @@ Uni createOrUpdateDataIndex(JsonObject jsonObject) { }); } - private Function>, Uni> _persistDocType( - DataIndex dataIndex) { + protected static List toDocTypeFields(Map mappings) { + return _toDocTypeFields(_toFlatFields(mappings)); + } - return m -> sessionFactory.withTransaction(session -> { + protected static Map> toDocTypeAndFieldsGroup( + Tuple2, List> t2) { - Set docTypeNames = m.keySet(); + List docTypeFields = t2.getItem1(); - return docTypeService.getDocTypesAndDocTypeFieldsByNames(docTypeNames) - .map(results -> { + List documentTypes = t2.getItem2(); - Set docTypes = new LinkedHashSet<>(docTypeNames.size()); + Map> grouped = docTypeFields + .stream() + .collect( + Collectors.groupingBy( + e -> + documentTypes + .stream() + .filter(dt -> e.getFieldName().startsWith(dt + ".") + || e.getFieldName().equals(dt)) + .findFirst() + .orElse("default"), + Collectors.toList() + ) + ); - for (String docTypeName : docTypeNames) { + _explodeDocTypeFirstLevel(grouped); - Optional first = - results - .stream() - .filter(docType -> docType.getName().equals( - docTypeName)) - .findFirst(); + return grouped; + } - DocType docType; + protected static Set mergeDocTypes( + Map> mappedDocTypeAndFields, + Collection existingDocTypes) { - if (first.isPresent()) { - docType = first.get(); - } - else { - docType = new DocType(); - docType.setName(docTypeName); - docType.setDescription("auto-generated"); - docType.setDocTypeFields(new LinkedHashSet<>()); - } + Set mappedDocTypeNames = mappedDocTypeAndFields.keySet(); - List docTypeFieldList = - m.getOrDefault(docTypeName, List.of()); - - for (DocTypeField docTypeField : docTypeFieldList) { - for (DocTypeField typeField : docType.getDocTypeFields()) { - - if (typeField.getFieldName().equals(docTypeField.getFieldName())) { - docTypeField.setId(typeField.getId()); - break; - } - - DocTypeField parentDocTypeField = - typeField.getParentDocTypeField(); - - if (parentDocTypeField != null) { - if (parentDocTypeField.getFieldName().equals( - docTypeField.getFieldName())) { - docTypeField.setId( - parentDocTypeField.getId()); - break; - } - } - - Set subDocTypeFields = - typeField.getSubDocTypeFields(); - - if (subDocTypeFields != null) { - Optional subDocTypeField = - subDocTypeFields - .stream() - .filter( - subTypeField -> subTypeField - .getFieldName() - .equals(docTypeField.getFieldName())) - .findFirst(); - - if (subDocTypeField.isPresent()) { - docTypeField.setId( - subDocTypeField.get().getId()); - break; - } - } + Set docTypes = new LinkedHashSet<>(mappedDocTypeNames.size()); - } - } + for (String docTypeName : mappedDocTypeNames) { + + DocType docType = + existingDocTypes + .stream() + .filter(d -> d.getName().equals(docTypeName)) + .findFirst() + .orElseGet(() -> { + DocType newDocType = new DocType(); + newDocType.setName(docTypeName); + newDocType.setDescription("auto-generated"); + newDocType.setDocTypeFields(new LinkedHashSet<>()); + return newDocType; + }); - Set docTypeFields = - docType.getDocTypeFields(); + List generatedFields = + mappedDocTypeAndFields.getOrDefault(docTypeName, List.of()); - docTypeFields.addAll(docTypeFieldList); + Set persistedFields = docType.getDocTypeFields(); - _setDocTypeToDocTypeFields(docType, docTypeFields); + List retainedFields = new ArrayList<>(); - docTypes.add(docType); + for (DocTypeField docTypeField : generatedFields) { + boolean retained = true; + for (DocTypeField existingField : persistedFields) { + if ((DocTypeFieldUtils.fieldPath(docTypeName, docTypeField)) + .equals(existingField.getPath())) { + + retained = false; + break; } + } + if (retained) { + retainedFields.add(docTypeField); + } + } - return docTypes; + persistedFields.addAll(retainedFields); + + _setDocTypeToDocTypeFields(docType, persistedFields); + + docTypes.add(docType); + } + + return docTypes; + + } + + private static void _explodeDocTypeFirstLevel(Map> grouped) { + for (String docTypeName : grouped.keySet()) { + if (!docTypeName.equals("default")) { + List groupedDocTypeFields = grouped.get(docTypeName); + groupedDocTypeFields + .stream() + .filter(docTypeField -> docTypeField + .getFieldName().equals(docTypeName) + ) + .findFirst() + .ifPresent(root -> { + Set subFields = root.getSubDocTypeFields(); + if (subFields != null && !subFields.isEmpty()) { + groupedDocTypeFields.remove(root); + for (DocTypeField subField : subFields) { + subField.setParentDocTypeField(null); + } + groupedDocTypeFields.addAll(subFields); + } + }); + } + } + } + + private Function>, Uni> _persistDocType( + DataIndex dataIndex) { - }) + return m -> sessionFactory.withTransaction(session -> { + + Set docTypeNames = m.keySet(); + + return docTypeService.getDocTypesAndDocTypeFieldsByNames(docTypeNames) + .map(docTypes -> mergeDocTypes(m, docTypes)) .flatMap(docTypes -> { dataIndex.setDocTypes(docTypes); return session.merge(dataIndex) @@ -197,30 +226,7 @@ private static void _setDocTypeToDocTypeFields( } - private Function, List>, Map>> _toDocTypeFieldMap() { - return t2 -> { - - List list = t2.getItem1(); - - List documentTypes = t2.getItem2(); - - return list - .stream() - .collect( - Collectors.groupingBy( - e -> - documentTypes - .stream() - .filter(dc -> e.getFieldName().startsWith(dc + ".") || e.getFieldName().equals(dc)) - .findFirst() - .orElse("default"), - Collectors.toList() - ) - ); - }; - } - - public Uni> _getDocumentTypes(String indexName) { + private Uni> _getDocumentTypes(String indexName) { return Uni .createFrom() .item(() -> { @@ -345,126 +351,71 @@ private static List _toDocTypeFields(Field root) { List docTypeFields = new ArrayList<>(); - _toDocTypeFields(root, new ArrayList<>(), null, docTypeFields); + for (Field subField : root.getSubFields()) { + if (!subField.isRoot()) { + _toDocTypeFields(subField, new ArrayList<>(), null, docTypeFields); + } + } return docTypeFields; } private static void _toDocTypeFields( - Field root, List acc, DocTypeField parent, + Field field, List acc, DocTypeField parent, Collection docTypeFields) { - String name = root.getName(); - - if (!root.isRoot()) { - acc.add(name); - } + String name = field.getName(); + acc.add(name); - String type = root.getType(); + String type = field.getType(); boolean isI18NField = - root + field .getSubFields() .stream() .map(Field::getName) .anyMatch(fieldName -> fieldName.equals("i18n")); - if (type != null || isI18NField) { - - String fieldName = String.join(".", acc); - - DocTypeField docTypeField = new DocTypeField(); - docTypeField.setName(fieldName); - docTypeField.setFieldName(fieldName); - docTypeField.setBoost(1.0); - FieldType fieldType = - isI18NField - ? FieldType.I18N - : FieldType.fromString(type); - docTypeField.setFieldType(fieldType); - docTypeField.setDescription("auto-generated"); - docTypeField.setSubDocTypeFields(new LinkedHashSet<>()); - if (root.getExtra() != null && !root.getExtra().isEmpty()) { - docTypeField.setJsonConfig( - new JsonObject(root.getExtra()).toString()); - } - - if (parent != null) { - docTypeField.setParentDocTypeField(parent); - } - - docTypeFields.add(docTypeField); - - switch (fieldType) { - case TEXT, KEYWORD, WILDCARD, CONSTANT_KEYWORD, I18N -> docTypeField.setSearchable(true); - default -> docTypeField.setSearchable(false); - } - - for (Field subField : root.getSubFields()) { + String fieldName = String.join(".", acc); + + DocTypeField docTypeField = new DocTypeField(); + docTypeField.setName(fieldName); + docTypeField.setFieldName(name); + docTypeField.setBoost(type != null ? 1.0 : null); + FieldType fieldType = isI18NField + ? FieldType.I18N + : type != null + ? FieldType.fromString(type) + : FieldType.OBJECT; + docTypeField.setFieldType(fieldType); + docTypeField.setDescription("auto-generated"); + docTypeField.setSubDocTypeFields(new LinkedHashSet<>()); + if (field.getExtra() != null && !field.getExtra().isEmpty()) { + docTypeField.setJsonConfig( + new JsonObject(field.getExtra()).toString()); + } - _toDocTypeFields( - subField, new ArrayList<>(acc), docTypeField, - docTypeField.getSubDocTypeFields()); + if (parent != null) { + docTypeField.setParentDocTypeField(parent); + } - } + docTypeFields.add(docTypeField); - } - else { - for (Field subField : root.getSubFields()) { - _toDocTypeFields( - subField, new ArrayList<>(acc), parent, docTypeFields); - } + switch (fieldType) { + case TEXT, KEYWORD, WILDCARD, CONSTANT_KEYWORD, I18N -> docTypeField.setSearchable(true); + default -> docTypeField.setSearchable(false); } - } + for (Field subField : field.getSubFields()) { + _toDocTypeFields( + subField, new ArrayList<>(acc), docTypeField, + docTypeField.getSubDocTypeFields()); - public static void main(String[] args) { - - String json = "{\n" + - " \"properties\" : {\n" + - " \"web\" : {\n" + - " \"properties\" : {\n" + - " \"title\" : {\n" + - " \"properties\" : {\n" + - " \"i18n\" : {\n" + - " \"properties\" : {\n" + - " \"en\" : {\n" + - " \"type\" : \"text\",\n" + - " \"fields\" : {\n" + - " \"keyword\" : {\n" + - " \"type\" : \"keyword\"\n" + - " }\n" + - " }\n" + - " }\n" + - " }\n" + - " },\n" + - " \"base\" : {\n" + - " \"type\" : \"text\",\n" + - " \"fields\" : {\n" + - " \"keyword\" : {\n" + - " \"type\" : \"keyword\"\n" + - " }\n" + - " }\n" + - " }\n" + - " }\n" + - " }\n" + - " }\n" + - " }\n" + - " }\n" + - " }"; - - ; - - Field field = _toFlatFields(new JsonObject(json).getMap()); - - List docTypeFields = _toDocTypeFields(field); - - System.out.println(docTypeFields); + } } - @Inject RestHighLevelClient client; diff --git a/core/app/datasource/src/main/java/io/openk9/datasource/searcher/SearcherService.java b/core/app/datasource/src/main/java/io/openk9/datasource/searcher/SearcherService.java index b514fb82a..13f342bbc 100644 --- a/core/app/datasource/src/main/java/io/openk9/datasource/searcher/SearcherService.java +++ b/core/app/datasource/src/main/java/io/openk9/datasource/searcher/SearcherService.java @@ -234,7 +234,7 @@ else if (docTypeField.isKeyword()) { for (Tuple2 tuple2 : suggestionDocTypeFields) { DocTypeField docTypeField = tuple2.getItem2(); - String name = docTypeField.getFieldName(); + String name = docTypeField.getPath(); compositeValuesSourceBuilders.add( new TermsValuesSourceBuilder(name) .field(name) @@ -273,7 +273,7 @@ else if (docTypeField.isKeyword()) { .map(Tuple2::getItem2) .map(DocTypeField::getParentDocTypeField) .filter(dtf -> dtf != null && dtf.getFieldType() == FieldType.TEXT) - .map(DocTypeField::getFieldName) + .map(DocTypeField::getPath) .distinct() .toArray(String[]::new); @@ -353,7 +353,7 @@ else if (docTypeField.isKeyword()) { .stream() .collect( Collectors.toMap( - t -> t.getItem2().getFieldName(), + t -> t.getItem2().getPath(), Tuple2::getItem1 ) ); @@ -452,11 +452,11 @@ private static DocTypeField getI18nDocTypeField(DocTypeField docTypeField, Strin for (DocTypeField e : docTypeFieldList) { - if (e.isKeyword() && e.getFieldName().startsWith(docTypeField.getFieldName())) { - if (e.getFieldName().contains(language)) { + if (e.isKeyword() && e.getPath().startsWith(docTypeField.getPath())) { + if (e.getPath().contains(language)) { return e; } - else if (e.getFieldName().contains(".base")) { + else if (e.getPath().contains(".base")) { docTypeFieldBase = e; } } @@ -761,7 +761,7 @@ private static void applyHighlightAndIncludeExclude( if (i18nParent != null) { i18nMap.compute(i18nParent, (k, v) -> { - String fieldName = docTypeField.getFieldName(); + String fieldName = docTypeField.getPath(); if (v == null) { v = Tuple2.of(new HashSet<>(), new HashSet<>()); } @@ -778,7 +778,7 @@ else if (fieldName.contains(".base")) { } else { - String name = docTypeField.getFieldName(); + String name = docTypeField.getPath(); if (docTypeField.isDefaultExclude()) { excludes.add(name); } @@ -799,7 +799,7 @@ else if (fieldName.contains(".base")) { !tuple.getItem2().isEmpty() ? tuple.getItem2() : tuple.getItem1(); for (DocTypeField docTypeField : docTypeFields) { - String name = docTypeField.getFieldName(); + String name = docTypeField.getPath(); if (docTypeField.isDefaultExclude()) { excludes.add(name); } @@ -877,7 +877,7 @@ private void applySort( docTypeFieldList .stream() .filter(DocTypeField::isSortable) - .map(DocTypeField::getFieldName) + .map(DocTypeField::getPath) .toList(); if (docTypeFieldNameSortable.isEmpty()) { diff --git a/core/app/datasource/src/main/java/io/openk9/datasource/searcher/parser/impl/DateOrderQueryParser.java b/core/app/datasource/src/main/java/io/openk9/datasource/searcher/parser/impl/DateOrderQueryParser.java index a2fabbc53..30e369bb3 100644 --- a/core/app/datasource/src/main/java/io/openk9/datasource/searcher/parser/impl/DateOrderQueryParser.java +++ b/core/app/datasource/src/main/java/io/openk9/datasource/searcher/parser/impl/DateOrderQueryParser.java @@ -39,7 +39,7 @@ public void accept(ParserContext parserContext) { Iterator iterator = Utils.getDocTypeFieldsFrom(currentTenant) .filter(DocTypeField::isSearchableAndDate) - .map(DocTypeField::getFieldName) + .map(DocTypeField::getPath) .distinct() .iterator(); diff --git a/core/app/datasource/src/main/java/io/openk9/datasource/searcher/parser/impl/DateQueryParser.java b/core/app/datasource/src/main/java/io/openk9/datasource/searcher/parser/impl/DateQueryParser.java index aad7935c8..95357186a 100644 --- a/core/app/datasource/src/main/java/io/openk9/datasource/searcher/parser/impl/DateQueryParser.java +++ b/core/app/datasource/src/main/java/io/openk9/datasource/searcher/parser/impl/DateQueryParser.java @@ -63,7 +63,7 @@ public void accept(ParserContext parserContext) { } - if (searchToken.getKeywordKey().equals(docTypeField.getFieldName())) { + if (searchToken.getKeywordKey().equals(docTypeField.getPath())) { return Stream.of(Tuple2.of(docTypeField, searchToken)); } } @@ -97,7 +97,7 @@ public void accept(ParserContext parserContext) { RangeQueryBuilder rangeQueryBuilder = QueryBuilders.rangeQuery( - docTypeField.getFieldName()); + docTypeField.getPath()); if (values.size() == 1) { gte = values.get(0); @@ -134,7 +134,7 @@ else if (values.size() == 2) { RangeQueryBuilder rangeQueryBuilder = QueryBuilders.rangeQuery( - docTypeField.getFieldName()); + docTypeField.getPath()); if (gte != null) { rangeQueryBuilder.gte(gte); diff --git a/core/app/datasource/src/main/java/io/openk9/datasource/searcher/parser/impl/SearchAsYouTypeQueryParser.java b/core/app/datasource/src/main/java/io/openk9/datasource/searcher/parser/impl/SearchAsYouTypeQueryParser.java index da3d7f215..b1026d71f 100644 --- a/core/app/datasource/src/main/java/io/openk9/datasource/searcher/parser/impl/SearchAsYouTypeQueryParser.java +++ b/core/app/datasource/src/main/java/io/openk9/datasource/searcher/parser/impl/SearchAsYouTypeQueryParser.java @@ -63,12 +63,12 @@ private void _termSearchAsYouTypeQueryValues( ( keywordKey == null || keywordKey.isEmpty() || - searchKeyword.getFieldName().equals(keywordKey) + searchKeyword.getPath().equals(keywordKey) ) ) .collect( Collectors.toMap( - DocTypeField::getFieldName, + DocTypeField::getPath, DocTypeField::getFloatBoost, Math::max, HashMap::new diff --git a/core/app/datasource/src/main/java/io/openk9/datasource/searcher/parser/impl/TextQueryParser.java b/core/app/datasource/src/main/java/io/openk9/datasource/searcher/parser/impl/TextQueryParser.java index 949c934fc..8332e532a 100644 --- a/core/app/datasource/src/main/java/io/openk9/datasource/searcher/parser/impl/TextQueryParser.java +++ b/core/app/datasource/src/main/java/io/openk9/datasource/searcher/parser/impl/TextQueryParser.java @@ -64,10 +64,10 @@ public void accept(ParserContext parserContext) { docTypeFieldList .stream() .filter(docTypeField -> - !keywordKeyIsPresent || docTypeField.getFieldName().equals(keywordKey)) + !keywordKeyIsPresent || docTypeField.getPath().equals(keywordKey)) .collect( Collectors.toMap( - DocTypeField::getFieldName, + DocTypeField::getPath, DocTypeField::getFloatBoost, Math::max, HashMap::new diff --git a/core/app/datasource/src/main/java/io/openk9/datasource/searcher/queryanalysis/annotator/AnnotatorFactory.java b/core/app/datasource/src/main/java/io/openk9/datasource/searcher/queryanalysis/annotator/AnnotatorFactory.java index a403495cd..b246de340 100644 --- a/core/app/datasource/src/main/java/io/openk9/datasource/searcher/queryanalysis/annotator/AnnotatorFactory.java +++ b/core/app/datasource/src/main/java/io/openk9/datasource/searcher/queryanalysis/annotator/AnnotatorFactory.java @@ -25,18 +25,18 @@ public Annotator getAnnotator( case DOCTYPE -> new DocTypeAnnotator( bucket, annotator, stopWords, client); case AGGREGATOR -> new AggregatorAnnotator( - annotator.getDocTypeField().getFieldName(), + annotator.getDocTypeField().getPath(), bucket, annotator, stopWords, client); case AUTOCOMPLETE -> new BaseAutoCompleteAnnotator( bucket, annotator, stopWords, client, annotator.getFieldName(), - annotator.getDocTypeField().getFieldName()); + annotator.getDocTypeField().getPath()); case NER_AUTOCOMPLETE -> new BaseAutoCompleteNerAnnotator( bucket, annotator, stopWords, annotator.getFieldName(), client, tenantResolver); case AUTOCORRECT -> new BaseAutoCorrectAnnotator( bucket, annotator, stopWords, client, annotator.getFieldName(), - annotator.getDocTypeField().getFieldName()); + annotator.getDocTypeField().getPath()); }; } diff --git a/core/app/datasource/src/main/java/io/openk9/datasource/service/AnnotatorService.java b/core/app/datasource/src/main/java/io/openk9/datasource/service/AnnotatorService.java index 0b2994ae9..e4ed740bb 100644 --- a/core/app/datasource/src/main/java/io/openk9/datasource/service/AnnotatorService.java +++ b/core/app/datasource/src/main/java/io/openk9/datasource/service/AnnotatorService.java @@ -90,6 +90,6 @@ public Uni> unbindDocTypeField( } @Inject - private DocTypeFieldService docTypeFieldService; + DocTypeFieldService docTypeFieldService; } diff --git a/core/app/datasource/src/main/java/io/openk9/datasource/service/DocTypeFieldService.java b/core/app/datasource/src/main/java/io/openk9/datasource/service/DocTypeFieldService.java index 05611309e..879ca613a 100644 --- a/core/app/datasource/src/main/java/io/openk9/datasource/service/DocTypeFieldService.java +++ b/core/app/datasource/src/main/java/io/openk9/datasource/service/DocTypeFieldService.java @@ -18,7 +18,6 @@ package io.openk9.datasource.service; import io.openk9.common.graphql.util.relay.Connection; -import io.openk9.common.graphql.util.relay.GraphqlId; import io.openk9.common.util.SortBy; import io.openk9.datasource.mapper.DocTypeFieldMapper; import io.openk9.datasource.model.Analyzer; @@ -36,7 +35,6 @@ import javax.persistence.criteria.CriteriaBuilder; import javax.persistence.criteria.CriteriaQuery; import javax.persistence.criteria.Path; -import javax.persistence.criteria.Predicate; import java.util.ArrayList; import java.util.Collection; import java.util.LinkedHashSet; @@ -124,32 +122,19 @@ public Uni createSubField( return withTransaction((s, tr) -> findById(parentDocTypeFieldId) .onItem() .ifNotNull() - .transformToUni(parentDocTypeField -> { - - String fieldName = docTypeFieldDTO.getFieldName(); - String parentFieldName = parentDocTypeField.getFieldName(); - - if (!fieldName.startsWith(parentFieldName)) { - return Uni.createFrom().failure( - new IllegalArgumentException( - "fieldName must start with parentFieldName: " + parentFieldName)); - } - - return Mutiny2 - .fetch(s, parentDocTypeField.getSubDocTypeFields()) - .onItem() - .ifNotNull() - .transformToUni(subList -> { - - DocTypeField docTypeField = mapper.create(docTypeFieldDTO); - docTypeField.setParentDocTypeField(parentDocTypeField); - docTypeField.setDocType(parentDocTypeField.getDocType()); - subList.add(docTypeField); - return persist(docTypeField); + .transformToUni(parentDocTypeField -> Mutiny2 + .fetch(s, parentDocTypeField.getSubDocTypeFields()) + .onItem() + .ifNotNull() + .transformToUni(subList -> { - }); + DocTypeField docTypeField = mapper.create(docTypeFieldDTO); + docTypeField.setParentDocTypeField(parentDocTypeField); + docTypeField.setDocType(parentDocTypeField.getDocType()); + subList.add(docTypeField); + return persist(docTypeField); - })); + }))); } @@ -170,25 +155,8 @@ public Uni> expandDocTypes(Collection docTypes) { .unis(docTypeField) .collectFailures() .combinedWith(e -> (List>) e) - .flatMap(list -> { - - Set>> innerDTFs = new LinkedHashSet<>(); - - for (Set docTypeFields : list) { - innerDTFs.add(expandDocTypeFields(docTypeFields)); - } - - return Uni - .combine() - .all() - .unis(innerDTFs) - .collectFailures() - .discardItems(); - - }) + .flatMap(this::loadAndExpandDocTypeFields) .replaceWith(docTypes); - - }); } else { @@ -197,7 +165,70 @@ public Uni> expandDocTypes(Collection docTypes) { } - public Uni> expandDocTypeFields(Collection docTypeFields) { + public Uni> findConnection( + long parentId, String after, String before, Integer first, Integer last, + String searchText, Set sortByList) { + + + CriteriaBuilder criteriaBuilder = getCriteriaBuilder(); + + CriteriaQuery query = + criteriaBuilder.createQuery(getEntityClass()); + + Path root = query.from(getEntityClass()); + + Path parentField = root.get(DocTypeField_.parentDocTypeField); + + return findConnection( + query, root, + parentId > 0 + ? criteriaBuilder.equal(parentField.get(DocTypeField_.id), parentId) + : criteriaBuilder.isNull(parentField), + getSearchFields(), + after, before, first, last, searchText, sortByList + ); + } + + + private Uni> loadAndExpandDocTypeFields(List> list) { + + List> loadedDTFs = new LinkedList<>(); + + for (Set typeFields : list) { + loadedDTFs.add(loadDocTypeField(typeFields)); + } + + return Uni + .combine() + .all() + .unis(loadedDTFs) + .collectFailures() + .discardItems() + .chain(() -> { + + List>> inner = new LinkedList<>(); + + for (Set typeFields : list) { + inner.add(expandDocTypeFields(typeFields)); + } + + return Uni + .combine() + .all() + .unis(inner) + .collectFailures() + .combinedWith(e -> { + List> expandInner = (List>) e; + return expandInner.stream() + .flatMap(Collection::stream) + .collect(Collectors.toSet()); + }); + + }); + + } + + private Uni> expandDocTypeFields(Collection docTypeFields) { if (docTypeFields == null || docTypeFields.isEmpty()) { return Uni.createFrom().item(Set.of()); @@ -220,74 +251,13 @@ public Uni> expandDocTypeFields(Collection docTy .unis(subDocTypeFieldUnis) .collectFailures() .combinedWith(e -> (List>)e) - .flatMap(list -> { - - List> loadedDTFs = new LinkedList<>(); - - for (Set typeFields : list) { - loadedDTFs.add(loadDocTypeField(typeFields)); - } - - return Uni - .combine() - .all() - .unis(loadedDTFs) - .collectFailures() - .discardItems() - .chain(() -> { - - List>> inner = new LinkedList<>(); - - for (Set typeFields : list) { - inner.add(expandDocTypeFields(typeFields)); - } - - return Uni - .combine() - .all() - .unis(inner) - .collectFailures() - .combinedWith(e -> { - List> expandInner = (List>) e; - return expandInner.stream() - .flatMap(Collection::stream) - .collect(Collectors.toSet()); - }); - - }); - - }); + .flatMap(this::loadAndExpandDocTypeFields); }); - } - public Uni> findConnection( - long parentId, String after, String before, Integer first, Integer last, - String searchText, Set sortByList) { - - - CriteriaBuilder criteriaBuilder = getCriteriaBuilder(); - - CriteriaQuery query = - criteriaBuilder.createQuery(getEntityClass()); - - Path root = query.from(getEntityClass()); - - Path parentField = root.get(DocTypeField_.parentDocTypeField); - - return findConnection( - query, root, - parentId > 0 - ? criteriaBuilder.equal(parentField.get(DocTypeField_.id), parentId) - : criteriaBuilder.isNull(parentField), - getSearchFields(), - after, before, first, last, searchText, sortByList - ); - } - - public Uni loadDocTypeField(Set typeFields) { + private Uni loadDocTypeField(Set typeFields) { return em.withTransaction(s -> { diff --git a/core/app/datasource/src/main/java/io/openk9/datasource/service/DocTypeService.java b/core/app/datasource/src/main/java/io/openk9/datasource/service/DocTypeService.java index 4e25cdd18..13a456ea2 100644 --- a/core/app/datasource/src/main/java/io/openk9/datasource/service/DocTypeService.java +++ b/core/app/datasource/src/main/java/io/openk9/datasource/service/DocTypeService.java @@ -23,6 +23,7 @@ import io.openk9.datasource.mapper.DocTypeMapper; import io.openk9.datasource.model.DocType; import io.openk9.datasource.model.DocTypeField; +import io.openk9.datasource.model.DocTypeField_; import io.openk9.datasource.model.DocTypeTemplate; import io.openk9.datasource.model.DocType_; import io.openk9.datasource.model.dto.DocTypeDTO; @@ -40,6 +41,7 @@ import javax.inject.Inject; import javax.persistence.criteria.CriteriaBuilder; import javax.persistence.criteria.CriteriaQuery; +import javax.persistence.criteria.Path; import javax.persistence.criteria.Root; import java.util.Collection; import java.util.List; @@ -61,12 +63,30 @@ public String[] getSearchFields() { public Uni> getDocTypeFieldsConnection( Long id, String after, String before, Integer first, Integer last, String searchText, Set sortByList, boolean notEqual) { + return findJoinConnection( id, DocType_.DOC_TYPE_FIELDS, DocTypeField.class, docTypeFieldService.getSearchFields(), after, before, first, last, searchText, sortByList, notEqual); } + public Uni> getDocTypeFieldsConnectionByParent( + long docTypeId, long parentId, String after, String before, Integer first, Integer last, + String searchText, Set sortByList, boolean notEqual) { + + return findJoinConnection( + docTypeId, DocType_.DOC_TYPE_FIELDS, DocTypeField.class, + docTypeFieldService.getSearchFields(), after, before, first, last, + searchText, sortByList, notEqual, + (criteriaBuilder, join) -> { + Path parentField = join.get(DocTypeField_.parentDocTypeField); + + return parentId > 0 + ? criteriaBuilder.equal(parentField.get(DocTypeField_.id), parentId) + : criteriaBuilder.isNull(parentField); + }); + } + public Uni> getDocTypeFields( long docTypeId, Pageable pageable) { return getDocTypeFields(docTypeId, pageable, Filter.DEFAULT); diff --git a/core/app/datasource/src/main/java/io/openk9/datasource/service/TokenTabService.java b/core/app/datasource/src/main/java/io/openk9/datasource/service/TokenTabService.java index a90ac5265..4aa636ba2 100644 --- a/core/app/datasource/src/main/java/io/openk9/datasource/service/TokenTabService.java +++ b/core/app/datasource/src/main/java/io/openk9/datasource/service/TokenTabService.java @@ -83,6 +83,6 @@ public Uni> getDocTypeFieldsNotInTokenTab( } @Inject - private DocTypeFieldService docTypeFieldService; + DocTypeFieldService docTypeFieldService; } diff --git a/core/app/datasource/src/main/java/io/openk9/datasource/sql/TransactionInvoker.java b/core/app/datasource/src/main/java/io/openk9/datasource/sql/TransactionInvoker.java index b6ef3d72a..953e32f3c 100644 --- a/core/app/datasource/src/main/java/io/openk9/datasource/sql/TransactionInvoker.java +++ b/core/app/datasource/src/main/java/io/openk9/datasource/sql/TransactionInvoker.java @@ -109,21 +109,21 @@ public Uni withStatelessTransaction( private Uni withTransaction( String schema, - Function>, Uni> fun, + Function>, Uni> withTx, java.util.function.Predicate isOpen, BiFunction> createNativeQuery, - BiFunction> fun2) { + BiFunction> stmt) { return Uni .createFrom() - .deferred(() -> fun.apply((s, t) -> { + .deferred(() -> withTx.apply((s, t) -> { Context context = Vertx.currentContext(); String currentSchema = context.getLocal("currentSchema"); if (!multiTenancyConfig.isEnabled() || currentSchema != null) { - return fun2.apply(s, t); + return stmt.apply(s, t); } if (!isOpen.test(s)) { @@ -155,7 +155,7 @@ else if (schema != null) { .invoke(() -> context.putLocal("currentSchema", newSchema)) .flatMap((ignore) -> { try { - return fun2.apply(s, t); + return stmt.apply(s, t); } catch (Exception e) { if (e instanceof K9Error) { return Uni.createFrom().failure(e); diff --git a/core/app/datasource/src/main/java/io/openk9/datasource/validation/Alnum.java b/core/app/datasource/src/main/java/io/openk9/datasource/validation/Alnum.java new file mode 100644 index 000000000..603633a10 --- /dev/null +++ b/core/app/datasource/src/main/java/io/openk9/datasource/validation/Alnum.java @@ -0,0 +1,21 @@ +package io.openk9.datasource.validation; + +import javax.validation.Constraint; +import javax.validation.Payload; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Constraint(validatedBy={AlnumValidator.class}) +@Target({ElementType.FIELD, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface Alnum { + + String message() default "is not an alphanumeric string"; + + Class[] groups() default {}; + + Class[] payload() default {}; + +} diff --git a/core/app/datasource/src/main/java/io/openk9/datasource/validation/AlnumValidator.java b/core/app/datasource/src/main/java/io/openk9/datasource/validation/AlnumValidator.java new file mode 100644 index 000000000..afa262095 --- /dev/null +++ b/core/app/datasource/src/main/java/io/openk9/datasource/validation/AlnumValidator.java @@ -0,0 +1,21 @@ +package io.openk9.datasource.validation; + +import javax.enterprise.context.ApplicationScoped; +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; +import java.util.function.Predicate; +import java.util.regex.Pattern; + +@ApplicationScoped +public class AlnumValidator implements ConstraintValidator { + + @Override + public boolean isValid(String value, ConstraintValidatorContext context) { + return ALNUM_PATTERN.test(value); + } + + private final static Predicate ALNUM_PATTERN = Pattern + .compile("^[A-Za-z][A-Za-z0-9]*$") + .asMatchPredicate(); + +} diff --git a/core/app/datasource/src/main/java/io/openk9/datasource/web/dto/PartialDocTypeFieldDTO.java b/core/app/datasource/src/main/java/io/openk9/datasource/web/dto/PartialDocTypeFieldDTO.java index 2c7ec4409..9ef173546 100644 --- a/core/app/datasource/src/main/java/io/openk9/datasource/web/dto/PartialDocTypeFieldDTO.java +++ b/core/app/datasource/src/main/java/io/openk9/datasource/web/dto/PartialDocTypeFieldDTO.java @@ -6,7 +6,7 @@ public record PartialDocTypeFieldDTO(String field, Long id, String label) { public static PartialDocTypeFieldDTO of(DocTypeField docTypeField) { return new PartialDocTypeFieldDTO( - docTypeField.getFieldName(), + docTypeField.getPath(), docTypeField.getId(), docTypeField.getName() ); diff --git a/core/app/datasource/src/main/resources/cluster.conf b/core/app/datasource/src/main/resources/cluster.conf index 57d4a2e4d..34f01e6f7 100644 --- a/core/app/datasource/src/main/resources/cluster.conf +++ b/core/app/datasource/src/main/resources/cluster.conf @@ -26,6 +26,8 @@ akka.management { http { port = 8558 port = ${?HTTP_MGMT_PORT} + bind-hostname = 0.0.0.0 + bind-port = 8558 } cluster.bootstrap { contact-point-discovery { diff --git a/core/app/datasource/src/test/java/io/openk9/datasource/index/mappings/MappingsUtilTest.java b/core/app/datasource/src/test/java/io/openk9/datasource/index/mappings/MappingsUtilTest.java index 0c8debbe0..ac7f515cc 100644 --- a/core/app/datasource/src/test/java/io/openk9/datasource/index/mappings/MappingsUtilTest.java +++ b/core/app/datasource/src/test/java/io/openk9/datasource/index/mappings/MappingsUtilTest.java @@ -19,7 +19,7 @@ class MappingsUtilTest { @Test void docTypesToMappings() { Map result = - MappingsUtil.docTypesToMappings(List.of(docType)); + MappingsUtil.docTypesToMappings(List.of(defaultDocType, webDocType)); Assertions.assertEquals(expectedJson, JsonObject.mapFrom(result)); } @@ -66,20 +66,14 @@ void docTypesToMappings() { } } }, - "description": { + "web": { "properties": { - "base": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword", - "ignore_above": 256 - } - } - }, - "i18n": { + "title": { + "type": "text" + }, + "description": { "properties": { - "en_US": { + "base": { "type": "text", "fields": { "keyword": { @@ -88,12 +82,25 @@ void docTypesToMappings() { } } }, - "de_DE": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword", - "ignore_above": 256 + "i18n": { + "properties": { + "en_US": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "de_DE": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } } } } @@ -105,19 +112,22 @@ void docTypesToMappings() { } """); - private static final DocType docType; + private static final DocType defaultDocType, webDocType; private static final DocTypeField + complexNumber, realPart, imaginaryPart, title, titleKeyword, titleTrigram, + title2, address, street, streetKeyword, streetSearchAsYouType, number, description, + descriptionI18n, descriptionBase, descriptionBaseKeyword, descriptionEn, @@ -126,27 +136,37 @@ void docTypesToMappings() { descriptionDeKeyword; static { - docType = new DocType(); - docType.setName("aDocType"); + defaultDocType = new DocType(); + defaultDocType.setName("default"); + + complexNumber = new DocTypeField(); + complexNumber.setDocType(defaultDocType); + complexNumber.setFieldName("complexNumber"); + complexNumber.setFieldType(FieldType.OBJECT); realPart = new DocTypeField(); - realPart.setDocType(docType); - realPart.setFieldName("complexNumber.realPart"); + realPart.setDocType(defaultDocType); + realPart.setFieldName("realPart"); realPart.setFieldType(FieldType.INTEGER); + realPart.setParentDocTypeField(complexNumber); imaginaryPart = new DocTypeField(); - imaginaryPart.setDocType(docType); - imaginaryPart.setFieldName("complexNumber.imaginaryPart"); + imaginaryPart.setDocType(defaultDocType); + imaginaryPart.setFieldName("imaginaryPart"); imaginaryPart.setFieldType(FieldType.INTEGER); + imaginaryPart.setParentDocTypeField(complexNumber); + + complexNumber.setSubDocTypeFields(new LinkedHashSet<>( + List.of(realPart, imaginaryPart))); title = new DocTypeField(); - title.setDocType(docType); + title.setDocType(defaultDocType); title.setFieldName("title"); title.setFieldType(FieldType.TEXT); titleKeyword = new DocTypeField(); - titleKeyword.setDocType(docType); - titleKeyword.setFieldName("title.keyword"); + titleKeyword.setDocType(defaultDocType); + titleKeyword.setFieldName("keyword"); titleKeyword.setFieldType(FieldType.KEYWORD); titleKeyword.setParentDocTypeField(title); @@ -154,8 +174,8 @@ void docTypesToMappings() { Analyzer trigram = new Analyzer(); trigram.setName("trigram"); trigram.setType("custom"); - titleTrigram.setDocType(docType); - titleTrigram.setFieldName("title.trigram"); + titleTrigram.setDocType(defaultDocType); + titleTrigram.setFieldName("trigram"); titleTrigram.setFieldType(FieldType.TEXT); titleTrigram.setAnalyzer(trigram); titleTrigram.setParentDocTypeField(title); @@ -163,25 +183,25 @@ void docTypesToMappings() { title.setSubDocTypeFields(new LinkedHashSet<>(List.of(titleKeyword, titleTrigram))); address = new DocTypeField(); - address.setDocType(docType); + address.setDocType(defaultDocType); address.setFieldName("address"); address.setFieldType(FieldType.OBJECT); street = new DocTypeField(); - street.setDocType(docType); - street.setFieldName("address.street"); + street.setDocType(defaultDocType); + street.setFieldName("street"); street.setFieldType(FieldType.TEXT); street.setParentDocTypeField(address); streetKeyword = new DocTypeField(); - streetKeyword.setDocType(docType); - streetKeyword.setFieldName("address.street.keyword"); + streetKeyword.setDocType(defaultDocType); + streetKeyword.setFieldName("keyword"); streetKeyword.setFieldType(FieldType.KEYWORD); streetKeyword.setParentDocTypeField(street); streetSearchAsYouType = new DocTypeField(); - streetSearchAsYouType.setDocType(docType); - streetSearchAsYouType.setFieldName("address.street.search_as_you_type"); + streetSearchAsYouType.setDocType(defaultDocType); + streetSearchAsYouType.setFieldName("search_as_you_type"); streetSearchAsYouType.setFieldType(FieldType.SEARCH_AS_YOU_TYPE); streetSearchAsYouType.setParentDocTypeField(street); @@ -189,41 +209,56 @@ void docTypesToMappings() { new LinkedHashSet<>(List.of(streetKeyword, streetSearchAsYouType))); number = new DocTypeField(); - number.setDocType(docType); - number.setFieldName("address.number"); + number.setDocType(defaultDocType); + number.setFieldName("number"); number.setFieldType(FieldType.INTEGER); number.setParentDocTypeField(address); address.setSubDocTypeFields(new LinkedHashSet<>(List.of(street, number))); + webDocType = new DocType(); + webDocType.setName("web"); + + title2 = new DocTypeField(); + title2.setDocType(webDocType); + title2.setFieldName("title"); + title2.setFieldType(FieldType.TEXT); + description = new DocTypeField(); - description.setDocType(docType); + description.setDocType(webDocType); description.setFieldName("description"); description.setFieldType(FieldType.I18N); descriptionBase = new DocTypeField(); - descriptionBase.setDocType(docType); - descriptionBase.setFieldName("description.base"); + descriptionBase.setDocType(webDocType); + descriptionBase.setFieldName("base"); descriptionBase.setFieldType(FieldType.TEXT); descriptionBase.setParentDocTypeField(description); descriptionBaseKeyword = new DocTypeField(); - descriptionBaseKeyword.setDocType(docType); - descriptionBaseKeyword.setFieldName("description.base.keyword"); + descriptionBaseKeyword.setDocType(webDocType); + descriptionBaseKeyword.setFieldName("keyword"); descriptionBaseKeyword.setFieldType(FieldType.KEYWORD); descriptionBaseKeyword.setParentDocTypeField(descriptionBase); descriptionBaseKeyword.setJsonConfig("{\"ignore_above\":256}"); + descriptionBase.setSubDocTypeFields(Set.of(descriptionBaseKeyword)); + descriptionI18n = new DocTypeField(); + descriptionI18n.setDocType(webDocType); + descriptionI18n.setFieldName("i18n"); + descriptionI18n.setFieldType(FieldType.OBJECT); + descriptionI18n.setParentDocTypeField(description); + descriptionEn = new DocTypeField(); - descriptionEn.setDocType(docType); - descriptionEn.setFieldName("description.i18n.en_US"); + descriptionEn.setDocType(webDocType); + descriptionEn.setFieldName("en_US"); descriptionEn.setFieldType(FieldType.TEXT); - descriptionEn.setParentDocTypeField(description); + descriptionEn.setParentDocTypeField(descriptionI18n); descriptionEnKeyword = new DocTypeField(); - descriptionEnKeyword.setDocType(docType); - descriptionEnKeyword.setFieldName("description.i18n.en_US.keyword"); + descriptionEnKeyword.setDocType(webDocType); + descriptionEnKeyword.setFieldName("keyword"); descriptionEnKeyword.setFieldType(FieldType.KEYWORD); descriptionEnKeyword.setParentDocTypeField(descriptionEn); descriptionEnKeyword.setJsonConfig("{\"ignore_above\":256}"); @@ -231,24 +266,28 @@ void docTypesToMappings() { descriptionEn.setSubDocTypeFields(Set.of(descriptionEnKeyword)); descriptionDe = new DocTypeField(); - descriptionDe.setDocType(docType); - descriptionDe.setFieldName("description.i18n.de_DE"); + descriptionDe.setDocType(webDocType); + descriptionDe.setFieldName("de_DE"); descriptionDe.setFieldType(FieldType.TEXT); - descriptionDe.setParentDocTypeField(description); + descriptionDe.setParentDocTypeField(descriptionI18n); descriptionDeKeyword = new DocTypeField(); - descriptionDeKeyword.setDocType(docType); - descriptionDeKeyword.setFieldName("description.i18n.de_DE.keyword"); + descriptionDeKeyword.setDocType(webDocType); + descriptionDeKeyword.setFieldName("keyword"); descriptionDeKeyword.setFieldType(FieldType.KEYWORD); descriptionDeKeyword.setParentDocTypeField(descriptionDe); descriptionDeKeyword.setJsonConfig("{\"ignore_above\":256}"); descriptionDe.setSubDocTypeFields(Set.of(descriptionDeKeyword)); + descriptionI18n.setSubDocTypeFields(new LinkedHashSet<>(List.of( + descriptionEn, descriptionDe))); + description.setSubDocTypeFields(new LinkedHashSet<>(List.of( - descriptionBase, descriptionEn, descriptionDe))); + descriptionBase, descriptionI18n))); - docType.setDocTypeFields(new LinkedHashSet<>(List.of( + defaultDocType.setDocTypeFields(new LinkedHashSet<>(List.of( + complexNumber, realPart, imaginaryPart, title, @@ -258,14 +297,20 @@ void docTypesToMappings() { street, streetKeyword, streetSearchAsYouType, - number, + number + ))); + + webDocType.setDocTypeFields(new LinkedHashSet<>(List.of( + title2, description, + descriptionI18n, descriptionBase, descriptionBaseKeyword, descriptionEn, descriptionEnKeyword, descriptionDe, - descriptionDeKeyword))); + descriptionDeKeyword + ))); } } \ No newline at end of file diff --git a/core/app/datasource/src/test/java/io/openk9/datasource/processor/indexwriter/IndexerEventsTest.java b/core/app/datasource/src/test/java/io/openk9/datasource/processor/indexwriter/IndexerEventsTest.java new file mode 100644 index 000000000..1980b3cea --- /dev/null +++ b/core/app/datasource/src/test/java/io/openk9/datasource/processor/indexwriter/IndexerEventsTest.java @@ -0,0 +1,338 @@ +package io.openk9.datasource.processor.indexwriter; + +import io.openk9.datasource.model.Analyzer; +import io.openk9.datasource.model.DocType; +import io.openk9.datasource.model.DocTypeField; +import io.openk9.datasource.model.FieldType; +import io.smallrye.mutiny.tuples.Tuple2; +import io.vertx.core.json.Json; +import io.vertx.core.json.JsonObject; +import org.junit.jupiter.api.Test; + +import java.util.Collection; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class IndexerEventsTest { + + @Test + void shouldMapToDocTypeFields() { + List docTypeFields = IndexerEvents.toDocTypeFields(mappings.getMap()); + Optional optField = docTypeFields + .stream() + .filter(f -> f.getFieldName().equals("acl")) + .flatMap(f -> f.getSubDocTypeFields().stream()) + .filter(f -> f.getFieldName().equals("public")) + .findFirst(); + assertTrue(optField.isPresent()); + DocTypeField field = optField.get(); + assertSame(field.getFieldType(), FieldType.BOOLEAN); + assertEquals("public", field.getFieldName()); + assertEquals(field.getPath(), "acl.public"); + + + Map> docTypeAndFieldsGroup = + IndexerEvents.toDocTypeAndFieldsGroup( + Tuple2.of(docTypeFields, List.of("web", "resources", "document")) + ); + + assertTrue( + docTypeAndFieldsGroup + .get("default") + .stream() + .filter(f -> f.getFieldName().equals("acl")) + .flatMap(f -> f.getSubDocTypeFields().stream()) + .anyMatch(f -> f.getFieldName().equals("public")) + ); + + assertTrue( + docTypeAndFieldsGroup + .get("web") + .stream() + .anyMatch(f -> f.getFieldName().equals("title")) + ); + + Set docTypes = + IndexerEvents.mergeDocTypes(docTypeAndFieldsGroup, List.of(docType)); + + assertTrue(docTypes + .stream() + .filter(dt -> dt.getName().equals("web")) + .map(DocType::getDocTypeFields) + .flatMap(Collection::stream) + .anyMatch(f -> f.equals(title)) + ); + + assertTrue(docTypes + .stream() + .filter(dt -> dt.getName().equals("web")) + .map(DocType::getDocTypeFields) + .flatMap(Collection::stream) + .noneMatch(f -> f.getFieldName().equals("title") + && f.getDescription().equals("auto-generated")) + ); + + docTypes.forEach(docType1 -> docType1 + .getDocTypeFields() + .forEach(docTypeField -> + _printDocTypeField(docTypeField, "") + ) + ); + + } + + private static final JsonObject mappings = (JsonObject) Json.decodeValue(""" + { + "properties" : { + "acl" : { + "properties" : { + "public" : { + "type" : "boolean" + } + } + }, + "contentId" : { + "type" : "text", + "fields" : { + "keyword" : { + "type" : "keyword", + "ignore_above" : 256 + } + } + }, + "datasourceId" : { + "type" : "long" + }, + "document" : { + "properties" : { + "content" : { + "type" : "text", + "fields" : { + "keyword" : { + "type" : "keyword", + "ignore_above" : 256 + } + } + }, + "contentType" : { + "type" : "text", + "fields" : { + "keyword" : { + "type" : "keyword", + "ignore_above" : 256 + } + } + }, + "title" : { + "type" : "text", + "fields" : { + "keyword" : { + "type" : "keyword", + "ignore_above" : 256 + } + } + }, + "url" : { + "type" : "text", + "fields" : { + "keyword" : { + "type" : "keyword", + "ignore_above" : 256 + } + } + } + } + }, + "documentTypes" : { + "type" : "text", + "fields" : { + "keyword" : { + "type" : "keyword", + "ignore_above" : 256 + } + } + }, + "file" : { + "properties" : { + "path" : { + "type" : "text", + "fields" : { + "keyword" : { + "type" : "keyword", + "ignore_above" : 256 + } + } + } + } + }, + "indexName" : { + "type" : "text", + "fields" : { + "keyword" : { + "type" : "keyword", + "ignore_above" : 256 + } + } + }, + "ingestionId" : { + "type" : "text", + "fields" : { + "keyword" : { + "type" : "keyword", + "ignore_above" : 256 + } + } + }, + "web" : { + "properties" : { + "content" : { + "properties" : { + "base" : { + "type" : "text", + "fields" : { + "keyword" : { + "type" : "keyword", + "ignore_above" : 256 + } + } + }, + "i18n" : { + "properties" : { + "de_DE" : { + "type" : "text", + "fields" : { + "keyword" : { + "type" : "keyword", + "ignore_above" : 256 + } + } + }, + "en_US" : { + "type" : "text", + "fields" : { + "keyword" : { + "type" : "keyword", + "ignore_above" : 256 + } + } + } + } + } + } + }, + "favicon" : { + "type" : "text", + "fields" : { + "keyword" : { + "type" : "keyword", + "ignore_above" : 256 + } + } + }, + "title" : { + "properties" : { + "base" : { + "type" : "text", + "fields" : { + "keyword" : { + "type" : "keyword", + "ignore_above" : 256 + } + } + }, + "i18n" : { + "properties" : { + "en_US" : { + "type" : "text", + "fields" : { + "keyword" : { + "type" : "keyword", + "ignore_above" : 256 + } + } + } + } + } + } + }, + "url" : { + "type" : "text", + "fields" : { + "keyword" : { + "type" : "keyword", + "ignore_above" : 256 + } + } + } + } + } + } + } + """); + + + private static final DocType docType; + private static final DocTypeField title, titleKeyword, titleTrigram; + + static { + docType = new DocType(); + docType.setName("web"); + docType.setId(1L); + + title = new DocTypeField(); + title.setId(2L); + title.setDocType(docType); + title.setFieldName("title"); + title.setName("web.title"); + title.setDescription("persisted"); + title.setFieldType(FieldType.TEXT); + + titleKeyword = new DocTypeField(); + titleKeyword.setId(3L); + titleKeyword.setDocType(docType); + titleKeyword.setDescription("persisted"); + titleKeyword.setFieldName("keyword"); + titleKeyword.setName("web.title.keyword"); + titleKeyword.setFieldType(FieldType.KEYWORD); + titleKeyword.setParentDocTypeField(title); + + titleTrigram = new DocTypeField(); + titleTrigram.setId(4L); + Analyzer trigram = new Analyzer(); + trigram.setId(5L); + trigram.setName("trigram"); + trigram.setType("custom"); + titleTrigram.setDocType(docType); + titleTrigram.setDescription("persisted"); + titleTrigram.setFieldName("trigram"); + titleTrigram.setName("web.title.trigram"); + titleTrigram.setFieldType(FieldType.TEXT); + titleTrigram.setAnalyzer(trigram); + titleTrigram.setParentDocTypeField(title); + + title.setSubDocTypeFields(new LinkedHashSet<>(List.of(titleKeyword, titleTrigram))); + + docType.setDocTypeFields(new LinkedHashSet<>(List.of(title))); + } + + private static void _printDocTypeField(DocTypeField docTypeField, String depth) { + System.out.println( + depth + + " fieldName: " + docTypeField.getFieldName() + + " name: " + docTypeField.getName() + + " description: " + docTypeField.getDescription() + + " path: " + docTypeField.getPath() + ); + for (DocTypeField child : docTypeField.getSubDocTypeFields()) { + _printDocTypeField(child, depth + "-"); + } + } + +} diff --git a/core/common/graphql-util/src/main/java/io/openk9/common/graphql/util/service/GraphQLService.java b/core/common/graphql-util/src/main/java/io/openk9/common/graphql/util/service/GraphQLService.java index 6d69cdf3d..0bd7bb7dc 100644 --- a/core/common/graphql-util/src/main/java/io/openk9/common/graphql/util/service/GraphQLService.java +++ b/core/common/graphql-util/src/main/java/io/openk9/common/graphql/util/service/GraphQLService.java @@ -80,6 +80,22 @@ public Uni> findJoinConnection( } + public Uni> findJoinConnection( + long entityId, String joinField, Class joinType, + String[] searchFields, String after, + String before, Integer first, Integer last, String searchText, + Set sortByList, boolean not, + BiFunction, Predicate> whereFun) { + + return findJoinConnection( + entityId, joinField, joinType, searchFields, after, before, first, + last, searchText, sortByList, not, + entityRoot -> entityRoot.join(joinField), + (i1, i2) -> List.of(), + whereFun); + + } + public Uni> findJoinConnection( long entityId, String joinField, Class joinType, String[] searchFields, String after, @@ -88,6 +104,24 @@ public Uni> findJoinConnection( Function, Path> mapper, BiFunction, List> defaultOrderFun) { + return findJoinConnection( + entityId, joinField, joinType, searchFields, after, before, first, + last, searchText, sortByList, not, + mapper, + defaultOrderFun, + (criteriaBuilder, tPath) -> criteriaBuilder.conjunction()); + + } + + public Uni> findJoinConnection( + long entityId, String joinField, Class joinType, + String[] searchFields, String after, + String before, Integer first, Integer last, String searchText, + Set sortByList, boolean not, + Function, Path> mapper, + BiFunction, List> defaultOrderFun, + BiFunction, Predicate> whereFun) { + CriteriaBuilder builder = getCriteriaBuilder(); CriteriaQuery joinEntityQuery = builder.createQuery(joinType); @@ -110,7 +144,9 @@ public Uni> findJoinConnection( return findConnection( joinEntityQuery, upperRoot, - builder.in(upperRoot.get(getIdAttribute())).value(subquery).not(), + builder.and( + whereFun.apply(builder, subJoin), + builder.in(upperRoot.get(getIdAttribute())).value(subquery).not()), searchFields, after, before, first, last, searchText, sortByList); } @@ -124,7 +160,9 @@ public Uni> findJoinConnection( return findConnection( joinEntityQuery.select(join), join, - builder.equal(entityRoot.get(getIdAttribute()), entityId), + builder.and( + whereFun.apply(builder, join), + builder.equal(entityRoot.get(getIdAttribute()), entityId)), searchFields, after, before, first, last, searchText, sortByList); } diff --git a/core/common/resources-common/src/main/resources/META-INF/microprofile-config.properties b/core/common/resources-common/src/main/resources/META-INF/microprofile-config.properties index 638ffc430..e40efa526 100644 --- a/core/common/resources-common/src/main/resources/META-INF/microprofile-config.properties +++ b/core/common/resources-common/src/main/resources/META-INF/microprofile-config.properties @@ -34,4 +34,7 @@ quarkus.opentelemetry.enabled=true quarkus.http.root-path=/api/${quarkus.application.name} quarkus.package.output-name=openk9-${quarkus.application.name}-${quarkus.application.version} -quarkus.micrometer.export.prometheus.path=/metrics \ No newline at end of file +quarkus.micrometer.export.prometheus.path=/metrics +quarkus.smallrye-health.root-path=/q/health +quarkus.smallrye-health.liveness-path=/q/health/live +quarkus.smallrye-health.readiness-path=/q/health/ready \ No newline at end of file diff --git a/js-packages/admin-ui/src/components/DataSources.tsx b/js-packages/admin-ui/src/components/DataSources.tsx index 335eedd82..3964dacc3 100644 --- a/js-packages/admin-ui/src/components/DataSources.tsx +++ b/js-packages/admin-ui/src/components/DataSources.tsx @@ -9,7 +9,7 @@ import { StyleToggle } from "./Form"; export const DataSourcesQuery = gql` query DataSources($searchText: String, $cursor: String) { - datasources(searchText: $searchText, first: 25, after: $cursor) { + datasources(searchText: $searchText, first: 50, after: $cursor) { edges { node { id diff --git a/js-packages/admin-ui/src/graphql-generated.ts b/js-packages/admin-ui/src/graphql-generated.ts index 410f90808..1dffae922 100644 --- a/js-packages/admin-ui/src/graphql-generated.ts +++ b/js-packages/admin-ui/src/graphql-generated.ts @@ -7958,7 +7958,7 @@ export type UnbindDataIndexToDataSourceMutationResult = Apollo.MutationResult; export const DataSourcesDocument = gql` query DataSources($searchText: String, $cursor: String) { - datasources(searchText: $searchText, first: 25, after: $cursor) { + datasources(searchText: $searchText, first: 50, after: $cursor) { edges { node { id @@ -12760,4 +12760,4 @@ export function useCreateYouTubeDataSourceMutation(baseOptions?: Apollo.Mutation export type CreateYouTubeDataSourceMutationHookResult = ReturnType; export type CreateYouTubeDataSourceMutationResult = Apollo.MutationResult; export type CreateYouTubeDataSourceMutationOptions = Apollo.BaseMutationOptions; -// Generated on 2023-10-04T11:36:52+02:00 +// Generated on 2023-10-04T14:09:21+02:00 diff --git a/js-packages/docs-website/src/components/SearchesAnimation.js b/js-packages/docs-website/src/components/SearchesAnimation.js index 4dc88bad3..f11bfbaa0 100644 --- a/js-packages/docs-website/src/components/SearchesAnimation.js +++ b/js-packages/docs-website/src/components/SearchesAnimation.js @@ -246,7 +246,7 @@ export function SearchesAnimation() { linear={true} camera={{ position: [3, 4, 9], fov: 20 }} shadows - frameloop="demand" + frameloop="always" dpr={[1, 2]} onCreated={({ camera }) => { camera.lookAt(0, 1, 0); diff --git a/js-packages/search-frontend/package.json b/js-packages/search-frontend/package.json index eecbcea9a..5ce03b800 100644 --- a/js-packages/search-frontend/package.json +++ b/js-packages/search-frontend/package.json @@ -53,6 +53,7 @@ "@fortawesome/fontawesome-svg-core": "6.2.0", "@fortawesome/free-solid-svg-icons": "6.2.0", "@fortawesome/react-fontawesome": "0.2.0", + "@fortawesome/free-brands-svg-icons": "6.2.0", "@pmmmwh/react-refresh-webpack-plugin": "^0.5.1", "@types/keycloak-js": "^3.4.1", "@types/lodash": "^4.14.178", diff --git a/js-packages/search-frontend/src/App.tsx b/js-packages/search-frontend/src/App.tsx index 8b05294f5..4ccecea6f 100644 --- a/js-packages/search-frontend/src/App.tsx +++ b/js-packages/search-frontend/src/App.tsx @@ -15,8 +15,8 @@ import { useTranslation } from "react-i18next"; import { ChangeLanguage } from "./components/ChangeLanguage"; export const openk9 = new OpenK9({ enabled: true, - searchAutoselect: true, - searchReplaceText: true, + searchAutoselect: false, + searchReplaceText: false, }); export function App() { @@ -210,7 +210,15 @@ export function App() { `} onClick={handleClick} className="openk9-update-configuration" - ref={(element) => openk9.updateConfiguration({ search: element })} + ref={(element) => + openk9.updateConfiguration({ + searchConfigurable: { + btnSearch: false, + isShowSyntax: true, + element, + }, + }) + } > - - + align-items: center; + gap: 3px; + @media (max-width: 480px) { + background: white; + border: 1px solid #d6012e; + width: 100%; + height: auto; + margin-top: 20px; + color: black; + border-radius: 50px; + display: flex; + justify-content: center; + color: var(--red-tones-500, #c0272b); + text-align: center; + font-size: 16px; + font-style: normal; + font-weight: 700; + line-height: normal; + align-items: center; + } + `} + onClick={() => { + onConfigurationChange({ filterTokens: [] }); + onConfigurationChangeExt && onConfigurationChangeExt(); + }} + > +
{t("remove-filters")}
+ + + + + )} ); @@ -232,6 +244,8 @@ function FiltersHorizontal({ }, unknown >, + haveValue: boolean, + setHaveValue: any, ): JSX.Element { const [isOpen, setIsOpen] = React.useState(true); const resultValue = suggestions.data?.pages || []; @@ -241,176 +255,179 @@ function FiltersHorizontal({ searchQuery, suggestion.id, ); + if (filters.length === 0) return
; + React.useEffect(() => { + setHaveValue(true); + }, []); return ( - {index !== 0 && ( -
- )} -
-
- {suggestion.name} -
- -
- - {isOpen && - filters.map((token: any, index: number) => { - const asSearchToken = mapSuggestionToSearchToken(token, true); - const checked = filterSelect.some((element) => { - return ( - element.values && - element.values[0] === token.value && - "goToSuggestion" in element - ); - }); - return ( - -
- - handleCheckboxChange(event, asSearchToken) - } - css={css` - width: 14px; - appearance: none; - min-width: 15px; - min-height: 15px; - border-radius: 4px; - border: 2px solid #ccc; - background-color: ${checked - ? "var(--openk9-embeddable-search--secondary-active-color)" - : "#fff"}; - background-size: 100%; - background-position: center; - background-repeat: no-repeat; - cursor: pointer; - margin-right: 10px; - `} - /> - -
-
- ); - })} -
- {suggestions.hasNextPage && ( -
- -
+
+ {suggestion.name} +
+ + + + {isOpen && + filters.map((token: any, index: number) => { + const asSearchToken = mapSuggestionToSearchToken(token, true); + const checked = filterSelect.some((element) => { + return ( + element.values && + element.values[0] === token.value && + "goToSuggestion" in element + ); + }); + return ( + +
+ + handleCheckboxChange(event, asSearchToken) + } + css={css` + width: 14px; + appearance: none; + min-width: 15px; + min-height: 15px; + border-radius: 4px; + border: 2px solid #ccc; + background-color: ${checked + ? "var(--openk9-embeddable-search--secondary-active-color)" + : "#fff"}; + background-size: 100%; + background-position: center; + background-repeat: no-repeat; + cursor: pointer; + margin-right: 10px; + `} + /> + +
+
+ ); + })} +
+ {suggestions.hasNextPage && ( +
+ +
+ )} +
)} ); @@ -503,3 +520,28 @@ export function useInfiniteSuggestions( return suggestionCategories; } + +function NoFilters() { + const { t } = useTranslation(); + + return ( +
+
+ +

{t("no-filters")}

+
+
+ ); +} diff --git a/js-packages/search-frontend/src/components/FiltersMobile.tsx b/js-packages/search-frontend/src/components/FiltersMobile.tsx index 400c9ad37..b8b4f4bfe 100644 --- a/js-packages/search-frontend/src/components/FiltersMobile.tsx +++ b/js-packages/search-frontend/src/components/FiltersMobile.tsx @@ -24,6 +24,8 @@ export type FiltersMobileProps = { setIsVisibleFilters: | React.Dispatch> | undefined; + language: string; + sortAfterKey: string; }; function FiltersMobile({ dynamicFilters, @@ -35,6 +37,8 @@ function FiltersMobile({ configuration, isVisibleFilters, setIsVisibleFilters, + language, + sortAfterKey, }: FiltersMobileProps) { const componet = ( @@ -86,6 +90,7 @@ function FiltersMobile({ + ); +} diff --git a/js-packages/search-frontend/src/components/Scrollbar.css b/js-packages/search-frontend/src/components/Scrollbar.css new file mode 100644 index 000000000..2b4e0f9f0 --- /dev/null +++ b/js-packages/search-frontend/src/components/Scrollbar.css @@ -0,0 +1,24 @@ +::-webkit-scrollbar { + width: 6px; + } + + /* Track */ + ::-webkit-scrollbar-track { + background-color: transparent; + + + } + + /* Handle */ + ::-webkit-scrollbar-thumb { + background: rgba(0, 0, 0, 0.4); + border-radius: 10px; + height:5px; + + } + + /* Handle on hover */ + ::-webkit-scrollbar-thumb:hover { + background: rgba(0, 0, 0, .55); + height:5px; + } \ No newline at end of file diff --git a/js-packages/search-frontend/src/components/Search.tsx b/js-packages/search-frontend/src/components/Search.tsx index ccaa1d5f8..98db7973e 100644 --- a/js-packages/search-frontend/src/components/Search.tsx +++ b/js-packages/search-frontend/src/components/Search.tsx @@ -15,23 +15,33 @@ import { SearchToken, SortField, } from "./client"; -import { SelectionsAction, SelectionsState } from "./useSelections"; +import { + SelectionsAction, + SelectionsActionOnClick, + SelectionsState, + SelectionsStateOnClick, +} from "./useSelections"; import { SearchDateRange } from "../embeddable/Main"; import { DeleteLogo } from "./DeleteLogo"; import { useTranslation } from "react-i18next"; import { ArrowLeftSvg } from "../svgElement/ArrowLeftSvg"; +import { divide } from "lodash"; type SearchProps = { configuration: Configuration; onDetail(detail: GenericResultItem | null): void; spans: Array; - selectionsState: SelectionsState; - selectionsDispatch(action: SelectionsAction): void; + selectionsState: SelectionsState | SelectionsStateOnClick; + selectionsDispatch(action: SelectionsAction | SelectionsActionOnClick): void; showSyntax: boolean; isMobile: boolean; filtersSelect: SearchToken[]; isVisibleFilters: boolean; mobileVersion?: boolean; + btnSearch?: boolean; + defaultValue?: string; + isSearchOnInputChange?: boolean; + saveSearchQuery?: React.Dispatch>; actionCloseMobileVersion?: | React.Dispatch> | undefined; @@ -45,7 +55,11 @@ export function Search({ showSyntax, isMobile, mobileVersion = false, + btnSearch = false, actionCloseMobileVersion, + isSearchOnInputChange = false, + saveSearchQuery, + defaultValue, }: SearchProps) { const autoSelect = configuration.searchAutoselect; const replaceText = configuration.searchReplaceText; @@ -57,6 +71,9 @@ export function Search({ const clickAwayRef = React.useRef(null); useClickAway([clickAwayRef], () => setOpenedDropdown(null)); + const [textBtn, setTextBtn] = React.useState( + defaultValue, + ); const inputRef = React.useRef(null); const [adjustedSelection, setAdjustedSelection] = React.useState<{ selectionStart: number; @@ -68,6 +85,23 @@ export function Search({ inputRef.current.selectionEnd = adjustedSelection.selectionEnd; } }, [adjustedSelection]); + React.useEffect(() => { + if ((defaultValue !== null || defaultValue !== undefined) && btnSearch) { + if (defaultValue === "") { + selectionsDispatch({ + type: "reset-search", + }); + } else { + selectionsDispatch({ + type: "set-text", + text: defaultValue, + textOnchange: defaultValue, + }); + } + setTextBtn(defaultValue); + } + }, [defaultValue]); + const { t } = useTranslation(); return ( @@ -83,15 +117,11 @@ export function Search({ flex-direction: column; margin-top: 15px; } + .openk9-focusable:has(input:focus) { + border: 1px solid #c22525; + } `} > -
{ - selectionsDispatch({ - type: "set-selection", - replaceText, - selection: { - text: span.text, - start: span.start, - end: span.end, - token, - isAuto: false, - }, - }); + if (isSearchOnInputChange) { + selectionsDispatch({ + type: "set-selection", + replaceText, + selection: { + text: span.text, + start: span.start, + end: span.end, + token, + isAuto: false, + }, + }); + } else { + selectionsDispatch({ + type: "set-selection", + replaceText, + selection: { + text: span.text, + textOnChange: span.text, + start: span.start, + end: span.end, + token, + isAuto: false, + }, + }); + } if ( inputRef.current?.selectionStart && inputRef.current?.selectionEnd @@ -208,6 +253,9 @@ export function Search({ isAutoSlected={isAutoSelected} setOpenedDropdown={setOpenedDropdown} selectionsDispatch={selectionsDispatch} + isColorSearch={isSearchOnInputChange} + setTextBtn={setTextBtn} + isBtnSearch={btnSearch} /> ); })} @@ -228,14 +276,30 @@ export function Search({ } type="text" placeholder={t("search") || "search..."} - value={selectionsState.text} + value={btnSearch ? textBtn ?? "" : selectionsState.text} onChange={(event) => { - selectionsDispatch({ - type: "set-text", - text: event.currentTarget.value, - }); - onDetail(null); - setOpenedDropdown(null); + if (!btnSearch) { + selectionsDispatch({ + type: "set-text", + text: event.currentTarget.value, + textOnchange: event.currentTarget.value, + }); + onDetail(null); + setOpenedDropdown(null); + } else { + setTextBtn(event.currentTarget.value); + if (isSearchOnInputChange) { + selectionsDispatch({ + type: "set-text", + text: event.currentTarget.value, + }); + } else { + selectionsDispatch({ + type: "set-text", + textOnchange: event.currentTarget.value, + }); + } + } }} css={css` position: relative; @@ -247,7 +311,11 @@ export function Search({ font-size: inherit; font-family: inherit; background-color: inherit; - color: ${showSyntax ? "transparent" : "inherit"}; + color: ${textBtn + ? "black" + : showSyntax + ? "transparent" + : "inherit"}; `} spellCheck="false" onSelect={(event) => { @@ -295,18 +363,48 @@ export function Search({ } } else if (event.key === "Enter") { event.preventDefault(); + if (btnSearch) { + if (textBtn === "") { + selectionsDispatch({ + type: "reset-search", + }); + } else { + selectionsDispatch({ + type: "set-text", + text: option?.value, + textOnchange: option?.value, + }); + } + } + option?.value ? setTextBtn(option?.value) : null; + if (span) { - selectionsDispatch({ - type: "set-selection", - replaceText, - selection: { - text: span.text, - start: span.start, - end: span.end, - token: option ?? null, - isAuto: false, - }, - }); + if (isSearchOnInputChange) { + selectionsDispatch({ + type: "set-selection", + replaceText, + selection: { + text: span.text, + start: span.start, + end: span.end, + token: option ?? null, + isAuto: false, + }, + }); + } else { + selectionsDispatch({ + type: "set-selection", + replaceText, + selection: { + text: span?.text || "", + textOnChange: span?.text || "", + start: span?.start || 0, + end: span?.end || 0, + token: option ?? null, + isAuto: false, + }, + }); + } if (replaceText) { const text = option && @@ -362,10 +460,21 @@ export function Search({ } `} onClick={() => { - selectionsDispatch({ - type: "set-text", - text: "", - }); + if (!btnSearch) { + selectionsDispatch({ + type: "set-text", + text: "", + }); + } else { + selectionsDispatch({ + type: "set-text", + textOnchange: " ", + text: " ", + }); + setTextBtn(""); + onDetail(null); + setOpenedDropdown(null); + } }} > @@ -373,6 +482,48 @@ export function Search({
+ {btnSearch && ( +
+ +
+ )}
); diff --git a/js-packages/search-frontend/src/components/SearchMobile.tsx b/js-packages/search-frontend/src/components/SearchMobile.tsx index 8d8f6e940..21b50da86 100644 --- a/js-packages/search-frontend/src/components/SearchMobile.tsx +++ b/js-packages/search-frontend/src/components/SearchMobile.tsx @@ -13,7 +13,12 @@ import { SortField, } from "./client"; import { useClickAway } from "./useClickAway"; -import { SelectionsAction, SelectionsState } from "./useSelections"; +import { + SelectionsAction, + SelectionsActionOnClick, + SelectionsState, + SelectionsStateOnClick, +} from "./useSelections"; import React from "react"; import { css } from "styled-components/macro"; import { ArrowLeftSvg } from "../svgElement/ArrowLeftSvg"; @@ -25,8 +30,8 @@ type SearchMobileProps = { onConfigurationChange: ConfigurationUpdateFunction; onDetail(detail: GenericResultItem | null): void; spans: Array; - selectionsState: SelectionsState; - selectionsDispatch(action: SelectionsAction): void; + selectionsState: SelectionsState | SelectionsStateOnClick; + selectionsDispatch(action: SelectionsAction | SelectionsActionOnClick): void; showSyntax: boolean; dateRange: SearchDateRange; onDateRangeChange(dateRange: SearchDateRange): void; @@ -115,15 +120,11 @@ export function SearchMobile({ flex-direction: column; margin-top: 15px; } + .openk9-focusable:has(input:focus) { + border: 1px solid #c22525; + } `} > -
>; + isColorSearch?: boolean; + isBtnSearch: boolean; setOpenedDropdown: React.Dispatch< React.SetStateAction<{ textPosition: number; optionPosition: number; } | null> >; + setTextBtn: React.Dispatch>; }; export function TokenSelect({ span, @@ -34,10 +38,34 @@ export function TokenSelect({ setOpenedDropdown, onSelectText, selectionsDispatch, + saveSearchQuery, + isColorSearch = true, + isBtnSearch = false, + setTextBtn, }: TokenSelectProps) { const isInteractive = span.tokens.length > 0; const [subtitle, setSubtitle] = React.useState(false); const { t } = useTranslation(); + const statusStyles: Record = { + "can-select": css` + display: ${isColorSearch ? "default" : "none"}; + color: ${isColorSearch + ? "var(--openk9-embeddable-search--primary-color)" + : "inerith"}; + `, + "auto-selected": css` + display: ${isColorSearch ? "default" : "none"}; + color: lightseagreen; + `, + "has-selected": css` + display: ${isColorSearch ? "default" : "none"}; + color: dodgerblue; + `, + "not-interactive": css` + display: ${isColorSearch ? "default" : "none"}; + color: black; + `, + }; const status: Status = isInteractive ? selected !== null ? isAutoSlected @@ -66,6 +94,7 @@ export function TokenSelect({ background-color: ${"var(--openk9-embeddable-search--secondary-background-color)"}; cursor: ${!isSelected ? "" : "not-allowed"}; `; + return (
{ + setTextBtn(getTokenLabel(option) || '') + isBtnSearch ? selectionsDispatch({ + type: "set-text", + text: option?.value, + textOnchange: option?.value, + }) : null if (option.tokenType === "AUTOCOMPLETE") { onSelectText(option); } else { + if (saveSearchQuery) saveSearchQuery((save) => !save); onSelect(option); + setOpenedDropdown(null); } }} onMouseEnter={() => { @@ -206,20 +244,7 @@ type Status = | "has-selected" | "auto-selected" | "not-interactive"; -const statusStyles: Record = { - "can-select": css` - color: var(--openk9-embeddable-search--primary-color); - `, - "auto-selected": css` - color: lightseagreen; - `, - "has-selected": css` - color: dodgerblue; - `, - "not-interactive": css` - color: black; - `, -}; + function getTokenLabel(token: AnalysisToken) { switch (token.tokenType) { case "DATASOURCE": @@ -230,7 +255,10 @@ function getTokenLabel(token: AnalysisToken) { return token.entityName; case "TEXT": return token.value; + case "AUTOCOMPLETE": + return token.value; } + return token.value; } function FactoryTokenType({ diff --git a/js-packages/search-frontend/src/components/TotalResults.tsx b/js-packages/search-frontend/src/components/TotalResults.tsx new file mode 100644 index 000000000..8c5bfd637 --- /dev/null +++ b/js-packages/search-frontend/src/components/TotalResults.tsx @@ -0,0 +1,11 @@ +import React from "react"; + +export function TotalResults({ + totalResult +}: { + totalResult: number | null; +}) { + return ( +
{totalResult}
+ ) +} \ No newline at end of file diff --git a/js-packages/search-frontend/src/components/client.ts b/js-packages/search-frontend/src/components/client.ts index fc8e36dc2..be71002b8 100644 --- a/js-packages/search-frontend/src/components/client.ts +++ b/js-packages/search-frontend/src/components/client.ts @@ -268,8 +268,10 @@ export function OpenK9Client({ }, async fetchQueryAnalysis( request: AnalysisRequest, - ): Promise { + ): Promise { const mock = false; + const isActiveQueryAnalysis = true; + if (!isActiveQueryAnalysis) return null; if (mock) return { searchText: "Questo è un esempio di testo per l'analisi", diff --git a/js-packages/search-frontend/src/components/dataRangePicker.css b/js-packages/search-frontend/src/components/dataRangePicker.css index 28c12d439..e34ff9ac7 100644 --- a/js-packages/search-frontend/src/components/dataRangePicker.css +++ b/js-packages/search-frontend/src/components/dataRangePicker.css @@ -80,6 +80,16 @@ .DateRangePicker_picker{ @media (max-width:480px) { bottom: 30px !important; + left: 50% !important; + transform: translate(-50%, 0) !important; + } +} + +.DateRangePicker_picker { + @media (min-width: 481px) and (max-width: 768px) { + bottom: 30px !important; + left:50% !important; + transform: translate(-50%, 0) !important; } } diff --git a/js-packages/search-frontend/src/components/dateRangePickerVertical.css b/js-packages/search-frontend/src/components/dateRangePickerVertical.css new file mode 100644 index 000000000..b686ae12a --- /dev/null +++ b/js-packages/search-frontend/src/components/dateRangePickerVertical.css @@ -0,0 +1,47 @@ +.SingleDatePickerInput_1 { + display: flex; + align-items: center; + justify-content: center; +} + +.SingleDatePickerInput_calendarIcon_1 { + display: flex; +} + +.DateRangePickerVertical-date-title { + text-overflow: ellipsis; + font-style: normal; + font-weight: 600; + line-height: 22px; + color: #000000; + margin: 5px 0; +} + +.DateRangePickerVertical-startDate-container { + margin: 0 0 10px 0; +} + +.SingleDatePickerInput__showClearDate { + padding-right: 0; +} + +.SingleDatePickerInput_clearDate { + margin: 0px 0px 0px 0px; + top: 47%; + right: 10%; + background-color: #fff; + padding: 0; +} + +.SingleDatePickerInput_clearDate__default:focus, .SingleDatePickerInput_clearDate__default:hover { + background-color: #fff; + border-radius: 0; +} + +.SingleDatePickerInput_clearDate__default:focus, .SingleDatePickerInput_clearDate__default:hover svg { + fill: red; +} + +.SingleDatePickerInput__disabled .SingleDatePickerInput_clearDate { + background-color: #f2f2f2; +} \ No newline at end of file diff --git a/js-packages/search-frontend/src/components/useSelections.tsx b/js-packages/search-frontend/src/components/useSelections.tsx index 22e11cc9b..24c5e3d73 100644 --- a/js-packages/search-frontend/src/components/useSelections.tsx +++ b/js-packages/search-frontend/src/components/useSelections.tsx @@ -28,18 +28,44 @@ export function useSelections() { return [state, dispatch] as const; } +export function useSelectionsOnClick() { + const [state, dispatch] = React.useReducer( + reducerOnClick, + initialOnClick, + (initialOnClick) => + loadQueryString() || initialOnClick, + ); + const [canSave, setCanSave] = React.useState(false); + const client = useOpenK9Client(); + React.useEffect(() => { + if (client.authInit) + client.authInit.then(() => { + setCanSave(true); + }); + }, []); + React.useEffect(() => { + if (canSave) { + saveQueryString(state); + } + }, [canSave, state]); + return [state, dispatch] as const; +} + export type SelectionsState = { - text: string; + text?: string; selection: Array; + textOnChange?: string; }; const initial: SelectionsState = { text: "", selection: [], + textOnChange: "", }; export type SelectionsAction = - | { type: "set-text"; text: string } + | { type: "set-text"; text?: string; textOnchange?: string } + | { type: "reset-search" } | { type: "set-selection"; replaceText: boolean; @@ -47,6 +73,33 @@ export type SelectionsAction = }; type Selection = { + text: string; + textOnChange: string; + start: number; + end: number; + token: AnalysisToken | null; + isAuto: boolean; +}; + +export type SelectionsStateOnClick = { + text: string; + selection: Array; +}; + +const initialOnClick: SelectionsStateOnClick = { + text: "", + selection: [], +}; + +export type SelectionsActionOnClick = + | { type: "set-text"; text: string } + | { + type: "set-selection"; + replaceText: boolean; + selection: SelectionOnClick; + }; + +type SelectionOnClick = { text: string; start: number; end: number; @@ -58,11 +111,90 @@ function reducer( state: SelectionsState, action: SelectionsAction, ): SelectionsState { + switch (action.type) { + case "set-text": { + return { + text: action.text || state.text || "", + textOnChange: action.textOnchange || state.textOnChange || "", + selection: shiftSelection( + state.textOnChange ?? "", + action.textOnchange || state.textOnChange || "", + state.selection, + ), + }; + } + case "reset-search" : { + return { + text: '', + textOnChange: '', + selection: [] + } + } + case "set-selection": { + const { text, selection } = (() => { + if ( + action.replaceText || + action.selection.token?.tokenType === "AUTOCORRECT" + ) { + // const textOnchange = action.selection.textOnChange; + const tokenText = action.selection.token + ? getTokenText(action.selection.token) + : state.textOnChange || + "".slice(action.selection.start, action.selection.end); + + const text = + state.textOnChange || + "".slice(0, action.selection.start) + + tokenText + + state.textOnChange || + "".slice(action.selection.end); + const selection: Selection | null = + action.selection.token?.tokenType === "AUTOCORRECT" + ? null + : { + text: tokenText, + textOnChange: tokenText, + start: action.selection.start, + end: action.selection.start + tokenText.length, + token: action.selection.token, + isAuto: action.selection.isAuto, + }; + return { + text, + selection, + }; + } else { + return { text: state.textOnChange, selection: action.selection }; + } + })(); + return { + text, + textOnChange: state.textOnChange || "", + selection: shiftSelection( + state.textOnChange || "", + state.textOnChange || "", + selection + ? state.selection.filter((s) => !isOverlapping(s, selection)) + : state.selection, + ).concat(selection ? [selection] : []), + }; + } + } +} + +function reducerOnClick( + state: SelectionsStateOnClick, + action: SelectionsActionOnClick, +): SelectionsStateOnClick { switch (action.type) { case "set-text": { return { text: action.text, - selection: shiftSelection(state.text, action.text, state.selection), + selection: shiftSelectionOnCLick( + state.text, + action.text, + state.selection, + ), }; } case "set-selection": { @@ -78,7 +210,7 @@ function reducer( state.text.slice(0, action.selection.start) + tokenText + state.text.slice(action.selection.end); - const selection: Selection | null = + const selection: SelectionOnClick | null = action.selection.token?.tokenType === "AUTOCORRECT" ? null : { @@ -98,7 +230,7 @@ function reducer( })(); return { text, - selection: shiftSelection( + selection: shiftSelectionOnCLick( state.text, text, selection @@ -147,6 +279,43 @@ function shiftSelection( return prefixAttributes.concat(suffixAttributes); } +function shiftSelectionOnCLick( + prevText: string, + nextText: string, + prevSelection: Array, +): Array { + if (prevText === nextText) { + return prevSelection; + } + const commonPrefixLength = findCommonPrefixLength(prevText, nextText); + const commonSuffixLength = findCommonSuffixLength( + prevText, + nextText, + commonPrefixLength, + ); + const changeStart = commonPrefixLength; + const changePrevEnd = prevText.length - commonSuffixLength; + const changeNextEnd = nextText.length - commonSuffixLength; + const changeDelta = changeNextEnd - changePrevEnd; + const prefixAttributes = prevSelection.filter( + (attribute) => + attribute.start <= changeStart && attribute.end <= changeStart, + ); + // const deletedAttributes = prevSelection.filter( + // (attribute) => + // !(attribute.start <= changeStart && attribute.end <= changeStart) && + // !(attribute.start >= changePrevEnd), + // ); + const suffixAttributes = prevSelection + .filter((attribute) => attribute.start >= changePrevEnd) + .map((attribute) => ({ + ...attribute, + start: attribute.start + changeDelta, + end: attribute.end + changeDelta, + })); + return prefixAttributes.concat(suffixAttributes); +} + function findCommonPrefixLength(a: string, b: string) { const length = Math.min(a.length, b.length); let prefixLength = 0; diff --git a/js-packages/search-frontend/src/embeddable/Main.tsx b/js-packages/search-frontend/src/embeddable/Main.tsx index 311464b8b..97cc82c18 100644 --- a/js-packages/search-frontend/src/embeddable/Main.tsx +++ b/js-packages/search-frontend/src/embeddable/Main.tsx @@ -7,6 +7,7 @@ import { getAutoSelections, isOverlapping, useSelections, + useSelectionsOnClick, } from "../components/useSelections"; import { LoginInfoComponentMemo } from "../components/LoginInfo"; import { @@ -40,6 +41,9 @@ import { DataRangePicker } from "../components/DateRangePicker"; import { SearchMobile } from "../components/SearchMobile"; import { CalendarMobile } from "../components/CalendarModal"; import { ChangeLanguage } from "../components/ChangeLanguage"; +import { DataRangePickerVertical } from "../components/DateRangePickerVertical"; +import { TotalResults } from "../components/TotalResults"; +import { ResultsPaginationMemo } from "../components/ResultListPagination"; type MainProps = { configuration: Configuration; onConfigurationChange: ConfigurationUpdateFunction; @@ -110,6 +114,9 @@ export function Main({ configuration.overrideTabs, languageSelect, ); + const [currentPage, setCurrentPage] = React.useState(0); + const isSearchOnInputChange = !configuration.searchConfigurable?.btnSearch; + const { searchQuery, spans, @@ -117,13 +124,24 @@ export function Main({ selectionsDispatch, isQueryAnalysisComplete, completelySort, - } = useSearch({ - configuration, - tabTokens, - filterTokens, - dateTokens, - onQueryStateChange, - }); + setIsSaveQuery, + } = isSearchOnInputChange + ? useSearchOnClick({ + configuration, + tabTokens, + filterTokens, + dateTokens, + onQueryStateChange, + setCurrentPage, + }) + : useSearch({ + configuration, + tabTokens, + filterTokens, + dateTokens, + onQueryStateChange, + setCurrentPage, + }); const { detail, setDetail } = useDetails(searchQuery); const { detailMobile, setDetailMobile } = useDetailsMobile(searchQuery); React.useEffect(() => { @@ -135,6 +153,8 @@ export function Main({ } }, [languageQuery.data, i18n]); const [sortAfterKey, setSortAfterKey] = React.useState(""); + const [totalResult, setTotalResult] = React.useState(null); + const numberOfResults = configuration.numberResult || 7; return ( {renderPortal( @@ -149,10 +169,36 @@ export function Main({ isMobile={isMobile} filtersSelect={configuration.filterTokens} isVisibleFilters={isVisibleFilters} + isSearchOnInputChange={isSearchOnInputChange} /> , configuration.search, )} + {renderPortal( + + + , + configuration.searchConfigurable + ? configuration.searchConfigurable.element + : null, + )} {renderPortal( , configuration.filters, )} + {renderPortal( + + + , + configuration.filtersConfigurable + ? configuration.filtersConfigurable.element + : null, + )} {renderPortal( , configuration.results, )} + {renderPortal( + + + , + configuration.totalResult, + )} {renderPortal( , configuration.resultList ? configuration.resultList.element : null, )} + {renderPortal( + + + , + configuration.resultListPagination, + )} {renderPortal( @@ -325,6 +429,8 @@ export function Main({ configuration={configuration} isVisibleFilters={configuration.filtersMobile?.isVisible || false} setIsVisibleFilters={configuration.filtersMobile?.setIsVisible} + language={languageSelect} + sortAfterKey={sortAfterKey} /> , configuration.filtersMobile?.element !== undefined @@ -354,6 +460,10 @@ export function Main({ selectedTabIndex={selectedTabIndex} viewTabs={configuration.filtersMobileLiveChange?.viewTabs ?? false} language={languageSelect} + isCollapsable={ + configuration.filtersMobileLiveChange?.isCollapsable ?? true + } + numberOfResults={numberOfResults} /> , configuration.filtersMobileLiveChange?.element !== undefined @@ -374,6 +484,21 @@ export function Main({ ? configuration.dataRangePicker?.element : null, )} + {renderPortal( + + + , + configuration.dataRangePickerVertical?.element !== undefined + ? configuration.dataRangePickerVertical?.element + : null, + )} {renderPortal( >; onQueryStateChange(queryState: QueryState): void; }) { const { searchAutoselect, searchReplaceText, defaultTokens, sort } = configuration; - const [selectionsState, selectionsDispatch] = useSelections(); + const [selectionsState, selectionsDispatch] = useSelectionsOnClick(); + const [isSvaleQuery, setIsSaveQuery] = React.useState(false); + const debounced = useDebounce(selectionsState, 600); const queryAnalysis = useQueryAnalysis({ - searchText: debounced.text, + searchText: debounced.text || "", tokens: debounced.selection.flatMap(({ text, start, end, token }) => token ? [{ text, start, end, token }] : [], ), }); const spans = React.useMemo( - () => calculateSpans(selectionsState.text, queryAnalysis.data?.analysis), + () => + calculateSpans(selectionsState.text || "", queryAnalysis.data?.analysis), [queryAnalysis.data?.analysis, selectionsState.text], ); const searchTokens = React.useMemo( @@ -500,6 +630,7 @@ function useSearch({ filterTokens, searchTokens, }); + setCurrentPage(0); }, [ onQueryStateChange, defaultTokens, @@ -540,6 +671,128 @@ function useSearch({ selectionsDispatch, isQueryAnalysisComplete, completelySort, + setIsSaveQuery, + }; +} + +function useSearch({ + configuration, + tabTokens, + filterTokens, + dateTokens, + onQueryStateChange, + setCurrentPage, +}: { + configuration: Configuration; + tabTokens: SearchToken[]; + filterTokens: SearchToken[]; + setCurrentPage: React.Dispatch>; + dateTokens: SearchToken[]; + onQueryStateChange(queryState: QueryState): void; +}) { + const { searchAutoselect, searchReplaceText, defaultTokens, sort } = + configuration; + const [selectionsState, selectionsDispatch] = useSelections(); + const [isSvaleQuery, setIsSaveQuery] = React.useState(false); + const debounced = useDebounce(selectionsState, 600); + + const queryAnalysis = useQueryAnalysis({ + searchText: debounced.textOnChange || "", + tokens: debounced.selection.flatMap(({ text, start, end, token }) => + token ? [{ text, start, end, token }] : [], + ), + }); + + const spans = React.useMemo( + () => + calculateSpans( + debounced.textOnChange || "", + queryAnalysis.data?.analysis, + ), + [queryAnalysis.data?.analysis, debounced.textOnChange], + ); + const searchTokens = React.useMemo(() => { + return deriveSearchQuery( + spans, + selectionsState.selection.flatMap(({ text, start, end, token }) => + token ? [{ text, start, end, token }] : [], + ), + ); + }, [isSvaleQuery, debounced.text]); + + const completelySort = React.useMemo(() => sort, [sort]); + const searchQueryMemo = React.useMemo( + () => [ + ...defaultTokens, + ...tabTokens, + ...filterTokens, + ...searchTokens, + ...dateTokens, + ], + [defaultTokens, tabTokens, filterTokens, dateTokens, searchTokens], + ); + const searchQuery = useDebounce(searchQueryMemo, 600); + const isQueryAnalysisComplete = + selectionsState.text === debounced.text && + queryAnalysis.data !== undefined && + !queryAnalysis.isPreviousData; + + React.useEffect(() => { + onQueryStateChange({ + defaultTokens, + tabTokens, + filterTokens, + searchTokens, + }); + setCurrentPage(0); + }, [ + onQueryStateChange, + defaultTokens, + tabTokens, + filterTokens, + searchTokens, + ]); + React.useEffect(() => { + if ( + searchAutoselect && + queryAnalysis.data && + queryAnalysis.data.searchText === selectionsState.text + ) { + const autoSelections = getAutoSelections( + selectionsState.selection, + queryAnalysis.data.analysis, + ); + for (const selection of autoSelections) { + selectionsDispatch({ + type: "set-selection", + replaceText: false, + selection: { + token: selection.token, + end: selection.end, + isAuto: selection.isAuto, + start: selection.start, + text: selection.text, + textOnChange: selection.text, + }, + }); + } + } + }, [ + searchAutoselect, + searchReplaceText, + selectionsDispatch, + queryAnalysis.data, + selectionsState.selection, + selectionsState.text, + ]); + return { + searchQuery, + spans, + selectionsState, + selectionsDispatch, + isQueryAnalysisComplete, + completelySort, + setIsSaveQuery, }; } @@ -698,10 +951,9 @@ function deriveSearchQuery( .filter((span) => span.text) .map((span): SearchToken => { const token = - selection.find( - (selection) => - selection.start === span.start && selection.end === span.end, - )?.token ?? null; + selection.find((selection) => { + return selection.start === span.start && selection.end === span.end; + })?.token ?? null; return ( (token && analysisTokenToSearchToken(token)) ?? { tokenType: "TEXT", @@ -775,6 +1027,15 @@ function calculateSpans( return spans.filter((span) => span.text); } +function useQueryAnalysisOnCLick(request: AnalysisRequest) { + const client = useOpenK9Client(); + return useQuery( + ["query-anaylis", request] as const, + async ({ queryKey: [, request] }) => + fixQueryAnalysisResult(await client.fetchQueryAnalysis(request)), + ); +} + function useQueryAnalysis(request: AnalysisRequest) { const client = useOpenK9Client(); return useQuery( @@ -784,26 +1045,27 @@ function useQueryAnalysis(request: AnalysisRequest) { ); } // TODO: togliere una volta implementata gestione sugestion sovrapposte -function fixQueryAnalysisResult(data: AnalysisResponse) { - return { - ...data, - analysis: data.analysis - .reverse() - .filter((entry, index, array) => - array - .slice(0, index) - .every((previous) => !isOverlapping(previous, entry)), - ) - .reverse() - .filter((entry) => { - // togliere validazione quando fixato lato be - const isValidEntry = entry.start >= 0; - if (!isValidEntry) { - console.warn(`Invalid entry: `, entry); - } - return isValidEntry; - }), - }; +function fixQueryAnalysisResult(data: AnalysisResponse | null) { + if (data) + return { + ...data, + analysis: data.analysis + .reverse() + .filter((entry, index, array) => + array + .slice(0, index) + .every((previous) => !isOverlapping(previous, entry)), + ) + .reverse() + .filter((entry) => { + // togliere validazione quando fixato lato be + const isValidEntry = entry.start >= 0; + if (!isValidEntry) { + console.warn(`Invalid entry: `, entry); + } + return isValidEntry; + }), + }; } export function remappingLanguage({ language }: { language: string }) { diff --git a/js-packages/search-frontend/src/embeddable/entry.tsx b/js-packages/search-frontend/src/embeddable/entry.tsx index b4593a546..beb73f422 100644 --- a/js-packages/search-frontend/src/embeddable/entry.tsx +++ b/js-packages/search-frontend/src/embeddable/entry.tsx @@ -170,12 +170,14 @@ type FiltersLiveMobileConfiguration = { isVisible: boolean; setIsVisible: React.Dispatch>; viewTabs?: boolean | null; + isCollapsable?: boolean | null; }; type SearchMobileConfiguration = { search: Element | string | null; isVisible: boolean; setIsVisible: React.Dispatch>; + isShowSyntax?: boolean | undefined | null; }; type CalendarMobileConfiguration = { @@ -198,6 +200,12 @@ type DataRangePickerProps = { end?: any; }; +type DataRangePickerVerticalProps = { + element: Element | string | null; + start?: any; + end?: any; +}; + type ResultListProps = { element: Element | string | null; changeOnOver: boolean; @@ -213,12 +221,27 @@ type SortableProps = { relevance: string; }; +type SearchProps = { + element: Element | string | null; + btnSearch: boolean; + isShowSyntax?: boolean | undefined | null; + defaultValue?: string | undefined | null; +}; + +type FilterProps = { + element: Element | string | null; + isCollapsable?: boolean; + numberItems?: number | null | undefined; +}; + export type Configuration = { enabled: boolean; search: Element | string | null; + searchConfigurable: SearchProps | null; activeFilters: Element | string | null; tabs: Element | string | null; filters: Element | string | null; + filtersConfigurable: FilterProps | null; calendarMobile: CalendarMobileConfiguration | null; resultList: ResultListProps | null; searchMobile: SearchMobileConfiguration | null; @@ -226,9 +249,11 @@ export type Configuration = { filtersMobile: FiltersHorizontalMobileConfiguration | null; filtersMobileLiveChange: FiltersLiveMobileConfiguration | null; dataRangePicker: DataRangePickerProps | null; + dataRangePickerVertical: DataRangePickerVerticalProps | null; filtersHorizontal: FiltersHorizontalConfiguration | null; sortable: Element | string | null; results: Element | string | null; + resultListPagination: Element | string | null; details: Element | string | null; calendar: Element | string | null; login: Element | string | null; @@ -242,7 +267,10 @@ export type Configuration = { defaultTokens: Array; resultsDisplayMode: ResultsDisplayMode; tenant: string | null; + numberResult: number | null | undefined; + isQueryAnalysis: boolean | null; token: string | null; + totalResult: Element | string | null; useKeycloak: boolean; overrideTabs: (tabs: Array) => Array; changeSortResult: ( @@ -253,31 +281,38 @@ export type Configuration = { const defaultConfiguration: Configuration = { enabled: false, search: null, + searchConfigurable: null, activeFilters: null, tabs: null, searchMobile: null, filtersMobile: null, filtersMobileLiveChange: null, dataRangePicker: null, + dataRangePickerVertical: null, calendarMobile: null, sortable: null, detailMobile: null, sort: [], filters: null, + filtersConfigurable: null, changeLanguage: null, filtersHorizontal: null, results: null, + resultListPagination: null, details: null, sortResultConfigurable: null, login: null, tenant: null, token: null, + totalResult: null, calendar: null, sortableConfigurable: null, resultList: null, useKeycloak: true, searchAutoselect: true, searchReplaceText: true, + isQueryAnalysis: true, + numberResult: null, filterTokens: [], defaultTokens: [], resultsDisplayMode: { type: "infinite" }, diff --git a/js-packages/search-frontend/src/renderer-components/Chip.tsx b/js-packages/search-frontend/src/renderer-components/Chip.tsx new file mode 100644 index 000000000..b7837b375 --- /dev/null +++ b/js-packages/search-frontend/src/renderer-components/Chip.tsx @@ -0,0 +1,38 @@ +import React from "react"; + +type ChipProps = { + title: string; + customStyles?: CustomStyles; +}; + +type CustomStyles = { + button?: React.CSSProperties; + div?: React.CSSProperties; +}; + +export function Chip({ title, customStyles }: ChipProps) { + const buttonStyles = { + padding: "4px 10px", + borderRadius: "20px", + border: "1px solid #C0272B", + background: "white", + fontSize: "10px", + fontWeight: "700", + lineHeight: "12px", + ...(customStyles?.button || {}), + }; + + const divStyles = { + display: "flex", + alignItems: "baseline", + color: "#c0272b", + gap: "8px", + ...(customStyles?.div || {}), + }; + + return ( + + ); +} diff --git a/js-packages/search-frontend/src/renderer-components/CustomFontAwesomeIcon.tsx b/js-packages/search-frontend/src/renderer-components/CustomFontAwesomeIcon.tsx new file mode 100644 index 000000000..4ee6612cc --- /dev/null +++ b/js-packages/search-frontend/src/renderer-components/CustomFontAwesomeIcon.tsx @@ -0,0 +1,33 @@ +import React from "react"; +import { GenericResultItem } from "../components/client"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faForumbee } from "@fortawesome/free-brands-svg-icons"; +import { faCalendar } from "@fortawesome/free-solid-svg-icons"; +import { faFileAlt } from "@fortawesome/free-solid-svg-icons/faFileAlt"; +import { faUser } from "@fortawesome/free-solid-svg-icons"; +import { faSitemap } from "@fortawesome/free-solid-svg-icons"; +import { faWikipediaW } from "@fortawesome/free-brands-svg-icons"; + +export function CustomFontAwesomeIcon({ + result +}: { + result: GenericResultItem; +}) { + const realResult = result as any + if (realResult.source.documentTypes.includes("forum")) { + return ; + } + if (realResult.source.documentTypes.includes("calendar")) { + return ; + } + if (realResult.source.documentTypes.includes("user")) { + return ; + } + if (realResult.source.documentTypes.includes("site")) { + return ; + } + if (realResult.source.documentTypes.includes("wiki")) { + return ; + } + return ; +} diff --git a/js-packages/search-frontend/src/renderer-components/DetailTitleExternalLink.tsx b/js-packages/search-frontend/src/renderer-components/DetailTitleExternalLink.tsx new file mode 100644 index 000000000..298bb3e99 --- /dev/null +++ b/js-packages/search-frontend/src/renderer-components/DetailTitleExternalLink.tsx @@ -0,0 +1,26 @@ +import React, { Children } from "react"; +import { faExternalLinkAlt } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { DetailTitle} from "./DetailTitle"; + +type DetailTitleExternalLinkProps = { href: string; children: React.ReactNode }; +export function DetailTitleExternalLink({ + href, + children, +}: DetailTitleExternalLinkProps) { + return ( + + ); +} \ No newline at end of file diff --git a/js-packages/search-frontend/src/renderer-components/ResultFavicon.tsx b/js-packages/search-frontend/src/renderer-components/ResultFavicon.tsx index 26535d231..4f2572262 100644 --- a/js-packages/search-frontend/src/renderer-components/ResultFavicon.tsx +++ b/js-packages/search-frontend/src/renderer-components/ResultFavicon.tsx @@ -1,16 +1,20 @@ import React from "react"; import { css } from "styled-components/macro"; -type ResultFaviconProps = { src: string }; -export function ResultFavicon({ src }: ResultFaviconProps) { +type ResultFaviconProps = { src: string; maxHei?: string; maxWid?: string }; +export function ResultFavicon({ + src, + maxHei = "30px", + maxWid = "30px", +}: ResultFaviconProps) { return ( ); diff --git a/js-packages/search-frontend/src/renderer-components/ResultTextContentThree.tsx b/js-packages/search-frontend/src/renderer-components/ResultTextContentThree.tsx new file mode 100644 index 000000000..b9db0b0cd --- /dev/null +++ b/js-packages/search-frontend/src/renderer-components/ResultTextContentThree.tsx @@ -0,0 +1,51 @@ +import React from "react"; +import { HighlightedText } from "./HighlightedText"; +import get from "lodash/get"; +import { css } from "styled-components/macro"; +import { truncatedLineStyle } from "./truncatedLineStyle"; +import { HighlightableTextProps } from "./HighlightableText"; + +export function ResultTextContentThree({ + result, + path, + isTruncate = true, +}: HighlightableTextProps & { isTruncate?: boolean }) { + const hihglithTextLines = result.highlight[path]; + const text = get(result.source, path); + return ( +

+ {hihglithTextLines ? ( + +

+ {hihglithTextLines.map((text, index) => ( + + ))} +
+
+ ) : ( +
+ {text} +
+ )} +

+ ); +} diff --git a/js-packages/search-frontend/src/renderer-components/ResultTitleExternalLink.tsx b/js-packages/search-frontend/src/renderer-components/ResultTitleExternalLink.tsx new file mode 100644 index 000000000..0da8b1aa6 --- /dev/null +++ b/js-packages/search-frontend/src/renderer-components/ResultTitleExternalLink.tsx @@ -0,0 +1,26 @@ +import React, { Children } from "react"; +import { faExternalLinkAlt } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { ResultTitle } from "./ResultTitle"; + +type ResultTitleExternalLinkProps = { href: string; children: React.ReactNode }; +export function ResultTitleExternalLink({ + href, + children, +}: ResultTitleExternalLinkProps) { + return ( + + ); +} \ No newline at end of file diff --git a/js-packages/search-frontend/src/renderer-components/index.ts b/js-packages/search-frontend/src/renderer-components/index.ts index 6f0399da2..e6893ac49 100644 --- a/js-packages/search-frontend/src/renderer-components/index.ts +++ b/js-packages/search-frontend/src/renderer-components/index.ts @@ -1,5 +1,6 @@ export * from "./Badge"; export * from "./BadgeContainer"; +export * from "./Chip"; export * from "./DateCard"; export * from "./DetailAttribute"; export * from "./DetailContainer"; @@ -21,6 +22,7 @@ export * from "./ResultFavicon"; export * from "./ResultLink"; export * from "./ResultTextContent"; export * from "./ResultTextContentTwo"; +export * from "./ResultTextContentThree"; export * from "./ResultTitle"; export * from "./ResultTitleTwo"; export * from "./ImagesViewer"; @@ -29,3 +31,6 @@ export * from "./ResultLinkTwo"; export * from "./SanitizeHtml"; export * from "./SortResultList"; export * from "./WrapperContainer"; +export * from "./DetailTitleExternalLink"; +export * from "./ResultTitleExternalLink"; +export * from "./CustomFontAwesomeIcon"; \ No newline at end of file diff --git a/js-packages/search-frontend/src/translations/translation_en.json b/js-packages/search-frontend/src/translations/translation_en.json index 9007008ef..1fcca17ae 100644 --- a/js-packages/search-frontend/src/translations/translation_en.json +++ b/js-packages/search-frontend/src/translations/translation_en.json @@ -14,9 +14,11 @@ "filters": "Filters", "filter-for-data": "Filter by date", "from": "From", + "gg/mm/aaaa": "mm/gg/aaaa", "insert-text-to-set-the-value-or-use-up-and-down-arrow-keys-to-navigate-the-suggestion-box": "insert text to set the value or use up and down arrow keys to navigate the suggestion box", "load-more": "Load More", "loading-more-results": "Loading more results...", + "load-more-results": "load more results", "login": "Login", "no-details": "No details", "no-result": "move the mouse over a result to see details about it", diff --git a/js-packages/search-frontend/src/translations/translation_es.json b/js-packages/search-frontend/src/translations/translation_es.json index bf7beb886..f41a05a7d 100644 --- a/js-packages/search-frontend/src/translations/translation_es.json +++ b/js-packages/search-frontend/src/translations/translation_es.json @@ -14,9 +14,11 @@ "filters": "Filtros", "filter-for-data": "Filtrar por fecha", "from": "De", + "gg/mm/aaaa": "dd/mm/aaaa", "insert-text-to-set-the-value-or-use-up-and-down-arrow-keys-to-navigate-the-suggestion-box": "inserte texto para establecer el valor o use las teclas de flecha arriba y abajo para navegar por el cuadro de sugerencias", "load-more": "Carga Más", "loading-more-results": "Cargando más resultados...", + "load-more-results": "cargar más resultados", "login": "Acceso", "no-details": "Sin detalles", "no-results-were-found": "No se encontraron resultados.", diff --git a/js-packages/search-frontend/src/translations/translation_fr.json b/js-packages/search-frontend/src/translations/translation_fr.json index 95a72a1ec..c40268777 100644 --- a/js-packages/search-frontend/src/translations/translation_fr.json +++ b/js-packages/search-frontend/src/translations/translation_fr.json @@ -14,10 +14,12 @@ "filters": "Filtres", "filter-for-data": "Filtrer par date", "from": "Depuis", + "gg/mm/aaaa": "dd/mm/aaaa", "insert-text-to-set-the-value-or-use-up-and-down-arrow-keys-to-navigate-the-suggestion-box": "insérez du texte pour définir la valeur ou utilisez les touches fléchées haut et bas pour naviguer dans la boîte de suggestion", "load-more": "Charger plus", "login": "Connexion", "loading-more-results": "Charger plus de résultats...", + "load-more-results": "Charger plus de résultats", "no-details": "Pas de détails", "no-filters": "Sans Filtres", "no-results-were-found": "Aucun resultat n'a été trouvé.", diff --git a/js-packages/search-frontend/src/translations/translation_it.json b/js-packages/search-frontend/src/translations/translation_it.json index d1045e102..15c2be3bd 100644 --- a/js-packages/search-frontend/src/translations/translation_it.json +++ b/js-packages/search-frontend/src/translations/translation_it.json @@ -14,9 +14,11 @@ "filters": "Filtri", "filter-for-data": "Filtra per data", "from": "Da", + "gg/mm/aaaa": "gg/mm/aaaa", "insert-text-to-set-the-value-or-use-up-and-down-arrow-keys-to-navigate-the-suggestion-box": "inserire il testo per impostare il valore o usa le frecce su e giù per navigare i suggerimenti all'interno del box", "load-more": "Carica Altro", "login": "Accedi", + "load-more-results": "Carica altri risultati", "loading-more-results": "Caricamento...", "no-details": "nessun dettaglio", "no-results-were-found": "Nessun risultato trovato", diff --git a/js-packages/search-frontend/webpack/standalone.dev.js b/js-packages/search-frontend/webpack/standalone.dev.js index 672b2db27..9ffb9c01c 100644 --- a/js-packages/search-frontend/webpack/standalone.dev.js +++ b/js-packages/search-frontend/webpack/standalone.dev.js @@ -62,11 +62,11 @@ module.exports = { changeOrigin: true, secure: false, }, - "/api/file-manager": { - target: "https://test.openk9.io", - changeOrigin: true, - secure: false, - } + "/api/file-manager": { + target: "https://test.openk9.io", + changeOrigin: true, + secure: false, + } }, }, }; diff --git a/yarn.lock b/yarn.lock index c18ebc4d0..9f0287818 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2588,6 +2588,13 @@ dependencies: "@fortawesome/fontawesome-common-types" "6.2.1" +"@fortawesome/free-brands-svg-icons@6.2.0": + version "6.2.0" + resolved "https://registry.yarnpkg.com/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-6.2.0.tgz#ce072179677f9b5d6767f918cfbf296f357cc1d0" + integrity sha512-fm1y4NyZ2qKYNmYhdMz9VAWRw1Et7PMHNunSw3W0SVAwKwv6o0qiJworLH3Y9SnmhHzAymXJwCX1op22FFvGiA== + dependencies: + "@fortawesome/fontawesome-common-types" "6.2.0" + "@fortawesome/free-solid-svg-icons@6.2.0": version "6.2.0" resolved "https://registry.yarnpkg.com/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.2.0.tgz#8dcde48109354fd7a5ece8ea48d678bb91d4b5f0"