diff --git a/scm-ui/ui-webapp/src/search/Syntax.tsx b/scm-ui/ui-webapp/src/search/Syntax.tsx index b389f1df76..d786cdfa16 100644 --- a/scm-ui/ui-webapp/src/search/Syntax.tsx +++ b/scm-ui/ui-webapp/src/search/Syntax.tsx @@ -27,6 +27,7 @@ import { useTranslation } from "react-i18next"; import { Button, copyToClipboard, + ErrorNotification, Icon, InputField, Loading, @@ -37,6 +38,7 @@ import { import { parse } from "date-fns"; import styled from "styled-components"; import classNames from "classnames"; +import { SearchableType } from "@scm-manager/ui-types"; const StyledTooltip = styled(Tooltip)` height: 40px; @@ -68,13 +70,99 @@ type Example = { explanation: string; }; -const Syntax: FC = () => { - const { t, i18n } = useTranslation(["commons", "plugins"]); - const { loading: isLoading, data: helpModalContent } = useSearchSyntaxContent(i18n.languages[0]); +type ExampleProps = { + searchableType: SearchableType; +}; + +const Examples: FC = ({ searchableType }) => { + const [t] = useTranslation(["commons", "plugins"]); + const examples = t(`plugins:search.types.${searchableType.name}.examples`, { + returnObjects: true, + defaultValue: [], + }); + + if (examples.length === 0) { + return null; + } + + return ( + <> +
{t("search.syntax.exampleQueries.title")}
+
{t("search.syntax.exampleQueries.description")}
+ + + + + + + {examples.map((example, index) => ( + + + + + + ))} +
{t("search.syntax.exampleQueries.table.description")}{t("search.syntax.exampleQueries.table.query")}{t("search.syntax.exampleQueries.table.explanation")}
{example.description}{example.query}{example.explanation}
+ + ); +}; + +const SearchableTypes: FC = () => { + const [t] = useTranslation(["commons", "plugins"]); + const { isLoading, error, data } = useSearchableTypes(); + + if (error) { + return ; + } + + if (isLoading || !data) { + return ; + } + + return ( + <> + {data.map((searchableType) => ( + + + + + + + + + {searchableType.fields.map((searchableField) => ( + + + + + + + ))} +
{t("search.syntax.fields.name")}{t("search.syntax.fields.type")}{t("search.syntax.fields.exampleValue")}{t("search.syntax.fields.hints")}
{searchableField.name}{searchableField.type} + {t(`plugins:search.types.${searchableType.name}.fields.${searchableField.name}.exampleValue`, { + defaultValue: "", + })} + + {t(`plugins:search.types.${searchableType.name}.fields.${searchableField.name}.hints`, { + defaultValue: "", + })} +
+ +
+ ))} + + ); +}; + +const TimestampConverter: FC = () => { + const [t] = useTranslation("commons"); const [datetime, setDatetime] = useState(""); const [timestamp, setTimestamp] = useState(""); const [copying, setCopying] = useState(false); - const { isLoading: isLoadingSearchableTypes, data: searchableTypes } = useSearchableTypes(); const convert = () => { const format = "yyyy-MM-dd HH:mm:ss"; @@ -87,106 +175,59 @@ const Syntax: FC = () => { copyToClipboard(timestamp).finally(() => setCopying(false)); }; - if (isLoading || isLoadingSearchableTypes) { - return ; - } - - const searchableTypesContent = searchableTypes!.map((searchableType) => { - const examples = t(`plugins:search.types.${searchableType.name}.examples`, { - returnObjects: true, - defaultValue: [], - }); - return ( - - - - - - - - - {searchableType.fields.map((searchableField) => ( - - - - - - - ))} -
{t("search.syntax.fields.name")}{t("search.syntax.fields.type")}{t("search.syntax.fields.exampleValue")}{t("search.syntax.fields.hints")}
{t(`plugins:search.types.${searchableType.name}.fields.${searchableField.name}.name`)}{searchableField.type} - {t(`plugins:search.types.${searchableType.name}.fields.${searchableField.name}.exampleValue`, { - defaultValue: "", - })} - - {t(`plugins:search.types.${searchableType.name}.fields.${searchableField.name}.hints`, { - defaultValue: "", - })} -
- {examples.length > 0 ? ( - <> -
{t("search.syntax.exampleQueries.title")}
-
{t("search.syntax.exampleQueries.description")}
- - - - - - - {examples.map((example) => ( - - - - - - ))} -
{t("search.syntax.exampleQueries.table.description")}{t("search.syntax.exampleQueries.table.query")}{t("search.syntax.exampleQueries.table.explanation")}
{example.description}{example.query}{example.explanation}
- - ) : null} -
- ); - }); - return ( - -
-

{t("search.syntax.exampleQueriesAndFields.title")}

-

{t("search.syntax.exampleQueriesAndFields.description")}

- {searchableTypesContent} -
- -

{t("search.syntax.utilities.title")}

-

{t("search.syntax.utilities.description")}

-
{t("search.syntax.utilities.datetime.label")}
-
- - - - - - - - {copying ? ( - - ) : ( - - )} - - -
+
+ + + + + + + + {copying ? ( + + ) : ( + + )} + + +
+ ); +}; + +const Syntax: FC = () => { + const { t, i18n } = useTranslation("commons"); + const { isLoading, data, error } = useSearchSyntaxContent(i18n.languages[0]); + return ( + + {data ? ( + <> +
+

{t("search.syntax.exampleQueriesAndFields.title")}

+

{t("search.syntax.exampleQueriesAndFields.description")}

+ +
+ +

{t("search.syntax.utilities.title")}

+

{t("search.syntax.utilities.description")}

+
{t("search.syntax.utilities.datetime.label")}
+ + + ) : null}
); }; diff --git a/scm-webapp/src/main/java/sonia/scm/search/AnalyzerFactory.java b/scm-webapp/src/main/java/sonia/scm/search/AnalyzerFactory.java index 7b5e5f6c24..84c3b33f20 100644 --- a/scm-webapp/src/main/java/sonia/scm/search/AnalyzerFactory.java +++ b/scm-webapp/src/main/java/sonia/scm/search/AnalyzerFactory.java @@ -66,7 +66,7 @@ public class AnalyzerFactory { Analyzer defaultAnalyzer = create(options); Map analyzerMap = new HashMap<>(); - for (LuceneSearchableField field : type.getFields()) { + for (LuceneSearchableField field : type.getAllFields()) { addFieldAnalyzer(analyzerMap, field); } diff --git a/scm-webapp/src/main/java/sonia/scm/search/LuceneSearchableField.java b/scm-webapp/src/main/java/sonia/scm/search/LuceneSearchableField.java index c83038a197..25c86ce151 100644 --- a/scm-webapp/src/main/java/sonia/scm/search/LuceneSearchableField.java +++ b/scm-webapp/src/main/java/sonia/scm/search/LuceneSearchableField.java @@ -42,6 +42,7 @@ class LuceneSearchableField implements SearchableField { private final boolean highlighted; private final PointsConfig pointsConfig; private final Indexed.Analyzer analyzer; + private final boolean searchable; LuceneSearchableField(Field field, Indexed indexed) { this.name = name(field, indexed); @@ -52,6 +53,7 @@ class LuceneSearchableField implements SearchableField { this.highlighted = indexed.highlighted(); this.pointsConfig = IndexableFields.pointConfig(field); this.analyzer = indexed.analyzer(); + this.searchable = indexed.type().isSearchable(); } Object value(Document document) { diff --git a/scm-webapp/src/main/java/sonia/scm/search/LuceneSearchableType.java b/scm-webapp/src/main/java/sonia/scm/search/LuceneSearchableType.java index de31bb4bde..61e1b5e793 100644 --- a/scm-webapp/src/main/java/sonia/scm/search/LuceneSearchableType.java +++ b/scm-webapp/src/main/java/sonia/scm/search/LuceneSearchableType.java @@ -34,6 +34,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.stream.Collectors; @Value public class LuceneSearchableType implements SearchableType { @@ -48,7 +49,7 @@ public class LuceneSearchableType implements SearchableType { Map boosts; Map pointsConfig; TypeConverter typeConverter; - + public LuceneSearchableType(Class type, IndexedType annotation, List fields) { this.type = type; this.name = name(type, annotation); @@ -101,7 +102,16 @@ public class LuceneSearchableType implements SearchableType { return Collections.unmodifiableMap(map); } + @Override public Collection getFields() { + return Collections.unmodifiableCollection( + fields.stream() + .filter(LuceneSearchableField::isSearchable) + .collect(Collectors.toList()) + ); + } + + public Collection getAllFields() { return Collections.unmodifiableCollection(fields); } } diff --git a/scm-webapp/src/main/java/sonia/scm/search/QueryResultFactory.java b/scm-webapp/src/main/java/sonia/scm/search/QueryResultFactory.java index 04504f9776..460eda6a30 100644 --- a/scm-webapp/src/main/java/sonia/scm/search/QueryResultFactory.java +++ b/scm-webapp/src/main/java/sonia/scm/search/QueryResultFactory.java @@ -77,7 +77,7 @@ public class QueryResultFactory { private Hit createHit(ScoreDoc scoreDoc) throws IOException, InvalidTokenOffsetsException { Document document = searcher.doc(scoreDoc.doc); Map fields = new HashMap<>(); - for (LuceneSearchableField field : searchableType.getFields()) { + for (LuceneSearchableField field : searchableType.getAllFields()) { field(document, field).ifPresent(f -> fields.put(field.getName(), f)); } return new Hit(document.get(FieldNames.ID), document.get(FieldNames.REPOSITORY), scoreDoc.score, fields); diff --git a/scm-webapp/src/main/resources/locales/de/plugins.json b/scm-webapp/src/main/resources/locales/de/plugins.json index 0858c7affc..0f38348bf7 100644 --- a/scm-webapp/src/main/resources/locales/de/plugins.json +++ b/scm-webapp/src/main/resources/locales/de/plugins.json @@ -442,28 +442,22 @@ "title": "Repositories", "fields": { "name": { - "name": "Name", "exampleValue": "scm-manager" }, "namespace": { - "name": "Namespace", "exampleValue": "cloudogu" }, "type": { - "name": "Typ", "exampleValue": "git, hg oder svn" }, "description": { - "name": "Beschreibung", "exampleValue": "Dieses Repository enthält den Quellcode des SCM-Managers." }, "creationDate": { - "name": "Erstellungsdatum", "exampleValue": "1628077286", "hints": "Zeitstempel der Erstellung in ms seit dem 01.01.1970. Bereichssuche möglich." }, "lastModified": { - "name": "Zuletzt geändert", "exampleValue": "1623057235", "hints": "Zeitstempel der letzten Änderung in ms seit dem 01.01.1970. Bereichssuche möglich." } @@ -487,25 +481,20 @@ "title": "Benutzer", "fields": { "name": { - "name": "Name", "exampleValue": "admin" }, "displayName": { - "name": "Anzeigename", "exampleValue": "Administrator" }, "creationDate": { - "name": "Erstellungsdatum", "exampleValue": "1628077286", "hints": "Zeitstempel der Erstellung in ms seit dem 01.01.1970. Bereichssuche möglich." }, "lastModified": { - "name": "Zuletzt geändert", "exampleValue": "1623057235", "hints": "Zeitstempel der letzten Änderung in ms seit dem 01.01.1970. Bereichssuche möglich." }, "mail": { - "name": "Mail", "exampleValue": "admin@hitchhiker.com" } } @@ -516,20 +505,16 @@ "title": "Gruppen", "fields": { "name": { - "name": "Name", "exampleValue": "Entwickler" }, "description": { - "name": "Beschreibung", "exampleValue": "Diese Gruppe enthält alle Softwareentwickler." }, "creationDate": { - "name": "Erstellungsdatum", "exampleValue": "1628077286", "hints": "Zeitstempel der Erstellung in ms seit dem 01.01.1970. Bereichssuche möglich." }, "lastModified": { - "name": "Zuletzt geändert", "exampleValue": "1623057235", "hints": "Zeitstempel der letzten Änderung in ms seit dem 01.01.1970. Bereichssuche möglich." } diff --git a/scm-webapp/src/main/resources/locales/en/plugins.json b/scm-webapp/src/main/resources/locales/en/plugins.json index 2eab2d6c06..039630e15a 100644 --- a/scm-webapp/src/main/resources/locales/en/plugins.json +++ b/scm-webapp/src/main/resources/locales/en/plugins.json @@ -386,28 +386,22 @@ "title": "Repositories", "fields": { "name": { - "name": "Name", "exampleValue": "scm-manager" }, "namespace": { - "name": "Namespace", "exampleValue": "cloudogu" }, "type": { - "name": "Type", "exampleValue": "git, hg or svn" }, "description": { - "name": "Description", "exampleValue": "This repository contains the source code for the SCM-Manager." }, "creationDate": { - "name": "Creation Date", "exampleValue": "1628077286", "hints": "Timestamp of creation in ms since 01-01-1970. Range search possible." }, "lastModified": { - "name": "Last Modified", "exampleValue": "1623057235", "hints": "Timestamp of last modification in ms since 01-01-1970. Range search possible." } @@ -431,25 +425,20 @@ "title": "Users", "fields": { "name": { - "name": "Name", "exampleValue": "admin" }, "displayName": { - "name": "Display Name", "exampleValue": "Administrator" }, "creationDate": { - "name": "Creation Date", "exampleValue": "1628077286", "hints": "Timestamp of creation in ms since 01-01-1970. Range search possible." }, "lastModified": { - "name": "Last Modified", "exampleValue": "1623057235", "hints": "Timestamp of last modification in ms since 01-01-1970. Range search possible." }, "mail": { - "name": "Mail", "exampleValue": "admin@hitchhiker.com" } } @@ -460,20 +449,16 @@ "title": "Groups", "fields": { "name": { - "name": "Name", "exampleValue": "Developers" }, "description": { - "name": "Description", "exampleValue": "This group contains all software developers." }, "creationDate": { - "name": "Creation Date", "exampleValue": "1628077286", "hints": "Timestamp of creation in ms since 01-01-1970. Range search possible." }, "lastModified": { - "name": "Last Modified", "exampleValue": "1623057235", "hints": "Timestamp of last modification in ms since 01-01-1970. Range search possible." } diff --git a/scm-webapp/src/test/java/sonia/scm/search/SearchableTypesTest.java b/scm-webapp/src/test/java/sonia/scm/search/SearchableTypesTest.java new file mode 100644 index 0000000000..5bca97a98a --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/search/SearchableTypesTest.java @@ -0,0 +1,64 @@ +/* + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and Contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package sonia.scm.search; + + +import lombok.Value; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.stream.Collectors; + +import static org.assertj.core.api.Assertions.assertThat; + +class SearchableTypesTest { + + @Test + void shouldNotReturnStoredOnlyFields() { + LuceneSearchableType luceneSearchableType = SearchableTypes.create(IndexedObject.class); + List fields = luceneSearchableType.getFields() + .stream() + .map(LuceneSearchableField::getName) + .collect(Collectors.toList()); + + assertThat(fields).containsOnly("searchable", "tokenized"); + } + + @Value + @IndexedType + public static class IndexedObject { + + @Indexed(type = Indexed.Type.STORED_ONLY) + String storedOnly; + + @Indexed + String searchable; + + @Indexed + String tokenized; + + } + +}