diff --git a/scm-ui-components/packages/ui-components/src/apiclient.js b/scm-ui-components/packages/ui-components/src/apiclient.js index 0b57abeada..bd19dcdf14 100644 --- a/scm-ui-components/packages/ui-components/src/apiclient.js +++ b/scm-ui-components/packages/ui-components/src/apiclient.js @@ -48,6 +48,14 @@ class ApiClient { return this.httpRequestWithJSONBody("PUT", url, contentType, payload); } + head(url: string) { + let options: RequestOptions = { + method: "HEAD" + }; + options = Object.assign(options, fetchOptions); + return fetch(createUrl(url), options).then(handleStatusCode); + } + delete(url: string): Promise { let options: RequestOptions = { method: "DELETE" diff --git a/scm-ui-components/packages/ui-components/src/buttons/DownloadButton.js b/scm-ui-components/packages/ui-components/src/buttons/DownloadButton.js new file mode 100644 index 0000000000..3a99dc876b --- /dev/null +++ b/scm-ui-components/packages/ui-components/src/buttons/DownloadButton.js @@ -0,0 +1,25 @@ +//@flow +import React from "react"; +import Button, { type ButtonProps } from "./Button"; +import type {File} from "@scm-manager/ui-types"; + +type Props = { + displayName: string, + url: string +}; + +class DownloadButton extends React.Component { + render() { + const {displayName, url} = this.props; + return ( + + + + + {displayName} + + ); + } +} + +export default DownloadButton; diff --git a/scm-ui-components/packages/ui-components/src/buttons/index.js b/scm-ui-components/packages/ui-components/src/buttons/index.js index d7da320fc2..2e166e1d93 100644 --- a/scm-ui-components/packages/ui-components/src/buttons/index.js +++ b/scm-ui-components/packages/ui-components/src/buttons/index.js @@ -7,4 +7,4 @@ export { default as DeleteButton } from "./DeleteButton.js"; export { default as EditButton } from "./EditButton.js"; export { default as RemoveEntryOfTableButton } from "./RemoveEntryOfTableButton.js"; export { default as SubmitButton } from "./SubmitButton.js"; - +export {default as DownloadButton} from "./DownloadButton.js"; diff --git a/scm-ui/package.json b/scm-ui/package.json index de42ff2a36..f88fe75c85 100644 --- a/scm-ui/package.json +++ b/scm-ui/package.json @@ -23,6 +23,7 @@ "react-redux": "^5.0.7", "react-router-dom": "^4.3.1", "react-router-redux": "^5.0.0-alpha.9", + "react-syntax-highlighter": "^9.0.1", "redux": "^4.0.0", "redux-devtools-extension": "^2.13.5", "redux-logger": "^3.0.6", diff --git a/scm-ui/public/locales/en/repos.json b/scm-ui/public/locales/en/repos.json index 60ee220318..d4fd950c45 100644 --- a/scm-ui/public/locales/en/repos.json +++ b/scm-ui/public/locales/en/repos.json @@ -51,7 +51,11 @@ "name": "Name", "length": "Length", "lastModified": "Last modified", - "description": "Description" + "description": "Description", + "branch": "Branch" + }, + "content": { + "downloadButton": "Download" } }, "changesets": { diff --git a/scm-ui/src/config/containers/GlobalConfig.js b/scm-ui/src/config/containers/GlobalConfig.js index 252e880a42..6046aa4a09 100644 --- a/scm-ui/src/config/containers/GlobalConfig.js +++ b/scm-ui/src/config/containers/GlobalConfig.js @@ -14,7 +14,7 @@ import { modifyConfigReset } from "../modules/config"; import { connect } from "react-redux"; -import type { Config, Link } from "@scm-manager/ui-types"; +import type { Config } from "@scm-manager/ui-types"; import ConfigForm from "../components/form/ConfigForm"; import { getConfigLink } from "../../modules/indexResource"; diff --git a/scm-ui/src/config/modules/config.test.js b/scm-ui/src/config/modules/config.test.js index 12c6b347c3..b6c97826b0 100644 --- a/scm-ui/src/config/modules/config.test.js +++ b/scm-ui/src/config/modules/config.test.js @@ -22,7 +22,6 @@ import reducer, { getConfig, getConfigUpdatePermission } from "./config"; -import { getConfigLink } from "../../modules/indexResource"; const CONFIG_URL = "/config"; const URL = "/api/v2" + CONFIG_URL; diff --git a/scm-ui/src/containers/App.js b/scm-ui/src/containers/App.js index 768b1776d4..50fc805eb2 100644 --- a/scm-ui/src/containers/App.js +++ b/scm-ui/src/containers/App.js @@ -19,9 +19,8 @@ import { Footer, Header } from "@scm-manager/ui-components"; -import type { Me, Link } from "@scm-manager/ui-types"; +import type { Me } from "@scm-manager/ui-types"; import { - fetchIndexResources, getConfigLink, getFetchIndexResourcesFailure, getGroupsLink, diff --git a/scm-ui/src/containers/Logout.js b/scm-ui/src/containers/Logout.js index 7875a6b92a..fe6662da42 100644 --- a/scm-ui/src/containers/Logout.js +++ b/scm-ui/src/containers/Logout.js @@ -11,7 +11,7 @@ import { getLogoutFailure } from "../modules/auth"; import { Loading, ErrorPage } from "@scm-manager/ui-components"; -import { fetchIndexResources, getLogoutLink } from "../modules/indexResource"; +import { getLogoutLink } from "../modules/indexResource"; type Props = { authenticated: boolean, diff --git a/scm-ui/src/groups/containers/AddGroup.js b/scm-ui/src/groups/containers/AddGroup.js index bcb19846b8..9b13ac0309 100644 --- a/scm-ui/src/groups/containers/AddGroup.js +++ b/scm-ui/src/groups/containers/AddGroup.js @@ -9,8 +9,7 @@ import { createGroup, isCreateGroupPending, getCreateGroupFailure, - createGroupReset, - getCreateGroupLink + createGroupReset } from "../modules/groups"; import type { Group } from "@scm-manager/ui-types"; import type { History } from "history"; diff --git a/scm-ui/src/index.js b/scm-ui/src/index.js index 3ecd38e6d0..08e3e8a58c 100644 --- a/scm-ui/src/index.js +++ b/scm-ui/src/index.js @@ -14,7 +14,6 @@ import type { BrowserHistory } from "history/createBrowserHistory"; import createReduxStore from "./createReduxStore"; import { ConnectedRouter } from "react-router-redux"; -import PluginLoader from "./containers/PluginLoader"; import { urls } from "@scm-manager/ui-components"; @@ -37,7 +36,7 @@ ReactDOM.render( {/* ConnectedRouter will use the store from Provider automatically */} - + , diff --git a/scm-ui/src/modules/auth.js b/scm-ui/src/modules/auth.js index fd5068aeb8..fe636ac9d3 100644 --- a/scm-ui/src/modules/auth.js +++ b/scm-ui/src/modules/auth.js @@ -7,8 +7,8 @@ import { isPending } from "./pending"; import { getFailure } from "./failure"; import { callFetchIndexResources, - FETCH_INDEXRESOURCES_SUCCESS, - fetchIndexResources, fetchIndexResourcesPending, + fetchIndexResources, + fetchIndexResourcesPending, fetchIndexResourcesSuccess } from "./indexResource"; @@ -156,7 +156,7 @@ export const login = ( return apiClient .post(loginLink, login_data) .then(response => { - dispatch(fetchIndexResourcesPending()) + dispatch(fetchIndexResourcesPending()); return callFetchIndexResources(); }) .then(response => { diff --git a/scm-ui/src/repos/sources/components/FileTree.js b/scm-ui/src/repos/sources/components/FileTree.js index 02aa22f942..e9b5c70d3d 100644 --- a/scm-ui/src/repos/sources/components/FileTree.js +++ b/scm-ui/src/repos/sources/components/FileTree.js @@ -7,7 +7,6 @@ import FileTreeLeaf from "./FileTreeLeaf"; import type { Repository, File } from "@scm-manager/ui-types"; import { ErrorNotification, Loading } from "@scm-manager/ui-components"; import { - fetchSources, getFetchSourcesFailure, isFetchSourcesPending, getSources @@ -29,7 +28,6 @@ type Props = { revision: string, path: string, baseUrl: string, - fetchSources: (Repository, string, string) => void, // context props classes: any, t: string => string, @@ -49,19 +47,6 @@ export function findParent(path: string) { } class FileTree extends React.Component { - componentDidMount() { - const { fetchSources, repository, revision, path } = this.props; - - fetchSources(repository, revision, path); - } - - componentDidUpdate(prevProps) { - const { fetchSources, repository, revision, path } = this.props; - if (prevProps.revision !== revision || prevProps.path !== path) { - fetchSources(repository, revision, path); - } - } - render() { const { error, @@ -167,18 +152,7 @@ const mapStateToProps = (state: any, ownProps: Props) => { }; }; -const mapDispatchToProps = dispatch => { - return { - fetchSources: (repository: Repository, revision: string, path: string) => { - dispatch(fetchSources(repository, revision, path)); - } - }; -}; - export default compose( withRouter, - connect( - mapStateToProps, - mapDispatchToProps - ) + connect(mapStateToProps) )(injectSheet(styles)(translate("repos")(FileTree))); diff --git a/scm-ui/src/repos/sources/components/FileTreeLeaf.js b/scm-ui/src/repos/sources/components/FileTreeLeaf.js index 033d3b9b8a..b4e2ad59ea 100644 --- a/scm-ui/src/repos/sources/components/FileTreeLeaf.js +++ b/scm-ui/src/repos/sources/components/FileTreeLeaf.js @@ -49,14 +49,18 @@ class FileTreeLeaf extends React.Component { ); } - return ; + return ( + + + + ); }; createFileName = (file: File) => { if (file.directory) { return {file.name}; } - return file.name; + return {file.name}; }; render() { diff --git a/scm-ui/src/repos/sources/components/content/DownloadViewer.js b/scm-ui/src/repos/sources/components/content/DownloadViewer.js new file mode 100644 index 0000000000..4b84d7a53d --- /dev/null +++ b/scm-ui/src/repos/sources/components/content/DownloadViewer.js @@ -0,0 +1,26 @@ +// @flow +import React from "react"; +import { translate } from "react-i18next"; +import type { File } from "@scm-manager/ui-types"; +import { DownloadButton } from "@scm-manager/ui-components"; + +type Props = { + t: string => string, + file: File +}; + +class DownloadViewer extends React.Component { + render() { + const { t, file } = this.props; + return ( +
+ +
+ ); + } +} + +export default translate("repos")(DownloadViewer); diff --git a/scm-ui/src/repos/sources/components/content/ImageViewer.js b/scm-ui/src/repos/sources/components/content/ImageViewer.js new file mode 100644 index 0000000000..67003fa357 --- /dev/null +++ b/scm-ui/src/repos/sources/components/content/ImageViewer.js @@ -0,0 +1,24 @@ +// @flow +import React from "react"; +import { translate } from "react-i18next"; +import type { File } from "@scm-manager/ui-types"; + +type Props = { + t: string => string, + file: File +}; + +class ImageViewer extends React.Component { + render() { + const { file } = this.props; + return ( +
+
+ {file._links.self.href} +
+
+ ); + } +} + +export default translate("repos")(ImageViewer); diff --git a/scm-ui/src/repos/sources/components/content/SourcecodeViewer.js b/scm-ui/src/repos/sources/components/content/SourcecodeViewer.js new file mode 100644 index 0000000000..e54f8e1b3b --- /dev/null +++ b/scm-ui/src/repos/sources/components/content/SourcecodeViewer.js @@ -0,0 +1,97 @@ +// @flow +import React from "react"; +import { translate } from "react-i18next"; +import { apiClient } from "@scm-manager/ui-components"; +import type { File } from "@scm-manager/ui-types"; +import { ErrorNotification, Loading } from "@scm-manager/ui-components"; +import SyntaxHighlighter from "react-syntax-highlighter"; +import { arduinoLight } from "react-syntax-highlighter/styles/hljs"; + +type Props = { + t: string => string, + file: File, + language: string +}; + +type State = { + content: string, + error?: Error, + loaded: boolean +}; + +class SourcecodeViewer extends React.Component { + constructor(props: Props) { + super(props); + + this.state = { + content: "", + loaded: false + }; + } + + componentDidMount() { + const { file } = this.props; + getContent(file._links.self.href) + .then(result => { + if (result.error) { + this.setState({ + ...this.state, + error: result.error, + loaded: true + }); + } else { + this.setState({ + ...this.state, + content: result, + loaded: true + }); + } + }) + .catch(err => {}); + } + + render() { + const { content, error, loaded } = this.state; + const language = this.props.language; + + if (error) { + return ; + } + + if (!loaded) { + return ; + } + + if (!content) { + return null; + } + + return ( + + {content} + + ); + } +} + +export function getLanguage(language: string) { + return language.toLowerCase(); +} + +export function getContent(url: string) { + return apiClient + .get(url) + .then(response => response.text()) + .then(response => { + return response; + }) + .catch(err => { + return { error: err }; + }); +} + +export default translate("repos")(SourcecodeViewer); diff --git a/scm-ui/src/repos/sources/components/content/SourcecodeViewer.test.js b/scm-ui/src/repos/sources/components/content/SourcecodeViewer.test.js new file mode 100644 index 0000000000..11e701f626 --- /dev/null +++ b/scm-ui/src/repos/sources/components/content/SourcecodeViewer.test.js @@ -0,0 +1,33 @@ +//@flow +import fetchMock from "fetch-mock"; +import { + getContent, + getLanguage +} from "./SourcecodeViewer"; + +describe("get content", () => { + const CONTENT_URL = "/repositories/scmadmin/TestRepo/content/testContent"; + + afterEach(() => { + fetchMock.reset(); + fetchMock.restore(); + }); + + it("should return content", done => { + fetchMock.getOnce("/api/v2" + CONTENT_URL, "This is a testContent"); + + getContent(CONTENT_URL).then(content => { + expect(content).toBe("This is a testContent"); + done(); + }); + }); +}); + +describe("get correct language type", () => { + it("should return javascript", () => { + expect(getLanguage("JAVASCRIPT")).toBe("javascript"); + }); + it("should return nothing for plain text", () => { + expect(getLanguage("")).toBe(""); + }); +}); diff --git a/scm-ui/src/repos/sources/containers/Content.js b/scm-ui/src/repos/sources/containers/Content.js new file mode 100644 index 0000000000..0b764aa6a1 --- /dev/null +++ b/scm-ui/src/repos/sources/containers/Content.js @@ -0,0 +1,217 @@ +// @flow +import React from "react"; +import { translate } from "react-i18next"; +import { getSources } from "../modules/sources"; +import type { Repository, File } from "@scm-manager/ui-types"; +import { + ErrorNotification, + Loading, + DateFromNow +} from "@scm-manager/ui-components"; +import { connect } from "react-redux"; +import ImageViewer from "../components/content/ImageViewer"; +import SourcecodeViewer from "../components/content/SourcecodeViewer"; +import DownloadViewer from "../components/content/DownloadViewer"; +import FileSize from "../components/FileSize"; +import injectSheet from "react-jss"; +import classNames from "classnames"; +import { ExtensionPoint } from "@scm-manager/ui-extensions"; +import { getContentType } from "./contentType"; + +type Props = { + loading: boolean, + error: Error, + file: File, + repository: Repository, + revision: string, + path: string, + classes: any, + t: string => string +}; + +type State = { + contentType: string, + language: string, + loaded: boolean, + collapsed: boolean, + error?: Error +}; + +const styles = { + toCenterContent: { + display: "block" + }, + pointer: { + cursor: "pointer" + } +}; + +class Content extends React.Component { + constructor(props: Props) { + super(props); + + this.state = { + contentType: "", + language: "", + loaded: false, + collapsed: true + }; + } + + componentDidMount() { + const { file } = this.props; + getContentType(file._links.self.href) + .then(result => { + if (result.error) { + this.setState({ + ...this.state, + error: result.error, + loaded: true + }); + } else { + this.setState({ + ...this.state, + contentType: result.type, + language: result.language, + loaded: true + }); + } + }) + .catch(err => {}); + } + + toggleCollapse = () => { + this.setState(prevState => ({ + collapsed: !prevState.collapsed + })); + }; + + showHeader() { + const { file, classes } = this.props; + const collapsed = this.state.collapsed; + const icon = collapsed ? "fa-angle-right" : "fa-angle-down"; + const fileSize = file.directory ? "" : ; + + return ( + +
+
+ +
+
+
{file.name}
+
+

{fileSize}

+
+
+ ); + } + + showMoreInformation() { + const collapsed = this.state.collapsed; + const { classes, file, revision } = this.props; + const date = ; + const description = file.description ? ( +

+ {file.description.split("\n").map((item, key) => { + return ( + + {item} +
+
+ ); + })} +

+ ) : null; + if (!collapsed) { + return ( +
+ + + + + + + + + + + + + + + + + + + +
Path{file.path}
Branch{revision}
Last modified{date}
Description{description}
+
+ ); + } + return null; + } + + showContent() { + const { file, revision } = this.props; + const { contentType, language } = this.state; + if (contentType.startsWith("image/")) { + return ; + } else if (language) { + return ; + } else if (contentType.startsWith("text/")) { + return ; + } else { + return ( + + + + ); + } + } + + render() { + const { file, classes } = this.props; + const { loaded, error } = this.state; + + if (!file || !loaded) { + return ; + } + if (error) { + return ; + } + + const header = this.showHeader(); + const content = this.showContent(); + const moreInformation = this.showMoreInformation(); + + return ( +
+ +
+ ); + } +} + +const mapStateToProps = (state: any, ownProps: Props) => { + const { repository, revision, path } = ownProps; + + const file = getSources(state, repository, revision, path); + + return { + file + }; +}; + +export default injectSheet(styles)( + connect(mapStateToProps)(translate("repos")(Content)) +); diff --git a/scm-ui/src/repos/sources/containers/Sources.js b/scm-ui/src/repos/sources/containers/Sources.js index cf072e958e..1a9f1d62e7 100644 --- a/scm-ui/src/repos/sources/containers/Sources.js +++ b/scm-ui/src/repos/sources/containers/Sources.js @@ -13,6 +13,8 @@ import { isFetchBranchesPending } from "../../modules/branches"; import { compose } from "redux"; +import Content from "./Content"; +import { fetchSources, isDirectory } from "../modules/sources"; type Props = { repository: Repository, @@ -22,9 +24,11 @@ type Props = { branches: Branch[], revision: string, path: string, + currentFileIsDirectory: boolean, // dispatch props fetchBranches: Repository => void, + fetchSources: (Repository, string, string) => void, // Context props history: any, @@ -33,14 +37,26 @@ type Props = { class Sources extends React.Component { componentDidMount() { - const { fetchBranches, repository } = this.props; + const { + fetchBranches, + repository, + revision, + path, + fetchSources + } = this.props; fetchBranches(repository); + fetchSources(repository, revision, path); + } + componentDidUpdate(prevProps) { + const { fetchSources, repository, revision, path } = this.props; + if (prevProps.revision !== revision || prevProps.path !== path) { + fetchSources(repository, revision, path); + } } branchSelected = (branch?: Branch) => { const { baseUrl, history, path } = this.props; - let url; if (branch) { if (path) { @@ -55,7 +71,15 @@ class Sources extends React.Component { }; render() { - const { repository, baseUrl, loading, error, revision, path } = this.props; + const { + repository, + baseUrl, + loading, + error, + revision, + path, + currentFileIsDirectory + } = this.props; if (error) { return ; @@ -65,21 +89,28 @@ class Sources extends React.Component { return ; } - return ( - <> - {this.renderBranchSelector()} - - - ); + if (currentFileIsDirectory) { + return ( + <> + {this.renderBranchSelector()} + + + ); + } else { + return ( + + ); + } } renderBranchSelector = () => { const { repository, branches, revision } = this.props; + if (repository._links.branches) { return ( { const { repository, match } = ownProps; const { revision, path } = match.params; const decodedRevision = revision ? decodeURIComponent(revision) : undefined; - const loading = isFetchBranchesPending(state, repository); const error = getFetchBranchesFailure(state, repository); const branches = getBranches(state, repository); + const currentFileIsDirectory = decodedRevision + ? isDirectory(state, repository, decodedRevision, path) + : isDirectory(state, repository, revision, path); return { repository, @@ -110,7 +143,8 @@ const mapStateToProps = (state, ownProps) => { path, loading, error, - branches + branches, + currentFileIsDirectory }; }; @@ -118,6 +152,9 @@ const mapDispatchToProps = dispatch => { return { fetchBranches: (repository: Repository) => { dispatch(fetchBranches(repository)); + }, + fetchSources: (repository: Repository, revision: string, path: string) => { + dispatch(fetchSources(repository, revision, path)); } }; }; diff --git a/scm-ui/src/repos/sources/containers/contentType.js b/scm-ui/src/repos/sources/containers/contentType.js new file mode 100644 index 0000000000..bf9888834b --- /dev/null +++ b/scm-ui/src/repos/sources/containers/contentType.js @@ -0,0 +1,16 @@ +//@flow +import { apiClient } from "@scm-manager/ui-components"; + +export function getContentType(url: string) { + return apiClient + .head(url) + .then(response => { + return { + type: response.headers.get("Content-Type"), + language: response.headers.get("X-Programming-Language") + }; + }) + .catch(err => { + return { error: err }; + }); +} diff --git a/scm-ui/src/repos/sources/containers/contentType.test.js b/scm-ui/src/repos/sources/containers/contentType.test.js new file mode 100644 index 0000000000..c3ab85ed80 --- /dev/null +++ b/scm-ui/src/repos/sources/containers/contentType.test.js @@ -0,0 +1,29 @@ +//@flow +import fetchMock from "fetch-mock"; +import { getContentType } from "./contentType"; + +describe("get content type", () => { + const CONTENT_URL = "/repositories/scmadmin/TestRepo/content/testContent"; + + afterEach(() => { + fetchMock.reset(); + fetchMock.restore(); + }); + + it("should return content", done => { + let headers = { + "Content-Type": "application/text", + "X-Programming-Language": "JAVA" + }; + + fetchMock.head("/api/v2" + CONTENT_URL, { + headers + }); + + getContentType(CONTENT_URL).then(content => { + expect(content.type).toBe("application/text"); + expect(content.language).toBe("JAVA"); + done(); + }); + }); +}); diff --git a/scm-ui/src/repos/sources/modules/sources.js b/scm-ui/src/repos/sources/modules/sources.js index 719770d75c..641c1550b6 100644 --- a/scm-ui/src/repos/sources/modules/sources.js +++ b/scm-ui/src/repos/sources/modules/sources.js @@ -102,6 +102,20 @@ export default function reducer( // selectors +export function isDirectory( + state: any, + repository: Repository, + revision: string, + path: string +): boolean { + const currentFile = getSources(state, repository, revision, path); + if (currentFile && !currentFile.directory) { + return false; + } else { + return true; //also return true if no currentFile is found since it is the "default" path + } +} + export function getSources( state: any, repository: Repository, diff --git a/scm-ui/src/repos/sources/modules/sources.test.js b/scm-ui/src/repos/sources/modules/sources.test.js index 068fa39e8f..1a5c81e908 100644 --- a/scm-ui/src/repos/sources/modules/sources.test.js +++ b/scm-ui/src/repos/sources/modules/sources.test.js @@ -1,6 +1,6 @@ // @flow -import type { Repository } from "@scm-manager/ui-types"; +import type { Repository, File } from "@scm-manager/ui-types"; import configureMockStore from "redux-mock-store"; import thunk from "redux-thunk"; import fetchMock from "fetch-mock"; @@ -14,7 +14,8 @@ import { isFetchSourcesPending, default as reducer, getSources, - fetchSourcesSuccess + fetchSourcesSuccess, + isDirectory } from "./sources"; const sourcesUrl = @@ -79,6 +80,21 @@ const collection = { } }; +const noDirectory: File = { + name: "src", + path: "src", + directory: true, + length: 176, + revision: "abc", + _links: { + self: { + href: + "http://localhost:8081/scm/rest/api/v2/repositories/scm/core/sources/76aae4bb4ceacf0e88938eb5b6832738b7d537b4/src" + } + }, + _embedded: collection +}; + describe("sources fetch", () => { const mockStore = configureMockStore([thunk]); @@ -168,6 +184,28 @@ describe("reducer tests", () => { }); describe("selector tests", () => { + it("should return false if it is no directory", () => { + const state = { + sources: { + "scm/core/abc/src/main/package.json": { + noDirectory + } + } + }; + expect( + isDirectory(state, repository, "abc", "src/main/package.json") + ).toBeFalsy(); + }); + + it("should return true if it is directory", () => { + const state = { + sources: { + "scm/core/abc/src": noDirectory + } + }; + expect(isDirectory(state, repository, "abc", "src")).toBe(true); + }); + it("should return null", () => { expect(getSources({}, repository)).toBeFalsy(); }); @@ -181,7 +219,7 @@ describe("selector tests", () => { expect(getSources(state, repository)).toBe(collection); }); - it("should return the source collection without revision and path", () => { + it("should return the source collection with revision and path", () => { const state = { sources: { "scm/core/abc/src/main": collection diff --git a/scm-ui/yarn.lock b/scm-ui/yarn.lock index 18b198622c..ec4789b544 100644 --- a/scm-ui/yarn.lock +++ b/scm-ui/yarn.lock @@ -1181,7 +1181,7 @@ babel-register@^6.26.0: mkdirp "^0.5.1" source-map-support "^0.4.15" -babel-runtime@^6.22.0, babel-runtime@^6.26.0: +babel-runtime@^6.18.0, babel-runtime@^6.22.0, babel-runtime@^6.26.0: version "6.26.0" resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe" dependencies: @@ -1739,6 +1739,18 @@ chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0: escape-string-regexp "^1.0.5" supports-color "^5.3.0" +character-entities-legacy@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/character-entities-legacy/-/character-entities-legacy-1.1.2.tgz#7c6defb81648498222c9855309953d05f4d63a9c" + +character-entities@^1.0.0: + version "1.2.2" + resolved "https://registry.yarnpkg.com/character-entities/-/character-entities-1.2.2.tgz#58c8f371c0774ef0ba9b2aca5f00d8f100e6e363" + +character-reference-invalid@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/character-reference-invalid/-/character-reference-invalid-1.1.2.tgz#21e421ad3d84055952dab4a43a04e73cd425d3ed" + chardet@^0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" @@ -1840,6 +1852,14 @@ cli-width@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.0.tgz#ff19ede8a9a5e579324147b0c11f0fbcbabed639" +clipboard@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/clipboard/-/clipboard-2.0.1.tgz#a12481e1c13d8a50f5f036b0560fe5d16d74e46a" + dependencies: + good-listener "^1.2.2" + select "^1.1.2" + tiny-emitter "^2.0.0" + cliui@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/cliui/-/cliui-3.2.0.tgz#120601537a916d29940f934da3b48d585a39213d" @@ -1946,6 +1966,12 @@ combined-stream@^1.0.5, combined-stream@~1.0.5, combined-stream@~1.0.6: dependencies: delayed-stream "~1.0.0" +comma-separated-tokens@^1.0.0: + version "1.0.5" + resolved "https://registry.yarnpkg.com/comma-separated-tokens/-/comma-separated-tokens-1.0.5.tgz#b13793131d9ea2d2431cf5b507ddec258f0ce0db" + dependencies: + trim "0.0.1" + commander@^2.11.0, commander@^2.17.1, commander@^2.2.0, commander@^2.9.0: version "2.18.0" resolved "https://registry.yarnpkg.com/commander/-/commander-2.18.0.tgz#2bf063ddee7c7891176981a2cc798e5754bc6970" @@ -2327,6 +2353,10 @@ delayed-stream@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" +delegate@^3.1.2: + version "3.2.0" + resolved "https://registry.yarnpkg.com/delegate/-/delegate-3.2.0.tgz#b66b71c3158522e8ab5744f720d8ca0c2af59166" + delegates@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" @@ -3068,6 +3098,12 @@ fast-xml-parser@^3.12.0: dependencies: nimnjs "^1.3.2" +fault@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/fault/-/fault-1.0.2.tgz#c3d0fec202f172a3a4d414042ad2bb5e2a3ffbaa" + dependencies: + format "^0.2.2" + fb-watchman@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.0.tgz#54e9abf7dfa2f26cd9b1636c588c1afc05de5d58" @@ -3280,6 +3316,10 @@ form-data@~2.3.1, form-data@~2.3.2: combined-stream "1.0.6" mime-types "^2.1.12" +format@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/format/-/format-0.2.2.tgz#d6170107e9efdc4ed30c9dc39016df942b5cb58b" + fragment-cache@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/fragment-cache/-/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19" @@ -3569,6 +3609,12 @@ glogg@^1.0.0: dependencies: sparkles "^1.0.0" +good-listener@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/good-listener/-/good-listener-1.2.2.tgz#d53b30cdf9313dffb7dc9a0d477096aa6d145c50" + dependencies: + delegate "^3.1.2" + got@^7.1.0: version "7.1.0" resolved "https://registry.yarnpkg.com/got/-/got-7.1.0.tgz#05450fd84094e6bbea56f451a43a9c289166385a" @@ -3814,6 +3860,19 @@ hash.js@^1.0.0, hash.js@^1.0.3: inherits "^2.0.3" minimalistic-assert "^1.0.1" +hast-util-parse-selector@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/hast-util-parse-selector/-/hast-util-parse-selector-2.2.0.tgz#2175f18cdd697308fc3431d5c29a9e48dfa4817a" + +hastscript@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/hastscript/-/hastscript-4.1.0.tgz#ea5593fa6f6709101fc790ced818393ddaa045ce" + dependencies: + comma-separated-tokens "^1.0.0" + hast-util-parse-selector "^2.2.0" + property-information "^4.0.0" + space-separated-tokens "^1.0.0" + hawk@~3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/hawk/-/hawk-3.1.3.tgz#078444bd7c1640b0fe540d2c9b73d59678e8e1c4" @@ -3823,6 +3882,10 @@ hawk@~3.1.3: hoek "2.x.x" sntp "1.x.x" +highlight.js@~9.12.0: + version "9.12.0" + resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-9.12.0.tgz#e6d9dbe57cbefe60751f02af336195870c90c01e" + history@^4.7.2: version "4.7.2" resolved "https://registry.yarnpkg.com/history/-/history-4.7.2.tgz#22b5c7f31633c5b8021c7f4a8a954ac139ee8d5b" @@ -4115,6 +4178,17 @@ is-accessor-descriptor@^1.0.0: dependencies: kind-of "^6.0.0" +is-alphabetical@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-alphabetical/-/is-alphabetical-1.0.2.tgz#1fa6e49213cb7885b75d15862fb3f3d96c884f41" + +is-alphanumerical@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-alphanumerical/-/is-alphanumerical-1.0.2.tgz#1138e9ae5040158dc6ff76b820acd6b7a181fd40" + dependencies: + is-alphabetical "^1.0.0" + is-decimal "^1.0.0" + is-arrayish@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" @@ -4165,6 +4239,10 @@ is-date-object@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.1.tgz#9aa20eb6aeebbff77fbd33e74ca01b33581d3a16" +is-decimal@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-decimal/-/is-decimal-1.0.2.tgz#894662d6a8709d307f3a276ca4339c8fa5dff0ff" + is-descriptor@^0.1.0: version "0.1.6" resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-0.1.6.tgz#366d8240dde487ca51823b1ab9f07a10a78251ca" @@ -4251,6 +4329,10 @@ is-glob@^4.0.0: dependencies: is-extglob "^2.1.1" +is-hexadecimal@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-hexadecimal/-/is-hexadecimal-1.0.2.tgz#b6e710d7d07bb66b98cb8cece5c9b4921deeb835" + is-in-browser@^1.0.2, is-in-browser@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/is-in-browser/-/is-in-browser-1.1.3.tgz#56ff4db683a078c6082eb95dad7dc62e1d04f835" @@ -5320,6 +5402,13 @@ lowercase-keys@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.1.tgz#6f9e30b47084d971a7c820ff15a6c5167b74c26f" +lowlight@~1.9.1: + version "1.9.2" + resolved "https://registry.yarnpkg.com/lowlight/-/lowlight-1.9.2.tgz#0b9127e3cec2c3021b7795dd81005c709a42fdd1" + dependencies: + fault "^1.0.2" + highlight.js "~9.12.0" + lru-cache@2: version "2.7.3" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-2.7.3.tgz#6d4524e8b955f95d4f5b58851ce21dd72fb4e952" @@ -6175,6 +6264,17 @@ parse-asn1@^5.0.0: evp_bytestokey "^1.0.0" pbkdf2 "^3.0.3" +parse-entities@^1.1.2: + version "1.2.0" + resolved "https://registry.yarnpkg.com/parse-entities/-/parse-entities-1.2.0.tgz#9deac087661b2e36814153cb78d7e54a4c5fd6f4" + dependencies: + character-entities "^1.0.0" + character-entities-legacy "^1.0.0" + character-reference-invalid "^1.0.0" + is-alphanumerical "^1.0.0" + is-decimal "^1.0.0" + is-hexadecimal "^1.0.0" + parse-filepath@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/parse-filepath/-/parse-filepath-1.0.2.tgz#a632127f53aaf3d15876f5872f3ffac763d6c891" @@ -6422,6 +6522,12 @@ pretty-hrtime@^1.0.0: version "1.0.3" resolved "https://registry.yarnpkg.com/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz#b7e3ea42435a4c9b2759d99e0f201eb195802ee1" +prismjs@^1.8.4, prismjs@~1.15.0: + version "1.15.0" + resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.15.0.tgz#8801d332e472091ba8def94976c8877ad60398d9" + optionalDependencies: + clipboard "^2.0.0" + private@^0.1.6, private@^0.1.8: version "0.1.8" resolved "https://registry.yarnpkg.com/private/-/private-0.1.8.tgz#2381edb3689f7a53d653190060fcf822d2f368ff" @@ -6456,6 +6562,12 @@ prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2: loose-envify "^1.3.1" object-assign "^4.1.1" +property-information@^4.0.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/property-information/-/property-information-4.2.0.tgz#f0e66e07cbd6fed31d96844d958d153ad3eb486e" + dependencies: + xtend "^4.0.1" + ps-tree@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/ps-tree/-/ps-tree-1.1.0.tgz#b421b24140d6203f1ed3c76996b4427b08e8c014" @@ -6653,6 +6765,16 @@ react-router@^4.2.0, react-router@^4.3.1: prop-types "^15.6.1" warning "^4.0.1" +react-syntax-highlighter@^9.0.1: + version "9.0.1" + resolved "https://registry.yarnpkg.com/react-syntax-highlighter/-/react-syntax-highlighter-9.0.1.tgz#cad91692e1976f68290f24762ac3451b1fec3d26" + dependencies: + babel-runtime "^6.18.0" + highlight.js "~9.12.0" + lowlight "~1.9.1" + prismjs "^1.8.4" + refractor "^2.4.1" + react-test-renderer@^16.0.0-0, react-test-renderer@^16.4.1: version "16.5.2" resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.5.2.tgz#92e9d2c6f763b9821b2e0b22f994ee675068b5ae" @@ -6811,6 +6933,14 @@ redux@^4.0.0: loose-envify "^1.1.0" symbol-observable "^1.2.0" +refractor@^2.4.1: + version "2.6.0" + resolved "https://registry.yarnpkg.com/refractor/-/refractor-2.6.0.tgz#6b0d88f654c8534eefed3329a35bc7bb74ae0979" + dependencies: + hastscript "^4.0.0" + parse-entities "^1.1.2" + prismjs "~1.15.0" + regenerate-unicode-properties@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-7.0.0.tgz#107405afcc4a190ec5ed450ecaa00ed0cafa7a4c" @@ -7172,6 +7302,10 @@ scss-tokenizer@^0.2.3: js-base64 "^2.1.8" source-map "^0.4.2" +select@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/select/-/select-1.1.2.tgz#0e7350acdec80b1108528786ec1d4418d11b396d" + "semver@2 || 3 || 4 || 5", semver@^5.0.1, semver@^5.3.0, semver@^5.4.1, semver@^5.5.0, semver@^5.5.1: version "5.5.1" resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.1.tgz#7dfdd8814bdb7cabc7be0fb1d734cfb66c940477" @@ -7440,6 +7574,12 @@ source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.0, source-map@~0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" +space-separated-tokens@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/space-separated-tokens/-/space-separated-tokens-1.1.2.tgz#e95ab9d19ae841e200808cd96bc7bd0adbbb3412" + dependencies: + trim "0.0.1" + sparkles@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/sparkles/-/sparkles-1.0.1.tgz#008db65edce6c50eec0c5e228e1945061dd0437c" @@ -7834,6 +7974,10 @@ timers-ext@^0.1.5: es5-ext "~0.10.46" next-tick "1" +tiny-emitter@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/tiny-emitter/-/tiny-emitter-2.0.2.tgz#82d27468aca5ade8e5fd1e6d22b57dd43ebdfb7c" + tmp@^0.0.33: version "0.0.33" resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" @@ -7917,6 +8061,10 @@ trim-right@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003" +trim@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/trim/-/trim-0.0.1.tgz#5858547f6b290757ee95cccc666fb50084c460dd" + "true-case-path@^1.0.2": version "1.0.3" resolved "https://registry.yarnpkg.com/true-case-path/-/true-case-path-1.0.3.tgz#f813b5a8c86b40da59606722b144e3225799f47d" diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ContentResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ContentResource.java index dc7c305823..99ed29e6f6 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ContentResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ContentResource.java @@ -136,7 +136,7 @@ public class ContentResource { private void appendContentHeader(String path, byte[] head, Response.ResponseBuilder responseBuilder) { ContentType contentType = ContentTypes.detect(path, head); responseBuilder.header("Content-Type", contentType.getRaw()); - contentType.getLanguage().ifPresent(language -> responseBuilder.header("Language", language)); + contentType.getLanguage().ifPresent(language -> responseBuilder.header("X-Programming-Language", language)); } private byte[] getHead(String revision, String path, RepositoryService repositoryService) throws IOException, PathNotFoundException, RevisionNotFoundException { diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ContentResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ContentResourceTest.java index 3d898119fb..e6a0972b78 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ContentResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ContentResourceTest.java @@ -93,7 +93,7 @@ public class ContentResourceTest { Response response = contentResource.get(NAMESPACE, REPO_NAME, REV, "SomeGoCode.go"); assertEquals(200, response.getStatus()); - assertEquals("GO", response.getHeaderString("Language")); + assertEquals("GO", response.getHeaderString("X-Programming-Language")); assertEquals("text/x-go", response.getHeaderString("Content-Type")); } @@ -104,7 +104,7 @@ public class ContentResourceTest { Response response = contentResource.get(NAMESPACE, REPO_NAME, REV, "Dockerfile"); assertEquals(200, response.getStatus()); - assertEquals("DOCKERFILE", response.getHeaderString("Language")); + assertEquals("DOCKERFILE", response.getHeaderString("X-Programming-Language")); assertEquals("text/plain", response.getHeaderString("Content-Type")); }