Merge remote-tracking branch 'origin/develop' into feature/browse_commit_with_limit

# Conflicts:
#	CHANGELOG.md
This commit is contained in:
René Pfeuffer
2020-03-12 11:21:13 +01:00
48 changed files with 1660 additions and 1052 deletions

View File

@@ -10,17 +10,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Create OpenAPI specification during build
- Extension point entries with supplied extensionName are sorted ascending
- Possibility to configure git core config entries for jgit like core.trustfolderstat and core.supportsatomicfilecreation
- Babel-plugin-styled-components for persistent generated classnames
- By default, only 100 files will be listed in source view in one request
### Changed
- New footer design
- Update jgit to version 5.6.1.202002131546-r-scm1
- Update svnkit to version 1.10.1-scm1
- Secondary navigation collapsable
### Fixed
- Modification for mercurial repositories with enabled XSRF protection
- Does not throw NullPointerException when merge fails without normal merge conflicts
- Keep file attributes on modification
- Drop Down Component works again with translations
### Removed
- Enunciate rest documentation

155
Jenkinsfile vendored
View File

@@ -1,24 +1,21 @@
#!groovy
// Keep the version in sync with the one used in pom.xml in order to get correct syntax completion.
@Library('github.com/cloudogu/ces-build-lib@1.35.1')
// switch back to a stable tag, after pr 22 is mreged an the next version is released
// see https://github.com/cloudogu/ces-build-lib/pull/22
@Library('github.com/cloudogu/ces-build-lib@8e9194e8')
import com.cloudogu.ces.cesbuildlib.*
node('docker') {
// Change this as when we go back to default - necessary for proper SonarQube analysis
mainBranch = 'develop'
properties([
// Keep only the last 10 build to preserve space
buildDiscarder(logRotator(numToKeepStr: '10')),
disableConcurrentBuilds(),
parameters([
string(name: 'dockerTag', trim: true, defaultValue: 'latest', description: 'Extra Docker Tag for cloudogu/scm-manager image')
])
disableConcurrentBuilds()
])
timeout(activity: true, time: 40, unit: 'MINUTES') {
timeout(activity: true, time: 60, unit: 'MINUTES') {
Git git = new Git(this)
@@ -30,16 +27,47 @@ node('docker') {
checkout scm
}
if (isReleaseBranch()) {
stage('Set Version') {
String releaseVersion = getReleaseVersion();
// set maven versions
mvn "versions:set -DgenerateBackupPoms=false -DnewVersion=${releaseVersion}"
// set versions for ui packages
// we need to run 'yarn install' in order to set version with ui-scripts
mvn "-pl :scm-ui buildfrontend:install@install"
mvn "-pl :scm-ui buildfrontend:run@set-version"
// stage pom changes
sh "git status --porcelain | sed s/^...// | grep pom.xml | xargs git add"
// stage package.json changes
sh "git status --porcelain | sed s/^...// | grep package.json | xargs git add"
// stage lerna.json changes
sh "git add lerna.json"
// commit changes
sh "git -c user.name='CES Marvin' -c user.email='cesmarvin@cloudogu.com' commit -m 'release version ${releaseVersion}'"
// merge release branch into master
sh "git checkout master"
sh "git merge --ff-only ${env.BRANCH_NAME}"
// set tag
sh "git -c user.name='CES Marvin' -c user.email='cesmarvin@cloudogu.com' tag -m 'release version ${releaseVersion}' ${releaseVersion}"
}
}
stage('Build') {
mvn 'clean install -DskipTests'
}
stage('Unit Test') {
mvn 'test -Pcoverage -Dmaven.test.failure.ignore=true'
junit allowEmptyResults: true, testResults: '**/target/surefire-reports/TEST-*.xml,**/target/jest-reports/TEST-*.xml'
}
stage('Integration Test') {
mvn 'verify -Pit -pl :scm-webapp,:scm-it -Dmaven.test.failure.ignore=true -Dscm.git.core.trustfolderstat=false'
mvn 'verify -Pit -pl :scm-webapp,:scm-it -Dmaven.test.failure.ignore=true -Dscm.git.core.supportsatomicfilecreation=false'
junit allowEmptyResults: true, testResults: '**/target/failsafe-reports/TEST-*.xml'
}
stage('SonarQube') {
@@ -51,10 +79,7 @@ node('docker') {
}
}
def commitHash = git.getCommitHash()
def dockerImageTag = "2.0.0-dev-${commitHash.substring(0,7)}-${BUILD_NUMBER}"
if (isMainBranch()) {
if (isMainBranch() || isReleaseBranch()) {
stage('Lifecycle') {
try {
@@ -66,37 +91,105 @@ node('docker') {
}
}
if (isBuildSuccessful()) {
def commitHash = git.getCommitHash()
def imageVersion = mvn.getVersion()
if (imageVersion.endsWith('-SNAPSHOT')) {
imageVersion = imageVersion.replace('-SNAPSHOT', "-${commitHash.substring(0,7)}-${BUILD_NUMBER}")
}
stage('Archive') {
archiveArtifacts 'scm-webapp/target/scm-webapp.war'
archiveArtifacts 'scm-server/target/scm-server-app.*'
}
stage('Maven Deployment') {
// TODO why is the server recreated
// delete appassembler target, because the maven plugin fails to recreate the tar
sh "rm -rf scm-server/target/appassembler"
// deploy java artifacts
mvn.useRepositoryCredentials([id: 'maven.scm-manager.org', url: 'https://maven.scm-manager.org/nexus', credentialsId: 'maven.scm-manager.org', type: 'Nexus2'])
mvn.deployToNexusRepository()
// deploy frontend bits
withCredentials([string(credentialsId: 'cesmarvin_npm_token', variable: 'NPM_TOKEN')]) {
writeFile encoding: 'UTF-8', file: '.npmrc', text: "//registry.npmjs.org/:_authToken='${NPM_TOKEN}'"
writeFile encoding: 'UTF-8', file: '.yarnrc', text: '''
registry "https://registry.npmjs.org/"
always-auth true
email cesmarvin@cloudogu.com
'''.trim()
// we are tricking lerna by pretending that we are not a git repository
sh "mv .git .git.disabled"
try {
mvn "-pl :scm-ui buildfrontend:run@deploy"
} finally {
sh "mv .git.disabled .git"
}
}
}
stage('Docker') {
def image = docker.build('cloudogu/scm-manager')
docker.withRegistry('', 'hub.docker.com-cesmarvin') {
image.push(dockerImageTag)
image.push('latest')
if (!'latest'.equals(params.dockerTag)) {
image.push(params.dockerTag)
def newDockerTag = "2.0.0-${commitHash.substring(0,7)}-dev-${params.dockerTag}"
currentBuild.description = newDockerTag
image.push(newDockerTag)
// push to cloudogu repository for internal usage
def image = docker.build('cloudogu/scm-manager')
image.push(imageVersion)
if (isReleaseBranch()) {
// push to official repository
image = docker.build('scmmanager/scm-manager')
image.push(imageVersion)
}
}
}
stage('Deployment') {
stage('Presentation Environment') {
build job: 'scm-manager/next-scm.cloudogu.com', propagate: false, wait: false, parameters: [
string(name: 'changeset', value: commitHash),
string(name: 'imageTag', value: dockerImageTag)
string(name: 'imageTag', value: imageVersion)
]
}
if (isReleaseBranch()) {
stage('Update Repository') {
// merge changes into develop
sh "git checkout develop"
// TODO what if we have a conflict
// e.g.: someone has edited the changelog during the release
sh "git merge master"
// set versions for maven packages
mvn "build-helper:parse-version versions:set -DgenerateBackupPoms=false -DnewVersion='\${parsedVersion.majorVersion}.\${parsedVersion.nextMinorVersion}.0-SNAPSHOT'"
// set versions for ui packages
mvn "-pl :scm-ui buildfrontend:run@set-version"
// stage pom changes
sh "git status --porcelain | sed s/^...// | grep pom.xml | xargs git add"
// stage package.json changes
sh "git status --porcelain | sed s/^...// | grep package.json | xargs git add"
// stage lerna.json changes
sh "git add lerna.json"
// commit changes
sh "git -c user.name='CES Marvin' -c user.email='cesmarvin@cloudogu.com' commit -m 'prepare for next development iteration'"
// push changes back to remote repository
withCredentials([usernamePassword(credentialsId: 'cesmarvin-github', usernameVariable: 'GIT_AUTH_USR', passwordVariable: 'GIT_AUTH_PSW')]) {
sh "git -c credential.helper=\"!f() { echo username='\$GIT_AUTH_USR'; echo password='\$GIT_AUTH_PSW'; }; f\" push origin master --tags"
sh "git -c credential.helper=\"!f() { echo username='\$GIT_AUTH_USR'; echo password='\$GIT_AUTH_PSW'; }; f\" push origin develop --tags"
sh "git -c credential.helper=\"!f() { echo username='\$GIT_AUTH_USR'; echo password='\$GIT_AUTH_PSW'; }; f\" push origin :${env.BRANCH_NAME}"
}
}
}
// Archive Unit and integration test results, if any
junit allowEmptyResults: true, testResults: '**/target/failsafe-reports/TEST-*.xml,**/target/surefire-reports/TEST-*.xml,**/target/jest-reports/TEST-*.xml'
}
}
}
mailIfStatusChanged(git.commitAuthorEmail)
}
@@ -107,7 +200,7 @@ String mainBranch
Maven setupMavenBuild() {
Maven mvn = new MavenWrapperInDocker(this, "scmmanager/java-build:11.0.6_10")
if (isMainBranch()) {
if (isMainBranch() || isReleaseBranch()) {
// Release starts javadoc, which takes very long, so do only for certain branches
mvn.additionalArgs += ' -DperformRelease'
// JDK8 is more strict, we should fix this before the next release. Right now, this is just not the focus, yet.
@@ -144,6 +237,14 @@ void analyzeWith(Maven mvn) {
}
}
boolean isReleaseBranch() {
return env.BRANCH_NAME.startsWith("release/");
}
String getReleaseVersion() {
return env.BRANCH_NAME.substring("release/".length());
}
boolean isMainBranch() {
return mainBranch.equals(env.BRANCH_NAME)
}

View File

@@ -27,7 +27,7 @@ for more information.
GmbH](https://www.scm-manager.org/scm-manager-2/scm-manager-2-gets-a-boost-by-cloudogu-gmbh/ "wikilink")
- \*\*2018-05-04\*\* - SCM-Manager 1.60 released
([download](http://www.scm-manager.org/download/ "wikilink") \|
[release notes](release-notes "wikilink"))
[release notes](release-notes.md "wikilink"))
- \*\*2018-04-11\*\* - SCM-Manager 1.59 released
[All news](http://www.scm-manager.org/news/ "wikilink")
@@ -42,59 +42,59 @@ for more information.
### Use SCM-Manager
- [Getting started](getting-started "wikilink")
- [Getting started](getting-started.md "wikilink")
- [Download latest
version](http://www.scm-manager.org/download/ "wikilink")
- [FAQ](faq "wikilink")
- [Upgrade SCM-Manager to a newer version](upgrade "wikilink")
- [FAQ](faq.md "wikilink")
- [Upgrade SCM-Manager to a newer version](upgrade.md "wikilink")
- [Download latest snapshot
release](download-snapshot-release "wikilink")
- [Download Archive](download-archive "wikilink")
- [Command line client](command-line-client "wikilink")
- [SCM-Server SSL](scm-server-ssl "wikilink")
release](download-snapshot-release.md "wikilink")
- [Download Archive](download-archive.md "wikilink")
- [Command line client](command-line-client.md "wikilink")
- [SCM-Server SSL](scm-server-ssl.md "wikilink")
- [ApplicationServer
(Tomcat/Glassfish/Jetty)](applicationserver "wikilink")
(Tomcat/Glassfish/Jetty)](applicationserver.md "wikilink")
- [Using SCM-Manager with Apache
mod\_proxy](apache/apache-mod_proxy "wikilink")
- [Using SCM-Manager with Nginx](nginx "wikilink")
mod\_proxy](apache/apache-mod_proxy.md "wikilink")
- [Using SCM-Manager with Nginx](nginx.md "wikilink")
- [Using SCM-Manager with ISS
(Helicon)](SCM-Manager%20on%20ISS%20Helicon "wikilink")
- [Permissions](https://bitbucket.org/sdorra/scm-manager/wiki/Permissions "wikilink")
(Helicon)](SCM-Manager%20on%20ISS%20Helicon.md "wikilink")
- [Permissions](Permissions.md "wikilink")
- [Plugins](http://plugins.scm-manager.org/scm-plugin-backend/page/index.html "wikilink")
- [Revision Control Plugin
Comparison](rv-plugin-comparison "wikilink")
Comparison](rv-plugin-comparison.md "wikilink")
- [Screenshots](http://www.scm-manager.org/screenshots/ "wikilink")
- [Mercurial Subrepositories](subrepositories "wikilink")
- [Unix Daemons and Windows Services](daemons "wikilink")
- [RPM and DEB packages](RPM%20and%20DEB%20packages "wikilink")
- [Mercurial Subrepositories](subrepositories.md "wikilink")
- [Unix Daemons and Windows Services](daemons.md "wikilink")
- [RPM and DEB packages](RPM%20and%20DEB%20packages.md "wikilink")
- [Build windows mercurial packages for
SCM-Manager](https://bitbucket.org/sdorra/build-win-hg-packages "wikilink")
### Plugin documentation
- [Active Directory Plugin](active-directory-plugin "wikilink")
- [Branch Write Protect Plugin](branchwp-plugin "wikilink")
- [Jenkins Plugin](jenkins-plugin "wikilink")
- [Jira Plugin](jira-plugin "wikilink")
- [Mail Plugin](mail-plugin "wikilink")
- [Path Write Protect Plugin](pathwp-plugin "wikilink")
- [Redmine Plugin](redmine-plugin "wikilink")
- [Scheduler Plugin](scheduler-plugin "wikilink")
- [Trac Plugin](trac-plugin "wikilink")
- [WebHook Plugin](webhook-plugin "wikilink")
- [Active Directory Plugin](active-directory-plugin.md "wikilink")
- [Branch Write Protect Plugin](branchwp-plugin.md "wikilink")
- [Jenkins Plugin](jenkins-plugin.md "wikilink")
- [Jira Plugin](jira-plugin.md "wikilink")
- [Mail Plugin](mail-plugin.md "wikilink")
- [Path Write Protect Plugin](pathwp-plugin.md "wikilink")
- [Redmine Plugin](redmine-plugin.md "wikilink")
- [Scheduler Plugin](scheduler-plugin.md "wikilink")
- [Trac Plugin](trac-plugin.md "wikilink")
- [WebHook Plugin](webhook-plugin.md "wikilink")
### Development
- [Building SCM-Manager from source](build-from-source "wikilink")
- [Java Client API](java-client-api "wikilink")
- [Building SCM-Manager from source](build-from-source.md "wikilink")
- [Java Client API](java-client-api.md "wikilink")
- [Code
Snippets](https://bitbucket.org/sdorra/scm-manager/wiki/code-snippets "wikilink")
- [Configuring Eclipse projects for
SCM-Manager](configure-eclipse "wikilink")
- [Plugin Descriptor](plugin-descriptor "wikilink")
- [ExtensionPoints](ExtensionPoints "wikilink")
- [How to create your own plugin](howto-create-a-plugin "wikilink")
- [Injection Objects](injectionObjects "wikilink")
SCM-Manager](configure-eclipse.md "wikilink")
- [Plugin Descriptor](plugin-descriptor.md "wikilink")
- [ExtensionPoints](ExtensionPoints.md "wikilink")
- [How to create your own plugin](howto-create-a-plugin.md "wikilink")
- [Injection Objects](injectionObjects.md "wikilink")
- [API
documentation](http://docs.scm-manager.org/apidocs/latest/ "wikilink")
- [WebService
@@ -103,24 +103,24 @@ for more information.
### SCM Manager 2
- [Configuration for Intellij
IDEA](v2/intellij-idea-configuration "wikilink")
IDEA](v2/intellij-idea-configuration.md "wikilink")
- [State of SCM-Manager 2
development](v2/State%20of%20SCM-Manager%202%20development "wikilink")
- [SCM v2 Test Cases](v2/SCMM-v2-Test-Cases "wikilink")
development](v2/State%20of%20SCM-Manager%202%20development.md "wikilink")
- [SCM v2 Test Cases](v2/SCMM-v2-Test-Cases.md "wikilink")
- [Table of decisions made during
development](v2/Decision-Table "wikilink")
- [Definition of done](Definition_of_done "wikilink")
- [Style Guide](v2/style-guide "wikilink")
- [Error Handling in REST, Java, UI](v2/error-handling "wikilink")
- [Create a new Plugin](v2/Create_a_new_Plugin "wikilink")
- [Migrate Plugin from v1](v2/Migrate_Plugin_from_v1 "wikilink")
- [Plugin Development](v2/Plugin_Development "wikilink")
- [i18n for Plugins](v2/i18n_for_Plugins "wikilink")
- [Extension Points](v2/Extension-Points "wikilink")
- [API changes](v2/API_changes "wikilink")
- [ui-components/ui-types](v2/UI:_Additions_or_Changes_to_ui-components_or_ui-types "wikilink")
- [Vulnerabilities](v2/vulnerabilities "wikilink")
- [Common pitfall](v2/Common_pitfall "wikilink")
- [Release process](v2/Release_process "wikilink")
- [Migration Wizard](v2/Migration-Wizard "wikilink")
- [Known Issues](v2/Known_Issues "wikilink")
development](v2/Decision-Table.md "wikilink")
- [Definition of done](Definition%20of%20done.md "wikilink")
- [Style Guide](v2/style-guide.md "wikilink")
- [Error Handling in REST, Java, UI](v2/error-handling.md "wikilink")
- [Create a new Plugin](v2/Create%20a%20new%20Plugin.md "wikilink")
- [Migrate Plugin from v1](v2/Migrate%20Plugin%20from%20v1.md "wikilink")
- [Plugin Development](v2/Plugin%20Development.md "wikilink")
- [i18n for Plugins](v2/i18n%20for%20Plugins.md "wikilink")
- [Extension Points](v2/Extension-Points.md "wikilink")
- [API changes](v2/API%20changes.md "wikilink")
- [ui-components/ui-types](v2/UI:%20Additions%20or%20Changes%20to%20ui-components%20or%20ui-types.md "wikilink")
- [Vulnerabilities](v2/vulnerabilities.md "wikilink")
- [Common pitfall](v2/Common%20pitfall.md "wikilink")
- [Release process](v2/Release%20process.md "wikilink")
- [Migration Wizard](v2/Migration-Wizard.md "wikilink")
- [Known Issues](v2/Known%20Issues.md "wikilink")

View File

@@ -50,13 +50,8 @@ import sonia.scm.plugin.PluginAnnotation;
//~--- JDK imports ------------------------------------------------------------
import java.io.Closeable;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.Writer;
import java.lang.annotation.Annotation;
@@ -89,11 +84,13 @@ import javax.tools.StandardLocation;
import javax.ws.rs.Path;
import javax.ws.rs.ext.Provider;
import javax.xml.XMLConstants;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
@@ -160,18 +157,6 @@ public final class ScmAnnotationProcessor extends AbstractProcessor {
return false;
}
private void close(Closeable closeable) {
if (closeable != null) {
try {
closeable.close();
} catch (IOException ex) {
printException("could not close closeable", ex);
}
}
}
private TypeElement findAnnotation(Set<? extends TypeElement> annotations,
Class<? extends Annotation> annotationClass) {
TypeElement annotation = null;
@@ -205,15 +190,12 @@ public final class ScmAnnotationProcessor extends AbstractProcessor {
private Document parseDocument(File file) {
Document doc = null;
InputStream input = null;
try {
DocumentBuilder builder =
DocumentBuilderFactory.newInstance().newDocumentBuilder();
DocumentBuilder builder = createDocumentBuilder();
if (file.exists()) {
input = new FileInputStream(file);
doc = builder.parse(input);
doc = builder.parse(file);
} else {
doc = builder.newDocument();
doc.appendChild(doc.createElement(EL_MODULE));
@@ -221,13 +203,17 @@ public final class ScmAnnotationProcessor extends AbstractProcessor {
} catch (ParserConfigurationException | SAXException | IOException
| DOMException ex) {
printException("could not parse document", ex);
} finally {
close(input);
}
return doc;
}
private DocumentBuilder createDocumentBuilder() throws ParserConfigurationException {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
return factory.newDocumentBuilder();
}
private String prepareArrayElement(Object obj) {
String v = obj.toString();
@@ -341,24 +327,25 @@ public final class ScmAnnotationProcessor extends AbstractProcessor {
private void writeDocument(Document doc, File file) {
Writer writer = null;
try {
file.getParentFile().mkdirs();
writer = new FileWriter(file);
Transformer transformer =
TransformerFactory.newInstance().newTransformer();
Transformer transformer = createTransformer();
transformer.setOutputProperty(OutputKeys.INDENT, PROPERTY_VALUE);
transformer.transform(new DOMSource(doc), new StreamResult(writer));
} catch (IOException | IllegalArgumentException | TransformerException ex) {
transformer.transform(new DOMSource(doc), new StreamResult(file));
} catch (IllegalArgumentException | TransformerException ex) {
printException("could not write document", ex);
} finally {
close(writer);
}
}
private Transformer createTransformer() throws TransformerConfigurationException {
TransformerFactory factory = TransformerFactory.newInstance();
factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
return factory.newTransformer();
}
private Map<String, String> getAttributesFromAnnotation(Element el,
TypeElement annotation) {

View File

@@ -97,6 +97,14 @@
<artifactId>maven-war-plugin</artifactId>
<configuration>
<failOnMissingWebXml>false</failOnMissingWebXml>
<!--
We need no war file.
We only use war packaging for the jetty plugin.
In order to avoid deploying huge useless artifacts,
we will create an empty war file.
This is useless too, but it should be small.
-->
<packagingExcludes>**</packagingExcludes>
</configuration>
</plugin>

View File

@@ -7,6 +7,10 @@ module.exports = api => {
require("@babel/preset-react"),
require("@babel/preset-typescript")
],
plugins: [require("@babel/plugin-proposal-class-properties"), require("@babel/plugin-proposal-optional-chaining")]
plugins: [
require("babel-plugin-styled-components"),
require("@babel/plugin-proposal-class-properties"),
require("@babel/plugin-proposal-optional-chaining")
]
};
};

View File

@@ -14,7 +14,8 @@
"@babel/preset-env": "^7.6.3",
"@babel/preset-flow": "^7.0.0",
"@babel/preset-react": "^7.6.3",
"@babel/preset-typescript": "^7.6.0"
"@babel/preset-typescript": "^7.6.0",
"babel-plugin-styled-components": "^1.10.7"
},
"publishConfig": {
"access": "public"

View File

@@ -115,6 +115,18 @@
</args>
</configuration>
</execution>
<execution>
<id>set-version</id>
<goals>
<goal>run</goal>
</goals>
<configuration>
<script>set-version</script>
<args>
<arg>${project.version}</arg>
</args>
</configuration>
</execution>
</executions>
</plugin>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,28 @@
import React from "react";
import { storiesOf } from "@storybook/react";
import DropDown from "./DropDown";
storiesOf("Forms|DropDown", module)
.add("Default", () => (
<DropDown
options={["en", "de", "es"]}
preselectedOption={"de"}
optionSelected={() => {
// nothing to do
}}
/>
))
.add("With Translation", () => (
<DropDown
optionValues={["hg2g", "dirk", "liff"]}
options={[
"The Hitchhiker's Guide to the Galaxy",
"Dirk Gentlys Holistic Detective Agency",
"The Meaning Of Liff"
]}
preselectedOption={"dirk"}
optionSelected={selection => {
// nothing to do
}}
/>
));

View File

@@ -6,25 +6,20 @@ type Props = {
optionValues?: string[];
optionSelected: (p: string) => void;
preselectedOption?: string;
className: string;
className?: string;
disabled?: boolean;
};
class DropDown extends React.Component<Props> {
render() {
const { options, optionValues, preselectedOption, className, disabled } = this.props;
if (preselectedOption && !options.includes(preselectedOption)) {
options.unshift(preselectedOption);
}
return (
<div className={classNames(className, "select", disabled ? "disabled" : "")}>
<select value={preselectedOption ? preselectedOption : ""} onChange={this.change} disabled={disabled}>
<option key={preselectedOption} />
{options.map((option, index) => {
const value = optionValues && optionValues[index] ? optionValues[index] : option;
return (
<option key={option} value={optionValues && optionValues[index] ? optionValues[index] : option}>
<option key={value} value={value} selected={value === preselectedOption}>
{option}
</option>
);

View File

@@ -0,0 +1,15 @@
import React from "react";
const MENU_COLLAPSED = "secondary-menu-collapsed";
export const MenuContext = React.createContext({
menuCollapsed: isMenuCollapsed(),
setMenuCollapsed: (collapsed: boolean) => {}
});
export function isMenuCollapsed() {
return localStorage.getItem(MENU_COLLAPSED) === "true";
}
export function storeMenuCollapsed(status: boolean) {
localStorage.setItem(MENU_COLLAPSED, String(status));
}

View File

@@ -10,6 +10,8 @@ type Props = {
label: string;
activeOnlyWhenExact?: boolean;
activeWhenMatch?: (route: any) => boolean;
collapsed?: boolean;
title?: string;
};
class NavLink extends React.Component<Props> {
@@ -23,7 +25,7 @@ class NavLink extends React.Component<Props> {
}
renderLink = (route: any) => {
const { to, icon, label } = this.props;
const { to, icon, label, collapsed, title } = this.props;
let showIcon = null;
if (icon) {
@@ -35,10 +37,13 @@ class NavLink extends React.Component<Props> {
}
return (
<li>
<Link className={this.isActive(route) ? "is-active" : ""} to={to}>
<li title={collapsed ? title : undefined}>
<Link
className={classNames(this.isActive(route) ? "is-active" : "", collapsed ? "has-text-centered" : "")}
to={to}
>
{showIcon}
{label}
{collapsed ? null : label}
</Link>
</li>
);

View File

@@ -0,0 +1,110 @@
import React, { FC, ReactElement, ReactNode, useContext, useEffect } from "react";
import styled from "styled-components";
import SubNavigation from "./SubNavigation";
import { matchPath, useLocation } from "react-router-dom";
import { isMenuCollapsed, MenuContext } from "./MenuContext";
type Props = {
label: string;
children: ReactElement[];
collapsed: boolean;
onCollapse?: (newStatus: boolean) => void;
};
type CollapsedProps = {
collapsed: boolean;
};
const SectionContainer = styled.aside<CollapsedProps>`
position: sticky;
position: -webkit-sticky; /* Safari */
top: 2rem;
width: ${props => (props.collapsed ? "5.5rem" : "20.5rem")};
`;
const Icon = styled.i<CollapsedProps>`
padding-left: ${(props: CollapsedProps) => (props.collapsed ? "0" : "0.5rem")};
padding-right: ${(props: CollapsedProps) => (props.collapsed ? "0" : "0.4rem")};
height: 1.5rem;
font-size: 24px;
margin-top: -0.75rem;
`;
const MenuLabel = styled.p<CollapsedProps>`
height: 3.2rem;
display: flex;
align-items: center;
justify-content: ${(props: CollapsedProps) => (props.collapsed ? "center" : "inherit")};
cursor: pointer;
`;
const SecondaryNavigation: FC<Props> = ({ label, children, collapsed, onCollapse }) => {
const location = useLocation();
const menuContext = useContext(MenuContext);
const subNavActive = isSubNavigationActive(children, location.pathname);
const isCollapsed = collapsed && !subNavActive;
useEffect(() => {
if (isMenuCollapsed()) {
menuContext.setMenuCollapsed(!subNavActive);
}
}, [subNavActive]);
const childrenWithProps = React.Children.map(children, (child: ReactElement) =>
React.cloneElement(child, { collapsed: isCollapsed })
);
const arrowIcon = isCollapsed ? <i className="fas fa-caret-down" /> : <i className="fas fa-caret-right" />;
return (
<SectionContainer className="menu" collapsed={isCollapsed}>
<div>
<MenuLabel
className="menu-label"
collapsed={isCollapsed}
onClick={onCollapse && !subNavActive ? () => onCollapse(!isCollapsed) : undefined}
>
{onCollapse && !subNavActive && (
<Icon color="info" className="is-medium" collapsed={isCollapsed}>
{arrowIcon}
</Icon>
)}
{isCollapsed ? "" : label}
</MenuLabel>
<ul className="menu-list">{childrenWithProps}</ul>
</div>
</SectionContainer>
);
};
const createParentPath = (to: string) => {
const parents = to.split("/");
parents.splice(-1, 1);
return parents.join("/");
};
const isSubNavigationActive = (children: ReactNode, url: string): boolean => {
const childArray = React.Children.toArray(children);
const match = childArray
.filter(child => {
// what about extension points?
// @ts-ignore
return child.type.name === SubNavigation.name;
})
.map(child => {
// @ts-ignore
return child.props;
})
.find(props => {
const path = createParentPath(props.to);
const matches = matchPath(url, {
path,
exact: props.activeOnlyWhenExact as boolean
});
return matches != null;
});
return match != null;
};
export default SecondaryNavigation;

View File

@@ -0,0 +1,45 @@
import React, { ReactElement, ReactNode } from "react";
import { MenuContext } from "./MenuContext";
import SubNavigation from "./SubNavigation";
import NavLink from "./NavLink";
type Props = {
to: string;
icon: string;
label: string;
title: string;
activeWhenMatch?: (route: any) => boolean;
activeOnlyWhenExact?: boolean;
children?: ReactElement[];
};
export default class SecondaryNavigationItem extends React.Component<Props> {
render() {
const { to, icon, label, title, activeWhenMatch, activeOnlyWhenExact, children } = this.props;
if (children) {
return (
<MenuContext.Consumer>
{({ menuCollapsed }) => (
<SubNavigation
to={to}
icon={icon}
label={label}
title={title}
activeWhenMatch={activeWhenMatch}
activeOnlyWhenExact={activeOnlyWhenExact}
collapsed={menuCollapsed}
>
{children}
</SubNavigation>
)}
</MenuContext.Consumer>
);
} else {
return (
<MenuContext.Consumer>
{({ menuCollapsed }) => <NavLink to={to} icon={icon} label={label} title={title} collapsed={menuCollapsed} />}
</MenuContext.Consumer>
);
}
}
}

View File

@@ -1,20 +0,0 @@
import React, { ReactNode } from "react";
type Props = {
label: string;
children?: ReactNode;
};
class Section extends React.Component<Props> {
render() {
const { label, children } = this.props;
return (
<div>
<p className="menu-label">{label}</p>
<ul className="menu-list">{children}</ul>
</div>
);
}
}
export default Section;

View File

@@ -1,5 +1,5 @@
import React, { ReactNode } from "react";
import { Link, Route } from "react-router-dom";
import React, { FC, ReactElement, useContext, useEffect } from "react";
import { Link, useRouteMatch } from "react-router-dom";
import classNames from "classnames";
type Props = {
@@ -8,52 +8,39 @@ type Props = {
label: string;
activeOnlyWhenExact?: boolean;
activeWhenMatch?: (route: any) => boolean;
children?: ReactNode;
children?: ReactElement[];
collapsed?: boolean;
title?: string;
};
class SubNavigation extends React.Component<Props> {
static defaultProps = {
activeOnlyWhenExact: false
};
const SubNavigation: FC<Props> = ({ to, activeOnlyWhenExact, icon, collapsed, title, label, children }) => {
const parents = to.split("/");
parents.splice(-1, 1);
const parent = parents.join("/");
isActive(route: any) {
const { activeWhenMatch } = this.props;
return route.match || (activeWhenMatch && activeWhenMatch(route));
}
renderLink = (route: any) => {
const { to, icon, label } = this.props;
const match = useRouteMatch({
path: parent,
exact: activeOnlyWhenExact
});
let defaultIcon = "fas fa-cog";
if (icon) {
defaultIcon = icon;
}
let children = null;
if (this.isActive(route)) {
children = <ul className="sub-menu">{this.props.children}</ul>;
let childrenList = null;
if (match && !collapsed) {
childrenList = <ul className="sub-menu">{children}</ul>;
}
return (
<li>
<Link className={this.isActive(route) ? "is-active" : ""} to={to}>
<i className={classNames(defaultIcon, "fa-fw")} /> {label}
<li title={collapsed ? title : undefined}>
<Link className={classNames(match != null ? "is-active" : "", collapsed ? "has-text-centered" : "")} to={to}>
<i className={classNames(defaultIcon, "fa-fw")} /> {collapsed ? "" : label}
</Link>
{children}
{childrenList}
</li>
);
};
render() {
const { to, activeOnlyWhenExact } = this.props;
// removes last part of url
const parents = to.split("/");
parents.splice(-1, 1);
const parent = parents.join("/");
return <Route path={parent} exact={activeOnlyWhenExact} children={this.renderLink} />;
}
}
export default SubNavigation;

View File

@@ -6,4 +6,6 @@ export { default as Navigation } from "./Navigation";
export { default as SubNavigation } from "./SubNavigation";
export { default as PrimaryNavigation } from "./PrimaryNavigation";
export { default as PrimaryNavigationLink } from "./PrimaryNavigationLink";
export { default as Section } from "./Section";
export { default as SecondaryNavigation } from "./SecondaryNavigation";
export { MenuContext, storeMenuCollapsed, isMenuCollapsed } from "./MenuContext";
export { default as SecondaryNavigationItem } from "./SecondaryNavigationItem";

View File

@@ -10,6 +10,8 @@ import Icon from "../Icon";
import { Change, ChangeEvent, DiffObjectProps, File, Hunk as HunkType } from "./DiffTypes";
import TokenizedDiffView from "./TokenizedDiffView";
import DiffButton from "./DiffButton";
import { MenuContext } from "@scm-manager/ui-components";
import { storeMenuCollapsed } from "../navigation";
const EMPTY_ANNOTATION_FACTORY = {};
@@ -100,10 +102,14 @@ class DiffFile extends React.Component<Props, State> {
}
};
toggleSideBySide = () => {
this.setState(state => ({
toggleSideBySide = (callback: () => void) => {
this.setState(
state => ({
sideBySide: !state.sideBySide
}));
}),
() => callback()
);
storeMenuCollapsed(true);
};
setCollapse = (collapsed: boolean) => {
@@ -259,11 +265,15 @@ class DiffFile extends React.Component<Props, State> {
file.hunks && file.hunks.length > 0 ? (
<ButtonWrapper className={classNames("level-right", "is-flex")}>
<ButtonGroup>
<MenuContext.Consumer>
{({ setMenuCollapsed }) => (
<DiffButton
icon={sideBySide ? "align-left" : "columns"}
tooltip={t(sideBySide ? "diff.combined" : "diff.sideBySide")}
onClick={this.toggleSideBySide}
onClick={() => this.toggleSideBySide(() => setMenuCollapsed(true))}
/>
)}
</MenuContext.Consumer>
{fileControls}
</ButtonGroup>
</ButtonWrapper>

View File

@@ -1,7 +1,7 @@
{
"admin": {
"menu": {
"navigationLabel": "Administrations Navigation",
"navigationLabel": "Administration",
"informationNavLink": "Informationen",
"settingsNavLink": "Einstellungen",
"generalNavLink": "Generell"

View File

@@ -61,7 +61,7 @@
"previous": "Zurück"
},
"profile": {
"navigationLabel": "Profil Navigation",
"navigationLabel": "Profil",
"informationNavLink": "Information",
"changePasswordNavLink": "Passwort ändern",
"settingsNavLink": "Einstellungen",

View File

@@ -1,6 +1,6 @@
{
"config": {
"navigationLabel": "Administrations Navigation",
"navigationLabel": "Administration",
"title": "Globale Einstellungen",
"errorTitle": "Fehler",
"errorSubtitle": "Unbekannter Einstellungen Fehler",

View File

@@ -18,7 +18,7 @@
"errorTitle": "Fehler",
"errorSubtitle": "Unbekannter Gruppen Fehler",
"menu": {
"navigationLabel": "Gruppen Navigation",
"navigationLabel": "Gruppen",
"informationNavLink": "Informationen",
"settingsNavLink": "Einstellungen",
"generalNavLink": "Generell",

View File

@@ -28,7 +28,7 @@
"errorTitle": "Fehler",
"errorSubtitle": "Unbekannter Repository Fehler",
"menu": {
"navigationLabel": "Repository Navigation",
"navigationLabel": "Repository",
"informationNavLink": "Informationen",
"branchesNavLink": "Branches",
"sourcesNavLink": "Code",

View File

@@ -32,7 +32,7 @@
"errorTitle": "Fehler",
"errorSubtitle": "Unbekannter Benutzer Fehler",
"menu": {
"navigationLabel": "Benutzer Navigation",
"navigationLabel": "Benutzer",
"informationNavLink": "Informationen",
"settingsNavLink": "Einstellungen",
"generalNavLink": "Generell",

View File

@@ -1,7 +1,7 @@
{
"admin": {
"menu": {
"navigationLabel": "Administration Navigation",
"navigationLabel": "Administration",
"informationNavLink": "Information",
"settingsNavLink": "Settings",
"generalNavLink": "General"

View File

@@ -62,7 +62,7 @@
"previous": "Previous"
},
"profile": {
"navigationLabel": "Profile Navigation",
"navigationLabel": "Profile",
"informationNavLink": "Information",
"changePasswordNavLink": "Change password",
"settingsNavLink": "Settings",

View File

@@ -1,6 +1,6 @@
{
"config": {
"navigationLabel": "Administration Navigation",
"navigationLabel": "Administration",
"title": "Global Configuration",
"errorTitle": "Error",
"errorSubtitle": "Unknown Config Error",

View File

@@ -18,7 +18,7 @@
"errorTitle": "Error",
"errorSubtitle": "Unknown group error",
"menu": {
"navigationLabel": "Group Navigation",
"navigationLabel": "Group",
"informationNavLink": "Information",
"settingsNavLink": "Settings",
"generalNavLink": "General",

View File

@@ -28,7 +28,7 @@
"errorTitle": "Error",
"errorSubtitle": "Unknown repository error",
"menu": {
"navigationLabel": "Repository Navigation",
"navigationLabel": "Repository",
"informationNavLink": "Information",
"branchesNavLink": "Branches",
"sourcesNavLink": "Code",

View File

@@ -32,7 +32,7 @@
"errorTitle": "Error",
"errorSubtitle": "Unknown user error",
"menu": {
"navigationLabel": "User Navigation",
"navigationLabel": "User",
"informationNavLink": "Information",
"settingsNavLink": "Settings",
"generalNavLink": "General",

View File

@@ -1,7 +1,7 @@
{
"admin": {
"menu": {
"navigationLabel": "Menú de administración",
"navigationLabel": "Administración",
"informationNavLink": "Información",
"settingsNavLink": "Ajustes",
"generalNavLink": "General"

View File

@@ -62,7 +62,7 @@
"previous": "Anterior"
},
"profile": {
"navigationLabel": "Menú de sección",
"navigationLabel": "Sección",
"informationNavLink": "Información",
"changePasswordNavLink": "Cambiar contraseña",
"settingsNavLink": "Ajustes",

View File

@@ -1,6 +1,6 @@
{
"config": {
"navigationLabel": "Menú de administración",
"navigationLabel": "Administración",
"title": "Configuración global",
"errorTitle": "Error",
"errorSubtitle": "Error de configuración desconocido",

View File

@@ -18,7 +18,7 @@
"errorTitle": "Error",
"errorSubtitle": "Error de grupo desconocido",
"menu": {
"navigationLabel": "Menú de grupo",
"navigationLabel": "Grupo",
"informationNavLink": "Información",
"settingsNavLink": "Ajustes",
"generalNavLink": "General",

View File

@@ -28,7 +28,7 @@
"errorTitle": "Error",
"errorSubtitle": "Error de repositorio desconocido",
"menu": {
"navigationLabel": "Menú de repositorio",
"navigationLabel": "Repositorio",
"informationNavLink": "Información",
"branchesNavLink": "Ramas",
"sourcesNavLink": "Código",

View File

@@ -32,7 +32,7 @@
"errorTitle": "Error",
"errorSubtitle": "Error de usuario desconocido",
"menu": {
"navigationLabel": "Menú de usuario",
"navigationLabel": "Usuario",
"informationNavLink": "Información",
"settingsNavLink": "Ajustes",
"generalNavLink": "General",

View File

@@ -2,11 +2,18 @@ import React from "react";
import { connect } from "react-redux";
import { compose } from "redux";
import { WithTranslation, withTranslation } from "react-i18next";
import { Redirect, Route, Switch } from "react-router-dom";
import { History } from "history";
import { Redirect, Route, RouteComponentProps, Switch } from "react-router-dom";
import { ExtensionPoint } from "@scm-manager/ui-extensions";
import { Links } from "@scm-manager/ui-types";
import { Navigation, NavLink, Page, Section, SubNavigation } from "@scm-manager/ui-components";
import {
NavLink,
Page,
SecondaryNavigation,
SubNavigation,
isMenuCollapsed,
MenuContext,
storeMenuCollapsed
} from "@scm-manager/ui-components";
import { getAvailablePluginsLink, getInstalledPluginsLink, getLinks } from "../../modules/indexResource";
import AdminDetails from "./AdminDetails";
import PluginsOverview from "../plugins/containers/PluginsOverview";
@@ -15,17 +22,30 @@ import RepositoryRoles from "../roles/containers/RepositoryRoles";
import SingleRepositoryRole from "../roles/containers/SingleRepositoryRole";
import CreateRepositoryRole from "../roles/containers/CreateRepositoryRole";
type Props = WithTranslation & {
type Props = RouteComponentProps &
WithTranslation & {
links: Links;
availablePluginsLink: string;
installedPluginsLink: string;
// context objects
match: any;
history: History;
};
class Admin extends React.Component<Props> {
type State = {
menuCollapsed: boolean;
};
class Admin extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
menuCollapsed: isMenuCollapsed()
};
}
onCollapseAdminMenu = (collapsed: boolean) => {
this.setState({ menuCollapsed: collapsed }, () => storeMenuCollapsed(collapsed));
};
stripEndingSlash = (url: string) => {
if (url.endsWith("/")) {
if (url.includes("role")) {
@@ -48,6 +68,7 @@ class Admin extends React.Component<Props> {
render() {
const { links, availablePluginsLink, installedPluginsLink, t } = this.props;
const { menuCollapsed } = this.state;
const url = this.matchedUrl();
const extensionProps = {
@@ -56,9 +77,12 @@ class Admin extends React.Component<Props> {
};
return (
<MenuContext.Provider
value={{ menuCollapsed, setMenuCollapsed: (collapsed: boolean) => this.setState({ menuCollapsed: collapsed }) }}
>
<Page>
<div className="columns">
<div className="column is-three-quarters">
<div className="column">
<Switch>
<Redirect exact from={url} to={`${url}/info`} />
<Route path={`${url}/info`} exact component={AdminDetails} />
@@ -97,15 +121,24 @@ class Admin extends React.Component<Props> {
<ExtensionPoint name="admin.route" props={extensionProps} renderAll={true} />
</Switch>
</div>
<div className="column is-one-quarter">
<Navigation>
<Section label={t("admin.menu.navigationLabel")}>
<NavLink to={`${url}/info`} icon="fas fa-info-circle" label={t("admin.menu.informationNavLink")} />
<div className={menuCollapsed ? "column is-1" : "column is-3"}>
<SecondaryNavigation
label={t("admin.menu.navigationLabel")}
onCollapse={() => this.onCollapseAdminMenu(!menuCollapsed)}
collapsed={menuCollapsed}
>
<NavLink
to={`${url}/info`}
icon="fas fa-info-circle"
label={t("admin.menu.informationNavLink")}
title={t("admin.menu.informationNavLink")}
/>
{(availablePluginsLink || installedPluginsLink) && (
<SubNavigation
to={`${url}/plugins/`}
icon="fas fa-puzzle-piece"
label={t("plugins.menu.pluginsNavLink")}
title={t("plugins.menu.pluginsNavLink")}
>
{installedPluginsLink && (
<NavLink to={`${url}/plugins/installed/`} label={t("plugins.menu.installedNavLink")} />
@@ -119,19 +152,24 @@ class Admin extends React.Component<Props> {
to={`${url}/roles/`}
icon="fas fa-user-shield"
label={t("repositoryRole.navLink")}
title={t("repositoryRole.navLink")}
activeWhenMatch={this.matchesRoles}
activeOnlyWhenExact={false}
/>
<ExtensionPoint name="admin.navigation" props={extensionProps} renderAll={true} />
<SubNavigation to={`${url}/settings/general`} label={t("admin.menu.settingsNavLink")}>
<SubNavigation
to={`${url}/settings/general`}
label={t("admin.menu.settingsNavLink")}
title={t("admin.menu.settingsNavLink")}
>
<NavLink to={`${url}/settings/general`} label={t("admin.menu.generalNavLink")} />
<ExtensionPoint name="admin.setting" props={extensionProps} renderAll={true} />
</SubNavigation>
</Section>
</Navigation>
</SecondaryNavigation>
</div>
</div>
</Page>
</MenuContext.Provider>
);
}
}

View File

@@ -1,24 +1,49 @@
import React from "react";
import { Route, withRouter } from "react-router-dom";
import { Route, RouteComponentProps, withRouter } from "react-router-dom";
import { getMe } from "../modules/auth";
import { compose } from "redux";
import { connect } from "react-redux";
import { WithTranslation, withTranslation } from "react-i18next";
import { Me } from "@scm-manager/ui-types";
import { ErrorPage, Navigation, NavLink, Page, Section, SubNavigation } from "@scm-manager/ui-components";
import {
ErrorPage,
isMenuCollapsed,
MenuContext,
NavLink,
Page,
SecondaryNavigation,
SubNavigation
} from "@scm-manager/ui-components";
import ChangeUserPassword from "./ChangeUserPassword";
import ProfileInfo from "./ProfileInfo";
import { ExtensionPoint } from "@scm-manager/ui-extensions";
import { storeMenuCollapsed } from "@scm-manager/ui-components/src";
type Props = WithTranslation & {
type Props = RouteComponentProps &
WithTranslation & {
me: Me;
// Context props
match: any;
};
type State = {};
type State = {
menuCollapsed: boolean;
};
class Profile extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
menuCollapsed: isMenuCollapsed()
};
}
onCollapseProfileMenu = (collapsed: boolean) => {
this.setState({ menuCollapsed: collapsed }, () => storeMenuCollapsed(collapsed));
};
stripEndingSlash = (url: string) => {
if (url.endsWith("/")) {
return url.substring(0, url.length - 2);
@@ -34,6 +59,7 @@ class Profile extends React.Component<Props, State> {
const url = this.matchedUrl();
const { me, t } = this.props;
const { menuCollapsed } = this.state;
if (!me) {
return (
@@ -54,26 +80,41 @@ class Profile extends React.Component<Props, State> {
};
return (
<MenuContext.Provider
value={{ menuCollapsed, setMenuCollapsed: (collapsed: boolean) => this.setState({ menuCollapsed: collapsed }) }}
>
<Page title={me.displayName}>
<div className="columns">
<div className="column is-three-quarters">
<div className="column">
<Route path={url} exact render={() => <ProfileInfo me={me} />} />
<Route path={`${url}/settings/password`} render={() => <ChangeUserPassword me={me} />} />
<ExtensionPoint name="profile.route" props={extensionProps} renderAll={true} />
</div>
<div className="column">
<Navigation>
<Section label={t("profile.navigationLabel")}>
<NavLink to={`${url}`} icon="fas fa-info-circle" label={t("profile.informationNavLink")} />
<SubNavigation to={`${url}/settings/password`} label={t("profile.settingsNavLink")}>
<div className={menuCollapsed ? "column is-1" : "column is-3"}>
<SecondaryNavigation
label={t("profile.navigationLabel")}
onCollapse={() => this.onCollapseProfileMenu(!menuCollapsed)}
collapsed={menuCollapsed}
>
<NavLink
to={`${url}`}
icon="fas fa-info-circle"
label={t("profile.informationNavLink")}
title={t("profile.informationNavLink")}
/>
<SubNavigation
to={`${url}/settings/password`}
label={t("profile.settingsNavLink")}
title={t("profile.settingsNavLink")}
>
<NavLink to={`${url}/settings/password`} label={t("profile.changePasswordNavLink")} />
<ExtensionPoint name="profile.setting" props={extensionProps} renderAll={true} />
</SubNavigation>
</Section>
</Navigation>
</SecondaryNavigation>
</div>
</div>
</Page>
</MenuContext.Provider>
);
}
}

View File

@@ -1,19 +1,29 @@
import React from "react";
import { connect } from "react-redux";
import { Route } from "react-router-dom";
import { Route, RouteComponentProps } from "react-router-dom";
import { WithTranslation, withTranslation } from "react-i18next";
import { History } from "history";
import { ExtensionPoint } from "@scm-manager/ui-extensions";
import { Group } from "@scm-manager/ui-types";
import { ErrorPage, Loading, Navigation, NavLink, Page, Section, SubNavigation } from "@scm-manager/ui-components";
import {
ErrorPage,
isMenuCollapsed,
Loading,
MenuContext,
NavLink,
Page,
SecondaryNavigation,
SubNavigation
} from "@scm-manager/ui-components";
import { getGroupsLink } from "../../modules/indexResource";
import { fetchGroupByName, getFetchGroupFailure, getGroupByName, isFetchGroupPending } from "../modules/groups";
import { Details } from "./../components/table";
import { EditGroupNavLink, SetPermissionsNavLink } from "./../components/navLinks";
import EditGroup from "./EditGroup";
import SetPermissions from "../../permissions/components/SetPermissions";
import { storeMenuCollapsed } from "@scm-manager/ui-components/src";
type Props = WithTranslation & {
type Props = RouteComponentProps &
WithTranslation & {
name: string;
group: Group;
loading: boolean;
@@ -22,17 +32,29 @@ type Props = WithTranslation & {
// dispatcher functions
fetchGroupByName: (p1: string, p2: string) => void;
// context objects
match: any;
history: History;
};
class SingleGroup extends React.Component<Props> {
type State = {
menuCollapsed: boolean;
};
class SingleGroup extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
menuCollapsed: isMenuCollapsed()
};
}
componentDidMount() {
this.props.fetchGroupByName(this.props.groupLink, this.props.name);
}
onCollapseGroupMenu = (collapsed: boolean) => {
this.setState({ menuCollapsed: collapsed }, () => storeMenuCollapsed(collapsed));
};
stripEndingSlash = (url: string) => {
if (url.endsWith("/")) {
return url.substring(0, url.length - 2);
@@ -46,6 +68,7 @@ class SingleGroup extends React.Component<Props> {
render() {
const { t, loading, error, group } = this.props;
const { menuCollapsed } = this.state;
if (error) {
return <ErrorPage title={t("singleGroup.errorTitle")} subtitle={t("singleGroup.errorSubtitle")} error={error} />;
@@ -63,9 +86,12 @@ class SingleGroup extends React.Component<Props> {
};
return (
<MenuContext.Provider
value={{ menuCollapsed, setMenuCollapsed: (collapsed: boolean) => this.setState({ menuCollapsed: collapsed }) }}
>
<Page title={group.name}>
<div className="columns">
<div className="column is-three-quarters">
<div className="column">
<Route path={url} exact component={() => <Details group={group} />} />
<Route path={`${url}/settings/general`} exact component={() => <EditGroup group={group} />} />
<Route
@@ -75,21 +101,33 @@ class SingleGroup extends React.Component<Props> {
/>
<ExtensionPoint name="group.route" props={extensionProps} renderAll={true} />
</div>
<div className="column">
<Navigation>
<Section label={t("singleGroup.menu.navigationLabel")}>
<NavLink to={`${url}`} icon="fas fa-info-circle" label={t("singleGroup.menu.informationNavLink")} />
<div className={menuCollapsed ? "column is-1" : "column is-3"}>
<SecondaryNavigation
label={t("singleGroup.menu.navigationLabel")}
onCollapse={() => this.onCollapseGroupMenu(!menuCollapsed)}
collapsed={menuCollapsed}
>
<NavLink
to={`${url}`}
icon="fas fa-info-circle"
label={t("singleGroup.menu.informationNavLink")}
title={t("singleGroup.menu.informationNavLink")}
/>
<ExtensionPoint name="group.navigation" props={extensionProps} renderAll={true} />
<SubNavigation to={`${url}/settings/general`} label={t("singleGroup.menu.settingsNavLink")}>
<SubNavigation
to={`${url}/settings/general`}
label={t("singleGroup.menu.settingsNavLink")}
title={t("singleGroup.menu.settingsNavLink")}
>
<EditGroupNavLink group={group} editUrl={`${url}/settings/general`} />
<SetPermissionsNavLink group={group} permissionsUrl={`${url}/settings/permissions`} />
<ExtensionPoint name="group.setting" props={extensionProps} renderAll={true} />
</SubNavigation>
</Section>
</Navigation>
</SecondaryNavigation>
</div>
</div>
</Page>
</MenuContext.Provider>
);
}
}

View File

@@ -10,6 +10,7 @@ type Props = {
activeWhenMatch?: (route: any) => boolean;
activeOnlyWhenExact: boolean;
icon?: string;
title?: string;
};
/**

View File

@@ -1,7 +1,7 @@
import React from "react";
import { connect } from "react-redux";
import { compose } from "redux";
import { withRouter } from "react-router-dom";
import { withRouter, RouteComponentProps } from "react-router-dom";
import { WithTranslation, withTranslation } from "react-i18next";
import { Branch, Changeset, PagedCollection, Repository } from "@scm-manager/ui-types";
import {
@@ -20,7 +20,8 @@ import {
selectListAsCollection
} from "../modules/changesets";
type Props = WithTranslation & {
type Props = RouteComponentProps &
WithTranslation & {
repository: Repository;
branch: Branch;
page: number;
@@ -33,9 +34,6 @@ type Props = WithTranslation & {
// Dispatch props
fetchChangesets: (p1: Repository, p2: Branch, p3: number) => void;
// context props
match: any;
};
class Changesets extends React.Component<Props> {
@@ -44,6 +42,10 @@ class Changesets extends React.Component<Props> {
fetchChangesets(repository, branch, page);
}
shouldComponentUpdate(nextProps: Readonly<Props>): boolean {
return this.props.changesets !== nextProps.changesets;
}
render() {
const { changesets, loading, error, t } = this.props;

View File

@@ -1,11 +1,20 @@
import React from "react";
import { connect } from "react-redux";
import { Redirect, Route, Switch } from "react-router-dom";
import { Redirect, Route, Switch, RouteComponentProps } from "react-router-dom";
import { WithTranslation, withTranslation } from "react-i18next";
import { History } from "history";
import { binder, ExtensionPoint } from "@scm-manager/ui-extensions";
import { Repository } from "@scm-manager/ui-types";
import { ErrorPage, Loading, Navigation, NavLink, Page, Section, SubNavigation } from "@scm-manager/ui-components";
import {
ErrorPage,
Loading,
NavLink,
Page,
SecondaryNavigation,
SubNavigation,
MenuContext,
storeMenuCollapsed,
isMenuCollapsed
} from "@scm-manager/ui-components";
import { fetchRepoByName, getFetchRepoFailure, getRepository, isFetchRepoPending } from "../modules/repos";
import RepositoryDetails from "../components/RepositoryDetails";
import EditRepo from "./EditRepo";
@@ -21,7 +30,8 @@ import CodeOverview from "../codeSection/containers/CodeOverview";
import ChangesetView from "./ChangesetView";
import SourceExtensions from "../sources/containers/SourceExtensions";
type Props = WithTranslation & {
type Props = RouteComponentProps &
WithTranslation & {
namespace: string;
name: string;
repository: Repository;
@@ -32,13 +42,21 @@ type Props = WithTranslation & {
// dispatch functions
fetchRepoByName: (link: string, namespace: string, name: string) => void;
// context props
history: History;
match: any;
};
class RepositoryRoot extends React.Component<Props> {
type State = {
menuCollapsed: boolean;
};
class RepositoryRoot extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
menuCollapsed: isMenuCollapsed()
};
}
componentDidMount() {
const { fetchRepoByName, namespace, name, repoLink } = this.props;
fetchRepoByName(repoLink, namespace, name);
@@ -87,8 +105,13 @@ class RepositoryRoot extends React.Component<Props> {
return `${url}/changesets`;
};
onCollapseRepositoryMenu = (collapsed: boolean) => {
this.setState({ menuCollapsed: collapsed }, () => storeMenuCollapsed(collapsed));
};
render() {
const { loading, error, indexLinks, repository, t } = this.props;
const { menuCollapsed } = this.state;
if (error) {
return (
@@ -117,9 +140,15 @@ class RepositoryRoot extends React.Component<Props> {
}
return (
<MenuContext.Provider
value={{
menuCollapsed,
setMenuCollapsed: (collapsed: boolean) => this.setState({ menuCollapsed: collapsed })
}}
>
<Page title={repository.namespace + "/" + repository.name}>
<div className="columns">
<div className="column is-three-quarters">
<div className="column">
<Switch>
<Redirect exact from={this.props.match.url} to={redirectedUrl} />
@@ -169,14 +198,18 @@ class RepositoryRoot extends React.Component<Props> {
<ExtensionPoint name="repository.route" props={extensionProps} renderAll={true} />
</Switch>
</div>
<div className="column">
<Navigation>
<Section label={t("repositoryRoot.menu.navigationLabel")}>
<div className={menuCollapsed ? "column is-1" : "column is-3"}>
<SecondaryNavigation
label={t("repositoryRoot.menu.navigationLabel")}
onCollapse={() => this.onCollapseRepositoryMenu(!menuCollapsed)}
collapsed={menuCollapsed}
>
<ExtensionPoint name="repository.navigation.topLevel" props={extensionProps} renderAll={true} />
<NavLink
to={`${url}/info`}
icon="fas fa-info-circle"
label={t("repositoryRoot.menu.informationNavLink")}
title={t("repositoryRoot.menu.informationNavLink")}
/>
<RepositoryNavLink
repository={repository}
@@ -186,6 +219,7 @@ class RepositoryRoot extends React.Component<Props> {
label={t("repositoryRoot.menu.branchesNavLink")}
activeWhenMatch={this.matchesBranches}
activeOnlyWhenExact={false}
title={t("repositoryRoot.menu.branchesNavLink")}
/>
<RepositoryNavLink
repository={repository}
@@ -195,18 +229,23 @@ class RepositoryRoot extends React.Component<Props> {
label={t("repositoryRoot.menu.sourcesNavLink")}
activeWhenMatch={this.matchesCode}
activeOnlyWhenExact={false}
title={t("repositoryRoot.menu.sourcesNavLink")}
/>
<ExtensionPoint name="repository.navigation" props={extensionProps} renderAll={true} />
<SubNavigation to={`${url}/settings/general`} label={t("repositoryRoot.menu.settingsNavLink")}>
<SubNavigation
to={`${url}/settings/general`}
label={t("repositoryRoot.menu.settingsNavLink")}
title={t("repositoryRoot.menu.settingsNavLink")}
>
<EditRepoNavLink repository={repository} editUrl={`${url}/settings/general`} />
<PermissionsNavLink permissionUrl={`${url}/settings/permissions`} repository={repository} />
<ExtensionPoint name="repository.setting" props={extensionProps} renderAll={true} />
</SubNavigation>
</Section>
</Navigation>
</SecondaryNavigation>
</div>
</div>
</Page>
</MenuContext.Provider>
);
}
}

View File

@@ -581,6 +581,31 @@ describe("changesets", () => {
]);
});
it("should return always the same changeset array for the given parameters", () => {
const state = {
changesets: {
"foo/bar": {
byId: {
id2: {
id: "id2"
},
id1: {
id: "id1"
}
},
byBranch: {
"": {
entries: ["id1", "id2"]
}
}
}
}
};
const one = getChangesets(state, repository);
const two = getChangesets(state, repository);
expect(one).toBe(two);
});
it("should return true, when fetching changesets is pending", () => {
const state = {
pending: {
@@ -639,5 +664,14 @@ describe("changesets", () => {
expect(collection.page).toBe(1);
expect(collection.pageTotal).toBe(10);
});
it("should return always the same empty object", () => {
const state = {
changesets: {}
};
const one = selectListAsCollection(state, repository);
const two = selectListAsCollection(state, repository);
expect(one).toBe(two);
});
});
});

View File

@@ -3,6 +3,7 @@ import { apiClient, urls } from "@scm-manager/ui-components";
import { isPending } from "../../modules/pending";
import { getFailure } from "../../modules/failure";
import { Action, Branch, PagedCollection, Repository } from "@scm-manager/ui-types";
import memoizeOne from "memoize-one";
export const FETCH_CHANGESETS = "scm/repos/FETCH_CHANGESETS";
export const FETCH_CHANGESETS_PENDING = `${FETCH_CHANGESETS}_${PENDING_SUFFIX}`;
@@ -254,10 +255,15 @@ export function getChangesets(state: object, repository: Repository, branch?: Br
return null;
}
return collectChangesets(stateRoot, changesets);
}
const mapChangesets = (stateRoot, changesets) => {
return changesets.entries.map((id: string) => {
return stateRoot.byId[id];
});
}
};
const collectChangesets = memoizeOne(mapChangesets);
export function getChangeset(state: object, repository: Repository, id: string) {
const key = createItemId(repository);
@@ -291,6 +297,8 @@ export function getFetchChangesetsFailure(state: object, repository: Repository,
return getFailure(state, FETCH_CHANGESETS, createItemId(repository, branch));
}
const EMPTY = {};
const selectList = (state: object, repository: Repository, branch?: Branch) => {
const repoId = createItemId(repository);
@@ -302,7 +310,7 @@ const selectList = (state: object, repository: Repository, branch?: Branch) => {
return repoState.byBranch[branchName];
}
}
return {};
return EMPTY;
};
const selectListEntry = (state: object, repository: Repository, branch?: Branch): object => {
@@ -310,7 +318,7 @@ const selectListEntry = (state: object, repository: Repository, branch?: Branch)
if (list.entry) {
return list.entry;
}
return {};
return EMPTY;
};
export const selectListAsCollection = (state: object, repository: Repository, branch?: Branch): PagedCollection => {

View File

@@ -3,6 +3,7 @@ import * as types from "../../modules/types";
import { Action, Repository, RepositoryCollection } from "@scm-manager/ui-types";
import { isPending } from "../../modules/pending";
import { getFailure } from "../../modules/failure";
import React from "react";
export const FETCH_REPOS = "scm/repos/FETCH_REPOS";
export const FETCH_REPOS_PENDING = `${FETCH_REPOS}_${types.PENDING_SUFFIX}`;
@@ -155,7 +156,12 @@ export function fetchRepoFailure(namespace: string, name: string, error: Error):
// create repo
export function createRepo(link: string, repository: Repository, initRepository: boolean, callback?: (repo: Repository) => void) {
export function createRepo(
link: string,
repository: Repository,
initRepository: boolean,
callback?: (repo: Repository) => void
) {
return function(dispatch: any) {
dispatch(createRepoPending());
const repoLink = initRepository ? link + "?initialize=true" : link;

View File

@@ -1,10 +1,18 @@
import React from "react";
import { connect } from "react-redux";
import { Route } from "react-router-dom";
import { History } from "history";
import { Route, RouteComponentProps } from "react-router-dom";
import { ExtensionPoint } from "@scm-manager/ui-extensions";
import { User } from "@scm-manager/ui-types";
import { ErrorPage, Loading, Navigation, NavLink, Page, Section, SubNavigation } from "@scm-manager/ui-components";
import {
ErrorPage,
isMenuCollapsed,
Loading,
MenuContext,
NavLink,
Page,
SecondaryNavigation,
SubNavigation
} from "@scm-manager/ui-components";
import { Details } from "./../components/table";
import EditUser from "./EditUser";
import { fetchUserByName, getFetchUserFailure, getUserByName, isFetchUserPending } from "../modules/users";
@@ -13,8 +21,10 @@ import { WithTranslation, withTranslation } from "react-i18next";
import { getUsersLink } from "../../modules/indexResource";
import SetUserPassword from "../components/SetUserPassword";
import SetPermissions from "../../permissions/components/SetPermissions";
import { storeMenuCollapsed } from "@scm-manager/ui-components/src";
type Props = WithTranslation & {
type Props = RouteComponentProps &
WithTranslation & {
name: string;
user: User;
loading: boolean;
@@ -23,13 +33,21 @@ type Props = WithTranslation & {
// dispatcher function
fetchUserByName: (p1: string, p2: string) => void;
// context objects
match: any;
history: History;
};
class SingleUser extends React.Component<Props> {
type State = {
menuCollapsed: boolean;
};
class SingleUser extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
menuCollapsed: isMenuCollapsed()
};
}
componentDidMount() {
this.props.fetchUserByName(this.props.usersLink, this.props.name);
}
@@ -41,12 +59,17 @@ class SingleUser extends React.Component<Props> {
return url;
};
onCollapseUserMenu = (collapsed: boolean) => {
this.setState({ menuCollapsed: collapsed }, () => storeMenuCollapsed(collapsed));
};
matchedUrl = () => {
return this.stripEndingSlash(this.props.match.url);
};
render() {
const { t, loading, error, user } = this.props;
const { menuCollapsed } = this.state;
if (error) {
return <ErrorPage title={t("singleUser.errorTitle")} subtitle={t("singleUser.errorSubtitle")} error={error} />;
@@ -64,9 +87,12 @@ class SingleUser extends React.Component<Props> {
};
return (
<MenuContext.Provider
value={{ menuCollapsed, setMenuCollapsed: (collapsed: boolean) => this.setState({ menuCollapsed: collapsed }) }}
>
<Page title={user.displayName}>
<div className="columns">
<div className="column is-three-quarters">
<div className="column">
<Route path={url} exact component={() => <Details user={user} />} />
<Route path={`${url}/settings/general`} component={() => <EditUser user={user} />} />
<Route path={`${url}/settings/password`} component={() => <SetUserPassword user={user} />} />
@@ -76,21 +102,33 @@ class SingleUser extends React.Component<Props> {
/>
<ExtensionPoint name="user.route" props={extensionProps} renderAll={true} />
</div>
<div className="column">
<Navigation>
<Section label={t("singleUser.menu.navigationLabel")}>
<NavLink to={`${url}`} icon="fas fa-info-circle" label={t("singleUser.menu.informationNavLink")} />
<SubNavigation to={`${url}/settings/general`} label={t("singleUser.menu.settingsNavLink")}>
<div className={menuCollapsed ? "column is-1" : "column is-3"}>
<SecondaryNavigation
label={t("singleUser.menu.navigationLabel")}
onCollapse={() => this.onCollapseUserMenu(!menuCollapsed)}
collapsed={menuCollapsed}
>
<NavLink
to={`${url}`}
icon="fas fa-info-circle"
label={t("singleUser.menu.informationNavLink")}
title={t("singleUser.menu.informationNavLink")}
/>
<SubNavigation
to={`${url}/settings/general`}
label={t("singleUser.menu.settingsNavLink")}
title={t("singleUser.menu.settingsNavLink")}
>
<EditUserNavLink user={user} editUrl={`${url}/settings/general`} />
<SetPasswordNavLink user={user} passwordUrl={`${url}/settings/password`} />
<SetPermissionsNavLink user={user} permissionsUrl={`${url}/settings/permissions`} />
<ExtensionPoint name="user.setting" props={extensionProps} renderAll={true} />
</SubNavigation>
</Section>
</Navigation>
</SecondaryNavigation>
</div>
</div>
</Page>
</MenuContext.Provider>
);
}
}

View File

@@ -3934,6 +3934,16 @@ babel-plugin-require-context-hook@^1.0.0:
babel-plugin-syntax-jsx "^6.18.0"
lodash "^4.17.11"
babel-plugin-styled-components@^1.10.7:
version "1.10.7"
resolved "https://registry.yarnpkg.com/babel-plugin-styled-components/-/babel-plugin-styled-components-1.10.7.tgz#3494e77914e9989b33cc2d7b3b29527a949d635c"
integrity sha512-MBMHGcIA22996n9hZRf/UJLVVgkEOITuR2SvjHLb5dSTUyR4ZRGn+ngITapes36FI3WLxZHfRhkA1ffHxihOrg==
dependencies:
"@babel/helper-annotate-as-pure" "^7.0.0"
"@babel/helper-module-imports" "^7.0.0"
babel-plugin-syntax-jsx "^6.18.0"
lodash "^4.17.11"
babel-plugin-syntax-jsx@^6.18.0:
version "6.18.0"
resolved "https://registry.yarnpkg.com/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz#0af32a9a6e13ca7a3fd5069e62d7b0f58d0d8946"