mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-12-23 08:49:48 +01:00
Accessible File Search
- keyboard combination g+f in code view - navigatable file search list with j+k - remove helptext tooltip
This commit is contained in:
@@ -10,8 +10,8 @@ verfügbaren Tastenkürzel mittels der `?`-Taste aufrufen.
|
|||||||
|
|
||||||
### Globale Tastenkürzel
|
### Globale Tastenkürzel
|
||||||
|
|
||||||
| Key Combination | Description |
|
| Tastenkürzel | Beschreibung |
|
||||||
|-----------------|-------------------------------------|
|
|--------------|-------------------------------------|
|
||||||
| ? | Öffne die Tastaturkürzelübersicht |
|
| ? | Öffne die Tastaturkürzelübersicht |
|
||||||
| / | Fokussiere die globale Schnellsuche |
|
| / | Fokussiere die globale Schnellsuche |
|
||||||
| alt r | Navigiere zur Repositoryübersicht |
|
| alt r | Navigiere zur Repositoryübersicht |
|
||||||
@@ -25,21 +25,26 @@ Einige Seiten mit Listen erlauben die Navigation per Tastatur.
|
|||||||
Wenn die Seite dieses unterstützt, tauchen die Tastaturkürzel in der Übersicht im SCM-Manager
|
Wenn die Seite dieses unterstützt, tauchen die Tastaturkürzel in der Übersicht im SCM-Manager
|
||||||
auf (`?`).
|
auf (`?`).
|
||||||
|
|
||||||
| Key Combination | Description |
|
| Tastenkürzel | Beschreibung |
|
||||||
|-----------------|---------------------------------------------------|
|
|--------------|---------------------------------------------------|
|
||||||
| j | Bewege den Fokus auf den nächsten Listeneintrag |
|
| j | Bewege den Fokus auf den nächsten Listeneintrag |
|
||||||
| k | Bewege den Fokus auf den vorherigen Listeneintrag |
|
| k | Bewege den Fokus auf den vorherigen Listeneintrag |
|
||||||
|
|
||||||
### Repositoryspezifische Tastenkürzel
|
### Repositoryspezifische Tastenkürzel
|
||||||
|
|
||||||
| Key Combination | Description |
|
| Tastenkürzel | Beschreibung |
|
||||||
|-----------------|------------------------------|
|
|--------------|------------------------------|
|
||||||
| g i | Wechsel zur Repository-Info |
|
| g i | Wechsel zur Repository-Info |
|
||||||
| g b | Wechsel zu den Branches |
|
| g b | Wechsel zu den Branches |
|
||||||
| g t | Wechsel zu den Tags |
|
| g t | Wechsel zu den Tags |
|
||||||
| g c | Wechsel zum Code |
|
| g c | Wechsel zum Code |
|
||||||
| g s | Wechsel zu den Einstellungen |
|
| g s | Wechsel zu den Einstellungen |
|
||||||
|
|
||||||
|
### Codespezifische Tastenkürzel
|
||||||
|
| Tastenkürzel | Beschreibung |
|
||||||
|
|--------------|------------------------|
|
||||||
|
| g f | Wechsel zur Dateisuche |
|
||||||
|
|
||||||
### Tastenkürzel aus Plugin
|
### Tastenkürzel aus Plugin
|
||||||
|
|
||||||
Plugins können selbst neue Tastenkürzel definieren.
|
Plugins können selbst neue Tastenkürzel definieren.
|
||||||
|
|||||||
@@ -39,6 +39,12 @@ If the page supports this feature, the shortcuts show up in the shortcut overvie
|
|||||||
| g c | Switch to code |
|
| g c | Switch to code |
|
||||||
| g s | Switch to settings |
|
| g s | Switch to settings |
|
||||||
|
|
||||||
|
### Code-specific Shortcuts
|
||||||
|
|
||||||
|
| Key Combination | Description |
|
||||||
|
|-----------------|---------------------------|
|
||||||
|
| g f | Switch to file search |
|
||||||
|
|
||||||
### Plugin Shortcuts
|
### Plugin Shortcuts
|
||||||
|
|
||||||
Plugins can introduce new shortcuts.
|
Plugins can introduce new shortcuts.
|
||||||
|
|||||||
4
gradle/changelog/file_search.yaml
Normal file
4
gradle/changelog/file_search.yaml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
- type: added
|
||||||
|
description: Keyboard shortcut (g+f) within code view for file search
|
||||||
|
- type: changed
|
||||||
|
description: File search page design; in particular with regard to accessibility
|
||||||
@@ -43,7 +43,7 @@ describe("Repository File Search", () => {
|
|||||||
// Act
|
// Act
|
||||||
cy.visit(`/repo/${namespace}/${name}/code/sources`);
|
cy.visit(`/repo/${namespace}/${name}/code/sources`);
|
||||||
cy.byTestId("file_search_button").click();
|
cy.byTestId("file_search_button").click();
|
||||||
cy.url().should("include", `/repo/${namespace}/${name}/code/search/main?q=`);
|
cy.url().should("include", `/repo/${namespace}/${name}/code/search/main`);
|
||||||
cy.byTestId("file_search_filter_input").type("README");
|
cy.byTestId("file_search_filter_input").type("README");
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ export { default as ConfigurationForm } from "./ConfigurationForm";
|
|||||||
export { default as SelectField } from "./select/SelectField";
|
export { default as SelectField } from "./select/SelectField";
|
||||||
export { default as ComboboxField } from "./combobox/ComboboxField";
|
export { default as ComboboxField } from "./combobox/ComboboxField";
|
||||||
export { default as Input } from "./input/Input";
|
export { default as Input } from "./input/Input";
|
||||||
|
export { default as InputField } from "./input/InputField";
|
||||||
export { default as Textarea } from "./input/Textarea";
|
export { default as Textarea } from "./input/Textarea";
|
||||||
export { default as Select } from "./select/Select";
|
export { default as Select } from "./select/Select";
|
||||||
export * from "./resourceHooks";
|
export * from "./resourceHooks";
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ type Props<T extends Record<string, unknown>> = Omit<
|
|||||||
rules?: ComponentProps<typeof Controller>["rules"];
|
rules?: ComponentProps<typeof Controller>["rules"];
|
||||||
name: Path<T>;
|
name: Path<T>;
|
||||||
label?: string;
|
label?: string;
|
||||||
|
icon?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
function ControlledInputField<T extends Record<string, unknown>>({
|
function ControlledInputField<T extends Record<string, unknown>>({
|
||||||
@@ -41,6 +42,7 @@ function ControlledInputField<T extends Record<string, unknown>>({
|
|||||||
defaultValue,
|
defaultValue,
|
||||||
readOnly,
|
readOnly,
|
||||||
className,
|
className,
|
||||||
|
icon,
|
||||||
...props
|
...props
|
||||||
}: Props<T>) {
|
}: Props<T>) {
|
||||||
const { control, t, readOnly: formReadonly, formId } = useScmFormContext();
|
const { control, t, readOnly: formReadonly, formId } = useScmFormContext();
|
||||||
@@ -66,6 +68,7 @@ function ControlledInputField<T extends Record<string, unknown>>({
|
|||||||
{...field}
|
{...field}
|
||||||
form={formId}
|
form={formId}
|
||||||
label={labelTranslation}
|
label={labelTranslation}
|
||||||
|
icon={icon}
|
||||||
helpText={helpTextTranslation}
|
helpText={helpTextTranslation}
|
||||||
descriptionText={descriptionTextTranslation}
|
descriptionText={descriptionTextTranslation}
|
||||||
error={
|
error={
|
||||||
|
|||||||
@@ -20,3 +20,7 @@ This will be our first form field molecule
|
|||||||
<Story name="WithWidth">
|
<Story name="WithWidth">
|
||||||
<InputField label="MyInput" className="column is-half" />
|
<InputField label="MyInput" className="column is-half" />
|
||||||
</Story>
|
</Story>
|
||||||
|
|
||||||
|
<Story name="WithIcon">
|
||||||
|
<InputField icon="fas fa-filter" />
|
||||||
|
</Story>
|
||||||
|
|||||||
@@ -23,24 +23,26 @@ import Input from "./Input";
|
|||||||
import Help from "../base/help/Help";
|
import Help from "../base/help/Help";
|
||||||
import { useAriaId } from "../../helpers";
|
import { useAriaId } from "../../helpers";
|
||||||
|
|
||||||
type InputFieldProps = {
|
export type InputFieldProps = {
|
||||||
label: string;
|
label: string;
|
||||||
|
labelClassName?: string;
|
||||||
helpText?: string;
|
helpText?: string;
|
||||||
descriptionText?: string;
|
descriptionText?: string;
|
||||||
error?: string;
|
error?: string;
|
||||||
|
icon?: string;
|
||||||
} & React.ComponentProps<typeof Input>;
|
} & React.ComponentProps<typeof Input>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @see https://bulma.io/documentation/form/input/
|
* @see https://bulma.io/documentation/form/input/
|
||||||
*/
|
*/
|
||||||
const InputField = React.forwardRef<HTMLInputElement, InputFieldProps>(
|
const InputField = React.forwardRef<HTMLInputElement, InputFieldProps>(
|
||||||
({ name, label, helpText, descriptionText, error, className, id, ...props }, ref) => {
|
({ name, label, helpText, descriptionText, error, icon, className, labelClassName, id, ...props }, ref) => {
|
||||||
const inputId = useAriaId(id ?? props.testId);
|
const inputId = useAriaId(id ?? props.testId);
|
||||||
const descriptionId = descriptionText ? `input-description-${name}` : undefined;
|
const descriptionId = descriptionText ? `input-description-${name}` : undefined;
|
||||||
const variant = error ? "danger" : undefined;
|
const variant = error ? "danger" : undefined;
|
||||||
return (
|
return (
|
||||||
<Field className={className}>
|
<Field className={className}>
|
||||||
<Label htmlFor={inputId}>
|
<Label htmlFor={inputId} className={labelClassName}>
|
||||||
{label}
|
{label}
|
||||||
{helpText ? <Help className="ml-1" text={helpText} /> : null}
|
{helpText ? <Help className="ml-1" text={helpText} /> : null}
|
||||||
</Label>
|
</Label>
|
||||||
@@ -49,8 +51,13 @@ const InputField = React.forwardRef<HTMLInputElement, InputFieldProps>(
|
|||||||
{descriptionText}
|
{descriptionText}
|
||||||
</p>
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
<Control>
|
<Control className="has-icons-left">
|
||||||
<Input variant={variant} ref={ref} id={inputId} aria-describedby={descriptionId} {...props}></Input>
|
<Input variant={variant} ref={ref} id={inputId} aria-describedby={descriptionId} {...props}></Input>
|
||||||
|
{icon ? (
|
||||||
|
<span className="icon is-small is-left">
|
||||||
|
<i className={icon} />
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
</Control>
|
</Control>
|
||||||
{error ? <FieldMessage variant={variant}>{error}</FieldMessage> : null}
|
{error ? <FieldMessage variant={variant}>{error}</FieldMessage> : null}
|
||||||
</Field>
|
</Field>
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ const Notification = React.forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElem
|
|||||||
const color = type !== "inherit" ? "is-" + type : "";
|
const color = type !== "inherit" ? "is-" + type : "";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classNames("notification", color, className)} role={role} ref={ref}>
|
<div className={classNames("notification", color, className)} role={role} ref={ref} {...props}>
|
||||||
{onClose ? <button className="delete" onClick={onClose} /> : null}
|
{onClose ? <button className="delete" onClick={onClose} /> : null}
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
"help": {
|
"help": {
|
||||||
"namespaceHelpText": "Der Namespace des Repository. Dieser wird Teil der URL des Repository sein.",
|
"namespaceHelpText": "Der Namespace des Repository. Dieser wird Teil der URL des Repository sein.",
|
||||||
"nameHelpText": "Der Name des Repository. Dieser wird Teil der URL des Repository sein.",
|
"nameHelpText": "Der Name des Repository. Dieser wird Teil der URL des Repository sein.",
|
||||||
"contactHelpText": "E-Mail Adresse der Person, die für das Repository verantwortlich ist.",
|
"contactHelpText": "E-Mail-Adresse der Person, die für das Repository verantwortlich ist.",
|
||||||
"descriptionHelpText": "Eine kurze Beschreibung des Repository.",
|
"descriptionHelpText": "Eine kurze Beschreibung des Repository.",
|
||||||
"initializeRepository": "Erstellt einen ersten Branch und committet eine README.md.",
|
"initializeRepository": "Erstellt einen ersten Branch und committet eine README.md.",
|
||||||
"importUrlHelpText": "Importiert das gesamte Repository inkl. aller Branches und Tags über die Remote URL.",
|
"importUrlHelpText": "Importiert das gesamte Repository inkl. aller Branches und Tags über die Remote URL.",
|
||||||
@@ -311,7 +311,7 @@
|
|||||||
"shortSummary": "Committet <0/> <1/>",
|
"shortSummary": "Committet <0/> <1/>",
|
||||||
"tags": "Tags",
|
"tags": "Tags",
|
||||||
"diffNotSupported": "Diff des Changesets wird von diesem Repositorytyp nicht unterstützt",
|
"diffNotSupported": "Diff des Changesets wird von diesem Repositorytyp nicht unterstützt",
|
||||||
"keyOwner": "Schlüssel Besitzer",
|
"keyOwner": "Schlüsselbesitzer",
|
||||||
"signatureStatus": "Status",
|
"signatureStatus": "Status",
|
||||||
"keyId": "Schlüssel-ID",
|
"keyId": "Schlüssel-ID",
|
||||||
"keyContacts": "Kontakte",
|
"keyContacts": "Kontakte",
|
||||||
@@ -527,8 +527,8 @@
|
|||||||
"modal": {
|
"modal": {
|
||||||
"title": "Repository umbenennen",
|
"title": "Repository umbenennen",
|
||||||
"label": {
|
"label": {
|
||||||
"repoName": "Repository Name",
|
"repoName": "Repository-Name",
|
||||||
"repoNamespace": "Repository Namespace"
|
"repoNamespace": "Repository-Namespace"
|
||||||
},
|
},
|
||||||
"button": {
|
"button": {
|
||||||
"rename": "Umbenennen",
|
"rename": "Umbenennen",
|
||||||
@@ -618,23 +618,25 @@
|
|||||||
},
|
},
|
||||||
"fileSearch": {
|
"fileSearch": {
|
||||||
"button": {
|
"button": {
|
||||||
"title": "Dateipfad Suche"
|
"title": "Dateipfadsuche"
|
||||||
},
|
},
|
||||||
"file": "Datei",
|
"file": "Datei",
|
||||||
"home": "Zurück zu Sources",
|
"home": "Zurück zu Sources",
|
||||||
"input": {
|
"input": {
|
||||||
"placeholder": "Dateipfad Suche",
|
"placeholder": "Dateipfadsuche",
|
||||||
"help": "Tippe 2 oder mehr Zeichen ein, um die Suche zu starten"
|
"help": "Tippen Sie mindestens zwei Zeichen ein, um die Suche zu starten."
|
||||||
},
|
},
|
||||||
|
"results_one": "{{count}} Ergebnis gefunden:",
|
||||||
|
"results_other": "{{count}} Ergebnisse gefunden:",
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"queryToShort": "Tippe mindestens 2 Zeichen ein, um die Suche zu starten",
|
"emptyResult": "Keine Ergebnisse gefunden für: <0>{{query}}</0>."
|
||||||
"emptyResult": "Es wurden keine Ergebnisse für <0>{{query}}</0> gefunden"
|
|
||||||
},
|
},
|
||||||
"searchWithRevisionAndNamespaceName": "Suche auf {{revision}} in {{namespace}}/{{name}}"
|
"searchWithRevisionAndNamespaceName": "Suche auf {{revision}} in {{namespace}}/{{name}}"
|
||||||
},
|
},
|
||||||
"shortcuts": {
|
"shortcuts": {
|
||||||
"info": "Wechsel zur Repository-Info",
|
"info": "Wechsel zur Repository-Info",
|
||||||
"branches": "Wechsel zu den Branches",
|
"branches": "Wechsel zu den Branches",
|
||||||
|
"fileSearch": "Wechsel zur Dateisuche",
|
||||||
"tags": "Wechsel zu den Tags",
|
"tags": "Wechsel zu den Tags",
|
||||||
"code": "Wechsel zum Code",
|
"code": "Wechsel zum Code",
|
||||||
"settings": "Wechsel zu den Einstellungen"
|
"settings": "Wechsel zu den Einstellungen"
|
||||||
|
|||||||
@@ -624,17 +624,19 @@
|
|||||||
"home": "Go back to source root",
|
"home": "Go back to source root",
|
||||||
"input": {
|
"input": {
|
||||||
"placeholder": "Search filepath",
|
"placeholder": "Search filepath",
|
||||||
"help": "Type 2 or more letters to search for a filepath in the repository"
|
"help": "Type at least two letters to start the search."
|
||||||
},
|
},
|
||||||
|
"results_one": "{{count}} result found:",
|
||||||
|
"results_other": "{{count}} results found:",
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"queryToShort": "Type at least two characters to start the search",
|
"emptyResult": "Nothing found for: <0>{{query}}</0>."
|
||||||
"emptyResult": "Nothing found for query <0>{{query}}</0>"
|
|
||||||
},
|
},
|
||||||
"searchWithRevisionAndNamespaceName": "Search on {{revision}} in {{namespace}}/{{name}}"
|
"searchWithRevisionAndNamespaceName": "Search on {{revision}} in {{namespace}}/{{name}}"
|
||||||
},
|
},
|
||||||
"shortcuts": {
|
"shortcuts": {
|
||||||
"info": "Switch to repository info",
|
"info": "Switch to repository info",
|
||||||
"branches": "Switch to branches",
|
"branches": "Switch to branches",
|
||||||
|
"fileSearch": "Switch to file search",
|
||||||
"tags": "Switch to tags",
|
"tags": "Switch to tags",
|
||||||
"code": "Switch to code",
|
"code": "Switch to code",
|
||||||
"settings": "Switch to settings"
|
"settings": "Switch to settings"
|
||||||
|
|||||||
@@ -18,9 +18,9 @@ import React, { FC } from "react";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { File, Repository } from "@scm-manager/ui-types";
|
import { File, Repository } from "@scm-manager/ui-types";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { Icon } from "@scm-manager/ui-components";
|
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import { urls } from "@scm-manager/ui-api";
|
import { getFileSearchLink } from "../utils/fileSearchLink";
|
||||||
|
import { Icon } from "@scm-manager/ui-core";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
repository: Repository;
|
repository: Repository;
|
||||||
@@ -35,18 +35,13 @@ const SearchIcon = styled(Icon)`
|
|||||||
|
|
||||||
const FileSearchButton: FC<Props> = ({ baseUrl, revision, currentSource, repository }) => {
|
const FileSearchButton: FC<Props> = ({ baseUrl, revision, currentSource, repository }) => {
|
||||||
const [t] = useTranslation("repos");
|
const [t] = useTranslation("repos");
|
||||||
const currentSourcePath =
|
|
||||||
repository.type === "svn"
|
|
||||||
? urls.createPrevSourcePathQuery(`${revision}/${currentSource.path}`)
|
|
||||||
: urls.createPrevSourcePathQuery(currentSource.path);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
to={`${baseUrl}/search/${encodeURIComponent(revision)}?${currentSourcePath}`}
|
to={getFileSearchLink(repository, revision, baseUrl, currentSource)}
|
||||||
aria-label={t("fileSearch.button.title")}
|
aria-label={t("fileSearch.button.title")}
|
||||||
data-testid="file_search_button"
|
data-testid="file_search_button"
|
||||||
>
|
>
|
||||||
<SearchIcon title={t("fileSearch.button.title")} name="search" color="inherit" />
|
<SearchIcon title={t("fileSearch.button.title")}>search</SearchIcon>
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2020 - present Cloudogu GmbH
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify it under
|
||||||
|
* the terms of the GNU Affero General Public License as published by the Free
|
||||||
|
* Software Foundation, version 3.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||||
|
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||||
|
* details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see https://www.gnu.org/licenses/.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { urls } from "@scm-manager/ui-api";
|
||||||
|
import { Icon, useKeyboardIteratorTarget } from "@scm-manager/ui-core";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
import styled from "styled-components";
|
||||||
|
|
||||||
|
type FileSearchHitProps = {
|
||||||
|
contentBaseUrl: string;
|
||||||
|
path: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const IconColumn = styled.td`
|
||||||
|
width: 16px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const LeftOverflowTd = styled.td`
|
||||||
|
overflow: hidden;
|
||||||
|
max-width: 1px;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
direction: rtl;
|
||||||
|
text-align: left !important;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export function FileSearchHit({ contentBaseUrl, path }: FileSearchHitProps) {
|
||||||
|
const [t] = useTranslation("repos");
|
||||||
|
const link = urls.concat(contentBaseUrl, path);
|
||||||
|
const ref = useKeyboardIteratorTarget();
|
||||||
|
return (
|
||||||
|
<tr>
|
||||||
|
<IconColumn aria-hidden="true">
|
||||||
|
<Icon title={t("fileSearch.file")}>file</Icon>
|
||||||
|
</IconColumn>
|
||||||
|
<LeftOverflowTd>
|
||||||
|
<Link title={path} to={link} data-testid="file_search_single_result" ref={ref} key={path}>
|
||||||
|
{path}
|
||||||
|
</Link>
|
||||||
|
</LeftOverflowTd>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -15,89 +15,34 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { FC } from "react";
|
import React, { FC } from "react";
|
||||||
import { Icon, Notification, urls } from "@scm-manager/ui-components";
|
import { FileSearchHit } from "./FileSearchHit";
|
||||||
import styled from "styled-components";
|
import { KeyboardIterator } from "@scm-manager/ui-core";
|
||||||
import { Link } from "react-router-dom";
|
|
||||||
import { Trans, useTranslation } from "react-i18next";
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
paths: string[];
|
paths: string[];
|
||||||
query: string;
|
|
||||||
contentBaseUrl: string;
|
contentBaseUrl: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const IconColumn = styled.td`
|
|
||||||
width: 16px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const LeftOverflowTd = styled.td`
|
|
||||||
overflow: hidden;
|
|
||||||
max-width: 1px;
|
|
||||||
white-space: nowrap;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
direction: rtl;
|
|
||||||
text-align: left !important;
|
|
||||||
`;
|
|
||||||
|
|
||||||
type PathResultRowProps = {
|
|
||||||
contentBaseUrl: string;
|
|
||||||
path: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const PathResultRow: FC<PathResultRowProps> = ({ contentBaseUrl, path }) => {
|
|
||||||
const [t] = useTranslation("repos");
|
|
||||||
const link = urls.concat(contentBaseUrl, path);
|
|
||||||
return (
|
|
||||||
<tr>
|
|
||||||
<IconColumn>
|
|
||||||
<Link to={link}>
|
|
||||||
<Icon title={t("fileSearch.file")} name="file" color="inherit" />
|
|
||||||
</Link>
|
|
||||||
</IconColumn>
|
|
||||||
<LeftOverflowTd>
|
|
||||||
<Link title={path} to={link} data-testid="file_search_single_result">
|
|
||||||
{path}
|
|
||||||
</Link>
|
|
||||||
</LeftOverflowTd>
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
type ResultTableProps = {
|
type ResultTableProps = {
|
||||||
contentBaseUrl: string;
|
contentBaseUrl: string;
|
||||||
paths: string[];
|
paths: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
const ResultTable: FC<ResultTableProps> = ({ contentBaseUrl, paths }) => (
|
const ResultTable: FC<ResultTableProps> = ({ contentBaseUrl, paths }) => {
|
||||||
|
return (
|
||||||
<table className="table table-hover table-sm is-fullwidth">
|
<table className="table table-hover table-sm is-fullwidth">
|
||||||
|
<KeyboardIterator>
|
||||||
<tbody>
|
<tbody>
|
||||||
{paths.map(path => (
|
{paths.map((path) => (
|
||||||
<PathResultRow contentBaseUrl={contentBaseUrl} path={path} />
|
<FileSearchHit contentBaseUrl={contentBaseUrl} path={path} key={path} />
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
</KeyboardIterator>
|
||||||
</table>
|
</table>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const FileSearchResults: FC<Props> = ({ query, contentBaseUrl, paths = [] }) => {
|
const FileSearchResults: FC<Props> = ({ contentBaseUrl, paths = [] }) => {
|
||||||
const [t] = useTranslation("repos");
|
return <ResultTable contentBaseUrl={contentBaseUrl} paths={paths} />;
|
||||||
let body;
|
|
||||||
if (query.length <= 1) {
|
|
||||||
body = (
|
|
||||||
<Notification className="m-4" type="info">
|
|
||||||
{t("fileSearch.notifications.queryToShort")}
|
|
||||||
</Notification>
|
|
||||||
);
|
|
||||||
} else if (paths.length === 0) {
|
|
||||||
const queryCmp = <strong>{query}</strong>;
|
|
||||||
body = (
|
|
||||||
<Notification className="m-4" type="info">
|
|
||||||
<Trans i18nKey="repos:fileSearch.notifications.emptyResult" values={{ query }} components={[queryCmp]} />
|
|
||||||
</Notification>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
body = <ResultTable contentBaseUrl={contentBaseUrl} paths={paths} />;
|
|
||||||
}
|
|
||||||
return <div className="panel-block">{body}</div>;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default FileSearchResults;
|
export default FileSearchResults;
|
||||||
|
|||||||
@@ -14,15 +14,15 @@
|
|||||||
* along with this program. If not, see https://www.gnu.org/licenses/.
|
* along with this program. If not, see https://www.gnu.org/licenses/.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { FC, useEffect, useState } from "react";
|
import React, { FC, useState } from "react";
|
||||||
import { Link, useHistory, useLocation, useParams } from "react-router-dom";
|
import { Link, useHistory, useLocation, useParams } from "react-router-dom";
|
||||||
import { useTranslation } from "react-i18next";
|
import { Trans, useTranslation } from "react-i18next";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import { Branch, Repository } from "@scm-manager/ui-types";
|
import { Branch, Repository } from "@scm-manager/ui-types";
|
||||||
import { urls, usePaths } from "@scm-manager/ui-api";
|
import { urls, usePaths } from "@scm-manager/ui-api";
|
||||||
import { createA11yId, ErrorNotification, FilterInput, Help, Icon, Loading } from "@scm-manager/ui-components";
|
import { createA11yId } from "@scm-manager/ui-components";
|
||||||
import { useDocumentTitle } from "@scm-manager/ui-core";
|
import { ErrorNotification, Icon, Loading, Notification, InputField, useDocumentTitle } from "@scm-manager/ui-core";
|
||||||
import CodeActionBar from "../components/CodeActionBar";
|
import CodeActionBar from "../components/CodeActionBar";
|
||||||
import FileSearchResults from "../components/FileSearchResults";
|
import FileSearchResults from "../components/FileSearchResults";
|
||||||
import { filepathSearch } from "../utils/filepathSearch";
|
import { filepathSearch } from "../utils/filepathSearch";
|
||||||
@@ -57,10 +57,10 @@ const FileSearch: FC<Props> = ({ repository, baseUrl, branches, selectedBranch }
|
|||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const { isLoading, error, data } = usePaths(repository, revision);
|
const { isLoading, error, data } = usePaths(repository, revision);
|
||||||
const [result, setResult] = useState<string[]>([]);
|
|
||||||
const query = urls.getQueryStringFromLocation(location) || "";
|
const query = urls.getQueryStringFromLocation(location) || "";
|
||||||
const prevSourcePath = urls.getPrevSourcePathFromLocation(location) || "";
|
const prevSourcePath = urls.getPrevSourcePathFromLocation(location) || "";
|
||||||
const [t] = useTranslation("repos");
|
const [t] = useTranslation("repos");
|
||||||
|
|
||||||
useDocumentTitle(
|
useDocumentTitle(
|
||||||
t("fileSearch.searchWithRevisionAndNamespaceName", {
|
t("fileSearch.searchWithRevisionAndNamespaceName", {
|
||||||
revision: decodeURIComponent(revision),
|
revision: decodeURIComponent(revision),
|
||||||
@@ -70,25 +70,32 @@ const FileSearch: FC<Props> = ({ repository, baseUrl, branches, selectedBranch }
|
|||||||
);
|
);
|
||||||
const [firstSelectedBranch] = useState<string | undefined>(selectedBranch);
|
const [firstSelectedBranch] = useState<string | undefined>(selectedBranch);
|
||||||
|
|
||||||
useEffect(() => {
|
let result: string[];
|
||||||
if (query.length > 1 && data) {
|
|
||||||
setResult(filepathSearch(data.paths, query));
|
|
||||||
} else {
|
|
||||||
setResult([]);
|
|
||||||
}
|
|
||||||
}, [data, query]);
|
|
||||||
|
|
||||||
const search = (query: string) => {
|
if (data && query && query.length > 1) {
|
||||||
|
result = filepathSearch(data.paths, query);
|
||||||
|
} else {
|
||||||
|
result = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const onSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const query = e.currentTarget.value;
|
||||||
const prevSourceQuery = urls.createPrevSourcePathQuery(prevSourcePath);
|
const prevSourceQuery = urls.createPrevSourcePathQuery(prevSourcePath);
|
||||||
history.push(`${location.pathname}?q=${encodeURIComponent(query)}${prevSourceQuery ? `&${prevSourceQuery}` : ""}`);
|
if (prevSourceQuery) {
|
||||||
|
history.push(`${location.pathname}?q=${encodeURIComponent(query)}`);
|
||||||
|
} else {
|
||||||
|
history.push(`${location.pathname}?q=${encodeURIComponent(query)}&${prevSourceQuery}`);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const onSelectBranch = (branch?: Branch) => {
|
const onSelectBranch = (branch?: Branch) => {
|
||||||
if (branch) {
|
if (branch) {
|
||||||
const prevSourceQuery = urls.createPrevSourcePathQuery(prevSourcePath);
|
const prevSourceQuery = urls.createPrevSourcePathQuery(prevSourcePath);
|
||||||
history.push(
|
if (prevSourceQuery) {
|
||||||
`${baseUrl}/search/${encodeURIComponent(branch.name)}?q=${query}${prevSourceQuery ? `&${prevSourceQuery}` : ""}`
|
history.push(`${baseUrl}/search/${encodeURIComponent(branch.name)}?q=${query}&${prevSourceQuery}`);
|
||||||
);
|
} else {
|
||||||
|
history.push(`${baseUrl}/search/${encodeURIComponent(branch.name)}?q=${query}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -107,11 +114,46 @@ const FileSearch: FC<Props> = ({ repository, baseUrl, branches, selectedBranch }
|
|||||||
}`;
|
}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return `${baseUrl}/changesets/${prevSourcePath ? `?${urls.createPrevSourcePathQuery(prevSourcePath)}` : ""}`;
|
if (prevSourcePath) {
|
||||||
|
return `${baseUrl}/changesets/${urls.createPrevSourcePathQuery(prevSourcePath)}`;
|
||||||
|
} else {
|
||||||
|
return `${baseUrl}/changesets/${prevSourcePath}`;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const contentBaseUrl = `${baseUrl}/sources/${revision}/`;
|
const contentBaseUrl = `${baseUrl}/sources/${revision}/`;
|
||||||
const id = createA11yId("file-search");
|
|
||||||
|
const fileSearchDescriptionId = createA11yId("fileSearchDescription");
|
||||||
|
let body;
|
||||||
|
|
||||||
|
if (query.length <= 1) {
|
||||||
|
body = (
|
||||||
|
<Notification className="m-4" type="info" hidden={query.length > 1}>
|
||||||
|
<p id={fileSearchDescriptionId}>{t("fileSearch.input.help")}</p>
|
||||||
|
</Notification>
|
||||||
|
);
|
||||||
|
} else if (!isLoading && result.length === 0) {
|
||||||
|
const queryCmp = <strong>{query}</strong>;
|
||||||
|
body = (
|
||||||
|
<p className="mt-3" id={fileSearchDescriptionId}>
|
||||||
|
<Trans i18nKey="repos:fileSearch.notifications.emptyResult" values={{ query }} components={[queryCmp]} />
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
body = (
|
||||||
|
<>
|
||||||
|
<p
|
||||||
|
className="pl-4 has-text-weight-semibold"
|
||||||
|
hidden={query.length <= 1}
|
||||||
|
id={fileSearchDescriptionId}
|
||||||
|
aria-hidden={true}
|
||||||
|
>
|
||||||
|
{t("fileSearch.results", { count: result.length })}
|
||||||
|
</p>
|
||||||
|
<FileSearchResults contentBaseUrl={contentBaseUrl} paths={result} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -135,27 +177,28 @@ const FileSearch: FC<Props> = ({ repository, baseUrl, branches, selectedBranch }
|
|||||||
>
|
>
|
||||||
<HomeLink
|
<HomeLink
|
||||||
className={classNames("mr-3", "pr-3")}
|
className={classNames("mr-3", "pr-3")}
|
||||||
|
aria-label={t("fileSearch.home")}
|
||||||
to={firstSelectedBranch !== selectedBranch ? contentBaseUrl : () => evaluateSwitchViewLink("sources")}
|
to={firstSelectedBranch !== selectedBranch ? contentBaseUrl : () => evaluateSwitchViewLink("sources")}
|
||||||
>
|
>
|
||||||
<HomeIcon
|
<HomeIcon title={t("fileSearch.home")} color="inherit">
|
||||||
title={t("fileSearch.home")}
|
{firstSelectedBranch !== selectedBranch ? "home" : "arrow-left"}
|
||||||
name={firstSelectedBranch !== selectedBranch ? "home" : "arrow-left"}
|
</HomeIcon>
|
||||||
color="inherit"
|
|
||||||
/>
|
|
||||||
</HomeLink>
|
</HomeLink>
|
||||||
<FilterInput
|
<InputField
|
||||||
className="is-full-width pr-2"
|
|
||||||
placeholder={t("fileSearch.input.placeholder")}
|
|
||||||
value={query}
|
|
||||||
filter={search}
|
|
||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
id={id}
|
placeholder={t("fileSearch.input.placeholder")}
|
||||||
|
className="is-full-width is-flex pr-2"
|
||||||
|
label=""
|
||||||
|
labelClassName="mr-2 mt-2"
|
||||||
|
defaultValue={query}
|
||||||
|
aria-describedby={fileSearchDescriptionId}
|
||||||
testId="file_search_filter_input"
|
testId="file_search_filter_input"
|
||||||
|
icon="fas fa-search"
|
||||||
|
onChange={onSearch}
|
||||||
/>
|
/>
|
||||||
<Help message={t("fileSearch.input.help")} id={id} />
|
|
||||||
</div>
|
</div>
|
||||||
<ErrorNotification error={error} />
|
<ErrorNotification error={error} />
|
||||||
{isLoading ? <Loading /> : <FileSearchResults contentBaseUrl={contentBaseUrl} query={query} paths={result} />}
|
<div className="panel-block">{isLoading ? <Loading /> : body}</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,168 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2020 - present Cloudogu GmbH
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify it under
|
||||||
|
* the terms of the GNU Affero General Public License as published by the Free
|
||||||
|
* Software Foundation, version 3.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||||
|
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||||
|
* details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see https://www.gnu.org/licenses/.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { File, Repository } from "@scm-manager/ui-types";
|
||||||
|
import { getFileSearchLink } from "./fileSearchLink";
|
||||||
|
|
||||||
|
describe("getFileSearchLink", () => {
|
||||||
|
describe("git", () => {
|
||||||
|
it("shouldCreateCorrectLinkFromGitRootDirectory", () => {
|
||||||
|
const link = getFileSearchLink(repository, "main", baseUrl, rootDirectory);
|
||||||
|
expect(link).toBe("repo/captain/kirk/code/search/main");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shouldCreateCorrectLinkFromGitFirstLevelDirectory", () => {
|
||||||
|
const link = getFileSearchLink(repository, "main", baseUrl, firstLevelDirectory);
|
||||||
|
expect(link).toBe("repo/captain/kirk/code/search/main?prevSourcePath=dir");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shouldCreateCorrectLinkFromGitSecondLevelDirectory", () => {
|
||||||
|
const link = getFileSearchLink(repository, "main", baseUrl, secondLevelDirectory);
|
||||||
|
expect(link).toBe("repo/captain/kirk/code/search/main?prevSourcePath=dir%2FsubDir");
|
||||||
|
});
|
||||||
|
|
||||||
|
const repository: Repository = {
|
||||||
|
_links: {},
|
||||||
|
name: "kirk",
|
||||||
|
namespace: "captain",
|
||||||
|
type: "git",
|
||||||
|
};
|
||||||
|
|
||||||
|
const firstLevelDirectory: File = {
|
||||||
|
_links: {},
|
||||||
|
directory: true,
|
||||||
|
name: "dir",
|
||||||
|
path: "dir",
|
||||||
|
revision: "main",
|
||||||
|
};
|
||||||
|
|
||||||
|
const secondLevelDirectory: File = {
|
||||||
|
_links: {},
|
||||||
|
directory: true,
|
||||||
|
name: "subDir",
|
||||||
|
path: "dir/subDir",
|
||||||
|
revision: "main",
|
||||||
|
};
|
||||||
|
|
||||||
|
const rootDirectory: File = {
|
||||||
|
_links: {},
|
||||||
|
directory: true,
|
||||||
|
name: "",
|
||||||
|
path: "",
|
||||||
|
revision: "main",
|
||||||
|
};
|
||||||
|
|
||||||
|
const baseUrl = "repo/captain/kirk/code";
|
||||||
|
});
|
||||||
|
describe("svn", () => {
|
||||||
|
it("shouldCreateCorrectLinkFromGitRootDirectory", () => {
|
||||||
|
const link = getFileSearchLink(repository, "42", baseUrl, rootDirectory);
|
||||||
|
expect(link).toBe("repo/subversion/space/code/search/42?prevSourcePath=42");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shouldCreateCorrectLinkFromGitFirstLevelDirectory", () => {
|
||||||
|
const link = getFileSearchLink(repository, "0", baseUrl, firstLevelDirectory);
|
||||||
|
expect(link).toBe("repo/subversion/space/code/search/0?prevSourcePath=0%2Ftrunk");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shouldCreateCorrectLinkFromGitSecondLevelDirectory", () => {
|
||||||
|
const link = getFileSearchLink(repository, "42", baseUrl, secondLevelDirectory);
|
||||||
|
expect(link).toBe("repo/subversion/space/code/search/42?prevSourcePath=42%2Ftrunk%2FsubDir");
|
||||||
|
});
|
||||||
|
|
||||||
|
const repository: Repository = {
|
||||||
|
_links: {},
|
||||||
|
name: "space",
|
||||||
|
namespace: "subversion",
|
||||||
|
type: "svn",
|
||||||
|
};
|
||||||
|
|
||||||
|
const firstLevelDirectory: File = {
|
||||||
|
_links: {},
|
||||||
|
directory: true,
|
||||||
|
name: "trunk",
|
||||||
|
path: "trunk",
|
||||||
|
revision: "0",
|
||||||
|
};
|
||||||
|
|
||||||
|
const secondLevelDirectory: File = {
|
||||||
|
_links: {},
|
||||||
|
directory: true,
|
||||||
|
name: "subDir",
|
||||||
|
path: "trunk/subDir",
|
||||||
|
revision: "42",
|
||||||
|
};
|
||||||
|
|
||||||
|
const rootDirectory: File = {
|
||||||
|
_links: {},
|
||||||
|
directory: true,
|
||||||
|
name: "",
|
||||||
|
path: "",
|
||||||
|
revision: "42",
|
||||||
|
};
|
||||||
|
|
||||||
|
const baseUrl = "repo/subversion/space/code";
|
||||||
|
});
|
||||||
|
describe("hg", () => {
|
||||||
|
it("shouldCreateCorrectLinkFromHgRootDirectory", () => {
|
||||||
|
const link = getFileSearchLink(repository, "default", baseUrl, rootDirectory);
|
||||||
|
expect(link).toBe("repo/heavy/metal/code/search/default");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shouldCreateCorrectLinkFromGitFirstLevelDirectory", () => {
|
||||||
|
const link = getFileSearchLink(repository, "default", baseUrl, firstLevelDirectory);
|
||||||
|
expect(link).toBe("repo/heavy/metal/code/search/default?prevSourcePath=dir");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shouldCreateCorrectLinkFromGitSecondLevelDirectory", () => {
|
||||||
|
const link = getFileSearchLink(repository, "default", baseUrl, secondLevelDirectory);
|
||||||
|
expect(link).toBe("repo/heavy/metal/code/search/default?prevSourcePath=dir%2FsubDir");
|
||||||
|
});
|
||||||
|
|
||||||
|
const repository: Repository = {
|
||||||
|
_links: {},
|
||||||
|
name: "metal",
|
||||||
|
namespace: "heavy",
|
||||||
|
type: "hg",
|
||||||
|
};
|
||||||
|
|
||||||
|
const firstLevelDirectory: File = {
|
||||||
|
_links: {},
|
||||||
|
directory: true,
|
||||||
|
name: "dir",
|
||||||
|
path: "dir",
|
||||||
|
revision: "default",
|
||||||
|
};
|
||||||
|
|
||||||
|
const secondLevelDirectory: File = {
|
||||||
|
_links: {},
|
||||||
|
directory: true,
|
||||||
|
name: "subDir",
|
||||||
|
path: "dir/subDir",
|
||||||
|
revision: "default",
|
||||||
|
};
|
||||||
|
|
||||||
|
const rootDirectory: File = {
|
||||||
|
_links: {},
|
||||||
|
directory: true,
|
||||||
|
name: "",
|
||||||
|
path: "",
|
||||||
|
revision: "default",
|
||||||
|
};
|
||||||
|
|
||||||
|
const baseUrl = "repo/heavy/metal/code";
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2020 - present Cloudogu GmbH
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify it under
|
||||||
|
* the terms of the GNU Affero General Public License as published by the Free
|
||||||
|
* Software Foundation, version 3.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful, but WITHOUT
|
||||||
|
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||||
|
* details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see https://www.gnu.org/licenses/.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { File, Repository } from "@scm-manager/ui-types";
|
||||||
|
import { urls } from "@scm-manager/ui-api";
|
||||||
|
|
||||||
|
export function getFileSearchLink(
|
||||||
|
repository: Repository,
|
||||||
|
revision: string,
|
||||||
|
baseUrl: string,
|
||||||
|
currentSource: File
|
||||||
|
): string {
|
||||||
|
let currentSourcePath = null;
|
||||||
|
|
||||||
|
if (repository.type === "svn" && currentSource.path) {
|
||||||
|
currentSourcePath = urls.createPrevSourcePathQuery(`${revision}/${currentSource.path}`);
|
||||||
|
} else if (repository.type === "svn") {
|
||||||
|
currentSourcePath = urls.createPrevSourcePathQuery(`${revision}`);
|
||||||
|
} else {
|
||||||
|
currentSourcePath = urls.createPrevSourcePathQuery(currentSource.path);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentSourcePath) {
|
||||||
|
return `${baseUrl}/search/${encodeURIComponent(revision)}?${currentSourcePath}`;
|
||||||
|
} else {
|
||||||
|
return `${baseUrl}/search/${encodeURIComponent(revision)}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,8 +19,8 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { useHistory, useLocation, useParams } from "react-router-dom";
|
import { useHistory, useLocation, useParams } from "react-router-dom";
|
||||||
import { RepositoryRevisionContextProvider, urls, useSources } from "@scm-manager/ui-api";
|
import { RepositoryRevisionContextProvider, urls, useSources } from "@scm-manager/ui-api";
|
||||||
import { Branch, Repository } from "@scm-manager/ui-types";
|
import { Branch, Repository } from "@scm-manager/ui-types";
|
||||||
import { Breadcrumb } from "@scm-manager/ui-components";
|
import { Breadcrumb, useScrollToElement, useShortcut } from "@scm-manager/ui-components";
|
||||||
import { Notification, ErrorNotification, Loading, useDocumentTitle } from "@scm-manager/ui-core";
|
import { ErrorNotification, Loading, Notification, useDocumentTitle } from "@scm-manager/ui-core";
|
||||||
import FileTree from "../components/FileTree";
|
import FileTree from "../components/FileTree";
|
||||||
import Content from "./Content";
|
import Content from "./Content";
|
||||||
import CodeActionBar from "../../codeSection/components/CodeActionBar";
|
import CodeActionBar from "../../codeSection/components/CodeActionBar";
|
||||||
@@ -30,7 +30,7 @@ import { isEmptyDirectory, isRootFile } from "../utils/files";
|
|||||||
import CompareLink from "../../compare/CompareLink";
|
import CompareLink from "../../compare/CompareLink";
|
||||||
import { encodePart } from "../components/content/FileLink";
|
import { encodePart } from "../components/content/FileLink";
|
||||||
import { ExtensionPoint, extensionPoints } from "@scm-manager/ui-extensions";
|
import { ExtensionPoint, extensionPoints } from "@scm-manager/ui-extensions";
|
||||||
import { useScrollToElement } from "@scm-manager/ui-components";
|
import { getFileSearchLink } from "../../codeSection/utils/fileSearchLink";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
repository: Repository;
|
repository: Repository;
|
||||||
@@ -110,6 +110,19 @@ const Sources: FC<Props> = ({ repository, branches, selectedBranch, baseUrl }) =
|
|||||||
enabled: !branches || !!selectedBranch,
|
enabled: !branches || !!selectedBranch,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useShortcut(
|
||||||
|
"g f",
|
||||||
|
() => {
|
||||||
|
if (file) {
|
||||||
|
history.push(getFileSearchLink(repository, file.revision, baseUrl, file));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: t("shortcuts.fileSearch"),
|
||||||
|
active: !!(repository._links.paths && file),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return <ErrorNotification error={error} />;
|
return <ErrorNotification error={error} />;
|
||||||
}
|
}
|
||||||
@@ -155,7 +168,7 @@ const Sources: FC<Props> = ({ repository, branches, selectedBranch, baseUrl }) =
|
|||||||
const permalink = file?.revision ? replaceBranchWithRevision(location.pathname, file.revision) : null;
|
const permalink = file?.revision ? replaceBranchWithRevision(location.pathname, file.revision) : null;
|
||||||
|
|
||||||
const buttons = [];
|
const buttons = [];
|
||||||
if (repository._links.paths) {
|
if (repository._links.paths && file) {
|
||||||
buttons.push(
|
buttons.push(
|
||||||
<FileSearchButton
|
<FileSearchButton
|
||||||
baseUrl={baseUrl}
|
baseUrl={baseUrl}
|
||||||
|
|||||||
Reference in New Issue
Block a user