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 - Create OpenAPI specification during build
- Extension point entries with supplied extensionName are sorted ascending - Extension point entries with supplied extensionName are sorted ascending
- Possibility to configure git core config entries for jgit like core.trustfolderstat and core.supportsatomicfilecreation - 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 - By default, only 100 files will be listed in source view in one request
### Changed ### Changed
- New footer design - New footer design
- Update jgit to version 5.6.1.202002131546-r-scm1 - Update jgit to version 5.6.1.202002131546-r-scm1
- Update svnkit to version 1.10.1-scm1 - Update svnkit to version 1.10.1-scm1
- Secondary navigation collapsable
### Fixed ### Fixed
- Modification for mercurial repositories with enabled XSRF protection - Modification for mercurial repositories with enabled XSRF protection
- Does not throw NullPointerException when merge fails without normal merge conflicts - Does not throw NullPointerException when merge fails without normal merge conflicts
- Keep file attributes on modification - Keep file attributes on modification
- Drop Down Component works again with translations
### Removed ### Removed
- Enunciate rest documentation - Enunciate rest documentation

175
Jenkinsfile vendored
View File

@@ -1,24 +1,21 @@
#!groovy #!groovy
// Keep the version in sync with the one used in pom.xml in order to get correct syntax completion. // switch back to a stable tag, after pr 22 is mreged an the next version is released
@Library('github.com/cloudogu/ces-build-lib@1.35.1') // see https://github.com/cloudogu/ces-build-lib/pull/22
@Library('github.com/cloudogu/ces-build-lib@8e9194e8')
import com.cloudogu.ces.cesbuildlib.* import com.cloudogu.ces.cesbuildlib.*
node('docker') { node('docker') {
// Change this as when we go back to default - necessary for proper SonarQube analysis
mainBranch = 'develop' mainBranch = 'develop'
properties([ properties([
// Keep only the last 10 build to preserve space // Keep only the last 10 build to preserve space
buildDiscarder(logRotator(numToKeepStr: '10')), buildDiscarder(logRotator(numToKeepStr: '10')),
disableConcurrentBuilds(), disableConcurrentBuilds()
parameters([
string(name: 'dockerTag', trim: true, defaultValue: 'latest', description: 'Extra Docker Tag for cloudogu/scm-manager image')
])
]) ])
timeout(activity: true, time: 40, unit: 'MINUTES') { timeout(activity: true, time: 60, unit: 'MINUTES') {
Git git = new Git(this) Git git = new Git(this)
@@ -30,16 +27,47 @@ node('docker') {
checkout scm 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') { stage('Build') {
mvn 'clean install -DskipTests' mvn 'clean install -DskipTests'
} }
stage('Unit Test') { stage('Unit Test') {
mvn 'test -Pcoverage -Dmaven.test.failure.ignore=true' 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') { 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') { stage('SonarQube') {
@@ -51,10 +79,7 @@ node('docker') {
} }
} }
def commitHash = git.getCommitHash() if (isMainBranch() || isReleaseBranch()) {
def dockerImageTag = "2.0.0-dev-${commitHash.substring(0,7)}-${BUILD_NUMBER}"
if (isMainBranch()) {
stage('Lifecycle') { stage('Lifecycle') {
try { try {
@@ -66,38 +91,106 @@ node('docker') {
} }
} }
stage('Archive') { if (isBuildSuccessful()) {
archiveArtifacts 'scm-webapp/target/scm-webapp.war'
archiveArtifacts 'scm-server/target/scm-server-app.*'
}
stage('Docker') { def commitHash = git.getCommitHash()
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}" def imageVersion = mvn.getVersion()
currentBuild.description = newDockerTag if (imageVersion.endsWith('-SNAPSHOT')) {
image.push(newDockerTag) 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') {
docker.withRegistry('', 'hub.docker.com-cesmarvin') {
// 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('Presentation Environment') {
build job: 'scm-manager/next-scm.cloudogu.com', propagate: false, wait: false, parameters: [
string(name: 'changeset', value: commitHash),
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}"
}
} }
} }
}
stage('Deployment') {
build job: 'scm-manager/next-scm.cloudogu.com', propagate: false, wait: false, parameters: [
string(name: 'changeset', value: commitHash),
string(name: 'imageTag', value: dockerImageTag)
]
} }
} }
} }
// 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) mailIfStatusChanged(git.commitAuthorEmail)
} }
} }
@@ -107,7 +200,7 @@ String mainBranch
Maven setupMavenBuild() { Maven setupMavenBuild() {
Maven mvn = new MavenWrapperInDocker(this, "scmmanager/java-build:11.0.6_10") 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 // Release starts javadoc, which takes very long, so do only for certain branches
mvn.additionalArgs += ' -DperformRelease' mvn.additionalArgs += ' -DperformRelease'
// JDK8 is more strict, we should fix this before the next release. Right now, this is just not the focus, yet. // 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() { boolean isMainBranch() {
return mainBranch.equals(env.BRANCH_NAME) 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") 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 - \*\*2018-05-04\*\* - SCM-Manager 1.60 released
([download](http://www.scm-manager.org/download/ "wikilink") \| ([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 - \*\*2018-04-11\*\* - SCM-Manager 1.59 released
[All news](http://www.scm-manager.org/news/ "wikilink") [All news](http://www.scm-manager.org/news/ "wikilink")
@@ -42,59 +42,59 @@ for more information.
### Use SCM-Manager ### Use SCM-Manager
- [Getting started](getting-started "wikilink") - [Getting started](getting-started.md "wikilink")
- [Download latest - [Download latest
version](http://www.scm-manager.org/download/ "wikilink") version](http://www.scm-manager.org/download/ "wikilink")
- [FAQ](faq "wikilink") - [FAQ](faq.md "wikilink")
- [Upgrade SCM-Manager to a newer version](upgrade "wikilink") - [Upgrade SCM-Manager to a newer version](upgrade.md "wikilink")
- [Download latest snapshot - [Download latest snapshot
release](download-snapshot-release "wikilink") release](download-snapshot-release.md "wikilink")
- [Download Archive](download-archive "wikilink") - [Download Archive](download-archive.md "wikilink")
- [Command line client](command-line-client "wikilink") - [Command line client](command-line-client.md "wikilink")
- [SCM-Server SSL](scm-server-ssl "wikilink") - [SCM-Server SSL](scm-server-ssl.md "wikilink")
- [ApplicationServer - [ApplicationServer
(Tomcat/Glassfish/Jetty)](applicationserver "wikilink") (Tomcat/Glassfish/Jetty)](applicationserver.md "wikilink")
- [Using SCM-Manager with Apache - [Using SCM-Manager with Apache
mod\_proxy](apache/apache-mod_proxy "wikilink") mod\_proxy](apache/apache-mod_proxy.md "wikilink")
- [Using SCM-Manager with Nginx](nginx "wikilink") - [Using SCM-Manager with Nginx](nginx.md "wikilink")
- [Using SCM-Manager with ISS - [Using SCM-Manager with ISS
(Helicon)](SCM-Manager%20on%20ISS%20Helicon "wikilink") (Helicon)](SCM-Manager%20on%20ISS%20Helicon.md "wikilink")
- [Permissions](https://bitbucket.org/sdorra/scm-manager/wiki/Permissions "wikilink") - [Permissions](Permissions.md "wikilink")
- [Plugins](http://plugins.scm-manager.org/scm-plugin-backend/page/index.html "wikilink") - [Plugins](http://plugins.scm-manager.org/scm-plugin-backend/page/index.html "wikilink")
- [Revision Control Plugin - [Revision Control Plugin
Comparison](rv-plugin-comparison "wikilink") Comparison](rv-plugin-comparison.md "wikilink")
- [Screenshots](http://www.scm-manager.org/screenshots/ "wikilink") - [Screenshots](http://www.scm-manager.org/screenshots/ "wikilink")
- [Mercurial Subrepositories](subrepositories "wikilink") - [Mercurial Subrepositories](subrepositories.md "wikilink")
- [Unix Daemons and Windows Services](daemons "wikilink") - [Unix Daemons and Windows Services](daemons.md "wikilink")
- [RPM and DEB packages](RPM%20and%20DEB%20packages "wikilink") - [RPM and DEB packages](RPM%20and%20DEB%20packages.md "wikilink")
- [Build windows mercurial packages for - [Build windows mercurial packages for
SCM-Manager](https://bitbucket.org/sdorra/build-win-hg-packages "wikilink") SCM-Manager](https://bitbucket.org/sdorra/build-win-hg-packages "wikilink")
### Plugin documentation ### Plugin documentation
- [Active Directory Plugin](active-directory-plugin "wikilink") - [Active Directory Plugin](active-directory-plugin.md "wikilink")
- [Branch Write Protect Plugin](branchwp-plugin "wikilink") - [Branch Write Protect Plugin](branchwp-plugin.md "wikilink")
- [Jenkins Plugin](jenkins-plugin "wikilink") - [Jenkins Plugin](jenkins-plugin.md "wikilink")
- [Jira Plugin](jira-plugin "wikilink") - [Jira Plugin](jira-plugin.md "wikilink")
- [Mail Plugin](mail-plugin "wikilink") - [Mail Plugin](mail-plugin.md "wikilink")
- [Path Write Protect Plugin](pathwp-plugin "wikilink") - [Path Write Protect Plugin](pathwp-plugin.md "wikilink")
- [Redmine Plugin](redmine-plugin "wikilink") - [Redmine Plugin](redmine-plugin.md "wikilink")
- [Scheduler Plugin](scheduler-plugin "wikilink") - [Scheduler Plugin](scheduler-plugin.md "wikilink")
- [Trac Plugin](trac-plugin "wikilink") - [Trac Plugin](trac-plugin.md "wikilink")
- [WebHook Plugin](webhook-plugin "wikilink") - [WebHook Plugin](webhook-plugin.md "wikilink")
### Development ### Development
- [Building SCM-Manager from source](build-from-source "wikilink") - [Building SCM-Manager from source](build-from-source.md "wikilink")
- [Java Client API](java-client-api "wikilink") - [Java Client API](java-client-api.md "wikilink")
- [Code - [Code
Snippets](https://bitbucket.org/sdorra/scm-manager/wiki/code-snippets "wikilink") Snippets](https://bitbucket.org/sdorra/scm-manager/wiki/code-snippets "wikilink")
- [Configuring Eclipse projects for - [Configuring Eclipse projects for
SCM-Manager](configure-eclipse "wikilink") SCM-Manager](configure-eclipse.md "wikilink")
- [Plugin Descriptor](plugin-descriptor "wikilink") - [Plugin Descriptor](plugin-descriptor.md "wikilink")
- [ExtensionPoints](ExtensionPoints "wikilink") - [ExtensionPoints](ExtensionPoints.md "wikilink")
- [How to create your own plugin](howto-create-a-plugin "wikilink") - [How to create your own plugin](howto-create-a-plugin.md "wikilink")
- [Injection Objects](injectionObjects "wikilink") - [Injection Objects](injectionObjects.md "wikilink")
- [API - [API
documentation](http://docs.scm-manager.org/apidocs/latest/ "wikilink") documentation](http://docs.scm-manager.org/apidocs/latest/ "wikilink")
- [WebService - [WebService
@@ -103,24 +103,24 @@ for more information.
### SCM Manager 2 ### SCM Manager 2
- [Configuration for Intellij - [Configuration for Intellij
IDEA](v2/intellij-idea-configuration "wikilink") IDEA](v2/intellij-idea-configuration.md "wikilink")
- [State of SCM-Manager 2 - [State of SCM-Manager 2
development](v2/State%20of%20SCM-Manager%202%20development "wikilink") development](v2/State%20of%20SCM-Manager%202%20development.md "wikilink")
- [SCM v2 Test Cases](v2/SCMM-v2-Test-Cases "wikilink") - [SCM v2 Test Cases](v2/SCMM-v2-Test-Cases.md "wikilink")
- [Table of decisions made during - [Table of decisions made during
development](v2/Decision-Table "wikilink") development](v2/Decision-Table.md "wikilink")
- [Definition of done](Definition_of_done "wikilink") - [Definition of done](Definition%20of%20done.md "wikilink")
- [Style Guide](v2/style-guide "wikilink") - [Style Guide](v2/style-guide.md "wikilink")
- [Error Handling in REST, Java, UI](v2/error-handling "wikilink") - [Error Handling in REST, Java, UI](v2/error-handling.md "wikilink")
- [Create a new Plugin](v2/Create_a_new_Plugin "wikilink") - [Create a new Plugin](v2/Create%20a%20new%20Plugin.md "wikilink")
- [Migrate Plugin from v1](v2/Migrate_Plugin_from_v1 "wikilink") - [Migrate Plugin from v1](v2/Migrate%20Plugin%20from%20v1.md "wikilink")
- [Plugin Development](v2/Plugin_Development "wikilink") - [Plugin Development](v2/Plugin%20Development.md "wikilink")
- [i18n for Plugins](v2/i18n_for_Plugins "wikilink") - [i18n for Plugins](v2/i18n%20for%20Plugins.md "wikilink")
- [Extension Points](v2/Extension-Points "wikilink") - [Extension Points](v2/Extension-Points.md "wikilink")
- [API changes](v2/API_changes "wikilink") - [API changes](v2/API%20changes.md "wikilink")
- [ui-components/ui-types](v2/UI:_Additions_or_Changes_to_ui-components_or_ui-types "wikilink") - [ui-components/ui-types](v2/UI:%20Additions%20or%20Changes%20to%20ui-components%20or%20ui-types.md "wikilink")
- [Vulnerabilities](v2/vulnerabilities "wikilink") - [Vulnerabilities](v2/vulnerabilities.md "wikilink")
- [Common pitfall](v2/Common_pitfall "wikilink") - [Common pitfall](v2/Common%20pitfall.md "wikilink")
- [Release process](v2/Release_process "wikilink") - [Release process](v2/Release%20process.md "wikilink")
- [Migration Wizard](v2/Migration-Wizard "wikilink") - [Migration Wizard](v2/Migration-Wizard.md "wikilink")
- [Known Issues](v2/Known_Issues "wikilink") - [Known Issues](v2/Known%20Issues.md "wikilink")

View File

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

View File

@@ -97,6 +97,14 @@
<artifactId>maven-war-plugin</artifactId> <artifactId>maven-war-plugin</artifactId>
<configuration> <configuration>
<failOnMissingWebXml>false</failOnMissingWebXml> <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> </configuration>
</plugin> </plugin>

View File

@@ -7,6 +7,10 @@ module.exports = api => {
require("@babel/preset-react"), require("@babel/preset-react"),
require("@babel/preset-typescript") 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-env": "^7.6.3",
"@babel/preset-flow": "^7.0.0", "@babel/preset-flow": "^7.0.0",
"@babel/preset-react": "^7.6.3", "@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": { "publishConfig": {
"access": "public" "access": "public"

View File

@@ -115,6 +115,18 @@
</args> </args>
</configuration> </configuration>
</execution> </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> </executions>
</plugin> </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[]; optionValues?: string[];
optionSelected: (p: string) => void; optionSelected: (p: string) => void;
preselectedOption?: string; preselectedOption?: string;
className: string; className?: string;
disabled?: boolean; disabled?: boolean;
}; };
class DropDown extends React.Component<Props> { class DropDown extends React.Component<Props> {
render() { render() {
const { options, optionValues, preselectedOption, className, disabled } = this.props; const { options, optionValues, preselectedOption, className, disabled } = this.props;
if (preselectedOption && !options.includes(preselectedOption)) {
options.unshift(preselectedOption);
}
return ( return (
<div className={classNames(className, "select", disabled ? "disabled" : "")}> <div className={classNames(className, "select", disabled ? "disabled" : "")}>
<select value={preselectedOption ? preselectedOption : ""} onChange={this.change} disabled={disabled}> <select value={preselectedOption ? preselectedOption : ""} onChange={this.change} disabled={disabled}>
<option key={preselectedOption} />
{options.map((option, index) => { {options.map((option, index) => {
const value = optionValues && optionValues[index] ? optionValues[index] : option;
return ( return (
<option key={option} value={optionValues && optionValues[index] ? optionValues[index] : option}> <option key={value} value={value} selected={value === preselectedOption}>
{option} {option}
</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; label: string;
activeOnlyWhenExact?: boolean; activeOnlyWhenExact?: boolean;
activeWhenMatch?: (route: any) => boolean; activeWhenMatch?: (route: any) => boolean;
collapsed?: boolean;
title?: string;
}; };
class NavLink extends React.Component<Props> { class NavLink extends React.Component<Props> {
@@ -23,7 +25,7 @@ class NavLink extends React.Component<Props> {
} }
renderLink = (route: any) => { renderLink = (route: any) => {
const { to, icon, label } = this.props; const { to, icon, label, collapsed, title } = this.props;
let showIcon = null; let showIcon = null;
if (icon) { if (icon) {
@@ -35,10 +37,13 @@ class NavLink extends React.Component<Props> {
} }
return ( return (
<li> <li title={collapsed ? title : undefined}>
<Link className={this.isActive(route) ? "is-active" : ""} to={to}> <Link
className={classNames(this.isActive(route) ? "is-active" : "", collapsed ? "has-text-centered" : "")}
to={to}
>
{showIcon} {showIcon}
{label} {collapsed ? null : label}
</Link> </Link>
</li> </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 React, { FC, ReactElement, useContext, useEffect } from "react";
import { Link, Route } from "react-router-dom"; import { Link, useRouteMatch } from "react-router-dom";
import classNames from "classnames"; import classNames from "classnames";
type Props = { type Props = {
@@ -8,52 +8,39 @@ type Props = {
label: string; label: string;
activeOnlyWhenExact?: boolean; activeOnlyWhenExact?: boolean;
activeWhenMatch?: (route: any) => boolean; activeWhenMatch?: (route: any) => boolean;
children?: ReactNode; children?: ReactElement[];
collapsed?: boolean;
title?: string;
}; };
class SubNavigation extends React.Component<Props> { const SubNavigation: FC<Props> = ({ to, activeOnlyWhenExact, icon, collapsed, title, label, children }) => {
static defaultProps = { const parents = to.split("/");
activeOnlyWhenExact: false parents.splice(-1, 1);
}; const parent = parents.join("/");
isActive(route: any) { const match = useRouteMatch({
const { activeWhenMatch } = this.props; path: parent,
return route.match || (activeWhenMatch && activeWhenMatch(route)); exact: activeOnlyWhenExact
});
let defaultIcon = "fas fa-cog";
if (icon) {
defaultIcon = icon;
} }
renderLink = (route: any) => { let childrenList = null;
const { to, icon, label } = this.props; if (match && !collapsed) {
childrenList = <ul className="sub-menu">{children}</ul>;
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>;
}
return (
<li>
<Link className={this.isActive(route) ? "is-active" : ""} to={to}>
<i className={classNames(defaultIcon, "fa-fw")} /> {label}
</Link>
{children}
</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} />;
} }
}
return (
<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>
{childrenList}
</li>
);
};
export default SubNavigation; export default SubNavigation;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,11 +1,20 @@
import React from "react"; import React from "react";
import { connect } from "react-redux"; 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 { WithTranslation, withTranslation } from "react-i18next";
import { History } from "history";
import { binder, ExtensionPoint } from "@scm-manager/ui-extensions"; import { binder, ExtensionPoint } from "@scm-manager/ui-extensions";
import { Repository } from "@scm-manager/ui-types"; 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 { fetchRepoByName, getFetchRepoFailure, getRepository, isFetchRepoPending } from "../modules/repos";
import RepositoryDetails from "../components/RepositoryDetails"; import RepositoryDetails from "../components/RepositoryDetails";
import EditRepo from "./EditRepo"; import EditRepo from "./EditRepo";
@@ -21,24 +30,33 @@ import CodeOverview from "../codeSection/containers/CodeOverview";
import ChangesetView from "./ChangesetView"; import ChangesetView from "./ChangesetView";
import SourceExtensions from "../sources/containers/SourceExtensions"; import SourceExtensions from "../sources/containers/SourceExtensions";
type Props = WithTranslation & { type Props = RouteComponentProps &
namespace: string; WithTranslation & {
name: string; namespace: string;
repository: Repository; name: string;
loading: boolean; repository: Repository;
error: Error; loading: boolean;
repoLink: string; error: Error;
indexLinks: object; repoLink: string;
indexLinks: object;
// dispatch functions // dispatch functions
fetchRepoByName: (link: string, namespace: string, name: string) => void; fetchRepoByName: (link: string, namespace: string, name: string) => void;
};
// context props type State = {
history: History; menuCollapsed: boolean;
match: any;
}; };
class RepositoryRoot extends React.Component<Props> { class RepositoryRoot extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
menuCollapsed: isMenuCollapsed()
};
}
componentDidMount() { componentDidMount() {
const { fetchRepoByName, namespace, name, repoLink } = this.props; const { fetchRepoByName, namespace, name, repoLink } = this.props;
fetchRepoByName(repoLink, namespace, name); fetchRepoByName(repoLink, namespace, name);
@@ -87,8 +105,13 @@ class RepositoryRoot extends React.Component<Props> {
return `${url}/changesets`; return `${url}/changesets`;
}; };
onCollapseRepositoryMenu = (collapsed: boolean) => {
this.setState({ menuCollapsed: collapsed }, () => storeMenuCollapsed(collapsed));
};
render() { render() {
const { loading, error, indexLinks, repository, t } = this.props; const { loading, error, indexLinks, repository, t } = this.props;
const { menuCollapsed } = this.state;
if (error) { if (error) {
return ( return (
@@ -117,66 +140,76 @@ class RepositoryRoot extends React.Component<Props> {
} }
return ( return (
<Page title={repository.namespace + "/" + repository.name}> <MenuContext.Provider
<div className="columns"> value={{
<div className="column is-three-quarters"> menuCollapsed,
<Switch> setMenuCollapsed: (collapsed: boolean) => this.setState({ menuCollapsed: collapsed })
<Redirect exact from={this.props.match.url} to={redirectedUrl} /> }}
>
<Page title={repository.namespace + "/" + repository.name}>
<div className="columns">
<div className="column">
<Switch>
<Redirect exact from={this.props.match.url} to={redirectedUrl} />
{/* redirect pre 2.0.0-rc2 links */} {/* redirect pre 2.0.0-rc2 links */}
<Redirect from={`${url}/changeset/:id`} to={`${url}/code/changeset/:id`} /> <Redirect from={`${url}/changeset/:id`} to={`${url}/code/changeset/:id`} />
<Redirect exact from={`${url}/sources`} to={`${url}/code/sources`} /> <Redirect exact from={`${url}/sources`} to={`${url}/code/sources`} />
<Redirect from={`${url}/sources/:revision/:path*`} to={`${url}/code/sources/:revision/:path*`} /> <Redirect from={`${url}/sources/:revision/:path*`} to={`${url}/code/sources/:revision/:path*`} />
<Redirect exact from={`${url}/changesets`} to={`${url}/code/changesets`} /> <Redirect exact from={`${url}/changesets`} to={`${url}/code/changesets`} />
<Redirect from={`${url}/branch/:branch/changesets`} to={`${url}/code/branch/:branch/changesets/`} /> <Redirect from={`${url}/branch/:branch/changesets`} to={`${url}/code/branch/:branch/changesets/`} />
<Route path={`${url}/info`} exact component={() => <RepositoryDetails repository={repository} />} /> <Route path={`${url}/info`} exact component={() => <RepositoryDetails repository={repository} />} />
<Route path={`${url}/settings/general`} component={() => <EditRepo repository={repository} />} /> <Route path={`${url}/settings/general`} component={() => <EditRepo repository={repository} />} />
<Route <Route
path={`${url}/settings/permissions`} path={`${url}/settings/permissions`}
render={() => ( render={() => (
<Permissions namespace={this.props.repository.namespace} repoName={this.props.repository.name} /> <Permissions namespace={this.props.repository.namespace} repoName={this.props.repository.name} />
)} )}
/> />
<Route <Route
exact exact
path={`${url}/code/changeset/:id`} path={`${url}/code/changeset/:id`}
render={() => <ChangesetView repository={repository} />} render={() => <ChangesetView repository={repository} />}
/> />
<Route <Route
path={`${url}/code/sourceext/:extension`} path={`${url}/code/sourceext/:extension`}
exact={true} exact={true}
render={() => <SourceExtensions repository={repository} />} render={() => <SourceExtensions repository={repository} />}
/> />
<Route <Route
path={`${url}/code/sourceext/:extension/:revision/:path*`} path={`${url}/code/sourceext/:extension/:revision/:path*`}
render={() => <SourceExtensions repository={repository} baseUrl={`${url}/code/sources`} />} render={() => <SourceExtensions repository={repository} baseUrl={`${url}/code/sources`} />}
/> />
<Route <Route
path={`${url}/code`} path={`${url}/code`}
render={() => <CodeOverview baseUrl={`${url}/code`} repository={repository} />} render={() => <CodeOverview baseUrl={`${url}/code`} repository={repository} />}
/> />
<Route <Route
path={`${url}/branch/:branch`} path={`${url}/branch/:branch`}
render={() => <BranchRoot repository={repository} baseUrl={`${url}/branch`} />} render={() => <BranchRoot repository={repository} baseUrl={`${url}/branch`} />}
/> />
<Route <Route
path={`${url}/branches`} path={`${url}/branches`}
exact={true} exact={true}
render={() => <BranchesOverview repository={repository} baseUrl={`${url}/branch`} />} render={() => <BranchesOverview repository={repository} baseUrl={`${url}/branch`} />}
/> />
<Route path={`${url}/branches/create`} render={() => <CreateBranch repository={repository} />} /> <Route path={`${url}/branches/create`} render={() => <CreateBranch repository={repository} />} />
<ExtensionPoint name="repository.route" props={extensionProps} renderAll={true} /> <ExtensionPoint name="repository.route" props={extensionProps} renderAll={true} />
</Switch> </Switch>
</div> </div>
<div className="column"> <div className={menuCollapsed ? "column is-1" : "column is-3"}>
<Navigation> <SecondaryNavigation
<Section label={t("repositoryRoot.menu.navigationLabel")}> label={t("repositoryRoot.menu.navigationLabel")}
onCollapse={() => this.onCollapseRepositoryMenu(!menuCollapsed)}
collapsed={menuCollapsed}
>
<ExtensionPoint name="repository.navigation.topLevel" props={extensionProps} renderAll={true} /> <ExtensionPoint name="repository.navigation.topLevel" props={extensionProps} renderAll={true} />
<NavLink <NavLink
to={`${url}/info`} to={`${url}/info`}
icon="fas fa-info-circle" icon="fas fa-info-circle"
label={t("repositoryRoot.menu.informationNavLink")} label={t("repositoryRoot.menu.informationNavLink")}
title={t("repositoryRoot.menu.informationNavLink")}
/> />
<RepositoryNavLink <RepositoryNavLink
repository={repository} repository={repository}
@@ -186,6 +219,7 @@ class RepositoryRoot extends React.Component<Props> {
label={t("repositoryRoot.menu.branchesNavLink")} label={t("repositoryRoot.menu.branchesNavLink")}
activeWhenMatch={this.matchesBranches} activeWhenMatch={this.matchesBranches}
activeOnlyWhenExact={false} activeOnlyWhenExact={false}
title={t("repositoryRoot.menu.branchesNavLink")}
/> />
<RepositoryNavLink <RepositoryNavLink
repository={repository} repository={repository}
@@ -195,18 +229,23 @@ class RepositoryRoot extends React.Component<Props> {
label={t("repositoryRoot.menu.sourcesNavLink")} label={t("repositoryRoot.menu.sourcesNavLink")}
activeWhenMatch={this.matchesCode} activeWhenMatch={this.matchesCode}
activeOnlyWhenExact={false} activeOnlyWhenExact={false}
title={t("repositoryRoot.menu.sourcesNavLink")}
/> />
<ExtensionPoint name="repository.navigation" props={extensionProps} renderAll={true} /> <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`} /> <EditRepoNavLink repository={repository} editUrl={`${url}/settings/general`} />
<PermissionsNavLink permissionUrl={`${url}/settings/permissions`} repository={repository} /> <PermissionsNavLink permissionUrl={`${url}/settings/permissions`} repository={repository} />
<ExtensionPoint name="repository.setting" props={extensionProps} renderAll={true} /> <ExtensionPoint name="repository.setting" props={extensionProps} renderAll={true} />
</SubNavigation> </SubNavigation>
</Section> </SecondaryNavigation>
</Navigation> </div>
</div> </div>
</div> </Page>
</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", () => { it("should return true, when fetching changesets is pending", () => {
const state = { const state = {
pending: { pending: {
@@ -639,5 +664,14 @@ describe("changesets", () => {
expect(collection.page).toBe(1); expect(collection.page).toBe(1);
expect(collection.pageTotal).toBe(10); 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 { isPending } from "../../modules/pending";
import { getFailure } from "../../modules/failure"; import { getFailure } from "../../modules/failure";
import { Action, Branch, PagedCollection, Repository } from "@scm-manager/ui-types"; 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 = "scm/repos/FETCH_CHANGESETS";
export const FETCH_CHANGESETS_PENDING = `${FETCH_CHANGESETS}_${PENDING_SUFFIX}`; 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 null;
} }
return collectChangesets(stateRoot, changesets);
}
const mapChangesets = (stateRoot, changesets) => {
return changesets.entries.map((id: string) => { return changesets.entries.map((id: string) => {
return stateRoot.byId[id]; return stateRoot.byId[id];
}); });
} };
const collectChangesets = memoizeOne(mapChangesets);
export function getChangeset(state: object, repository: Repository, id: string) { export function getChangeset(state: object, repository: Repository, id: string) {
const key = createItemId(repository); const key = createItemId(repository);
@@ -291,6 +297,8 @@ export function getFetchChangesetsFailure(state: object, repository: Repository,
return getFailure(state, FETCH_CHANGESETS, createItemId(repository, branch)); return getFailure(state, FETCH_CHANGESETS, createItemId(repository, branch));
} }
const EMPTY = {};
const selectList = (state: object, repository: Repository, branch?: Branch) => { const selectList = (state: object, repository: Repository, branch?: Branch) => {
const repoId = createItemId(repository); const repoId = createItemId(repository);
@@ -302,7 +310,7 @@ const selectList = (state: object, repository: Repository, branch?: Branch) => {
return repoState.byBranch[branchName]; return repoState.byBranch[branchName];
} }
} }
return {}; return EMPTY;
}; };
const selectListEntry = (state: object, repository: Repository, branch?: Branch): object => { const selectListEntry = (state: object, repository: Repository, branch?: Branch): object => {
@@ -310,7 +318,7 @@ const selectListEntry = (state: object, repository: Repository, branch?: Branch)
if (list.entry) { if (list.entry) {
return list.entry; return list.entry;
} }
return {}; return EMPTY;
}; };
export const selectListAsCollection = (state: object, repository: Repository, branch?: Branch): PagedCollection => { 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 { Action, Repository, RepositoryCollection } from "@scm-manager/ui-types";
import { isPending } from "../../modules/pending"; import { isPending } from "../../modules/pending";
import { getFailure } from "../../modules/failure"; import { getFailure } from "../../modules/failure";
import React from "react";
export const FETCH_REPOS = "scm/repos/FETCH_REPOS"; export const FETCH_REPOS = "scm/repos/FETCH_REPOS";
export const FETCH_REPOS_PENDING = `${FETCH_REPOS}_${types.PENDING_SUFFIX}`; 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 // 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) { return function(dispatch: any) {
dispatch(createRepoPending()); dispatch(createRepoPending());
const repoLink = initRepository ? link + "?initialize=true" : link; const repoLink = initRepository ? link + "?initialize=true" : link;

View File

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

View File

@@ -3934,6 +3934,16 @@ babel-plugin-require-context-hook@^1.0.0:
babel-plugin-syntax-jsx "^6.18.0" babel-plugin-syntax-jsx "^6.18.0"
lodash "^4.17.11" 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: babel-plugin-syntax-jsx@^6.18.0:
version "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" resolved "https://registry.yarnpkg.com/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz#0af32a9a6e13ca7a3fd5069e62d7b0f58d0d8946"